GX-TXT Scientific Plot Developer Manual

Indice

Target framework: GX-TXT 0.9.8.10-dev-031
Audience: developers writing scientific modules and plot-producing plugins
Language: English
License note: examples are intended for GPL-3.0-or-later GX-TXT modules unless the module declares otherwise.


1. Purpose

This manual defines the recommended way to produce scientific plots in GX-TXT without duplicating plotting infrastructure or bypassing framework state.

The central rule is:

A scientific module should own scientific meaning and scientific data. The framework should own reusable plotting presentation state. Generic scientific rendering should be delegated to the public plot.dataset APIs whenever the graph can be represented by the public PlotSpec / Graph Series model.

A conforming module should not invent a private renderer for functionality that already exists in GX-TXT. In particular, a module should not maintain its own independent implementations of:

The framework already exposes public APIs for these functions.

This manual covers the complete integration path:

Canonical Dataset / Analysis Source
        ↓
Dataset Variable Schema
        ↓
Table Stream API
        ↓
scientific calculation
        ↓
Dataset Publication API
        ↓
Graph Series API
        ↓
PlotSpec v2 through PLOT_DATASET_API_v3
        ↓
Graphics / Axes / Layout / Legend / Level Scale / Annotation
        ↓
Variants / USE / rerender
        ↓
PNG + .gxtxt_plotmeta + export/print

2. Architectural rule: never confuse data state with presentation state

GX-TXT deliberately separates scientific state from presentation state.

Scientific state

Scientific state includes:

Presentation state

Presentation state includes:

A common implementation mistake is to duplicate presentation fields inside the scientific module. Do not do that unless the setting changes scientific meaning.

For example, this is normally wrong:

[graph.my.plot.config.line_width]
type=double
default=1.5

if the value exists only to make the plotted line thicker.

Line width already belongs to Graphics inheritance and optional per-Series presentation. Adding another graph-level line-width control creates a competing authority.

A scientific configuration field is appropriate only when the value changes the scientific representation itself.


3. Public APIs used by scientific plotting modules

A modern plot-producing module should understand the following public surfaces.

API Responsibility
Analysis Source Map API v1 framework-owned N-source Dataset selection
Analysis Source Access Runtime v2 runtime Dataset/source metadata
Dataset Variable Schema API v3 stable variable identity and semantics
Table Stream API v3 backend-neutral chunked table access
Dataset Publication API v2 publish derived canonical Datasets
Graph Provider API v1 declare graph offerings
Graph Workflow API v1 activation, USE/NON USE, graph-addressed workflow
Graph Series API v1 persistent scientific X/Y series bindings
Plot Module API v1 persistent plot variants
Graphics Inheritance API v1 global → module → plot/variant graphics
PLOT_DATASET_API_v3 current public plotting facade
PLOT_DATASET_PLOTSPEC_v2 declarative 2D/3D plot model
Series Editor Extension API v1 provider-owned per-Series presentation controls
Graph Configuration Extension API v1 provider-specific graph controls
Axis Configuration Extension API v1 provider-specific axis-dialog controls
Level Scale API v1 linear/dB/dBFS/FS display transform
Plot Annotation API v1 interactive figure annotations
Plot Region Selection API v1 framework-owned data-coordinate selection
Multi-Y Axis Plot API v1 left/right Y rendering
Render Cache API v1 cache-only rerender path
Unit / Dimension APIs scientific unit compatibility and display conversion

For a normal new integration, activate PLOT_DATASET_API_v3, not v1 and v2 individually:

::GX::modulePublicApiActivate plot.dataset PLOT_DATASET_API_v3

The v3 facade loads the stable v1/v2 implementation as required.


Part I — Scientific data access

4. Use Dataset IDs and Variable IDs, not paths

A module must treat these as authoritative identities:

Dataset ID
Variable ID

Examples:

dataset_id = curve.replicate.profiles
x_variable_id = v0001
y_variable_id = v0004

Do not treat any of these as scientific identity:

CSV filename
absolute path
column number
display label
current variable name
PNG filename

A variable may be renamed while its Variable ID remains stable.

A Dataset may be stored as CSV today and HDF5 later without changing scientific identity.


5. Analysis Source Map: let the framework select inputs

A multi-source analysis module should opt into Analysis Source Map API v1.

Descriptor:

[analysis_sources]
api_version=1
ids=primary
dynamic=1

[analysis_source.primary]
required=1
contracts=

With dynamic=1, GX-TXT provides:

+ Add source
Remove

The module must not build a private Dataset selector for the same task.

Bindings may be source-qualified:

x=primary.x
family=primary.family_value
y=primary.y

An old unqualified binding remains an alias of the primary source where supported.

Important

The runtime may contain a convenience path= entry for compatibility, but a modern module must not use that path as its data-access authority.

Do not do this:

