Skip to main content

Save drawings separately

Overview

By default, the library stores all drawings and drawing groups within the chart layout. This means that drawings are locked to a specific layout; if a user creates a new layout, they must recreate their drawings from scratch.

The library API offers an alternative approach where drawings can be stored separately from the chart layout.

Key advantages

  • Per-symbol drawings: drawings can be associated with individual symbols. This allows a user to see the same drawings on, for example, "AAPL" regardless of which saved layout they open. This enables reuse and brings flexibility across different layouts or charts.
  • Efficient data size management: separating drawings from the chart layout properties reduces the size of the saved objects, optimizing load times and data storage on your server.

How to enable

To enable saving drawings separately, follow the steps below:

  1. Add saveload_separate_drawings_storage to the enabled_features array in the Widget Constructor.
  2. Implement additional methods for storing drawings separately based on your chosen approach:

How to migrate

If you are moving from the default "combined" storage to separate storage for drawings, you need to migrate your existing data. The library still supports loading chart layouts with drawings even when separate storage is enabled through saveload_separate_drawings_storage.

To migrate a layout:

  1. Track migration status. We recommend storing a flag that signifies in which mode the layout was saved. This flag will help you understand whether the data has already been migrated. Handling the saving and loading of this optional flag falls outside the API's scope and remains a detail to be implemented within your code.
  2. Listen for the chart_load_requested event to occur.
  3. Invoke the saveChartToServer method on the widget to trigger the layout to be saved again.

Migration with low-level API

If you use the low-level API methods, saving the chart layout requires storing the following two pieces of data.

widget.save(
(chartLayoutState) => {
const drawings = widget.activeChart().getLineToolsState();
// Send or save state as required...
// save drawings
// save chartLayoutState
},
{ includeDrawings: false }
);

Save load adapter (API handlers)

If you use custom API handlers for saving and loading, implement two additional methods in the IExternalSaveLoadAdapter object:

Both methods receive additional metadata that allows your adapter to route drawings to the correct storage destination on save and return the correct subset on load. The example below demonstrates a minimal implementation that handles sharing modes and load request types.

Click to reveal the example

