BravenChartWorkbench gives one mounted BravenChartPlus native Chart, Data,
Split, and generated Dart Source presentations plus a safe extension point for
host actions. It owns the generic presentation and extraction lifecycle; your
application still owns the chart configuration, persistence, permissions,
navigation, and action policy.
If the screen only needs an interactive chart, use BravenChartPlus directly.
Use Workbench when one chart must support several linked user tasks:
- interact with the visual chart;
- inspect the exact effective values;
- compare chart and data side by side;
- copy generated Dart for the effective configuration; or
- return the current chart to a host workflow such as Add to report.
Workbench does not save charts, upload data, or become a report builder. Its host action handle returns a portable artifact; the application decides what that action means and owns every side effect.
The mental model
Pick the smallest surface that matches the user's task:
| User need | Use | What it owns |
|---|---|---|
| Interact with one chart | BravenChartPlus |
Rendering and chart interaction |
| View that same chart as Chart, Data, Split, or Source | BravenChartWorkbench |
Linked presentation and safe host-action hooks |
| Save, transmit, or restore the effective chart | ChartArtifact |
Validated portable data, optional view state, and optional preview |
| Align and calculate deltas across saved charts | ChartComparisonBuilder |
Explicit cross-document comparison |
The Workbench is therefore a wrapper around one mounted chart, not another chart type and not a persistence service:
your BravenChartPlus
│
▼
BravenChartWorkbench
Chart · Data · Split · Source
│
│ host action calls extractArtifact()
▼
your application
save · attach · share · compare · discard
Until the host handles the successful artifact result, nothing has been saved, uploaded, authorized, or retained.
Use the public package barrel:
import 'package:braven_charts/braven_charts.dart';
What the showcase proves
The live Workbench showcase uses seeded, varied chart families to demonstrate four separate contracts:
- Linked presentation: Chart, Data, Split, and Source are four views of one mounted chart. A selected table row highlights the same durable chart points; switching views does not create another chart.
- Host boundary: Add to report calls
extractArtifact(). The result contains the effective document, requested durable view state, diagnostics, and optional PNG preview. The demo displays that result; a real application decides whether to store, upload, attach, or discard it. - Independent restoration: the demo derives three portable documents (current, plan +5%, and plan -8%), hydrates each with its own controller, and compares their source values explicitly. Hiding a series in one restored chart does not mutate the others.
- Controlled freshness: a bounded live chart continues changing while its Data view remains a deliberate snapshot until the user or host refreshes it.
The comparison section is not an automatic history service built into the Workbench. It is a visible proof that artifacts can be restored into independent runtimes and passed to the separate comparison API.
The companion artifact showcase focuses on capture, library inspection, deterministic JSON, and restoration.
What a host action receives
actionsBuilder, the chart context menu, and a chart overlay button can expose
the same host-owned command. The command receives a stable
ChartWorkbenchHandle; calling extractArtifact() returns one of:
ChartArtifactSuccess<ChartArtifact>with the portable result and warnings;ChartArtifactFailure<ChartArtifact>with a structured error and warnings.
The successful ChartArtifact is data, not a callback and not a saved record.
It is safe to encode with ChartArtifactJsonCodec, hand to application
storage, attach to a report model, or hydrate into a new chart. The package
does not choose that destination.
Minimal workbench
Give the workbench a chart builder. Always attach the
BravenChartController supplied to that builder:
BravenChartWorkbench(
initialDisplayMode: ChartDisplayMode.split,
chartBuilder: (context, chartController) {
return BravenChartPlus(
bravenChartController: chartController,
series: series,
);
},
)
The supplied controller is the connection between the mounted chart, the data table, and artifact extraction. Do not create another controller inside the builder.
The workbench keeps the chart subtree mounted in every mode:
ChartDisplayMode.chartshows the interactive chart;ChartDisplayMode.datashows a table derived from the chart's effective document while the chart remains mounted underneath; andChartDisplayMode.splitpresents both surfaces when enough width is available; andChartDisplayMode.sourceshows deterministic Dart generated from the same effective document used by Data and portable artifacts.
Source is opt-in so adding the current package version does not change an existing Workbench control:
BravenChartWorkbench(
availableDisplayModes: const {
ChartDisplayMode.chart,
ChartDisplayMode.data,
ChartDisplayMode.split,
ChartDisplayMode.source,
},
chartBuilder: (context, controller) => BravenChartPlus(
bravenChartController: controller,
series: series,
),
)
The table is never reconstructed from pixels or widget inputs. It comes from
BravenChartController.extractDocument(), so controller changes, resolved
series state, annotations, and requested durable view state follow the same
artifact extraction boundary.
Shared presentation scope
Use ChartWorkbenchGroupController when multiple Workbenches should present
one consistent Chart, Data, Split, or Source preference. ChartWorkbenchScope
applies that controller to every Workbench in its subtree:
class AnalysisScreenState extends State<AnalysisScreen> {
final presentation = ChartWorkbenchGroupController(
initialDisplayMode: ChartDisplayMode.chart,
);
@override
void dispose() {
presentation.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ChartWorkbenchScope(
controller: presentation,
child: const AnalysisCharts(),
);
}
}
Selecting a mode in any attached Workbench—or calling
setDisplayMode() on one of its ChartWorkbenchControllers—updates every
Workbench in the scope. The shared controller also owns selector visibility:
presentation.setDisplayMode(ChartDisplayMode.split);
presentation.setShowModeSwitcher(false);
showModeSwitcher: false on an individual Workbench remains a local capability
gate: a group may hide or reveal eligible selectors, but cannot force a locally
disabled selector to appear. Host actions remain visible when the selector is
hidden.
The group's availableDisplayModes is the intersection supported by every
mounted member. If a newly mounted chart cannot support the current preference,
the group reconciles all members to Chart when available, otherwise to their
first common mode. Requests outside the intersection return a structured
failure without changing the group.
Scopes are nestable. Put one controller above the application shell for a
system-wide preference, or insert a nearer scope around a Line, Bar, or radial
chart subtree for a chart-family preference. An explicit groupController on
BravenChartWorkbench takes precedence over the nearest inherited scope.
Split ratio, compact Split pane, table/source freshness, focus, and chart
interaction state remain local to each Workbench.
Responsive Split behavior
splitBreakpoint controls when a horizontal Split presentation has enough
space. Below the breakpoint, the user's requested mode remains split, but the
effective presentation becomes one compact pane with a Chart pane / Data pane
switch. When width returns, Split resumes automatically.
This distinction is public:
final requested = workbenchController.requestedMode;
final visibleNow = workbenchController.effectiveMode;
Use requestedMode for user preference and effectiveMode for telemetry or UI
that describes the current layout. Do not overwrite a saved Split preference
just because the current viewport is compact.
For a vertical split, set splitAxis: Axis.vertical. splitRatio is the chart
share of the available content dimension and must be greater than zero and less
than one.
Horizontal Split mode can auto-fit the table to its native column footprint,
capped by maximumAutoTablePaneExtent. The chart retains
minimumChartPaneExtent; the table retains minimumTablePaneExtent and uses
its native horizontal scrollbar if all columns cannot fit.
The divider is resizable by default. Users can drag its 12-pixel pointer strip, focus its 48-pixel semantic lane and use arrow keys, or press Escape/double- click to return to the configured automatic sizing. Configure this behavior with:
BravenChartWorkbench(
autoFitTablePane: true,
isSplitResizable: true,
minimumChartPaneExtent: 320,
minimumTablePaneExtent: 360,
maximumAutoTablePaneExtent: 640,
splitGap: 16,
onSplitRatioChanged: (chartShare) {
// Optionally retain the user's effective chart/table preference.
},
chartBuilder: (context, controller) => BravenChartPlus(
bravenChartController: controller,
series: series,
),
)
Set autoFitTablePane: true to enable content-aware sizing; otherwise
splitRatio defines the initial layout. Set isSplitResizable: false for a
deliberately fixed product surface.
Add host-defined actions
actionsBuilder receives a stable ChartWorkbenchHandle. The package manages
duplicate extraction state and structured failures; the host chooses the label,
artifact identity, metadata, and persistence destination.
BravenChartWorkbench(
chartBuilder: (context, controller) => BravenChartPlus(
bravenChartController: controller,
series: series,
),
actionsBuilder: (context, handle) => [
FilledButton.icon(
onPressed: handle.isExtractingArtifact
? null
: () async {
final captured = await handle.extractArtifact(
ChartArtifactExtractOptions(
artifactId: 'report-chart-42',
createdAt: DateTime.now().toUtc(),
includePreview: true,
provenance: ChartArtifactProvenance(
values: JsonObjectValue(const {
'destination': JsonStringValue('quarterly-report'),
}),
),
),
);
switch (captured) {
case ChartArtifactSuccess<ChartArtifact>():
final encoded = ChartArtifactJsonCodec.encode(
captured.value,
);
// Persist the successful encoded value in host storage.
case ChartArtifactFailure<ChartArtifact>():
// Present captured.error.code and captured.error.message.
}
},
icon: const Icon(Icons.bookmark_add_outlined),
label: const Text('Add to report'),
),
],
)
An action can be save, attach, share, add to a report, or add to comparisons.
braven_charts deliberately does not import a database, file picker, Firebase,
or a domain repository.
Preview capture works from Chart, Data, or Split because the chart remains
mounted and paintable. The preview is a convenience for thumbnails and loading
states; the portable ChartDocument remains the source used for restoration.
Put the same host action on the chart
Host actions can be exposed independently in three places: the Workbench action
row, the chart's native context menu, and a compact button over the chart. The
context menu and chart button are hidden unless their builders are supplied.
On a Workbench every builder receives the same stable
ChartWorkbenchHandle:
BravenChartWorkbench(
chartBuilder: (context, controller) => BravenChartPlus(
bravenChartController: controller,
series: series,
contextMenuConfig: const ChartContextMenuConfig(
enableLongPress: true,
),
),
contextActionsBuilder: (context, handle, invocation) => [
ChartContextAction(
id: 'host.addToReport',
label: 'Add to report',
icon: Icons.bookmark_add_outlined,
enabled: !handle.isExtractingArtifact,
onSelected: () async {
final result = await handle.extractArtifact(
const ChartArtifactExtractOptions(
artifactId: 'report-chart-42',
includePreview: true,
),
);
// The host decides how to present or persist result.
},
),
],
chartActionButtonBuilder: (context, handle) => ChartOverlayAction(
id: 'host.addToReport',
tooltip: 'Add chart to report',
semanticLabel: 'Add the current chart to the report',
icon: Icons.bookmark_add_outlined,
enabled: !handle.isExtractingArtifact,
onPressed: () async {
await handle.extractArtifact(
const ChartArtifactExtractOptions(
artifactId: 'report-chart-42',
includePreview: true,
),
);
},
),
chartActionButtonConfig: const ChartOverlayActionButtonConfig(
alignment: Alignment.topLeft,
margin: EdgeInsets.all(8),
iconSize: 18,
),
)
chartActionButtonBuilder returning null hides the button for the current
state. ChartOverlayActionButtonConfig controls alignment, margin, target size,
icon size, and optional ButtonStyle. The default uses the inherited
ColorScheme with a translucent, zero-elevation surface that becomes clearer
on hover, focus, and press. Its 48 logical-pixel target is deliberately larger
than the icon for touch and keyboard accessibility. This is a host action
surface, not portable chart configuration, so callbacks are not serialized
into a ChartDocument or generated Dart source.
Override only the Material properties your product owns:
chartActionButtonConfig: ChartOverlayActionButtonConfig(
style: IconButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
backgroundColor: Theme.of(context)
.colorScheme
.primaryContainer
.withValues(alpha: 0.72),
),
),
Developers can also render a completely external button through
actionsBuilder or their own application layout when an overlay does not suit
the product.
The builder works whether or not the chart has an AnnotationController.
Package annotation commands and host commands are composed in deterministic
groups: target editing, host commands, annotation creation, then destructive
commands. Duplicate action IDs keep the first registered action.
ChartContextInvocation contains only stable public information:
sourcedistinguishes secondary click, keyboard, and long press;localPositionandglobalPositionlocate the invocation;hitidentifies a background, series, data point, or annotation without exposing private render elements; andcapabilitiesdescribes whether annotations and a resolved data hit are available.
Mouse and trackpad secondary click are always supported. The Context Menu key
and Shift+F10 open the same menu at the focused or selected datum, falling back
to the plot center. Touch/stylus long press is deliberately opt-in through
ChartContextMenuConfig.enableLongPress so existing pan and zoom gestures do
not change silently. Menu rows provide 48 logical-pixel targets, keyboard
navigation, visible focus, theme-derived colours, assistive semantics, and
viewport clamping.
For a chart outside a Workbench, set
BravenChartPlus.contextActionsBuilder and/or
BravenChartPlus.chartActionButtonBuilder directly. Use these lower-level
forms when the action does not need artifact or Workbench state:
BravenChartPlus(
series: series,
contextActionsBuilder: (context, invocation) => [
ChartContextAction(
id: 'host.inspectPoint',
label: 'Inspect point',
enabled: invocation.hit.kind == ChartContextHitKind.point,
onSelected: () => inspect(invocation.hit),
),
],
chartActionButtonBuilder: (context) => ChartOverlayAction(
id: 'host.saveChart',
tooltip: 'Save chart',
icon: Icons.save_outlined,
onPressed: saveCurrentChart,
),
)
The chart releases its interaction coordinator before awaiting a selected action, reports builder/callback failures through Flutter's error pipeline, and restores chart focus when the menu closes. Actions should still perform their own permission and lifecycle checks because the host owns action policy.
Control the workbench
Provide a ChartWorkbenchController when another part of your widget needs to
change modes, refresh the table, inspect status, or extract an artifact:
final workbenchController = ChartWorkbenchController();
@override
void dispose() {
workbenchController.dispose();
super.dispose();
}
// Later, while the workbench is mounted:
final modeResult = workbenchController.setDisplayMode(ChartDisplayMode.data);
final tableResult = await workbenchController.refreshTable();
final sourceResult = await workbenchController.refreshSource();
setDisplayMode returns ChartArtifactFailure with
requested_display_mode_unavailable when the host excluded that mode through
availableDisplayModes. It does not throw for a normal unavailable-mode
request.
The handle and controller expose:
| Member | Meaning |
|---|---|
chartController |
Controller attached to the currently mounted chart |
requestedMode |
User-selected Chart, Data, Split, or Source preference |
effectiveMode |
Presentation actually visible at the current width |
tableSnapshot |
Immutable effective document used for the current table |
tableModel |
Current projected table, when ready |
tableIsStale |
Whether the chart revision moved past the table snapshot, including after a failed refresh |
tableState |
Table phase, model, warnings, and structured error |
artifactState |
Independent artifact-extraction phase, result, warnings, and error |
sourceState |
Independent source phase, snapshot, generated Dart, warnings, and error |
generatedSource |
Most recent usable ChartGeneratedSource, when ready |
sourceIsStale |
Whether the chart revision moved past the generated source |
refreshTable() |
Coalesced document extraction and table projection |
refreshSource() |
Coalesced effective-document extraction and Dart generation |
extractArtifact() |
Atomic document and optional preview extraction |
Table extraction and artifact extraction have separate state. A table refresh does not erase an artifact result, and an artifact failure does not replace a usable table.
Generated Dart Source
Source is generated from the chart's effective mounted document—not from pixels and not by replaying the host widget builder. It therefore reflects resolved series, axes, annotations, built-in or resolved custom themes, interaction options, and other portable configuration captured at that revision. Canvas legends retain their series, labelled trends, style, hidden state, and custom position.
BravenChartWorkbench(
availableDisplayModes: const {
ChartDisplayMode.chart,
ChartDisplayMode.data,
ChartDisplayMode.split,
ChartDisplayMode.source,
},
sourceOptions: const ChartDartSourceOptions(
includeImports: true,
includeViewState: false,
maxInlinePoints: 250,
variableName: 'chart',
),
chartBuilder: (context, controller) => BravenChartPlus(
bravenChartController: controller,
series: series,
),
)
The built-in Source surface provides selectable highlighted Dart, line
numbers, line wrapping, exact clipboard copy, freshness state, and explicit
warnings. It never silently samples a large dataset. When the configured
maxInlinePoints ceiling is exceeded, all point lists are replaced by clear
application-data placeholders and the result reports the omitted count.
Portable document and artifact extraction remains fail-closed when a runtime callback or formatter has no host descriptor. Source uses a separate capture adapter: it preserves host descriptors, creates stable source-only placeholders for missing runtime values, and reports them in the Source diagnostics. The generated Dart includes the portable configuration and marks the application callbacks that still need to be supplied; Source never weakens artifact portability rules.
Source is a live developer projection by default. While Source is visible,
ChartSourceRefreshPolicy.onDocumentRevision coalesces effective chart changes
onto a bounded 250 ms cadence and regenerates the Dart without exposing normal
revision movement as a manual stale state. Changes made while Source is hidden
are generated when Source next becomes visible. A failed regeneration retains
the previous usable source and exposes an explicit retry.
ChartSourceRefreshPolicy.manual and onModeEntry remain available for hosts
that intentionally want retained source snapshots or only want regeneration
when entering Source. Source and table policies remain independent: Data keeps
its snapshot-oriented onModeEntry default.
Set includeViewState: true when the copied result should also retain the
current viewport, hidden series, durable point selection, axis slots, selected
annotation, and canvas-legend position. The generated Dart then declares a
BravenChartController, attaches it to the chart, and includes a named restore
function with a clear call-after-mount instruction. View state stays off by
default so the usual copied result remains reusable across sessions.
Resolved custom themes are emitted as complete public ChartTheme
configuration. Series-theme markers use SeriesMarkerShape in copied Dart so
they remain distinct from point-annotation MarkerShape.
Table refresh and freshness
The table is a snapshot of effective mounted state, not a live view into mutable series lists.
ChartTableRefreshPolicy.manual captures on the first Data or Split use and
then only when refreshTable() is called. ChartTableRefreshPolicy.onModeEntry
also refreshes whenever Data or Split becomes effective.
ChartTableRefreshPolicy.onDocumentRevision listens to the mounted chart's
opaque effective revision, marks an older table stale immediately, and
coalesces refreshes onto a bounded 250 ms cadence while Data or Split is
visible. It is opt-in; onModeEntry remains the predictable default for large
or continuously streaming charts. The package does not show a stale-warning
banner during this normal automatic-refresh window; extraction progress and
real refresh failures remain visible, and tableIsStale is still observable.
When the host changes chart data while Data or Split is already visible, call
refreshTable() after the updated chart has built:
setState(() => series = nextSeries);
WidgetsBinding.instance.addPostFrameCallback((_) {
workbenchController.refreshTable();
});
Concurrent calls to refreshTable() share the same in-flight future. During a
refresh, a previous usable table remains visible. If the refresh fails, the
previous table remains available, tableIsStale becomes true, and
tableState.error explains the failure.
If the first table extraction fails, the workbench shows Retry table and
keeps the failed state observable. A layout rebuild does not silently retry the
operation. The user can retry explicitly, or the host can call
refreshTable() after correcting the source or runtime binding.
Every ChartDocumentSnapshot carries a ChartDocumentRevision. The mounted
controller exposes the same equality-only value:
final chartRevision =
workbenchController.chartController.effectiveDocumentRevision.value;
final tableRevision = workbenchController.tableSnapshot?.revision;
final isCurrent = chartRevision != null && chartRevision == tableRevision;
The signal changes for effective data, annotation, visibility, selection, and durable viewport changes. Hover, crosshair motion, tooltip animation, and focus painting do not change it. Direct live-stream data mutations are coalesced before publication, and revision-driven table refresh is separately bounded. The token is opaque: compare it only with another token emitted by the same mounted runtime; do not infer ordering or reconstruct it.
For high-rate streams, prefer manual or onModeEntry and refresh at a cadence
appropriate to the product. Do not refresh a large table at paint or sample
frequency. The showcase's bounded-stream example makes this distinction
visible: the chart advances inside a fixed-size buffer while the table retains
its previous row count until Refresh table is chosen.
Link table rows to chart points
Point identity is explicit and revision-bound. ChartPointRef contains a
stable seriesId and zero-based pointIndex, implements value equality, and
round-trips through artifact JSON. ChartTablePointReference remains as a
deprecated type alias for source compatibility; new code should use
ChartPointRef.
A long-form table row represents one point. An exact-X wide row represents a
collection: one reference for every populated series cell at that X value.
ChartDataTable.onRowFocused and onRowActivated therefore receive
List<ChartPointRef> rather than an arbitrary first cell. onRowFocusCleared
fires when keyboard focus leaves the row, and onRowHoverChanged reports a
row collection or null independently for standalone table experiences.
The workbench enables safe row linking by default:
- pointer hover temporarily applies the row's chart focus ring;
- keyboard focus applies a transient chart focus ring;
- pointer exit restores keyboard-driven focus when present;
- focus loss clears the ring when no row remains hovered;
- click or Enter replaces the durable point selection;
- Ctrl/Command-click or Ctrl/Command-Enter additively selects an unselected row and removes every point represented by an already selected row;
- Shift-click or Shift-Enter replaces selection with the contiguous row range from the last unmodified activation, following the current table sort;
- Ctrl/Command+Shift activation additively selects that sorted row range;
- Ctrl/Command+A selects every point in the current sorted table projection, while Escape clears durable selection and keeps row focus in place;
- Home and End move focus to the first or last displayed row, while Page Up and Page Down move by the current virtualized viewport height;
- selected chart points are mirrored into the table with a themed row fill, persistent leading indicator, and selected semantics; and
- the table summary reports the selected point count and exposes a compact Clear selection action that keeps subsequent row references current;
- chart-controller point focus is mirrored into the table with the focused-row treatment and reveals the matching row without taking keyboard focus;
- a newly selected chart point is scrolled into the table viewport once, without taking keyboard focus or overriding later manual scrolling; and
- a wide row focuses or selects every point it represents.
Because durable selection is captured document state, it advances the mounted
document revision. After a successful package-owned row activation, the
workbench immediately refreshes its snapshot so another row remains safe to
select even when ChartTableRefreshPolicy.manual is configured. This targeted
refresh does not make independent data, annotation, visibility, or viewport
changes live; those changes continue to follow the configured refresh policy.
Transient focus is not captured in ChartViewState. Durable selection is
stored in ChartViewState.selectedPointRefs, survives JSON transport and
hydration, and remains independent in each hydrated runtime.
Standalone tables can render the same durable state by passing a set of
ChartPointRef values to ChartDataTable.selectedPointRefs. A long row is
selected when its point is present. A shared-X row is selected only when every
populated series point represented by that row is present. Override the fill
with ChartDataTableTheme.selectedRowColor; the leading indicator and
accessibility semantics remain package-owned.
Supply ChartDataTable.onClearSelection to expose the summary toolbar's Clear
selection action for standalone tables. The table reports point count rather
than row count because a shared-X row can represent several selected points.
Pass ChartDataTable.focusedPointRefs to mirror transient chart focus in a
standalone table. A long row matches its point; a shared-X row matches when any
represented point is focused. This uses the focused-row visual treatment but
does not claim keyboard or accessibility focus for the table row.
ChartDataTable.autoRevealFocusedPoints defaults to true and scrolls only when
the focused point or projected model changes. Set it to false when the host
owns vertical navigation or maps high-frequency chart hover into controller
focus. Package-owned table hover remains stable because its row is already in
the visible viewport.
ChartDataTable.autoRevealSelectedPoints defaults to true. Set it to false
when the host owns vertical table navigation. In a wide table, a partial
series selection still reveals its shared-X row, but the row receives complete
selection styling only when all populated points in that row are selected.
Hosts can drive the same behavior directly, but must supply the revision that issued the references:
final snapshot = workbenchController.tableSnapshot;
if (snapshot != null) {
final result = workbenchController.chartController.selectPoints(
const [
ChartPointRef(seriesId: 'power', pointIndex: 7),
ChartPointRef(seriesId: 'heart-rate', pointIndex: 7),
],
revision: snapshot.revision,
additive: false,
reveal: true,
);
if (result case ChartArtifactFailure<void>()) {
// Refresh for stale_point_reference; fix the source for
// invalid_point_reference.
}
}
focusPoint / focusPoints and selectPoint / selectPoints reject a stale
revision or an unknown series/index before changing state. reveal defaults to
false; when true, the chart pans or expands its X viewport only when needed.
additive is available for selection and defaults to replacement. Use
clearPointFocus() and clearPointSelection() for explicit cleanup.
A valid reference to a hidden series is retained deterministically, including in captured view state, but cannot paint a ring until that series is visible. If chart data changes, refresh the table and use references from the new snapshot instead of retrying an old reference against a new revision.
Set linkTableRowsToChart: false to disable the workbench defaults. Supply
onTableRowFocused, onTableRowFocusCleared, onTableRowHoverChanged, or
onTableRowActivation to replace individual defaults with product-specific
behavior. onTableRowActivation receives modifier-aware
ChartTableRowActivationDetails; Shift ranges arrive as the complete ordered
point collection and take precedence over the legacy
onTableRowActivated callback when both are supplied. Override
onTableSelectAllPoints or onTableSelectionCleared when the host owns those
keyboard selection commands. Use
onPointLinkError to observe the same structured error shown by the workbench.
Configure the data table
Pass ChartTableOptions directly to the workbench:
BravenChartWorkbench(
tableOptions: const ChartTableOptions(
rowLayout: ChartTableRowLayout.wide,
dataScope: ChartTableDataScope.visibleSeries,
viewportOnly: true,
),
documentOptions: ChartDocumentExtractOptions(
includeViewState: true,
yAxisFormatterDescriptors: {
'y': ChartFormatterDescriptor(
id: 'braven.number.fixed',
arguments: {'decimals': JsonNumberValue(2)},
).toDocument(),
},
),
chartBuilder: (context, controller) => BravenChartPlus(
bravenChartController: controller,
series: series,
),
)
Wide layout produces one shared X column plus a value column for each series.
Long layout produces one row per point. ChartDataTable retains its native
sorting, bounded whole-dataset clipboard copy, per-row copy, and raw CSV export
inside the workbench. Sorting changes presentation order only; point references
continue to address the original effective document indices.
Changing tableOptions affects the next projection. Call refreshTable() if a
table is already visible and must immediately use the new options.
Status and failures
Use onStatusChanged when status must leave the workbench subtree:
BravenChartWorkbench(
onStatusChanged: (status) {
analytics.record(
requestedMode: status.requestedMode,
effectiveMode: status.effectiveMode,
tablePhase: status.table.phase,
artifactPhase: status.artifact.phase,
);
},
// ...
)
Expected operational failures use ChartArtifactResult,
ChartArtifactError, and ChartArtifactWarning. Important workbench codes are:
chart_not_attached: the operation ran without a mounted chart;capture_in_progress: a second artifact request arrived while one was active;requested_display_mode_unavailable: the requested mode is host-disabled;table_projection_failed: extraction succeeded but the table could not be projected;stale_point_reference: the supplied ref came from a different effective document revision; andinvalid_point_reference: the series ID or point index does not exist in the current effective document.
Display the code and message where useful for support, but write user-facing
recovery copy for your product. Programming errors such as attaching one
ChartWorkbenchController to two simultaneous workbenches still throw
StateError.
The package-owned failure presentation always pairs the error with a recovery
action: Retry table when no usable table exists, or Retry refresh while
the previous table remains available. Warning, failure, table, and artifact
states remain independently observable through ChartWorkbenchStatus.
Controller ownership
The workbench disposes only controllers it creates. If you supply any of these, you retain ownership and must dispose them:
chartController;workbenchController; andtableController.
One workbench controller can be attached to only one mounted workbench at a time. A caller-owned controller can be detached and reused after the previous workbench unmounts.
Scope boundary
The workbench composes one chart and its data. It does not persist artifacts or align multiple documents. A comparison library can use host actions to capture artifacts now, then hydrate each saved document into an independent workbench. Cross-document series mapping, X alignment, and deltas require an explicit comparison model rather than name-based guessing.
For the transport, validation, hydration, preview, and external-payload contracts behind workbench extraction, continue with Portable Chart Artifacts.
For the complete contract a future chart family must satisfy before Data, Source, artifacts, and the Workbench can support it, see Chart family integration.