path = gxtxt_ini_get(cfg, 'analysis_source.primary.path', '');
if (exist(path,'file') != 2)
  error('missing dataset');
endif

That rejects valid provider-backed Datasets and reintroduces storage coupling.

Use the Dataset ID exposed by the Analysis Source runtime and open it through the framework.


6. Table Stream API v3: the default way to read table-shaped Datasets

For rank-1 variables sharing one logical row dimension, use Table Stream API v3.

Octave Analysis Source form

opts = struct('start', 0, 'count', -1, 'require_direct', false);

S = gxtxt_table_stream_open_source(
      runtime_cfg,
      'primary',
      {'v_time','v_voltage'},
      4096,
      opts);

while (true)
  [S, chunk] = gxtxt_table_stream_next(S);
  if (chunk.eof)
    break;
  endif

  % Consume only this bounded chunk.
endwhile

meta = gxtxt_table_stream_metadata(S);
diag = gxtxt_table_stream_diagnostics(S);
S = gxtxt_table_stream_close(S);

Direct Dataset form

S = gxtxt_table_stream_open_dataset(
      session,
      'waveform.main',
      {'v_time','v_voltage'},
      4096,
      opts);

Why this matters

Table Stream v3 is backend-neutral:

module
  -> Table Stream API v3
  -> Canonical Dataset API v2
  -> CSV / HDF5 / future provider

The module must not detect the backend itself.

Never do this in new scientific code

fid = fopen(dataset_path, 'r');
header = fgetl(fid);
csvread(...);
dlmread(...);
textscan(...);

unless the file is a user-supplied non-Dataset input whose format is explicitly owned by that module.

Once a scientific object has been imported and registered as a GX-TXT Dataset, use Dataset APIs.


7. Table Stream transport: do not assume blocking line I/O

GX-TXT's standard Octave bridges may use popen2, whose output is nonblocking.

A bridge protocol record may arrive in fragments.

Do not write code that assumes:

line = fgetl(out_fid);

always returns one complete response immediately.

Use the framework bridge reader when available:

line = gxtxt_persistent_bridge_readline(out_fid, pid);

This is especially important for large Base64 typed-binary records.

A premature close can create the misleading sequence:

empty bridge response
broken pipe

where the real problem is transport framing rather than scientific calculation.


8. General n-dimensional Datasets

Table Stream is for table-shaped data.

Do not flatten:

just to make them fit Table Stream.

Use Canonical Dataset API v2 / the appropriate domain API directly for n-dimensional data.


Part II — Publishing derived scientific results

9. Publish reusable results as canonical Datasets

If a calculation produces data that another module, plot, worksheet or exporter may need, publish a Dataset.

Examples:

fit.predictions
fit.residuals
curve.replicate.traces
curve.replicate.profiles
statistics.summary

Do not create one private CSV per visual trace only because plotting is easier that way.

Prefer long-form canonical Datasets.

Example:

x, family_value, mean, lower, upper
0.0, 25, ...
0.1, 25, ...
...
0.0, 50, ...

This can drive:


10. Dataset Publication API v2

Use Dataset Publication API v2 for derived results.

A published Dataset should provide:

Do not manually append records to metadata/datasets.ini.

Do not invent a parallel Dataset registry.

Do not rely on current canonical column names as permanent scientific identity.


Part III — Declaring graphs

11. Graph Provider API v1

A module declares stable graph types in module.ini.

Example:

[graphs]
provider_api_version=1
ids=my.analysis.overlay;my.analysis.residuals
default_ids=my.analysis.overlay
rerender=standard_octave

[graph.my.analysis.overlay]
name=Measured and fitted
default_enabled=1
file=01_overlay.png
supports_variants=1
variant_data_model=series
series_editor=standard
supports_legend=1
legend_editor=standard
supports_annotations=1
annotation_editor=standard
supports_region_selection=1
region_selection=rectangle
config_fields=title;x_label;y_label

Graph IDs are types, not dynamic instances.

Correct:

my.analysis.overlay
my.analysis.residuals

Wrong:

my.analysis.overlay.family25
my.analysis.overlay.family50
my.analysis.overlay.family100

Dynamic scientific members should normally be represented as Series, filters or Dataset values.


12. Graph Provider responsibilities

The framework owns:

The module owns:

When generic scientific plotting is sufficient, the module should delegate actual rendering to plot.dataset.


Part IV — Graph Series

13. Graph Series are the persistent scientific binding model

A Graph Series binds:

module_id
graph_id
variant_id
series_id
dataset_id
x_variable_id
y_variable_id
axis
label
enabled
order
filters
family/secondary/order variables

Basic Tcl example:

::GX::graphSeriesSet my.module my.graph measured [dict create \
    dataset_id fit.result \
    x_variable_id v0001 \
    y_variable_id v0002 \
    axis left \
    label "Measured" \
    enabled 1 \
    order 1]

Graph Series identity is persistent.

Do not rebuild scientific identity from display names on every run.


