Drawing storage
Drawings can travel inside a chart layout or live in revisioned records of their own. Choose the mode before designing backend keys: it determines identity, sharing, conflict handling, and which record wins during restore.
For drawing creation and chart-side snapshot APIs, see Drawing persistence.
Choose a mode
Set features.drawings.persistenceMode:
| Mode | Stored records | Use it when |
|---|---|---|
embedded (default) | Drawings are inside ChartLayout; no separate drawing record | Drawings belong to one saved workspace |
separate | Chart layouts omit drawings; one DrawingState exists per sharing bucket | Drawings need independent sharing, permissions, revisions, or live updates |
both | The SDK writes both the chart layout and separate drawing record; the separate record wins during restore | Migrating between embedded and separate storage |
Separate drawing rows appear in the storage menu for separate and both;
storageToolbar may override only their visibility, not their persistence
behavior.
Sharing bucket identity
One separate record belongs to one exact bucket. The local adapter serializes these dimensions as colon-delimited keys; a backend may use columns instead but must preserve the same identity:
| Sharing mode | Exact bucket dimensions |
|---|---|
not-shared | userId : workspaceId : not-shared : layoutId : chartId : symbol : interval |
shared-in-layout | userId : workspaceId : shared-in-layout : layoutId |
globally-shared | userId : workspaceId : globally-shared : symbol |
When fields are absent, the local adapter uses local, default, layout,
chart, symbol, and interval placeholders. Production integrations should
supply stable user/workspace and instrument identities rather than relying on
those defaults.
The built-in sharing control switches between not-shared and
shared-in-layout. globally-shared is programmatic for products that
intentionally share drawings across every chart layout for the same symbol. In
a multi-chart workspace, drawing sync selects the shared chart layout bucket.
Embedded mode
No drawing-specific methods are required. The chart layout controller captures and restores drawings with the rest of the workspace:
const layouts = widget.chartLayouts();
await layouts.saveAs('drawn-layout', 'Drawn layout');
await layouts.load('drawn-layout');
This is the correct starting point unless drawings need an identity outside the chart layout.
Separate mode
Implement saveDrawings and loadDrawings, enable separate persistence, then
round-trip one bucket:
import {
createTradeScriptSdk,
type MarketDataFeed,
type SdkSymbolInfo,
} from '@tradescript/pro/sdk/core';
import type { ChartStorageAdapter } from '@tradescript/pro/sdk';
declare const deploymentLease: string;
declare const datafeed: MarketDataFeed;
declare const storage: ChartStorageAdapter;
const symbol: SdkSymbolInfo = {
ticker: 'AAPL',
exchange: 'NASDAQ',
type: 'stock',
};
const sdk = await createTradeScriptSdk({ lease: deploymentLease });
const mounted = sdk.chart.mount({
mount: '#chart',
symbol,
interval: '1D',
datafeed,
storage,
features: {
drawings: { persistenceMode: 'separate' },
},
});
const chart = (await mounted.ready()).chart();
const saved = await chart.saveDrawings({
layoutId: 'opening-drive',
sharingMode: 'not-shared',
});
console.log(saved?.revision);
const restored = await chart.loadDrawings({
layoutId: 'opening-drive',
sharingMode: 'not-shared',
});
if (restored) {
console.log(restored.revision, restored.drawings.length);
}
The controller fills missing chart, symbol, and interval context from the live chart. Supply the user and workspace through adapter defaults or request context.
Verify before continuing:
- The save reaches the exact expected bucket. A CAS-capable adapter returns a
revision plus a
savedAtUnix-millisecond timestamp. - A later load returns the same
DrawingStateand revision. - A
nullload means no record exists; it leaves the current live drawings unchanged, including when local drawings are dirty. - A permission, transport, or invalid-payload failure rejects instead of pretending the bucket is empty.
Revisions and conflicts
Separate drawing saves use compare-and-swap:
- A load returns
DrawingState.revision. - The controller remembers it and sends it as
baseRevisionon the next save unless the caller supplies one explicitly. - The backend compares that token with the stored revision.
- A CAS-capable save returns a new
{ revision, savedAt }. - A stale save returns
409with the remote revision and state. The REST adapter maps it toDrawingRevisionConflictError.
The controller keeps deletion tombstones in removed and removedGroups, so a
deleted drawing or group does not reappear on the next merge. An explicit load
replaces local drawings by default; pass applicationMode: 'merge' for a
three-way merge against the last synchronized baseline.
Use force: true only after an explicit Keep mine conflict decision. It is not
a normal retry policy.
Permissions
Implement getDrawingSharingPermissions when access differs by sharing scope.
The REST adapter calls its drawing-permissions endpoint; a custom adapter may
resolve permissions through any transport. If the method is absent, the SDK's
default is read/write for every scope.
Read-only permission disables drawing mutations, save actions, and
drawing-changing undo/redo at the controller and built-in UI boundaries. The
REST adapter maps drawing 401/403 responses to DrawingPermissionError.
Live updates and teardown
createRestChartStorageAdapter does not implement subscribeDrawings. With the
REST path, refresh through explicit loads or chart layout application.
A custom adapter may implement subscribeDrawings(request, callback). It must
return an unsubscribe function, possibly asynchronously. The controller stops
the active subscription when the sharing mode, chart context, symbol, chart
layout, or storage adapter changes, and when the chart is destroyed. A late
async subscription must also clean itself up when its original context is
stale.
Remote updates use the same baseline/merge rules as explicit loads; concurrent edits to the same drawing enter conflict state rather than silently overwriting local work.
Advanced state and partition fields
DrawingState carries the drawing snapshot plus optional layoutId, chartId,
symbol, interval, sharingMode, revision, permissions, tombstones,
updatedAt, and host metadata.
Use ownerSource and seriesSourceId for indicator-owned partitions. Use
requestType on loadDrawings() only when the backend intentionally supports
the migration-compatible allLineTools, mainSeriesLineTools,
lineToolsWithoutSymbol, or indicatorsLineTools partitions.
The complete contracts are in the Storage API reference.
Failure behavior
| Outcome | Result |
|---|---|
| Bucket is missing | loadDrawings() resolves null; live drawings stay unchanged |
| Revision is stale | DrawingRevisionConflictError; controller retains conflict details and both sides |
| Scope is read-only | DrawingPermissionError; controller enters read-only state |
| Adapter lacks load or save | The controller throws storage.unavailable for that manual operation |
| Custom subscription fails | Drawing-sharing state becomes error; existing live drawings remain available |
Next steps
- REST chart storage adapter — implement REST buckets, revisions, permissions, and error responses.
- Backend integration — choose REST refreshes or a custom subscribed adapter.
- Drawing persistence — chart-side snapshots, identity, and round-trip verification.
- Templates — persist reusable drawing presets separately from live drawing state.