Skip to main content

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:

ModeStored recordsUse it when
embedded (default)Drawings are inside ChartLayout; no separate drawing recordDrawings belong to one saved workspace
separateChart layouts omit drawings; one DrawingState exists per sharing bucketDrawings need independent sharing, permissions, revisions, or live updates
bothThe SDK writes both the chart layout and separate drawing record; the separate record wins during restoreMigrating 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 modeExact bucket dimensions
not-shareduserId : workspaceId : not-shared : layoutId : chartId : symbol : interval
shared-in-layoutuserId : workspaceId : shared-in-layout : layoutId
globally-shareduserId : 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:

separateDrawings.ts
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 savedAt Unix-millisecond timestamp.
  • A later load returns the same DrawingState and revision.
  • A null load 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:

  1. A load returns DrawingState.revision.
  2. The controller remembers it and sends it as baseRevision on the next save unless the caller supplies one explicitly.
  3. The backend compares that token with the stored revision.
  4. A CAS-capable save returns a new { revision, savedAt }.
  5. A stale save returns 409 with the remote revision and state. The REST adapter maps it to DrawingRevisionConflictError.

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

OutcomeResult
Bucket is missingloadDrawings() resolves null; live drawings stay unchanged
Revision is staleDrawingRevisionConflictError; controller retains conflict details and both sides
Scope is read-onlyDrawingPermissionError; controller enters read-only state
Adapter lacks load or saveThe controller throws storage.unavailable for that manual operation
Custom subscription failsDrawing-sharing state becomes error; existing live drawings remain available

Next steps