Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/gleplot/axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,7 @@ def plot(
markersize=gle_markersize,
linestyle=linestyle,
linewidth=linewidth,
nomiss=False,
label=label,
yaxis=yaxis, # 'y' or 'y2'
offset=float(offset),
Expand Down Expand Up @@ -1248,6 +1249,7 @@ def line_from_file(
color=gle_color,
linestyle=linestyle,
linewidth=float(linewidth),
nomiss=False,
label=label,
yaxis=yaxis,
)
Expand Down
4 changes: 4 additions & 0 deletions src/gleplot/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2028,6 +2028,7 @@ def _write_axes_content(
label=series_data["label"],
marker=series_data.get("marker"),
markersize=series_data.get("markersize", 0.1),
nomiss=series_data.get("nomiss", False),
yaxis=series_data.get("yaxis", "y"),
offset=series_data.get("offset", 0.0),
column_names=series_data.get("column_names"),
Expand Down Expand Up @@ -2083,6 +2084,9 @@ def _write_axes_content(
linewidth=fs_data.get("linewidth", 1.0),
label=fs_data.get("label"),
yaxis=fs_data.get("yaxis", "y"),
marker=fs_data.get("marker"),
markersize=fs_data.get("markersize", 0.1),
nomiss=fs_data.get("nomiss", False),
)
elif series_type == "bar":
writer.add_bar_from_file(
Expand Down
88 changes: 87 additions & 1 deletion src/gleplot/parser/recognizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any, Dict, List, Optional, Tuple, Union, cast

import numpy as np

Expand Down Expand Up @@ -319,6 +319,24 @@
_DATASET_RE = re.compile(r"^d\d+$", re.IGNORECASE)


def _bar_stack_names(toks: List[Token]) -> Optional[Tuple[str, str]]:
"""``(dN, dM)`` for a ``bar dN from dM ...`` statement, else ``None``.

GLE's stacked-bar form (the manual's 'stacked bar chart' example): ``dN``
draws stacked on top of ``dM``. Detected up front, in pass 1, so an
EARLIER, independent ``bar dM fill ...`` statement can still be
recognized as part of this later stack -- see ``_bar_stack_datasets``
and its use in :meth:`_Recognizer._parse_bar_command`.
"""
words = [t.value.lower() for t in toks]
for i, word in enumerate(words):
if word == "from" and 0 < i < len(words) - 1:
prev_word, next_word = words[i - 1], words[i + 1]
if _DATASET_RE.match(prev_word) and _DATASET_RE.match(next_word):
return prev_word, next_word
return None


@dataclass
class RecognizedFigure:
"""Result of :func:`parse_gle_figure`.
Expand Down Expand Up @@ -1157,6 +1175,14 @@ def _parse_graph_block(self, block: GraphBlock, marker_cfg, smooth_flags) -> dic
# consumed indirectly through the alias -- see _parse_let_command
# and the 'data'-statement reconciliation in _parse_graph_block.
"_let_source_datasets": set(),
# Both dataset names of every 'bar dN from dM ...' (GLE's
# stacked-bar form) found ANYWHERE in this block, collected up
# front in pass 1 -- so an EARLIER, independent 'bar dM fill
# ...' statement can still recognize dM as part of a later
# stack and stay raw instead of being modeled twice over (once
# as its own BarSeries, once folded into the stack's raw text).
# See _parse_bar_command.
"_bar_stack_datasets": set(),
}
# This graph block's own (begin, end) line span -- the fallback
# location for a note about the block as a whole (no single statement
Expand Down Expand Up @@ -1218,6 +1244,16 @@ def _parse_graph_block(self, block: GraphBlock, marker_cfg, smooth_flags) -> dic
# already known here in pass 1).
self._parse_let_command(_words_and_values(child), datasets, info)
continue
if kw == "bar":
# Look ahead for GLE's stacked-bar form ('bar dN from dM
# ...') anywhere in the block, before pass 2 decides
# whether an EARLIER, independent 'bar dM fill ...'
# becomes its own BarSeries (see _parse_bar_command). No
# 'continue': pass 2 still needs to dispatch this
# statement itself.
stacked = _bar_stack_names(_words_and_values(child))
if stacked is not None:
cast(set, info["_bar_stack_datasets"]).update(stacked)
if kw is not None and _DATASET_RE.match(kw):
name = kw
if name not in merged_attr_toks:
Expand Down Expand Up @@ -1335,6 +1371,22 @@ def _parse_graph_block(self, block: GraphBlock, marker_cfg, smooth_flags) -> dic
# whole original line would duplicate the modeled portion;
# dropping it silently loses the rest. Surface it instead of
# guessing.
#
# This case is now rare in practice: the one recurring
# source of it -- a 'bar dM fill ...' modeled independently
# while a LATER 'bar dN from dM ...' (GLE's stacked-bar
# form) needed dM to stay raw alongside dN -- is headed off
# up front by ``_bar_stack_datasets`` (see
# _parse_bar_command), which keeps BOTH names raw so this
# statement is fully unconsumed instead of mixed. A
# genuinely mixed statement still reaching here has no
# narrower fix available: synthesizing a second 'data'
# clause for just the orphaned names would reference columns
# the modeled series' OWN regenerated sidecar no longer
# carries (GLEWriter always rewrites an owned series' data
# file with exactly the columns it uses -- see
# GLEWriter.add_bar_chart/_write_columns), which would
# silently corrupt the file instead of fixing the reference.
self._note(
ImportCategory.DATA,
"'"
Expand Down Expand Up @@ -2238,6 +2290,22 @@ def _parse_bar_command(self, toks, datasets, info, stmt=None):
if d_name is None or d_name not in datasets:
info["passthrough"].append(self._bar_fill_passthrough_line(toks, stmt))
return
if d_name in info["_bar_stack_datasets"]:
# This dataset ALSO appears in a 'bar dN from dM ...' statement
# elsewhere in the block (pass 1's look-ahead, ``_bar_stack_names``)
# -- GLE's stacked-bar relationship has no BarSeries model, so
# this otherwise-ordinary single-dataset 'bar' must stay raw
# alongside it rather than being modeled independently. Modeling
# it anyway would have the writer regenerate ITS dataset's own
# '.dat' sidecar with only the columns IT uses (GLEWriter always
# rewrites an owned series' data file that way -- see
# add_bar_chart/_write_columns), silently truncating away the
# column(s) the stack statement still needs from that same
# file. The stack statement's own "multi-dataset bar group" note
# already explains the group to the no-silent-drops contract, so
# none is duplicated here.
info["passthrough"].append(self._bar_fill_passthrough_line(toks, stmt))
return
info["_key_suppress_datasets"].add(d_name)
data_file, xcol, ycol = datasets[d_name]
loaded = self._load_series(
Expand Down Expand Up @@ -2557,6 +2625,7 @@ def _parse_series_command(
markersize=markersize,
linestyle=linestyle,
linewidth=linewidth,
nomiss=attrs["nomiss"],
label=attrs["label"],
yaxis="y2" if attrs["y2axis"] else "y",
offset=info["_dataset_offsets"].get(d_name, 0.0),
Expand All @@ -2580,6 +2649,7 @@ def _scan_series_attrs(self, toks) -> dict:
a = {
"has_line": False,
"smooth": False,
"nomiss": False,
"color": None,
"lwidth": None,
"lstyle": None,
Expand All @@ -2606,6 +2676,10 @@ def _scan_series_attrs(self, toks) -> dict:
a["smooth"] = True
i += 1
continue
if w == "nomiss":
a["nomiss"] = True
i += 1
continue
if w == "color" and i + 1 < m:
val, nxt = _collect_color(toks, i + 1)
a["color"] = val
Expand All @@ -2621,6 +2695,17 @@ def _scan_series_attrs(self, toks) -> dict:
if v is not None:
a["lstyle"] = int(v)
a["linestyle"] = LSTYLE_TO_MATPLOTLIB.get(int(v), "-")
# GLE draws a line whenever EITHER the 'line' keyword is
# given OR 'lstyle' names a style at all (GLE's own
# draw-a-line test, graph2.cpp: 'dp[dn]->line ||
# dp[dn]->lstyle[0] != 0') -- 'dN lstyle 2 color red'
# with no 'line' keyword anywhere (the GLE manual's own
# nomiss example) is a real, visible line, not a bare
# dataset. Missing this made has_line False, which fed
# linestyle="none" into the model and, downstream, an
# unconditional 'marker None' from the writer's
# no-line branch (see add_plot_line).
a["has_line"] = True
i = nxt
continue
i += 2
Expand Down Expand Up @@ -2969,6 +3054,7 @@ def _build_file_series(
if attrs["lwidth"] is not None
else 1.0
),
nomiss=attrs["nomiss"],
label=attrs["label"],
yaxis="y2" if attrs["y2axis"] else "y",
)
Expand Down
8 changes: 8 additions & 0 deletions src/gleplot/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,11 @@ class _XYSeries(Series):
markersize: float
linestyle: str
linewidth: float
#: GLE's ``nomiss`` qualifier: draw the line through a missing value
#: instead of leaving a gap (GLE default: gap). Only meaningful when a
#: line is drawn, but preserved verbatim on scatter-only series too so a
#: hand-written file that sets it never loses the setting on re-save.
nomiss: bool
label: Optional[str]
yaxis: str
offset: float
Expand Down Expand Up @@ -600,6 +605,9 @@ class FileSeries(Series):
markersize: float
linestyle: str
linewidth: float
#: ``'line'`` variant only: GLE's ``nomiss`` qualifier (see
#: ``_XYSeries.nomiss``).
nomiss: bool
label: Optional[str]
capsize: Optional[float]
yaxis: str
Expand Down
43 changes: 37 additions & 6 deletions src/gleplot/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,7 @@ def add_plot_line(
label: Optional[str] = None,
marker: Optional[str] = None,
markersize: float = 0.1,
nomiss: bool = False,
yaxis: str = "y",
offset: float = 0.0,
column_names: Optional[List[str]] = None,
Expand Down Expand Up @@ -1624,6 +1625,10 @@ def add_plot_line(
GLE marker name
markersize : float
Marker size for GLE (msize)
nomiss : bool
When the line has missing values, draw through them instead of
leaving a gap (GLE's ``nomiss`` qualifier). No effect when the
series draws no line.
yaxis : str
Which y-axis to use: 'y' (left, default) or 'y2' (right)
column_names : list of str, optional
Expand Down Expand Up @@ -1683,6 +1688,8 @@ def add_plot_line(
if has_line:
# Line plot; ``smooth`` only when opted in (see _line_token).
line_cmd += self._line_token()
if nomiss:
line_cmd += " nomiss"
line_cmd += f" color {color} lwidth {self._format_number(gle_lwidth)}"

# Use configured line styles from style config. (A stray second
Expand All @@ -1700,10 +1707,17 @@ def add_plot_line(
if marker:
# Marker overlaid on the line (line+markers).
line_cmd += f" marker {marker} msize {self._format_number(markersize)}"
else:
elif marker:
# No line: marker-only (scatter). Preserve the historical token
# order ``marker <name> msize <size> color <color>``.
line_cmd += f" marker {marker} msize {self._format_number(markersize)} color {color}"
# else: neither a line nor a marker (e.g. ``ax.plot(x, y,
# linestyle='none')`` with no marker -- a real, if pointless,
# degenerate case matplotlib itself allows) -- the series draws
# nothing, so nothing is emitted here. This branch used to be
# unconditional and interpolated ``marker`` even when it was
# ``None``, putting the literal text "marker None" into the script
# (GLE then rejects it: "invalid marker name 'None'").

# Add y2axis directive if using secondary y-axis
if yaxis == "y2":
Expand Down Expand Up @@ -2175,8 +2189,22 @@ def add_plot_line_from_file(
linewidth: float = 1.0,
label: Optional[str] = None,
yaxis: str = "y",
marker: Optional[str] = None,
markersize: float = 0.1,
nomiss: bool = False,
):
"""Add a line series that references columns in an external data file."""
"""Add a line series that references columns in an external data file.

``marker``/``markersize`` overlay a marker on the line (GLE natively
supports both on one dataset) -- this function is only ever called
for a series that already has a line (see ``_build_file_series``'s
``has_line`` branch; a no-line, marker-only reference is emitted via
``add_errorbar_from_file`` instead), so there is no separate
no-line/marker-only case to guard here, unlike ``add_plot_line``.

``nomiss`` : draw the line through a missing value instead of
leaving a gap (GLE's ``nomiss`` qualifier).
"""
d_main = f"d{self.dataset_index}"
self.dataset_index += 1

Expand All @@ -2189,17 +2217,20 @@ def add_plot_line_from_file(
else:
gle_lwidth = linewidth_pt_to_cm(linewidth)

line_cmd = (
f" {d_main}{self._line_token()} color {color} "
f"lwidth {self._format_number(gle_lwidth)}"
)
line_cmd = f" {d_main}{self._line_token()}"
if nomiss:
line_cmd += " nomiss"
line_cmd += f" color {color} lwidth {self._format_number(gle_lwidth)}"
if linestyle == "--":
line_cmd += f" lstyle {self.style.line_style_dashed}"
elif linestyle == ":":
line_cmd += f" lstyle {self.style.line_style_dotted}"
elif linestyle == "-.":
line_cmd += f" lstyle {self.style.line_style_dashdot}"

if marker:
line_cmd += f" marker {marker} msize {self._format_number(markersize)}"

if yaxis == "y2":
line_cmd += " y2axis"

Expand Down
Loading
Loading