14. Row filters

Use Graph Series filters instead of generating per-member CSV files.

Example:

set f [dict create \
    filter_variable_id v0003 \
    predicate exact_equal \
    value 25]

::GX::graphSeriesSet my.module my.graph family_25 [dict create \
    dataset_id curve.family \
    x_variable_id v0001 \
    y_variable_id v0002 \
    axis left \
    label "25 mA" \
    row_filters [list $f]]

Supported predicates include:

exact_equal
not_equal
less_than
less_equal
greater_than
greater_equal
between_inclusive
in_set
is_finite
is_nonfinite

Filters combine with AND semantics.


15. Family expansion

For ordinary family traces, use the public family expansion helper where appropriate:

set created [::GX::graphSeriesExpandGroups \
    plot.dataset plot.dataset.xy \
    family_dataset \
    v_x v_y v_family \
    [dict create \
        series_prefix spectrum \
        label_template {Spectrum {member}}]]

This creates one ordinary exact-value filtered Graph Series per family member.

Why explicit expansion is sometimes required

Not every representation can be combined with family_variable_id.

A concrete example is an uncertainty band.

This may be rejected:

one Series
  family_variable_id = family
  band = lower/upper

The correct representation is:

Series family_25
  row_filter family == 25
  line + band

Series family_50
  row_filter family == 50
  line + band

General rule:

If a provider capability does not support implicit family grouping for a representation, expand the family into ordinary filtered Series. Do not modify plot.dataset to accept an ambiguous representation.


16. Standard Series editor

Declare:

series_editor=standard

The standard editor owns generic scientific fields:

Do not duplicate these fields in a private GUI.


17. Provider-specific Series presentation

If the module needs additional representation controls, use Series Editor Extension API v1.

Example registration:

::GX::seriesEditorExtensionRegister my.module.series [dict create \
    module_id my.module \
    graph_ids {my.analysis.overlay;my.analysis.residuals} \
    build_proc ::MyModule::seriesBuild \
    load_proc ::MyModule::seriesLoad \
    validate_proc ::MyModule::seriesValidate \
    save_proc ::MyModule::seriesSave \
    close_proc ::MyModule::seriesClose]

The extension may own presentation such as:

kind
color
line_style
line_width
marker
marker_size
marker_fill
opacity

but it must not create a second scientific Series registry.

Scope provider state by at least:

module_id + graph_id + variant_id + series_id

Part V — PlotSpec and plot.dataset

18. Use PLOT_DATASET_API_v3

For new integrations:

::GX::modulePublicApiActivate plot.dataset PLOT_DATASET_API_v3

Use namespace:

::GX::PlotDatasetAPIv3

API v3 is the current facade. Declarative plot structure remains PlotSpec v2.


19. Minimal lifecycle

::GX::modulePublicApiActivate plot.dataset PLOT_DATASET_API_v3

set spec [::GX::PlotDatasetAPIv3::newSpec \
    -graph_id plot.dataset.xy \
    -title "Result"]

# Fill panels and Series using stable Dataset/Variable IDs.

::GX::PlotDatasetAPIv3::validateSpec $spec

set ref [::GX::PlotDatasetAPIv3::createPlotFromSpec $spec variant]

::GX::PlotDatasetAPIv3::render $ref

set a [::GX::PlotDatasetAPIv3::artifacts $ref]
set png [dict get $a graph_artifact]

For persistent producer integration:

set ref [::GX::PlotDatasetAPIv3::upsertPlotFromSpec \
    my.module \
    "my.module:result:overlay" \
    $spec]

upsertPlotFromSpec reuses a persistent provider-keyed Plot Dataset Variant and applies the validated PlotSpec.


20. PlotSpec structure

Conceptually:

PlotSpec
 ├─ schema
 ├─ graph_type
 ├─ graph_id
 ├─ title
 ├─ multi_panel
 ├─ defaults.style
 ├─ panels[]
 │   ├─ id
 │   ├─ title
 │   ├─ axes
 │   ├─ series[]
 │   └─ annotations[]
 └─ output

For ordinary 2D scientific graphs:

graph_type = cartesian_2d
graph_id   = plot.dataset.xy

Use plot.dataset.xy_region only when its single-panel region-specific semantics are required.


21. One-panel example

set spec [::GX::PlotDatasetAPIv3::newSpec \
    -graph_id plot.dataset.xy \
    -title "Measured response"]

set series [dict create \
    id measured \
    dataset_id experiment.result \
    x_variable_id v0001 \
    y_variable_id v0002 \
    axis left \
    kind line_marker \
    label "Measured"]

set panel [dict create \
    id main \
    title "" \
    axes [dict create] \
    annotations {} \
    series [list $series]]

dict set spec panels [list $panel]

::GX::PlotDatasetAPIv3::validateSpec $spec

22. Multi-panel example

