// @ts-check /** @import {IChartApi, ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData, SeriesType, IPaneApi, BaselineStyleOptions} from './v5.0.7-treeshaked/types' */ /** * @typedef {[number, number, number, number]} OHLCTuple * * @typedef {Object} Valued * @property {number} value * * @typedef {Object} Indexed * @property {number} index */ /** * @template T * @typedef {T & Valued & Indexed} ChartData */ /** * @typedef {ChartData<_SingleValueData>} SingleValueData * @typedef {ChartData<_CandlestickData>} CandlestickData */ export default import("./v5.0.7-treeshaked/script.js").then((lc) => { const oklchToRGBA = createOklchToRGBA(); /** * @param {Object} args * @param {HTMLElement} args.element * @param {Signals} args.signals * @param {Colors} args.colors * @param {Index} args.index * @param {Utilities} args.utils * @param {Elements} args.elements * @param {DeepPartial} [args.options] */ function createLightweightChart({ element, signals, colors, index, utils, elements, options: _options = {}, }) { /** @satisfies {DeepPartial} */ const options = { autoSize: true, layout: { fontFamily: elements.style.fontFamily, background: { color: "transparent" }, attributionLogo: false, colorSpace: "display-p3", colorParsers: [oklchToRGBA], }, grid: { vertLines: { visible: false }, horzLines: { visible: false }, }, localization: { priceFormatter: utils.locale.numberToShortUSFormat, locale: "en-us", }, ..._options, }; /** @type {IChartApi} */ const chart = lc.createChart(element, options); chart.priceScale("right").applyOptions({ minimumWidth: 80, }); signals.createEffect( () => ({ defaultColor: colors.default(), offColor: colors.gray(), borderColor: colors.border(), }), ({ defaultColor, offColor, borderColor }) => { chart.applyOptions({ layout: { textColor: offColor, panes: { separatorColor: borderColor, }, }, rightPriceScale: { borderVisible: false, }, timeScale: { borderVisible: false, timeVisible: index === /** @satisfies {Height} */ (5) || index === /** @satisfies {DifficultyEpoch} */ (2) || index === /** @satisfies {HalvingEpoch} */ (4), minBarSpacing: index === /** @satisfies {MonthIndex} */ (7) ? 1 : index === /** @satisfies {QuarterIndex} */ (19) ? 3 : index === /** @satisfies {YearIndex} */ (23) ? 12 : index === /** @satisfies {DecadeIndex} */ (1) ? 120 : undefined, }, crosshair: { horzLine: { color: offColor, labelBackgroundColor: defaultColor, }, vertLine: { color: offColor, labelBackgroundColor: defaultColor, }, mode: 3, }, }); }, ); return chart; } /** * @param {Object} args * @param {string} args.id * @param {HTMLElement} args.parent * @param {Signals} args.signals * @param {Colors} args.colors * @param {Utilities} args.utils * @param {Elements} args.elements * @param {VecsResources} args.vecsResources * @param {Owner | null} [args.owner] * @param {true} [args.fitContentOnResize] * @param {{unit: Unit; blueprints: AnySeriesBlueprint[]}[]} [args.config] */ function createChartElement({ parent, signals, colors, utils, elements, id, vecsResources, owner: _owner, fitContentOnResize, config, }) { let owner = _owner || signals.getOwner(); const div = window.document.createElement("div"); div.classList.add("chart"); parent.append(div); const legendTop = createLegend({ parent: div, utils, signals, paneIndex: 0, }); const chartDiv = window.document.createElement("div"); chartDiv.classList.add("lightweight-chart"); div.append(chartDiv); const legendBottom = createLegend({ parent: div, utils, signals, paneIndex: 1, }); let ichart = /** @type {IChartApi | null} */ (null); let timeScaleSet = false; if (fitContentOnResize) { new ResizeObserver(() => ichart?.timeScale().fitContent()).observe( chartDiv, ); } /** @type {Index} */ let vecIndex = 0; // Default value, overwritten let timeResource = /** @type {VecResource| null} */ (null); let timeScaleSetCallback = /** @type {((unknownTimeScaleCallback: VoidFunction) => void) | null} */ ( null ); /** * @param {ISeriesApi} series * @param {VecResource} valuesResource */ function createSetFetchedDataEffect(series, valuesResource) { const fetchedKey = vecsResources.defaultFetchedKey; signals.runWithOwner(owner, () => signals.createEffect( () => [ timeResource?.fetched[fetchedKey].vec(), valuesResource.fetched[fetchedKey].vec(), ], ([indexes, _ohlcs]) => { if (!ichart) throw Error("IChart should be initialized"); if (!indexes || !_ohlcs) return; const ohlcs = /** @type {OHLCTuple[]} */ (_ohlcs); let length = Math.min(indexes.length, ohlcs.length); const data = new Array(length); let prevTime = null; let offset = 0; for (let i = 0; i < length; i++) { const time = indexes[i]; if (prevTime === time) { offset += 1; } const v = ohlcs[i]; if (v === null) { data[i - offset] = { time, value: NaN, }; } else if (typeof v === "number") { data[i - offset] = { time, value: v, }; } else { data[i - offset] = { time, open: v[0], high: v[1], low: v[2], close: v[3], }; } prevTime = time; } data.length -= offset; series.setData(data); timeScaleSetCallback?.(() => { if ( !timeScaleSet && (vecIndex === /** @satisfies {QuarterIndex} */ (19) || vecIndex === /** @satisfies {YearIndex} */ (23) || vecIndex === /** @satisfies {DecadeIndex} */ (1)) ) { ichart ?.timeScale() .setVisibleLogicalRange({ from: -1, to: data.length }); } }); timeScaleSet = true; }, ), ); } const activeResources = /** @type {VecResource[]} */ ([]); ichart?.subscribeCrosshairMove( utils.debounce(() => { activeResources.forEach((v) => { v.fetch(); }); }), ); const chart = { inner: () => ichart, /** * @param {Object} args * @param {Index} args.index * @param {((unknownTimeScaleCallback: VoidFunction) => void)} [args.timeScaleSetCallback] */ create({ index: _index, timeScaleSetCallback: _timeScaleSetCallback }) { vecIndex = _index; timeScaleSetCallback = _timeScaleSetCallback || null; if (ichart) throw Error("IChart shouldn't be initialized"); timeResource = vecsResources.getOrCreate( vecIndex, vecIndex === /** @satisfies {Height} */ (5) ? "timestamp-fixed" : "timestamp", ); timeResource.fetch(); ichart = createLightweightChart({ index: vecIndex, element: chartDiv, signals, colors, utils, elements, }); if (fitContentOnResize) { ichart.applyOptions({ handleScroll: false, handleScale: false, timeScale: { minBarSpacing: 0.001, }, }); } }, /** * @param {Object} args * @param {string} args.name * @param {Unit} args.unit * @param {VecId} [args.vecId] * @param {Accessor} [args.data] * @param {number} [args.paneIndex] * @param {boolean} [args.defaultActive] * @param {boolean} [args.inverse] */ addCandlestickSeries({ vecId, name, unit, paneIndex: _paneIndex, defaultActive, data, inverse, }) { const paneIndex = _paneIndex ?? 0; if (!ichart || !timeResource) throw Error("Chart not fully set"); const green = inverse ? colors.red() : colors.green(); const red = inverse ? colors.green() : colors.red(); const series = ichart.addSeries( /** @type {SeriesDefinition<'Candlestick'>} */ (lc.CandlestickSeries), { upColor: green, downColor: red, wickUpColor: green, wickDownColor: red, borderVisible: false, visible: defaultActive !== false, }, paneIndex, ); let url = /** @type {string | undefined} */ (undefined); if (vecId) { const valuesResource = vecsResources.getOrCreate(vecIndex, vecId); valuesResource.fetch(); activeResources.push(valuesResource); createSetFetchedDataEffect(series, valuesResource); url = valuesResource.url; } else if (data) { signals.runWithOwner(owner, () => signals.createEffect(data, (data) => { series.setData(data); }), ); } (paneIndex ? legendBottom : legendTop).add({ series, name, defaultActive, colors: [colors.green, colors.red], url, }); createPaneHeightObserver({ ichart, paneIndex, signals, utils, }); this.addPriceScaleSelectorIfNeeded({ paneIndex, seriesType: "Candlestick", id: `${id}-${paneIndex}`, unit, }); return series; }, /** * @param {Object} args * @param {string} args.name * @param {Unit} args.unit * @param {Accessor} [args.data] * @param {VecId} [args.vecId] * @param {Color} [args.color] * @param {number} [args.paneIndex] * @param {boolean} [args.defaultActive] * @param {DeepPartial} [args.options] */ addLineSeries({ vecId, name, unit, color, paneIndex: _paneIndex, defaultActive, data, options, }) { if (!ichart || !timeResource) throw Error("Chart not fully set"); const paneIndex = _paneIndex ?? 0; color ||= unit === "USD" ? colors.green : colors.orange; const series = ichart.addSeries( /** @type {SeriesDefinition<'Line'>} */ (lc.LineSeries), { lineWidth: /** @type {any} */ (1.5), visible: defaultActive !== false, priceLineVisible: false, color: color(), ...options, }, paneIndex, ); const priceLineOptions = options?.createPriceLine; if (priceLineOptions) { createPriceLine(series, priceLineOptions, colors); } let url = /** @type {string | undefined} */ (undefined); if (vecId) { const valuesResource = vecsResources.getOrCreate(vecIndex, vecId); valuesResource.fetch(); activeResources.push(valuesResource); createSetFetchedDataEffect(series, valuesResource); url = valuesResource.url; } else if (data) { signals.runWithOwner(owner, () => signals.createEffect(data, (data) => { series.setData(data); ichart ?.timeScale() .setVisibleLogicalRange({ from: -1, to: data.length }); }), ); } (paneIndex ? legendBottom : legendTop).add({ series, colors: [color], name, defaultActive, url, }); createPaneHeightObserver({ ichart, paneIndex, signals, utils, }); this.addPriceScaleSelectorIfNeeded({ paneIndex, seriesType: "Line", id: `${id}-${paneIndex}`, unit, }); return series; }, /** * @param {Object} args * @param {string} args.name * @param {Unit} args.unit * @param {Accessor} [args.data] * @param {VecId} [args.vecId] * @param {number} [args.paneIndex] * @param {boolean} [args.defaultActive] * @param {DeepPartial} [args.options] */ addBaselineSeries({ vecId, name, unit, paneIndex: _paneIndex, defaultActive, data, options, }) { if (!ichart || !timeResource) throw Error("Chart not fully set"); const paneIndex = _paneIndex ?? 0; const series = ichart.addSeries( /** @type {SeriesDefinition<'Baseline'>} */ (lc.BaselineSeries), { lineWidth: /** @type {any} */ (1.5), visible: defaultActive !== false, baseValue: { price: options?.createPriceLine?.value ?? 0, }, ...options, topLineColor: options?.topLineColor ?? colors.green(), bottomLineColor: options?.bottomLineColor ?? colors.red(), priceLineVisible: false, bottomFillColor1: "transparent", bottomFillColor2: "transparent", topFillColor1: "transparent", topFillColor2: "transparent", lineVisible: true, }, paneIndex, ); const priceLineOptions = options?.createPriceLine; if (priceLineOptions) { createPriceLine(series, priceLineOptions, colors); } let url = /** @type {string | undefined} */ (undefined); if (vecId) { const valuesResource = vecsResources.getOrCreate(vecIndex, vecId); valuesResource.fetch(); activeResources.push(valuesResource); createSetFetchedDataEffect(series, valuesResource); url = valuesResource.url; } else if (data) { signals.runWithOwner(owner, () => signals.createEffect(data, (data) => { series.setData(data); ichart ?.timeScale() .setVisibleLogicalRange({ from: -1, to: data.length }); }), ); } (paneIndex ? legendBottom : legendTop).add({ series, colors: [ () => options?.topLineColor ?? colors.green(), () => options?.bottomLineColor ?? colors.red(), ], name, defaultActive, url, }); createPaneHeightObserver({ ichart, paneIndex, signals, utils, }); this.addPriceScaleSelectorIfNeeded({ paneIndex, seriesType: "Baseline", id: `${id}-${paneIndex}`, unit, }); return series; }, /** * @param {Object} args * @param {Unit} args.unit * @param {string} args.id * @param {SeriesType} args.seriesType * @param {number} args.paneIndex */ addPriceScaleSelectorIfNeeded({ unit, paneIndex, id, seriesType }) { id = `${id}-scale`; this.addFieldsetIfNeeded({ id, paneIndex, position: "sw", createChild({ owner, pane }) { const { field, selected } = utils.dom.createHorizontalChoiceField({ choices: /** @type {const} */ (["lin", "log"]), id: utils.stringToId(`${id} ${unit}`), defaultValue: unit === "USD" && seriesType !== "Baseline" ? "log" : "lin", key: `${id}-price-scale-${paneIndex}`, signals, }); signals.runWithOwner(owner, () => { signals.createEffect(selected, (selected) => { try { pane.priceScale("right").applyOptions({ mode: selected === "lin" ? 0 : 1, }); } catch {} }); }); return field; }, }); }, /** * @param {Object} args * @param {string} args.id * @param {number} args.paneIndex * @param {"nw" | "ne" | "se" | "sw"} args.position * @param {number} [args.timeout] * @param {(args: {owner: Owner | null, pane: IPaneApi