class SaveLoadAdapterWithDrawings extends SaveLoadAdapter {
constructor() {
super();
this.drawings = {};
this.drawingGroups = {};
}

async saveLineToolsAndGroups(layoutId, chartId, state, context) {
const isGlobalSave = context?.sharingMode === 'GloballyShared';
const bucketKey = isGlobalSave
? this._getGlobalDrawingKey()
: this._getDrawingKey(layoutId, chartId);

// When saving globally, remove any stale copies previously stored
// under the scoped bucket for the same IDs. Tombstones (null values)
// are also cleaned up so they do not linger on the scoped side.
if (isGlobalSave && layoutId !== undefined) {
const scopedKey = this._getDrawingKey(layoutId, chartId);
this._removeIds(this.drawings[scopedKey], state.sources);
this._removeIds(this.drawingGroups[scopedKey], state.groups);
}

this._applyEntries(this.drawings, bucketKey, state.sources);
this._applyEntries(this.drawingGroups, bucketKey, state.groups);
}

async loadLineToolsAndGroups(layoutId, chartId, requestType, requestContext) {
const scopedKey = layoutId ? this._getDrawingKey(layoutId, chartId) : null;
const globalKey = this._getGlobalDrawingKey();
const sharingMode = requestContext?.sharingMode;

const sources = new Map();
const groups = new Map();

if (sharingMode === 'GloballyShared') {
// Explicit global request: read the global bucket only.
this._mergeBucket(globalKey, sources, groups);
} else if (sharingMode != null) {
// Explicit non-global request: read the scoped bucket only.
if (scopedKey) this._mergeBucket(scopedKey, sources, groups);
} else {
// Backward compatibility: older library versions did not pass
// a sharing mode in the load request. Merge both buckets, but
// keep the global entry if the same ID exists in both.
this._mergeBucket(globalKey, sources, groups);
if (scopedKey) this._mergeBucket(scopedKey, sources, groups, false);
}

// For explicit non-global requests, drop any drawings that also
// exist in the global bucket so stale scoped copies do not appear.
if (sharingMode != null && sharingMode !== 'GloballyShared') {
this._dropGlobalDuplicates(sources);
}

if (sources.size === 0 && groups.size === 0) return null;

const filteredSources = this._filterSourcesByRequestType(sources, requestType, requestContext);
const filteredGroups = this._filterGroupsBySources(groups, filteredSources);

if (filteredSources.size === 0 && filteredGroups.size === 0) return null;
return { sources: filteredSources, groups: filteredGroups };
}

_getDrawingKey(layoutId, chartId) {
return `${layoutId}/${chartId}`;
}

_getGlobalDrawingKey() {
return '__global_drawings__';
}

_applyEntries(store, bucketKey, entries) {
if (!entries) return;
if (!store[bucketKey]) store[bucketKey] = {};
for (const [id, entry] of entries) {
// A `null` value is a tombstone: remove the stored record.
if (entry === null) delete store[bucketKey][id];
else store[bucketKey][id] = entry;
}
}

_removeIds(bucket, entries) {
if (!bucket || !entries) return;
for (const [id] of entries) delete bucket[id];
}

_mergeBucket(bucketKey, sources, groups, overwrite = true) {
const rawSources = this.drawings[bucketKey];
if (rawSources) {
for (const [id, source] of Object.entries(rawSources)) {
if (overwrite || !sources.has(id)) sources.set(id, source);
}
}
const rawGroups = this.drawingGroups[bucketKey];
if (rawGroups) {
for (const [id, group] of Object.entries(rawGroups)) {
if (overwrite || !groups.has(id)) groups.set(id, group);
}
}
}

_dropGlobalDuplicates(sources) {
const globalSources = this.drawings[this._getGlobalDrawingKey()];
if (!globalSources) return;
for (const id of Object.keys(globalSources)) sources.delete(id);
}

_filterSourcesByRequestType(sources, requestType, requestContext) {
if (requestType === 'allLineTools') return sources;

const filtered = new Map();
const symbol = requestContext?.symbol;
const seriesSourceId = requestContext?.seriesSourceId;

for (const [id, source] of sources) {
if (!source) continue;
const hasSymbol = Boolean(source.symbol);
const sourceOwner = source.ownerSource;

if (requestType === 'mainSeriesLineTools') {
// Drawings on the main series for the current symbol.
if (
hasSymbol
&& source.symbol === symbol
&& (!seriesSourceId || sourceOwner === seriesSourceId)
) {
filtered.set(id, source);
}
} else if (requestType === 'lineToolsWithoutSymbol') {
// Drawings on the main series pane not tied to a symbol.
if (!hasSymbol && (!seriesSourceId || sourceOwner === seriesSourceId)) {
filtered.set(id, source);
}
} else if (requestType === 'studiesLineTools') {
// Drawings that belong to an indicator / study pane.
if (!seriesSourceId || sourceOwner !== seriesSourceId) {
filtered.set(id, source);
}
}
}
return filtered;
}

_filterGroupsBySources(groups, filteredSources) {
if (groups.size === 0) return groups;

const usedGroupIds = new Set();
for (const [, source] of filteredSources) {
if (source?.groupId) usedGroupIds.add(source.groupId);
}

const filtered = new Map();
for (const [id, group] of groups) {
if (usedGroupIds.has(id)) filtered.set(id, group);
}
return filtered;
}
}

Save request context

A single user action may trigger multiple saveLineToolsAndGroups calls. The optional SaveLineToolsContext argument provides metadata that explains why a particular call is being made and where the data should be routed:

  • sharingMode: the scope of the drawings in this call. Persist the data accordingly so that the user's sync settings are respected.
  • symbol: the name or identifier of the symbol displayed as the main series on the chart. Use it to determine where or how to save the data.
  • requestId: a unique identifier for the save request that can be used for debugging and logging.

Sharing mode

The sharingMode property indicates the scope of the drawings:

  • NotShared: the default state when no sync toggles are active. These drawings belong only to a specific chart instance.
  • SharedInLayout: applies when the Sync drawings to all charts toggle is active. These drawings sync only between charts within the same layout.
  • GloballyShared: applies when the New drawings sync globally toggle is active on the drawing toolbar. Drawings associated with the symbol appear on any layout where that symbol is loaded.
info

Save/load adapters should persist each call according to the sharingMode to ensure the user's settings are respected. In the example above, GloballyShared drawings are routed to a dedicated bucket so that they can be loaded for the same symbol across any layout.

Load request context