set spec [::GX::PlotDatasetAPIv3::newSpec \
    -graph_id plot.dataset.xy \
    -title "Experiment" \
    -multi_panel 1]

set p1 [dict create \
    id electrical \
    title "Electrical" \
    axes [dict create] \
    annotations {} \
    series [list \
        [dict create id voltage \
            dataset_id dA \
            x_variable_id v0001 \
            y_variable_id v0002 \
            axis left]]]

set p2 [dict create \
    id thermal \
    title "Thermal" \
    axes [dict create] \
    annotations {} \
    series [list \
        [dict create id temperature \
            dataset_id dB \
            x_variable_id v0101 \
            y_variable_id v0102 \
            axis left]]]

dict set spec panels [list $p1 $p2]

Panels share the X display domain but may use different source Datasets and different source X variables if scientifically compatible.

Do not implement stacked axes manually.


Part VI — Series representation and style

23. Supported 2D kinds

Common PlotSpec kinds include:

auto
line
scatter
line_marker
step
errorbar
band
bar
histogram
box_whisker

Use representation semantics rather than drawing primitives manually.


24. Style inheritance

Style precedence is:

framework/global defaults
        ↓
plot.dataset graph defaults
        ↓
PlotSpec defaults.style
        ↓
series.style

Common style fields:

color
line_style
line_width
marker
marker_size
marker_fill
opacity

Critical distinction

These are not equivalent:

field absent
inherit
auto
concrete value

Meaning:

Important rule for module authors

If Graphics should control line width, do not write:

dict set series style line_width 1.5

as a default.

That concrete value overrides Graphics inheritance.

Correct:

# no line_width key at all

or, if a field must be serialized:

line_width = inherit

Only write a concrete per-Series line width when the user explicitly requested a Series override.

This rule applies equally to markers, color, opacity and other style fields.


Part VII — Uncertainty and errors

25. Error bars use Variable bindings

Do not embed error vectors in configuration.

Symmetric:

dict set series y_error [dict create \
    mode symmetric \
    variable_id v0004]

Asymmetric:

dict set series y_error [dict create \
    mode asymmetric \
    lower_variable_id v0005 \
    upper_variable_id v0006]

The error variables must refer to the same Dataset rows as X/Y.


26. Uncertainty bands use absolute lower/upper variables

dict set series band [dict create \
    lower_variable_id v0005 \
    upper_variable_id v0006 \
    draw_center 1]

The band uses absolute endpoints, not ± magnitudes.

Example scientific Dataset:

x
mean
lower_95
upper_95

Then:

Y = mean
band.lower = lower_95
band.upper = upper_95

No interpolation should be introduced merely for plotting.


27. Family + band: expand first

Do not assume this is legal:

dict create \
    family_variable_id family \
    band $band

For representations unsupported with implicit family grouping, expand into explicit filtered Series.

This was an important real-world failure mode:

plot.dataset API: family grouping does not accept band

The correct response is to change the producer PlotSpec, not to patch the renderer.


Part VIII — Graphics inheritance

28. Graphics inheritance hierarchy

Graphics precedence begins with:

global graphics
    ↓
module graphics override
        ↓
plot / variant-specific presentation

Global graphics owns:

Module graphics may override only selected fields.

Blank/inherit falls through.


29. Runtime effective graphics

Resolved values are transported to the renderer and recorded under:

[graphics_effective]

Typical process environment also contains values such as:

GXTXT_PLOT_WIDTH_PX
GXTXT_PLOT_HEIGHT_PX
GXTXT_PNG_DPI
GXTXT_PLOT_FONT_SIZE
GXTXT_PLOT_LINE_WIDTH
...

A module should consume the resolved state, not invent a competing default.


Part IX — Axes, grid and Plot layout

30. CORE Axes state is authoritative

For current GX-TXT, common Axes state is framework-owned.

Use the shared:

Axes / grid / 3D / color

dialog.

Do not create duplicate graph config fields for:

x_min
x_max
y_min
y_max
y2_min
y2_max
tick step
tick format
grid
display unit

unless the value is scientifically different from the generic axis concept.


31. PlotSpec axis fields are compatibility/API input, not a second final authority

PlotSpec may contain:

label
display_unit
scale
min
max

But overlapping saved CORE Axes state is authoritative for display unit and manual bounds for that graph/variant.

This means a plugin should not repeatedly force PlotSpec ranges on every run after the user has edited the shared Axes dialog.


32. Plot layout is per graph

GX-TXT Plot layout includes:

layout_auto
layout_margin_left
layout_margin_right
layout_margin_top
layout_margin_bottom
layout_gap
layout_ylabel_dist

These values are part of the common per-graph axis/layout state.

A delegated renderer must receive the state for the graph being rendered.

A common integration bug is:

user edits layout for producer graph A
renderer actually uses backend graph B
backend B never receives A's layout state

Result: sliders move in the GUI but do nothing to the rendered graph.

