Skip to main content

Chart image storage

This is an optional part of chart storage. Use it only when users can add local images to charts and those images must remain available after a chart workspace layout, drawing record, or template is reopened.

When a user selects a JPG, PNG, or WebP file, the Image tool initially keeps it inside the drawing as an inline data: URL. ChartImageStorageAdapter uploads those image bytes and replaces the inline value with a durable URL before the parent JSON reaches ChartStorageAdapter.

In user-facing terms:

  • your file service stores the uploaded image;
  • chart storage keeps the hosted URL and optional asset metadata;
  • reopening the saved chart loads the image from that URL.

This hook does not store exported chart screenshots, watermarks, symbol logos, or thumbnails, and it does not manage deletion or orphan cleanup. Without an image adapter, the inline data URL remains in the saved JSON. That can be acceptable for local prototypes, but it creates larger records and can exceed browser or backend limits.

Implement the upload boundary

imageStorage.ts
import type { ChartImageStorageAdapter } from '@tradescript/pro/sdk';

interface UploadResponse {
imageUrl: string;
assetId: string;
}

function isUploadResponse(value: unknown): value is UploadResponse {
if (!value || typeof value !== 'object') return false;
const candidate = value as Record<string, unknown>;
return typeof candidate.imageUrl === 'string'
&& typeof candidate.assetId === 'string';
}

export const imageStorageAdapter = {
getMaxImageSizeInBytes: () => 5 * 1024 * 1024,

async saveImage({ dataUrl, drawing, context }) {
const source = await fetch(dataUrl);
const file = await source.blob();
const form = new FormData();
form.append('image', file, `${drawing.id ?? 'chart-image'}.png`);
form.append('source', context.source);

const response = await fetch('/api/chart-images', {
method: 'POST',
credentials: 'include',
body: form,
});
if (!response.ok) {
throw new Error(`Image upload failed: ${response.status}`);
}

const value: unknown = await response.json();
if (!isUploadResponse(value)) {
throw new Error('Image upload returned an invalid response');
}

return {
imageUrl: value.imageUrl,
metadata: { assetId: value.assetId },
};
},
} satisfies ChartImageStorageAdapter;

Pass the optional image adapter alongside chart storage when mounting the widget:

const mounted = sdk.chart.mount({
mount: '#chart',
symbol,
interval: '1D',
datafeed,
storage,
imageStorageAdapter,
});

The SDK clones the object being persisted, replaces inline image URLs with the returned hosted URL, and then invokes the JSON adapter. Repeated references to the same data URL are uploaded once within that save operation.

Own the file lifecycle

Your file service must:

  • authenticate uploads and authorize reads for the intended storage scope;
  • validate MIME type and decoded byte size instead of trusting the data URL;
  • return a non-empty URL reachable wherever the saved chart is restored;
  • define retention and deletion for files no longer referenced by saved JSON;
  • avoid logging inline image payloads or signed credentials.

The SDK does not delete orphaned uploads or migrate an existing hosted URL. When saveImage rejects or returns no usable URL, the parent chart workspace layout, drawing, or template save rejects instead of persisting a broken reference.

Verify the user feature

  1. Add one image drawing and save a chart workspace layout.
  2. Confirm the file service received one upload and the stored chart workspace layout contains the hosted URL, not a data: URL.
  3. Load the chart workspace layout in a clean browser session and confirm the image renders.
  4. Repeat with separate drawings and a drawing template.
  5. Reject an oversized or invalid file and confirm no JSON record is committed.

Next steps