On load, the library calls loadLineToolsAndGroups, providing LineToolsAndGroupsLoadRequestType and LineToolsAndGroupsLoadRequestContext. These helps your adapter to return only the drawings that are relevant to the current request. Your adapter must filter the stored state to match the request. Returning drawings that do not match may cause them to be reassigned to the wrong pane. For example, drawings saved on an indicator pane ending up on the main series pane when the layout is reloaded.

The LineToolsAndGroupsLoadRequestType value tells your adapter which subset of drawings the library needs:

  • allLineTools: returns every stored drawing regardless of pane or symbol.
  • mainSeriesLineTools: returns only drawings whose symbol matches requestContext.symbol and whose ownerSource matches seriesSourceId (when provided).
  • lineToolsWithoutSymbol: returns drawings that are not tied to a specific symbol and that belong to the main series source.
  • studiesLineTools: returns drawings whose ownerSource differs from seriesSourceId. These are the drawings that live on indicator panes.

The LineToolsAndGroupsLoadRequestContext object contains:

  • symbol: the symbol displayed as the main series on the chart.
  • seriesSourceId: the source ID of the main series. Compare it with each drawing's ownerSource to decide whether the drawing lives on the main series pane or on an indicator pane.
  • sharingMode: the sharing mode associated with the request, if any. Use it to decide which storage bucket to read from.
caution

Each drawing's state object exposes an ownerSource field that identifies the chart source it was created on. Use ownerSource === requestContext.seriesSourceId to tell main-series drawings apart from drawings that belong to an indicator pane. Without this check, drawings created on an indicator pane can be reassigned to the main series pane the next time the layout is loaded.

Handling deletions

The library does not use a separate delete method. Deletions are encoded as null values (tombstones) within the state.sources or state.groups maps. Your backend should remove the corresponding records when a null value is received. See Understanding the LineToolsAndGroupsState interface.

REST API

If you use the REST API for chart storage, you should implement the following endpoints in addition to the endpoints mentioned in the Develop your own storage section.

Save drawings

POST request: charts_storage_url/charts_storage_api_version/drawings?client=client_id&user=user_id&chart=chart_id&layout=layout_id

  1. state: LineToolsAndGroupsState object

RESPONSE: JSON Object

  1. statusok or error

Load drawings

GET request: charts_storage_url/charts_storage_api_version/drawings?client=client_id&user=user_id&chart=chart_id&layout=layout_id

RESPONSE: JSON Object

  1. statusok or error
  2. data: Object
    1. state: LineToolsAndGroupsState object

Low-level API methods

The low-level API has additional methods on the chart widget when you enable the saveload_separate_drawings_storage featureset.

getLineToolsState

This function captures the current drawings state from the chart. You can benefit from this if you need to programmatically capture and store the drawings state.

const state = widget.activeChart().getLineToolsState();
// Send or save state as required...

applyLineToolsState

Enable restoring the drawings on the chart by implementing a previously saved LineToolsAndGroupsState object.

const state = // previously saved state
widget.activeChart().applyLineToolsState(state).then(() => {
console.log('Drawings state restored!');
});

reloadLineToolsFromServer

Triggers a re-request of drawings from the server (via the Save Load Adapter or REST API depending on your implementation).

widget.activeChart().reloadLineToolsFromServer();

Customizing the chart save method

The save method on the IChartingLibraryWidget interface now includes options to adjust its behavior. There is an includeDrawings option in SaveChartOptions which determines whether to include drawings in the chart layout object returned by the save method. This can be useful in conjunction with the low-level API methods described above.

widget.save(state => {
// Handle saved state...
}, { includeDrawings: false });

Understanding the LineToolsAndGroupsState interface

The LineToolsAndGroupsState interface plays a crucial role in maintaining the state of chart drawings, providing a structure for both individual drawings and drawing groups.

The sources property is a Map, which constructs key-value pairs to represent a distinct drawing. The key, in this case, is an identifier or UUID for a drawing, and the value accompanies a state object exclusive to that drawing. The state object also encapsulates the symbol associated with the drawings, adding another defining layer to the data representation.

Similarly, the groups property is a Map accounting for groups of drawings. Each key-value pair comprises an identifier key or UUID and an array of UUIDs forming the drawing group.

For both sources and groups, a UUID associated with a null value indicates that the respective drawing or drawing group is to be removed. This signifies when a previously existing drawing has been deleted by the user and is no longer present on the chart.

caution

The state objects (LineToolState and LineToolsGroupState) which represent the drawings' state should be treated essentially as black boxes. They are managed by the library and are not expected to be directly modified outside of it.