If a producer-owned graph delegates to a backend plot, explicitly project the producer's layout/axis state onto the backend graph key before render.


33. Multi-panel layout

Do not create manual subplot() geometry for normal GX-TXT scientific panels.

Use PlotSpec panels and shared layout state.

The framework owns:


Part X — Legend

34. Use the standard legend service

Descriptor example:

supports_legend=1
legend_editor=standard
legend_placements=inside;above;below;left;right;manual
legend_layouts=row;stacked;wrapped

The shared editor owns presentation:

placement
location
alignment
layout
columns
font
background
border
external margin
manual X/Y

Do not call Octave legend() directly in a module-private renderer when the graph declares standard legend support.


35. Legend content versus legend presentation

These are different.

The module may define scientific content such as:

legend_mode=family
legend_mode=source_replicate
legend_mode=none

The framework defines presentation such as:

below + row
right + stacked
inside + manual position

Do not mix them into one setting.


Part XI — Level Scale

36. Level Scale is a presentation transform

Level Scale API v1 supports:

linear
dB
dBFS
FS

It does not rewrite Dataset values.

Example:

::GX::levelScaleSet graph_id y [dict create \
    scale dB \
    quantity_kind amplitude \
    amplitude_basis peak \
    reference_value 1 \
    reference_unit V \
    zero_policy missing]

The transform occurs after unit conversion.

Do not pre-convert canonical Dataset data to dB merely because the user wants a dB display.


37. Selection and nonlinear Level Scale

A level-transformed Y axis is not generally affine-invertible to signed canonical data.

GX-TXT metadata records the transform.

X-only selection remains valid.

Y rectangle conversion may be refused rather than returning an ambiguous inverse.

A plugin should not override that safety behavior.


Part XII — Annotations

38. Interactive annotations

Descriptor:

supports_annotations=1
annotation_editor=standard
annotation_types=text;line;arrow;rectangle;ellipse
annotation_coordinate_spaces=figure_norm

Interactive annotations are framework presentation state.

The plugin should not maintain a private annotation store for the same purpose.


39. PlotSpec reference annotations

PlotSpec also supports scientific reference annotations attached to a panel, such as:

dict set panel annotations [list \
    [dict create \
        type reference_line \
        axis x \
        value 3.0 \
        label "Lamp on" \
        line_style dashed]]

These are distinct from interactive figure annotations.

Use them when the annotation is a declarative part of the plotted scientific result.


Part XIII — Region Selection / Select Area

40. Descriptor opt-in

For a selectable graph:

supports_region_selection=1
region_selection=rectangle

or:

region_selection=x_range

No custom selection widget is required.


41. Framework ownership

The framework owns:

The plugin receives:

[plot.region.my.graph]
enabled=1
space=data
axis=primary
x_min=...
x_max=...
y_min=...
y_max=...

Octave:

R = gxtxt_plot_region_from_runtime(runtime_cfg, 'my.graph');

42. .gxtxt_plotmeta is mandatory for correct graphical selection

Plots exported through the standard graphics path have metadata under:

<plot-dir>/.gxtxt_plotmeta/<plot-name>.ini

The sidecar records:

If a module copies a backend PNG to a producer-owned graph artifact, it must preserve the corresponding metadata sidecar as well.

Copying only the PNG can make the image visible while breaking Select Region.

Never synthesize .gxtxt_plotmeta manually.


43. Region Selection limitations

Current rectangle mapping supports normal single-panel 2D geometry.

Do not claim support for a graph geometry that the API cannot map reliably.

The framework should refuse unsupported geometry rather than guess.


Part XIV — Variants

44. Declare variants explicitly

Descriptor:

supports_variants=1
variant_data_model=series

For Graph-Series-based scientific plots, variant_data_model=series is normally the correct choice.

A variant should isolate:


45. Do not use module_cache when the user needs full Series functionality

A cache-only variant model is intentionally more limited.

If the graph should expose:

use the Series data model.


46. Provider-owned state must include variant ID

If an extension stores:

series style
panel membership
provider-specific configuration

scope it by:

module
graph
variant
series

Otherwise p0001 and p0002 can overwrite each other.


Part XV — USE / NON USE and graph scheduling

47. Graph Provider enabled is not the same as USE

GX-TXT keeps these concepts separate.

Do not infer one from another.

The runtime's effective scheduling state should come from Graph Workflow API, not a plugin-owned boolean.


Part XVI — Render and rerender

48. Full render

Use:

::GX::PlotDatasetAPIv3::render $ref

Full render may:


49. Rerender

Use:

::GX::PlotDatasetAPIv3::rerender $ref

Rerender should be used for presentation-only changes when preparation remains valid.

Examples:

title
line style
line width
legend
axes range
display units
layout
annotation
Level Scale

A change to an auxiliary data binding such as a different uncertainty variable may invalidate preparation and require full render.

