Guides
Remember the reader's chart
A reader arranges a chart, adds two indicators, draws a trend line, and returns the next day to none of it. This article describes how to save that state and restore it, and why your page is the one that stores it.
We store nothing about your reader
The embed has no account for the person looking at it, no cookie it can read across your site, and no row anywhere with their name on it. That is the whole security model, and it is stated on the security page as a promise rather than an implementation detail.
So chart state is handed out, as plain data, and handed back when you want it. You already know who is looking and you already have somewhere to put things; we have neither and would rather not.
Two pairs, and they are a choice
Which one you want depends on a single question: does your page decide the instrument, or does the reader?
| If… | Use | Because |
|---|---|---|
| Your page decides the symbol and interval | getDrawings / setDrawings | A whole state carries the symbol and interval it was saved on. Restoring one on a page that puts a chart of ACME on the ACME article would replace it with whatever the reader was last looking at. |
| The reader decides, and the chart is the product | saveState / loadState | Everything comes back: indicators, drawings, panes, the symbol, the interval. This is the terminal case. |
Just the annotations
The common case. Your page owns the instrument; the reader owns what is drawn on it.
const chart = vpEmbed.chart(document.querySelector('.vp-chart'));
// Restore whatever they had, once the chart is up.
chart.on('dataLoaded', async () => {
const saved = localStorage.getItem('notes:' + symbol);
if (saved) await chart.setDrawings(JSON.parse(saved));
});
// Keep it. There is no "drawing changed" event yet, so save on a rhythm
// that suits your page: a blur, a route change, or a timer.
window.addEventListener('pagehide', async () => {
const drawings = await chart.getDrawings();
localStorage.setItem('notes:' + symbol, JSON.stringify(drawings));
});setDrawings replaces every drawing on the chart rather than appending. This is what makes restoring twice safe: an appending call would duplicate a reader's annotations on every return visit.The whole chart
When the chart is the product and the reader chooses what they are looking at, save the lot.
// Their layout, keyed by them. You know who they are; we do not.
const KEY = 'chart:' + currentUser.id;
chart.on('dataLoaded', async () => {
const saved = await fetch('/api/chart/' + currentUser.id).then((r) => r.json());
if (saved) await chart.loadState(saved);
});
async function persist() {
const state = await chart.saveState();
await fetch('/api/chart/' + currentUser.id, {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(state),
});
}It is your reader’s own writing
Worth saying plainly, because the shape of the data hides it: a drawing carries the text your reader typed into it. A note, a callout, a label on a trend line — getDrawings hands all of it over along with the anchors. That is the feature working (there is nothing to restore otherwise), and it also means whatever you do with the result — store it, log it, sync it to an account — you are doing to their words. We keep none of it, so the whole of that decision is yours.
To read what is on the chart without reading where, use getShapes, which returns ids and tool names only. Those ids are what removeEntity accepts, so it is also how you remove one drawing rather than all of them.
Try it
Draw a trend line on this chart, press Save, then re-run the pen. The drawings come back. Storage here is localStorage because a pen has nowhere else to put them; on your page it is whatever you already use.
<script src="https://view.verticalprop.com/embed.js"></script>
<div class="vp-chart"
data-symbol="DEMO"
data-interval="60"
data-toolbar="1"
data-theme="dark"
data-datafeed="myFeed"
style="height: 420px"></div>
<p>
<button id="save">Save drawings</button>
<button id="list">List drawings</button>
</p>
<pre id="log"></pre>
<style>
body { margin: 0; background: #0b0f19; color: #e6e8eb;
font: 13px/1.5 ui-monospace, monospace; }
#log { padding: 8px 12px; margin: 0; white-space: pre-wrap; }
</style>
<script>
// A deterministic series, so the pen never depends on a venue and the
// same code always draws the same chart.
function bars(from, to, stepSec) {
const out = [];
let price = 100;
for (let t = Math.ceil(from / stepSec) * stepSec; t <= to; t += stepSec) {
price += Math.sin(t / 5000) * 0.6;
const open = price;
const close = price + Math.cos(t / 3000) * 0.8;
out.push({
time: t * 1000, // a bar's time is MILLISECONDS
open, close,
high: Math.max(open, close) + 0.5,
low: Math.min(open, close) - 0.5,
volume: 1000 + (t % 500),
});
}
return out;
}
vpEmbed.datafeed('myFeed', {
onReady: () => ({ supported_resolutions: ['60'] }),
resolveSymbol: (name) => ({
name, ticker: name, pricescale: 100, session: '24x7', timezone: 'Etc/UTC',
}),
// from and to are SECONDS; a bar's own time is MILLISECONDS.
getBars: (symbol, resolution, from, to) => bars(from, to, 3600),
subscribeBars: () => {},
unsubscribeBars: () => {},
});
const log = (m) => { document.getElementById('log').textContent += m + '\n'; };
const chart = vpEmbed.chart(document.querySelector('.vp-chart'));
const KEY = 'pen-drawings';
chart.on('dataLoaded', async () => {
const saved = localStorage.getItem(KEY);
if (saved) {
// setDrawings REPLACES, so restoring twice is safe.
await chart.setDrawings(JSON.parse(saved));
log('restored ' + JSON.parse(saved).length + ' drawing(s)');
} else {
log('draw something, then press Save');
}
});
// There is no drawingsChanged event, so save on something your page
// already knows about. Here that is a button; on a real page it is
// usually pagehide or a route change.
document.getElementById('save').addEventListener('click', async () => {
const drawings = await chart.getDrawings();
localStorage.setItem(KEY, JSON.stringify(drawings));
log('saved ' + drawings.length + ' drawing(s). Re-run the pen.');
});
document.getElementById('list').addEventListener('click', async () => {
// Ids and tool names only. getDrawings is the one that returns geometry.
log(JSON.stringify(await chart.getShapes()));
});
</script>
List drawings prints what getShapes returns: ids and tool names, without the geometry.
Restore after the bars, not before
Both restores above hang off dataLoaded, and that is not incidental. A drawing is anchored to a time and a price, so applying one to a chart with no bars in it gives the engine nothing to anchor against. Wait for the event; it fires on the first history load and on every one after.
There is no drawingsChanged event, so nothing signals the moment a reader finishes a trend line. Save on an event your page already handles, such as leaving the page, closing a panel or a periodic flush, rather than polling getDrawings on a timer.
What it will refuse
| Rule | What happens |
|---|---|
| A state or drawing list past the size cap | commandError, naming the size. The caps are listed under Limits. They are deliberately generous, since a chart state is a whole document, and they bound what the frame will hold rather than what the product allows. |
A drawing with no tool | commandError, naming the index. It is the one field the engine keys on; everything else in a drawing is the engine's own shape and is passed through unread. |
loadState with something that is not an object | commandError. An array or a string is not a state. |
The clock is separate, and it is a parameter too
A reader’s timezone usually belongs with the rest of what you remember about them, but it is set on its own because a page with no picker needs it right on the first paint:
<div class="vp-chart"
data-symbol="AAPL"
data-datafeed="myfeed"
data-timezone="America/New_York"></div>await chart.setTimezone('Europe/Paris');The number format is the same shape. Left alone it follows the reader’s own locale, which is usually what you want; a page whose numbers must read one way for everybody pins a preset:
<div class="vp-chart"
data-symbol="AAPL"
data-datafeed="myfeed"
data-numberFormat="european"></div>await chart.setNumberFormat('european'); // auto standard european plain spaced+02:00 is refused because it cannot express daylight saving, so a chart set with one is wrong for half the year. Etc/GMT+5 is a real zone and is accepted; a bare GMT+5 is not one and is not.