import { createChart as _createChart, CandlestickSeries, HistogramSeries, LineSeries, BaselineSeries, // } from "../modules/lightweight-charts/5.1.0/dist/lightweight-charts.standalone.development.mjs"; } from "../modules/lightweight-charts/5.1.0/dist/lightweight-charts.standalone.production.mjs"; const createChart = /** @type {CreateChart} */ (_createChart); import { createChoiceField, createLabeledInput, createSpanName, } from "../utils/dom.js"; import { createOklchToRGBA } from "./oklch.js"; import { throttle } from "../utils/timing.js"; import { serdeBool } from "../utils/serde.js"; import { stringToId } from "../utils/format.js"; import { style } from "../utils/elements.js"; import { resources } from "../resources.js"; /** * @typedef {Object} Valued * @property {number} value * * @typedef {Object} Indexed * @property {number} index * * @typedef {_ISeriesApi} ISeries * @typedef {_ISeriesApi<'Candlestick'>} CandlestickISeries * @typedef {_ISeriesApi<'Histogram'>} HistogramISeries * @typedef {_ISeriesApi<'Line'>} LineISeries * @typedef {_ISeriesApi<'Baseline'>} BaselineISeries * * @typedef {_LineSeriesPartialOptions} LineSeriesPartialOptions * @typedef {_HistogramSeriesPartialOptions} HistogramSeriesPartialOptions * @typedef {_BaselineSeriesPartialOptions} BaselineSeriesPartialOptions * @typedef {_CandlestickSeriesPartialOptions} CandlestickSeriesPartialOptions */ /** * @template T * @typedef {Object} Series * @property {string} id * @property {Signal} active * @property {Signal} hasData * @property {Signal} url * @property {() => Record} getOptions * @property {(options: Record) => void} applyOptions * @property {() => readonly T[]} getData * @property {(data: T) => void} update * @property {VoidFunction} remove */ /** * @typedef {Series} AnySeries */ /** * @typedef {_SingleValueData} SingleValueData * @typedef {_CandlestickData} CandlestickData * @typedef {_LineData} LineData * @typedef {_BaselineData} BaselineData * @typedef {_HistogramData} HistogramData * * @typedef {Object} Legend * @property {HTMLLegendElement} element * @property {function({ series: AnySeries, name: string, order: number, colors: Color[] }): void} addOrReplace * @property {function(number): void} removeFrom */ const oklchToRGBA = createOklchToRGBA(); const lineWidth = /** @type {any} */ (1.5); /** * @param {Object} args * @param {string} args.id * @param {HTMLElement} args.parent * @param {Signals} args.signals * @param {Colors} args.colors * @param {BrkClient} args.brk * @param {Accessor} args.index * @param {((unknownTimeScaleCallback: VoidFunction) => void)} [args.timeScaleSetCallback] * @param {true} [args.fitContent] * @param {{unit: Unit; blueprints: AnySeriesBlueprint[]}[]} [args.config] */ export function createChartElement({ parent, signals, colors, id: chartId, index, brk, timeScaleSetCallback, fitContent, config, }) { const div = window.document.createElement("div"); div.classList.add("chart"); parent.append(div); const legendTop = createLegend(signals); div.append(legendTop.element); const chartDiv = window.document.createElement("div"); chartDiv.classList.add("lightweight-chart"); div.append(chartDiv); const legendBottom = createLegend(signals); div.append(legendBottom.element); const ichart = createChart( chartDiv, /** @satisfies {DeepPartial} */ ({ autoSize: true, layout: { fontFamily: style.fontFamily, background: { color: "transparent" }, attributionLogo: false, colorSpace: "display-p3", colorParsers: [oklchToRGBA], }, grid: { vertLines: { visible: false }, horzLines: { visible: false }, }, rightPriceScale: { borderVisible: false, }, timeScale: { borderVisible: false, ...(fitContent ? { minBarSpacing: 0.001, } : {}), }, localization: { priceFormatter: numberToShortUSFormat, locale: "en-us", }, crosshair: { mode: 3, }, ...(fitContent ? { handleScale: false, handleScroll: false, } : {}), // ..._options, }), ); // Takes a bit more space sometimes but it's better UX than having the scale being resized on option change ichart.priceScale("right").applyOptions({ minimumWidth: 80, }); ichart.panes().at(0)?.setStretchFactor(1); const visibleBarsCount = signals.createSignal(0); ichart.timeScale().subscribeVisibleLogicalRangeChange((range) => { if (range) { visibleBarsCount.set(range.to - range.from); } }); signals.createEffect( () => ({ defaultColor: colors.default(), offColor: colors.gray(), borderColor: colors.border(), }), ({ defaultColor, offColor, borderColor }) => { ichart.applyOptions({ layout: { textColor: offColor, panes: { separatorColor: borderColor, }, }, crosshair: { horzLine: { color: offColor, labelBackgroundColor: defaultColor, }, vertLine: { color: offColor, labelBackgroundColor: defaultColor, }, }, }); }, ); signals.createEffect(index, (index) => { const minBarSpacing = index === "monthindex" ? 1 : index === "quarterindex" ? 2 : index === "semesterindex" ? 3 : index === "yearindex" ? 6 : index === "decadeindex" ? 60 : 0.5; ichart.applyOptions({ timeScale: { timeVisible: index === "height", ...(!fitContent ? { minBarSpacing, } : {}), }, }); }); const activeResources = /** @type {Set>} */ ( new Set() ); ichart.subscribeCrosshairMove( throttle(() => { activeResources.forEach((v) => { v.fetch(); }); }), ); if (fitContent) { new ResizeObserver(() => ichart.timeScale().fitContent()).observe(chartDiv); } /** * @param {Object} args * @param {string} args.id * @param {number} args.paneIndex * @param {"nw" | "ne" | "se" | "sw"} args.position * @param {number} [args.timeout] * @param {(pane: IPaneApi