Do not implement "rerender" by always rerunning the complete scientific analysis.


50. renderCommand for a host-controlled process

A producer that already owns scheduling may request the public command:

set item [::GX::PlotDatasetAPIv3::renderCommand $ref render]

or:

set item [::GX::PlotDatasetAPIv3::renderCommand $ref rerender]

This returns the framework-built queue item.

The producer should not reconstruct:

runtime.ini
Octave command line
private plot.dataset entrypoints
artifact paths

itself.


Part XVII — Two integration patterns

51. Pattern A: plot.dataset owns the visible plot

This is the simplest and preferred pattern when producer ownership is not required.

Flow:

module publishes Dataset
        ↓
module creates/upserts plot.dataset PlotSpec
        ↓
plot.dataset Variant is visible in Graphs
        ↓
all plot.dataset UI/API functions work directly

Advantages:

The Graphs module column will identify plot.dataset.


52. Pattern B: scientific module owns graph identity, plot.dataset renders it

Use this only when the Graphs UI must continue to identify the producing module/graph.

This pattern is more demanding.

upsertPlotFromSpec() does not transfer canonical ownership from plot.dataset to another module. It creates/reuses a plot.dataset Variant keyed to the producer.

Therefore a producer-owned wrapper must synchronize presentation state correctly.

Conceptual flow:

producer graph state
  - Graph Series
  - axes/layout
  - legend
  - Level Scale
  - Graphics
  - region
  - variant
        ↓
build PlotSpec
        ↓
upsert hidden/stable plot.dataset backend Variant
        ↓
project producer presentation state to backend
        ↓
renderCommand(render|rerender)
        ↓
backend artifact
        ↓
producer artifact + matching .gxtxt_plotmeta

Critical rules for this pattern

  1. Producer Graph Series remain the scientific authority.
  2. Do not let the backend Variant become a stale second scientific authority.
  3. Reapply the current PlotSpec when Series identity changes.
  4. Project current axes/layout state before render.
  5. Project legend state before render.
  6. Project Level Scale before render.
  7. Project effective Graphics before render.
  8. Preserve per-variant isolation.
  9. Copy the matching .gxtxt_plotmeta with the PNG if the visible artifact is producer-owned.
  10. Use renderCommand(..., rerender) for rerender.
  11. Do not expose the hidden backend as a duplicate visible plot if the producer artifact is the intended UI object.

This pattern should be encapsulated in a reusable bridge rather than reimplemented differently in every module.


Part XVIII — Graph configuration extensions

53. Graph Configuration Extension API v1

Use this only for provider-specific controls that do not belong in the generic descriptor fields.

Registration:

::GX::graphConfigExtensionRegister my.renderer.graph_config [dict create \
    module_id my.renderer \
    graph_ids {my.renderer.xy} \
    build_proc ::MyRenderer::graphConfigBuild \
    close_proc ::MyRenderer::graphConfigClose]

Build only inside the supplied parent.

Do not inspect private CORE widget paths.


54. Axis Configuration Extension API v1

Use this only for provider-specific axis/panel presentation not already owned by CORE.

Registration:

::GX::axisConfigExtensionRegister my.plot.axes [dict create \
    match_proc ::MyPlot::matchesArtifact \
    build_proc ::MyPlot::buildAxisControls \
    close_proc ::MyPlot::closeAxisControls]

CORE remains authoritative for common:

min/max
tick step
tick format
grid
panel Y/Y2 numeric state
alignment

Do not duplicate those controls in an extension.


Part XIX — Common failure modes

55. Failure: module reads analysis_source.*.path

Symptom

Works for one CSV Session, fails for provider-backed or rematerialized Datasets.

Cause

Storage path is being treated as scientific identity.

Fix

Use:

analysis_source.<id>.dataset_id
Variable IDs
Table Stream API v3

56. Failure: line width from Module Graphics appears ignored

Symptom

GXTXT_PLOT_LINE_WIDTH=12 is visible in the process command, but the graph still renders with a thin fixed line.

Cause

The producer inserted:

series.style.line_width = 1.5

into the PlotSpec.

Per-Series style wins.

Fix

Omit line_width or set it to inherit unless the user explicitly selected a Series override.


57. Failure: Plot layout sliders move but output does not change

Cause

The GUI is editing graph key A but the delegated renderer is using graph key B.

Fix

Project the producer graph's common axis/layout state to the backend graph/variant before generating the backend runtime.


58. Failure: Select Region button exists but selection is wrong or unavailable

