
Lightweight Charts
- 414 installs
- 16.9k repo stars
- Updated July 8, 2026
- tradingview/lightweight-charts
Helps with ai & agent building tasks.
About
lightweight-charts is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- lightweight-charts
- AI & Agent Building
- AI-coding skill
Lightweight Charts by the numbers
- 414 all-time installs (skills.sh)
- +62 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,929 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tradingview/lightweight-charts --skill lightweight-chartsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 414 |
|---|---|
| repo stars | ★ 16.9k |
| Last updated | July 8, 2026 |
| Repository | tradingview/lightweight-charts ↗ |
What it does
Helps with ai & agent building tasks.
Files
Lightweight Charts skill
Works the same whether the project is a downstream npm consumer app or an upstream lightweight-charts source checkout. Detect which one you are in (below) and resolve every API name from whatever typings are locally available.
Source lookup order
Do not assume you are inside the upstream source repository.
1. In a consumer app, inspect the installed package first:
node_modules/lightweight-charts/package.jsonfor the actual version.node_modules/lightweight-charts/dist/typings.d.tsfor the API surface.
2. In the upstream repo, inspect dist/typings.d.ts first, then src/ if generated output is unavailable. 3. Use official docs/examples as supporting evidence, but local typings win when they disagree.
Verify before answering (copy-paste):
node -p "require('./node_modules/lightweight-charts/package.json').version"
rg -n "createSeriesMarkers|addSeries|IPrimitivePaneRenderer|createImageWatermark|createOptionsChart" node_modules/lightweight-charts/dist/typings.d.ts
# upstream checkout instead of a consumer app:
rg -n "createSeriesMarkers|IPrimitivePaneRenderer|createImageWatermark|createOptionsChart" dist/typings.d.ts srcIf the relevant file is unavailable, say what could not be verified. Do not invent option names, methods, exports, or wrapper behavior.
If the user's code imports from lightweight_charts import ... (underscore), uses pandas/asyncio, or talks about a Python window wrapper, they are probably using the third-party Python wrapper rather than the TypeScript package. Say that clearly before giving core lightweight-charts v5 JavaScript advice.
Mental model
Eight layers, in dependency order. Most bugs come from confusing one for another:
1. Chart — createChart(container, options) returns an IChartApi. One chart per container element. 2. Series — added via chart.addSeries(SeriesType, options, paneIndex?). Each call returns an ISeriesApi. v5 requires importing the series type explicitly. 3. Scales — chart.timeScale() (ITimeScaleApi), chart.priceScale(id) (IPriceScaleApi). Govern range, ticks, autoscale, visibility. 4. Data model — arrays of { time, … } points. Time = UTCTimestamp | BusinessDay | string. UTCTimestamp is seconds, not milliseconds. Times must be unique and ascending per series. 5. Interaction — chart.subscribeCrosshairMove, chart.subscribeClick, timeScale.subscribeVisibleLogicalRangeChange, etc. 6. Layout — chart.panes(), chart.addPane(). Adding a series with an out-of-range paneIndex auto-creates the pane. 7. Extension — pane primitives, series primitives, custom series, watermarks, custom renderers (canvas via fancy-canvas). 8. Wrappers — React/Vue/Web Components/iOS/Android. Lifecycle is the wrapper's responsibility; the core library is framework-agnostic.
v5 essentials
The conventions that catch people copying older snippets:
import { createChart, CandlestickSeries, LineSeries } from 'lightweight-charts';
const chart = createChart(container, { /* ChartOptions */ });
const candles = chart.addSeries(CandlestickSeries, { /* options */ });
candles.setData([
{ time: 1700000000, open: 1, high: 2, low: 0.5, close: 1.5 },
]);Key v5 changes versus older snippets:
chart.addSeries(SeriesType, options, paneIndex?)replacesaddLineSeries/addCandlestickSeries/ …createSeriesMarkers(series, markers)replacesseries.setMarkers(...). The returned primitive owns.setMarkers(...)/.markers().createTextWatermark(pane, options)andcreateImageWatermark(pane, imageUrl, options)replace thewatermarkchart option.- Series types are tree-shaken — importing them is required for ESM.
If a snippet calls addLineSeries or series.setMarkers, it's v4 or older. Confirm the user's version before forwarding it.
When the task targets v5, show the v5 API directly — do not hedge by mixing in v4 syntax.
Triage (problem → API → avoid)
| User asks about | First check | Answer with | Avoid |
|---|---|---|---|
| v5 series creation | package version + typings | chart.addSeries(SeriesType, options) | addLineSeries / addCandlestickSeries |
| markers | createSeriesMarkers export | marker primitive + required color | series.setMarkers |
| timezone | formatter need vs data semantics | Intl.DateTimeFormat in formatters | fake timeScale.timezone |
| realtime updates | last-bar vs full replace | series.update(point) | setData on every tick |
| panes | paneIndex + chart.panes() | v5 pane APIs, pane.setHeight | private widget / DOM hacks |
| per-bar colors | data-point color fields | color/borderColor/wickColor on points | extra series / markers to recolor |
| plugins | primitive vs custom series | scaffold/examples + public coordinate APIs | invented renderer names |
v4 → v5 replacement matrix
Stale copy-pasted snippets are the most common failure. Map them directly:
| Old or wrong | v5 replacement |
|---|---|
chart.addLineSeries(options) | chart.addSeries(LineSeries, options) |
chart.addCandlestickSeries(options) | chart.addSeries(CandlestickSeries, options) |
series.setMarkers(markers) | createSeriesMarkers(series, markers) |
chart watermark option | createTextWatermark(chart.panes()[0], options) |
createImageWatermark(pane, options) | createImageWatermark(pane, imageUrl, options) |
IPanePrimitivePaneRenderer | IPrimitivePaneRenderer |
timeScale: { timezone } | label formatters using Intl.DateTimeFormat |
time: Date.now() (ms) | time: Math.floor(Date.now() / 1000) for UTCTimestamp |
Routing map (problem → where to look)
Use local typings first. The repo paths below exist only in the upstream lightweight-charts checkout; in a consumer app, translate them to the installed package typings or official docs.
| Task | Canonical sources |
|---|---|
| First chart / series | website/docs/intro.mdx, website/tutorials/customization/ |
| Time / range behavior | website/docs/time-scale.md, website/docs/time-zones.md |
| Realtime / history | website/tutorials/demos/realtime-updates.{mdx,js}, website/tutorials/demos/infinite-history.{mdx,js} |
| Whitespace / gaps | website/tutorials/demos/whitespace.{mdx,js} |
| Price format / locale | website/tutorials/customization/price-format.mdx, website/tutorials/demos/custom-locale.{mdx,js} |
| Two scales / inverted | website/tutorials/how_to/two-price-scales.{mdx,js}, website/tutorials/how_to/inverted-price-scale.{mdx,js} |
| Panes / price + volume | website/docs/panes.md, website/tutorials/how_to/panes.{mdx,js}, website/tutorials/how_to/price-and-volume.{mdx,js} |
| Markers / tooltips / legends | website/tutorials/how_to/series-markers.{mdx,js}, website/tutorials/how_to/tooltips.mdx, website/tutorials/how_to/legends.mdx |
| Crosshair (programmatic) | website/tutorials/how_to/set-crosshair-position*.js |
| Watermarks | website/tutorials/how_to/watermark.mdx |
| Plugins / custom series / primitives | website/docs/plugins/, plugin-examples/src/plugins/ |
| Coordinate conversion / hit-testing | dist/typings.d.ts for subscribeClick, coordinateToPrice, timeScale().coordinateToLogical |
| Non-time horizontal scales / options chart | dist/typings.d.ts for createOptionsChart, createChartEx, IHorzScaleBehavior |
| Pixel-perfect rendering | website/docs/plugins/pixel-perfect-rendering/ |
| React / Vue / Web Components | website/tutorials/react/, website/tutorials/vuejs/, website/tutorials/webcomponents/ |
| SSR / script loading / bundlers | installed package dist/ files, package exports, framework docs for client-only components |
| iOS / Android wrappers | website/docs/ios.md, website/docs/android.md |
| Version migration | website/docs/migrations/from-v2-to-v3.md, from-v3-to-v4.md, from-v4-to-v5.md |
If these repo paths do not exist in the user's project, do not ask them to create or clone the source tree just to answer a normal usage question. Use node_modules/lightweight-charts/dist/typings.d.ts and concise v5 examples.
Foot-guns (assertions, not categories)
Time
- `UTCTimestamp` is in seconds. Use
Math.floor(Date.now() / 1000), neverDate.now(). - There is no built-in `timeScale.timezone` option. For display-only timezone conversion, use
timeScale.tickMarkFormatterfor axis labels andlocalization.timeFormatter(or your own tooltip formatter) for crosshair/hover labels. UseIntl.DateTimeFormat(..., { timeZone }), Luxon, or date-fns-tz for DST-safe IANA timezone formatting. - Pre-shifting timestamps changes chart semantics. It can make ticks line up in a chosen timezone, but it also changes where bars sit on the UTC time scale. Prefer formatter-based display unless the user intentionally wants shifted data.
- Series data must be strictly ascending by `time`, with unique values. Duplicates replace silently; out-of-order points are dropped or throw.
- One time format per series.
BusinessDay('YYYY-MM-DD'string or{ year, month, day }) orUTCTimestamp, but do not mix within a single series. - Whitespace points carry `time` only (no
value/open/ …). Use them to create gaps or align overlays; do not set values tonull. - `setData` replaces the dataset and can reset the visible range. `update(point)` appends if `time > last` and replaces if `time === last`.
update(point, true)can update an existing historical point, but it is slower and does not insert missing old points. To prepend older history, build a new array and callsetDataagain — there is no prepend method; save and restore the visible logical range to avoid the chart snapping. - `timeScale.setVisibleLogicalRange` is logical (bar indices), not time-based. Use
setVisibleRangefor time bounds, orfitContent()to reset.
Scales and panes
- Two visible price scales requires both assigning series with
priceScaleId: 'right'/'left'and making the left scale visible. Usechart.applyOptions({ leftPriceScale: { visible: true }, rightPriceScale: { visible: true } })orchart.priceScale('left').applyOptions({ visible: true }). - Overlay series (
priceScaleId: ''or any custom id) get an autoscaled, hidden scale by default. Make it visible explicitly if you want axis labels. - `autoscaleInfoProvider` runs on every layout pass. Keep it cheap; do not allocate per call.
- `addSeries(_, _, paneIndex)` creates the pane if
paneIndexis one past the current count. Pane order is creation order; sizes are controlled viachart.panes()[i].setHeight(px). - Keep pane references if panes can move.
const mainPane = chart.panes()[0]remains the sameIPaneApi;mainPane.paneIndex()gives its current index afterswapPanesor moves. - Sync separate charts with public time-scale events. Use
subscribeVisibleLogicalRangeChangeandsetVisibleLogicalRange; avoid_private__chartWidget,paneWidgets, or DOM offset hacks. - Price-scale width is readable, not directly settable. Use
series.priceScale().width()orchart.priceScale(id).width()to measure when aligning adjacent containers.
Markers and interaction
- v5 markers are a primitive.
const m = createSeriesMarkers(series, [...]); m.setMarkers([...]).series.setMarkersdoes not exist onISeriesApiin v5. - Marker `time` must match an existing data point's `time` for that series. Misaligned markers are dropped silently.
- `subscribeCrosshairMove` fires with
param.time === undefinedoutside the data range. Always null-check before reading. - `param.seriesData.get(series)` returns
undefinedbetween bars or before the first bar. - Click/crosshair coordinates need API conversion. Use
series.coordinateToPrice(y)/series.priceToCoordinate(price)andchart.timeScale().coordinateToLogical(x)/logicalToCoordinate(logical)/timeToCoordinate(time); do not infer price/time from canvas DOM geometry.
Plugins and custom rendering
- Start plugins from the scaffold/examples. Prefer
npm create lwc-plugin@latestandplugin-examples/before writing a primitive from scratch. - Interactive drawing tools need two parts: a primitive/custom series for rendering, and chart interaction handlers (
subscribeClick,subscribeCrosshairMove, drag state) that convert coordinates with public APIs. - `CanvasRenderingTarget2D` is imported from `fancy-canvas`, not from
lightweight-charts. - Use
target.useBitmapCoordinateSpace(scope => …)for pixel-aligned strokes; usetarget.useMediaCoordinateSpace(scope => …)for CSS-pixel logic. Mixing them causes blur or sub-pixel jitter on HiDPI displays. - `IPanePrimitive` views render across the whole pane; `ISeriesPrimitive` views are clipped to the series. Pick the right base.
- The renderer interface is `IPrimitivePaneRenderer`, not `IPanePrimitivePaneRenderer`. The view is
IPanePrimitivePaneViewand itsrenderer()returnsIPrimitivePaneRenderer | null— the names are deliberately asymmetric. There is noIPanePrimitivePaneRendererexport; grepdist/typings.d.tsif unsure. - `ICustomSeriesPaneView` is heavier than a primitive — only use it when you need a true series API (data, scales, autoscale). For decorations, use a primitive.
Wrappers (React/Vue/etc.)
- Create the chart once in
useEffect/onMountedand destroy it in cleanup withchart.remove(). Recreating on every render duplicates DOM and leaks listeners. - Resize can be automatic in v5 with
autoSize: truewhenResizeObserveris available. If you need manual control, subscribe aResizeObserverto the container and callchart.resize(width, height)(orapplyOptions({ width, height })). - Don't drive `setData` from props on every render. Use
series.update(...)for incremental changes; only callsetDatawhen the dataset truly replaces. - Next.js/SSR must be client-only. Put chart code in a
'use client'component, create it inuseEffect, and import that component withnext/dynamic(..., { ssr: false })from server-rendered pages when needed. - Plain HTML is not npm resolution. The standalone
.jsbuild exposeswindow.LightweightCharts; ESM in the browser must import an actual.mjsURL or use an import map.import { createChart } from 'lightweight-charts'only works when a bundler/runtime resolves the package name.
Canonical recipes
Realtime updates
import { createChart, CandlestickSeries } from 'lightweight-charts';
const chart = createChart(container, {});
const series = chart.addSeries(CandlestickSeries, {});
series.setData(initialBars); // full history once
ws.onmessage = ev => {
const bar = JSON.parse(ev.data); // { time, open, high, low, close }
series.update(bar); // appends if time > last; replaces if time === last
};update() only handles the latest bar. To insert in the middle or prepend, rebuild the array and call setData.
Two price scales (left + right)
const right = chart.addSeries(LineSeries, { priceScaleId: 'right' });
const left = chart.addSeries(LineSeries, { priceScaleId: 'left' });
chart.applyOptions({
leftPriceScale: { visible: true },
rightPriceScale: { visible: true },
});Volume in its own pane
const price = chart.addSeries(CandlestickSeries, {}, 0);
const volume = chart.addSeries(
HistogramSeries,
{ priceFormat: { type: 'volume' } },
1,
);
chart.panes()[1].setHeight(120);Per-bar / per-point colors
Color individual bars by putting the color on the data point, not via a separate API:
// Candlesticks: per-point color / borderColor / wickColor override series defaults
candles.setData([
{ time: 1700000000, open: 1, high: 2, low: 0.5, close: 1.5,
color: '#26a69a', borderColor: '#26a69a', wickColor: '#26a69a' },
{ time: 1700003600, open: 1.5, high: 1.8, low: 1.1, close: 1.2,
color: '#ef5350', borderColor: '#ef5350', wickColor: '#ef5350' },
]);
// Histogram (e.g. up/down volume): per-point color
volume.setData([
{ time: 1700000000, value: 100, color: 'rgba(38,166,154,0.5)' },
{ time: 1700003600, value: 80, color: 'rgba(239,83,80,0.5)' },
]);Line/Area points also take a per-point color. An omitted point color falls back to the series option. Do not create extra series or use markers to recolor bars.
Series markers
import { createSeriesMarkers } from 'lightweight-charts';
const markers = createSeriesMarkers(candles, [
{ time: 1700000000, position: 'aboveBar', color: '#e53935', shape: 'arrowDown', text: 'Sell' },
]);
markers.setMarkers([
{ time: 1700003600, position: 'belowBar', color: '#43a047', shape: 'arrowUp', text: 'Buy' },
]);color is required on every marker (position, shape, time too); text, size, id, price are optional. Do not put marker objects into setData() points. Do not call series.setMarkers(...) in v5.
Timezone display
const zone = 'America/New_York';
const formatTime = (timestamp: number) =>
new Intl.DateTimeFormat('en-US', {
timeZone: zone,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}).format(new Date(timestamp * 1000));
chart.applyOptions({
timeScale: {
timeVisible: true,
secondsVisible: true,
tickMarkFormatter: (time) =>
typeof time === 'number' ? formatTime(time) : String(time),
},
localization: {
timeFormatter: (time) =>
typeof time === 'number' ? formatTime(time) : String(time),
},
});Do not invent timeScale: { timezone: 'America/New_York' }. If the user needs exchange-session boundaries to generate in a local timezone, explain the trade-off of shifting source timestamps separately from display formatting.
Text watermark
import { createTextWatermark } from 'lightweight-charts';
createTextWatermark(chart.panes()[0], {
horzAlign: 'center',
vertAlign: 'center',
lines: [{ text: 'DEMO', color: 'rgba(0, 0, 0, 0.25)', fontSize: 48 }],
});The old watermark chart option is not the v5 API.
Whitespace gaps
line.setData([
{ time: '2024-01-01', value: 10 },
{ time: '2024-01-02' }, // whitespace point: time only
{ time: '2024-01-03', value: 12 },
]);Do not use value: null, undefined, or NaN to create gaps.
Sync separate charts or panes
let syncing = false;
priceChart.timeScale().subscribeVisibleLogicalRangeChange((range) => {
if (!range || syncing) return;
syncing = true;
indicatorChart.timeScale().setVisibleLogicalRange(range);
syncing = false;
});
indicatorChart.timeScale().subscribeVisibleLogicalRangeChange((range) => {
if (!range || syncing) return;
syncing = true;
priceChart.timeScale().setVisibleLogicalRange(range);
syncing = false;
});For v5 panes inside one chart, prefer chart.addSeries(SeriesType, options, paneIndex) and chart.panes()[i].setHeight(px). If the original pane can move, keep the IPaneApi reference and call pane.paneIndex() instead of assuming index 0 forever.
Coordinate conversion
chart.subscribeClick((param) => {
if (!param.point || param.time === undefined) return;
const price = series.coordinateToPrice(param.point.y);
const logical = chart.timeScale().coordinateToLogical(param.point.x);
if (price === null || logical === null) return;
// Store { time: param.time, price } or convert logical back later:
const x = chart.timeScale().logicalToCoordinate(logical);
const y = series.priceToCoordinate(price);
});Use the public conversion APIs for drawing tools and annotations. DOM canvas coordinates alone cannot tell you the chart price/time.
Next.js client-only component
'use client';
import { createChart, LineSeries } from 'lightweight-charts';
import type { ISeriesApi, LineData, Time } from 'lightweight-charts';
import { useEffect, useRef } from 'react';
export function LwcChart({ data }: { data: LineData<Time>[] }) {
const ref = useRef<HTMLDivElement | null>(null);
const seriesRef = useRef<ISeriesApi<'Line'> | null>(null);
useEffect(() => {
if (!ref.current) return;
const chart = createChart(ref.current, { autoSize: true });
const series = chart.addSeries(LineSeries);
seriesRef.current = series;
return () => {
seriesRef.current = null;
chart.remove();
};
}, []);
useEffect(() => {
seriesRef.current?.setData(data);
}, [data]);
return <div ref={ref} style={{ width: '100%', height: 320 }} />;
}From a server-rendered Next.js page, import the component with:
import dynamic from 'next/dynamic';
const LwcChart = dynamic(() => import('./LwcChart').then((m) => m.LwcChart), {
ssr: false,
});Plain HTML loading
Use the standalone IIFE build when there is no bundler:
<div id="chart" style="width: 600px; height: 300px"></div>
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
<script>
const chart = LightweightCharts.createChart(document.getElementById('chart'));
const series = chart.addSeries(LightweightCharts.LineSeries);
</script>Choose the loading shape by environment:
- Bundler (Vite/webpack/Next): bare
import { createChart } from 'lightweight-charts'. The packageexportsresolve the right ESM/CJS build. - Browser `<script>` (no bundler): the standalone IIFE build above exposes the
LightweightChartsglobal; there is no bare-specifier resolution. - Browser native ESM (`<script type="module">`): import from a concrete
.mjsURL (e.g. the standalone.mjs) or add an import map; a bare'lightweight-charts'specifier will not resolve. - SSR frameworks: load client-only (see the Next.js recipe above) — never import at module top level in a server component.
Plugin starter
npm create lwc-plugin@latestUse the generated project and plugin-examples/ as the base. Decide the extension type first:
- Primitive (
IPanePrimitive/ISeriesPrimitive, attached viaattachPrimitive) — for drawings, overlays, labels, bands, annotations, hit-testing. This is the default; reach for it unless you need real series behavior. - Custom series (
ICustomSeriesPaneView) — only when the extension needs its own data, autoscale, or price/time-scale integration. It is heavier; do not use it for decorations.
For draggable drawings, attach a primitive with series.attachPrimitive(...) or pane.attachPrimitive(...), then manage interaction with subscribeClick / subscribeCrosshairMove and coordinate conversion APIs. Import renderer-only CanvasRenderingTarget2D from fancy-canvas. The renderer interface is IPrimitivePaneRenderer (not IPanePrimitivePaneRenderer).
Non-financial data
import { createOptionsChart } from 'lightweight-charts';
const chart = createOptionsChart(container);Decide first whether the horizontal axis is truly non-time:
- True custom horizontal scale (option chains, sensor sweeps by a non-time key): use
createOptionsChartfor a numeric x-axis, orcreateChartExwith a customIHorzScaleBehavior(verify both in local typings). - Just non-financial y units (temperature, load) on an ordinary time/category axis: keep the normal
createChartseries model and setpriceFormat/localization.priceFormatterso labels match the domain. - Categorical x that is really ordered: mapping categories to ascending
BusinessDaystrings is simpler than a custom scale — but say that the axis is then date-like, not truly categorical.
Custom price formatter
chart.applyOptions({
localization: {
priceFormatter: (p: number) => `$${p.toFixed(2)}`,
},
});For per-series formatting, set priceFormat: { type: 'custom', formatter, minMove } in the series options — the chart-level priceFormatter is the fallback.
Minimal pane primitive
import type {
IPanePrimitive,
IPanePrimitivePaneView,
IPrimitivePaneRenderer,
Time,
} from 'lightweight-charts';
import type { CanvasRenderingTarget2D } from 'fancy-canvas';
class Overlay implements IPanePrimitive<Time> {
paneViews(): IPanePrimitivePaneView[] { return [new View()]; }
}
class View implements IPanePrimitivePaneView {
renderer(): IPrimitivePaneRenderer { return new Renderer(); }
}
class Renderer implements IPrimitivePaneRenderer {
draw(target: CanvasRenderingTarget2D): void {
target.useMediaCoordinateSpace(({ context, mediaSize }) => {
context.fillStyle = 'rgba(0,0,0,0.05)';
context.fillRect(0, 0, mediaSize.width, mediaSize.height);
});
}
}
chart.panes()[0].attachPrimitive(new Overlay());Working examples live under plugin-examples/src/plugins/ — copy structure from there before writing one from scratch.
Worked example
"My chart snaps to the right edge every time new data arrives."
1. Layers involved: data model (4) and time scale (3). 2. Suspect: the code calls series.setData(...) on every tick instead of series.update(...). setData resets the visible range. 3. Verify: ask whether the call path is setData or update. If setData, ask whether the visible logical range is captured before and restored after — the v4-era workaround. 4. Fix: switch to series.update(bar) for incremental ticks; reserve setData for full-history replacement; only use chart.timeScale().scrollToRealTime() if the user explicitly wants follow-the-latest behavior. 5. References: website/tutorials/demos/realtime-updates.js, website/docs/time-scale.md.
Code-generation rules
- Default to v5. Assume v5 APIs unless the user's installed package or prompt pins an older version. If they're on v4, explain the v5 migration point instead of mixing syntaxes.
- Use real option names. In consumer apps, grep
node_modules/lightweight-charts/dist/typings.d.ts; in the upstream repo, grepdist/typings.d.tsorsrc/api/options/. Many similarly-named options exist across chart/series/scale — confirm which level owns the option. - Minimal snippets. One feature per code block. Combining markers + watermark + custom pane primitive in one snippet hides which API does what.
- Match the user's framework. A React user wants the
useEffectlifecycle; a vanilla user does not. - Don't invent. If an API name does not appear in the installed typings or upstream source, it does not exist.
Answer contract
When answering a lightweight-charts question:
1. Name the relevant v5 API. 2. Show one minimal snippet, not a mega-demo. 3. Call out the main foot-gun for that task. 4. Say what local source was checked (version/typings), or state that it could not be verified.