Check:

  1. descriptor has supports_region_selection=1;
  2. geometry is supported;
  3. current PNG has a matching .gxtxt_plotmeta/*.ini;
  4. metadata was produced for the current PNG;
  5. copied artifacts preserved the sidecar;
  6. display unit / Level Scale state is current.

Never guess axes geometry from margins.


59. Failure: family grouping does not accept band

Cause

A grouped family Series is carrying a representation unsupported by implicit grouping.

Fix

Expand into ordinary exact-value filtered Series, one per family.


60. Failure: old Series remain after rerun

Cause

Producer reused a backend Variant but did not apply the new PlotSpec/Graph Series state.

Fix

Use:

upsertPlotFromSpec

or explicitly applySpec on the existing public plot reference.

Do not assume a historical ensure operation updates an existing template automatically.


61. Failure: empty bridge response + broken pipe

Cause

A nonblocking popen2 pipe was read as if it were blocking/record-oriented.

Fix

Use the GX-TXT persistent bridge record reader.

Also make Tcl bridge failures visible on stderr so a transport error does not hide the original exception.


62. Failure: graph is visible but editing tools act on the wrong module

Cause

The rendered artifact is physically owned by a plot.dataset Variant while the producer assumes ownership was transferred.

Fix

Choose one integration model deliberately:

Do not mix the two models implicitly.


Part XX — Anti-patterns

63. Do not write a private renderer for standard scientific plots

Avoid production module code such as:

figure(...)
plot(...)
scatter(...)
bar(...)
errorbar(...)
legend(...)
xlim(...)
ylim(...)
subplot(...)
print(...)

when the same graph can be represented using public Plot Dataset APIs.

This is not just a code-style issue. A private renderer will usually miss some of:


64. Do not manually edit framework persistence

Do not manually patch:

metadata/manifest.ini
metadata/datasets.ini
Graph Series numbered sections
plot_variant sections
runtime.ini
render_cache.mat

Use the public APIs.


65. Do not create one Dataset per trace unless scientifically necessary

Prefer:

one long-form Dataset
+ family variable
+ filters

over:

trace_001.csv
trace_002.csv
trace_003.csv
...

Long-form representation keeps identity, filtering and reuse coherent.


66. Do not use names as stable bindings

Wrong:

Y = column named "Voltage"

Correct:

dataset_id = d001
y_variable_id = v0007

Display names are presentation.


Part XXI — Recommended module workflow

67. Calculation run

Recommended full sequence:

1. Framework writes module runtime.
2. Module resolves Analysis Sources by Dataset ID.
3. Module resolves stable Variable IDs.
4. Module opens Table Stream API v3.
5. Module performs scientific calculation.
6. Module publishes derived Datasets through Dataset Publication API.
7. Module creates/updates Graph Series on those Datasets.
8. Module builds current PlotSpec.
9. Module validates PlotSpec.
10. Module upserts/updates the plot.dataset backend.
11. Module projects common presentation state if using producer-owned graph identity.
12. Module invokes public render.
13. Module preserves standard metadata sidecars.
14. Framework verifies output contract.

68. Rerender run

1. Framework regenerates current presentation runtime.
2. Module does not redo scientific calculation.
3. Module loads current cache / backend plot reference.
4. Module reapplies current presentation state.
5. Module invokes public rerender.
6. New PNG and metadata replace the old artifact atomically.

Part XXII — Minimal descriptor template

[graphs]
provider_api_version=1
ids=my.analysis.result
default_ids=my.analysis.result
rerender=standard_octave

[graph.my.analysis.result]
name=Analysis result
default_enabled=1
file=01_result.png

supports_variants=1
variant_data_model=series
series_editor=standard

supports_legend=1
legend_editor=standard
legend_placements=inside;above;below;left;right;manual
legend_layouts=row;stacked;wrapped

supports_annotations=1
annotation_editor=standard
annotation_types=text;line;arrow;rectangle;ellipse
annotation_coordinate_spaces=figure_norm

supports_region_selection=1
region_selection=rectangle

config_fields=title;x_label;y_label

Do not add generic style/range/layout controls here unless they have scientific semantics not already represented by the common APIs.


Part XXIII — Minimal Tcl plotting service example

70. Create/update one persistent plot.dataset Variant

::GX::modulePublicApiActivate plot.dataset PLOT_DATASET_API_v3

set spec [::GX::PlotDatasetAPIv3::newSpec \
    -graph_id plot.dataset.xy \
    -title "Analysis result"]

set s1 [dict create \
    id result \
    dataset_id analysis.result \
    x_variable_id v0001 \
    y_variable_id v0002 \
    axis left \
    kind line \
    label "Result"]

set panel [dict create \
    id main \
    axes [dict create] \
    annotations {} \
    series [list $s1]]

dict set spec panels [list $panel]

::GX::PlotDatasetAPIv3::validateSpec $spec

set ref [::GX::PlotDatasetAPIv3::upsertPlotFromSpec \
    my.analysis \
    "my.analysis:result" \
    $spec]

set renderResult [::GX::PlotDatasetAPIv3::render $ref]

set artifacts [::GX::PlotDatasetAPIv3::artifacts $ref]
set png [dict get $artifacts graph_artifact]

Part XXIV — Validation checklist

71. Data access

Before release, verify:


72. Graph identity


73. Rendering


74. Graphics and style


75. Axes and layout


76. Legend and annotations


77. Region selection


78. Cache and persistence


Part XXV — Debugging method

79. Diagnose by authority layer

When a graph ignores a setting, do not immediately modify the renderer.

Determine which layer owns the setting.

Example: line width.

Check in order:

1. Global Graphics
2. Module Graphics
3. Variant Graphics
4. PlotSpec defaults.style
5. Series style

The last concrete value wins.

Example: Y range.

Check:

1. CORE Axes state
2. PlotSpec compatibility fallback
3. renderer receives final state

Example: Select Region.

Check:

1. descriptor capability
2. visible artifact identity
3. sidecar metadata
4. pixel calibration
5. data/display-unit mapping

80. Inspect runtime before changing code

Useful evidence includes:

[graphics_effective]
[graph_provider]
[graph_series]
[axis.<graph-id>]
[level_scale.<graph-id>.<axis>]
[plot.region.<graph-id>]

The process command also exposes resolved graphics environment variables.

If the correct value is already in runtime but not in the image, investigate downstream precedence.

If the value is absent from runtime, investigate framework state projection or descriptor/API integration.


81. Validate PlotSpec before render

Always call:

::GX::PlotDatasetAPIv3::validateSpec $spec

before blaming the renderer.

Validation is expected to reject unsupported combinations.

An explicit validation error is preferable to a visually plausible but scientifically ambiguous graph.


Part XXVI — Framework source references

For GX-TXT 0.9.8.10-dev-031, the primary reference documents are:

docs/api/SCIENTIFIC_PLOT_FRAMEWORK_API_v1.md
docs/api/PLOT_DATASET_API_v1.md
docs/api/PLOT_DATASET_API_v2.md
docs/api/PLOT_DATASET_API_v3.md
docs/api/GRAPH_PROVIDER_API_v1.md
docs/api/GRAPH_WORKFLOW_API_v1.md
docs/api/GRAPH_SERIES_API_v1.md
docs/api/PLOT_MODULE_API_v1.md
docs/api/GRAPHICS_INHERITANCE_API_v1.md
docs/api/SERIES_EDITOR_EXTENSION_API_v1.md
docs/api/GRAPH_CONFIG_EXTENSION_API_v1.md
docs/api/AXIS_CONFIG_EXTENSION_API_v1.md
docs/api/LEVEL_SCALE_API_v1.md
docs/api/PLOT_ANNOTATION_API_v1.md
docs/api/PLOT_REGION_SELECTION_API_v1.md
docs/api/TABLE_STREAM_API_v3.md
docs/api/DATASET_VARIABLE_SCHEMA_API_v3.md
docs/api/DATASET_PUBLICATION_API_v2.md
docs/api/ANALYSIS_SOURCE_MAP_API_v1.md
docs/api/ANALYSIS_SOURCE_ACCESS_RUNTIME_v2.md

Relevant implementation surfaces include:

core/tcl/graph_series_api_v1.tcl
core/tcl/graph_workflow_api_v1.tcl
core/tcl/plot_layout_controls_v1.tcl
core/tcl/plot_region_selection_api_v1.tcl
core/tcl/scientific_plot_framework_api_v1.tcl
core/tcl/series_editor_extension_api_v1.tcl
core/tcl/level_scale_api_v1.tcl

plugins/modules/plot.dataset/api/plot_dataset_api_v1.tcl
plugins/modules/plot.dataset/api/plot_dataset_api_v2.tcl
plugins/modules/plot.dataset/api/plot_dataset_api_v3.tcl

Part XXVII — Final rules

A correct GX-TXT scientific plotting module should follow these rules:

  1. Use Dataset IDs and stable Variable IDs.
  2. Read registered table Datasets through Table Stream API v3.
  3. Publish reusable derived results through Dataset Publication API.
  4. Represent scientific curves through Graph Series.
  5. Represent generic scientific plots through PlotSpec and PLOT_DATASET_API_v3.
  6. Do not create a private renderer for functionality already provided by plot.dataset.
  7. Let Graphics inheritance own generic visual defaults.
  8. Do not override Graphics with hard-coded Series styles unless the user explicitly requested an override.
  9. Let CORE Axes/Grid/Layout own generic axes and geometry.
  10. Use the standard Legend, Annotation, Level Scale and Region Selection APIs.
  11. Preserve .gxtxt_plotmeta whenever the visible graph artifact is copied or proxied.
  12. Use Series variants when the user needs full Dataset/X/Y/Series editing.
  13. Keep render and rerender separate.
  14. Validate PlotSpec and accept explicit API failures instead of introducing silent renderer fallbacks.
  15. When a required representation is missing from public APIs, extend the central public plotting API rather than creating a module-private implementation.

Following these rules makes a graph participate in the entire GX-TXT plotting environment rather than merely producing a PNG.