// @ts-check /** * @import { OptionPath, PartialOption, PartialOptionsGroup, PartialOptionsTree, Option, OptionsGroup, Series, PriceSeriesType, ResourceDataset, TimeScale, SerializedHistory, TimeRange, Unit, Marker, Weighted, DatasetPath, OHLC, FetchedJSON, DatasetValue, FetchedResult, AnyDatasetPath, SeriesBlueprint, BaselineSpecificSeriesBlueprint, CandlestickSpecificSeriesBlueprint, LineSpecificSeriesBlueprint, SpecificSeriesBlueprintWithChart, Signal, Color, SettingsTheme, DatasetCandlestickData, FoldersFilter, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons } from "./types/self" * @import {createChart as CreateClassicChart, createChartEx as CreateCustomChart} from "./packages/lightweight-charts/v4.2.0/types"; * @import * as _ from "./packages/ufuzzy/v1.0.14/types" * @import { DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, SingleValueData, ISeriesApi, Time, LogicalRange, SeriesMarker, CandlestickData, SeriesType, BaselineStyleOptions, SeriesOptionsCommon } from "./packages/lightweight-charts/v4.2.0/types" * @import { DatePath, HeightPath, LastPath } from "./types/paths"; * @import { SignalOptions, untrack as Untrack } from "./packages/solid-signals/2024-04-17/types/core" * @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "./packages/solid-signals/2024-04-17/types/owner" * @import { createSignal as CreateSignal, createEffect as CreateEffect, Accessor, Setter, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner } from "./packages/solid-signals/2024-04-17/types/signals"; */ function importSignals() { return import("./packages/solid-signals/2024-04-17/script.js").then( (_signals) => { const signals = { createSolidSignal: /** @type {CreateSignal} */ (_signals.createSignal), createEffect: /** @type {CreateEffect} */ (_signals.createEffect), createMemo: /** @type {CreateMemo} */ (_signals.createMemo), createRoot: /** @type {CreateRoot} */ (_signals.createRoot), untrack: /** @type {Untrack} */ (_signals.untrack), getOwner: /** @type {GetOwner} */ (_signals.getOwner), runWithOwner: /** @type {RunWithOwner} */ (_signals.runWithOwner), onCleanup: /** @type {OnCleanup} */ (_signals.onCleanup), flushSync: _signals.flushSync, /** * @template T * @param {T} initialValue * @param {SignalOptions} [options] * @returns {Signal} */ createSignal(initialValue, options) { const [get, set] = this.createSolidSignal(initialValue, options); // @ts-ignore get.set = set; // @ts-ignore return get; }, /** * @param {(dispose: VoidFunction) => void} callback */ createUntrackedRoot: (callback) => signals.untrack(() => { signals.createRoot(callback); }), }; return signals; } ); } const signalsPromise = importSignals(); const utils = { /** * @param {string} serialized * @returns {boolean} */ isSerializedBooleanTrue(serialized) { return serialized === "true" || serialized === "1"; }, /** * @param {number} ms */ sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); }, yield() { return this.sleep(0); }, array: { /** * @template T * @param {T[]} array */ getRandomIndex(array) { return Math.floor(Math.random() * array.length); }, /** * @template T * @param {T[]} array */ getRandomElement(array) { return array[this.getRandomIndex(array)]; }, }, dom: { /** * @param {string} id * @returns {HTMLElement} */ getElementById(id) { const element = window.document.getElementById(id); if (!element) throw `Element with id = "${id}" should exist`; return element; }, /** * @param {string} name */ queryOrCreateMetaElement(name) { let meta = /** @type {HTMLMetaElement | null} */ ( window.document.querySelector(`meta[name="${name}"]`) ); if (!meta) { meta = window.document.createElement("meta"); meta.name = name; elements.head.appendChild(meta); } return meta; }, /** * @param {HTMLElement} element */ isHidden(element) { return element.tagName !== "BODY" && !element.offsetParent; }, /** * * @param {HTMLElement} element * @param {VoidFunction} callback */ onFirstIntersection(element, callback) { const observer = new IntersectionObserver((entries) => { for (let i = 0; i < entries.length; i++) { if (entries[i].isIntersecting) { callback(); observer.disconnect(); } } }); observer.observe(element); }, /** * @param {string} name */ createSpanName(name) { const spanName = window.document.createElement("span"); spanName.classList.add("name"); const [first, second, third] = name.split("-"); spanName.innerHTML = first; if (second) { const smallRest = window.document.createElement("small"); smallRest.innerHTML = `— ${second}`; spanName.append(smallRest); if (third) { throw "Shouldn't have more than one dash"; } } return spanName; }, /** * @param {Object} args * @param {string} args.inputName * @param {string} args.inputId * @param {string} args.inputValue * @param {boolean} [args.inputChecked=false] * @param {string} args.labelTitle * @param {(event: MouseEvent) => void} [args.onClick] */ createLabeledInput({ inputId, inputName, inputValue, inputChecked = false, labelTitle, onClick, }) { const label = window.document.createElement("label"); const input = window.document.createElement("input"); input.type = "radio"; input.name = inputName; input.id = inputId; input.value = inputValue; input.checked = inputChecked; label.append(input); label.id = `${inputId}-label`; // @ts-ignore label.for = inputId; label.title = labelTitle; label.addEventListener("click", onClick || (() => {})); return { label, input, }; }, /** * @param {Object} args * @param {string} args.name * @param {string} args.inputName * @param {string} args.inputId * @param {string} args.inputValue * @param {string} args.labelTitle * @param {(event: MouseEvent) => void} args.onClick */ createComplexLabeledInput({ inputId, inputName, inputValue, labelTitle, name, onClick, }) { const { label, input } = this.createLabeledInput({ inputId, inputName, inputValue, labelTitle, onClick, }); const spanMain = window.document.createElement("span"); spanMain.classList.add("main"); label.append(spanMain); const spanName = this.createSpanName(name); spanMain.append(spanName); return { label, input, spanMain, spanName, }; }, /** * @param {HTMLElement} parent * @param {HTMLElement} child * @param {number} index */ insertElementAtIndex(parent, child, index) { if (!index) index = 0; if (index >= parent.children.length) { parent.appendChild(child); } else { parent.insertBefore(child, parent.children[index]); } }, /** @param {string} url */ open(url) { console.log(`open: ${url}`); const a = window.document.createElement("a"); elements.body.append(a); a.href = url; a.rel = "noopener noreferrer"; a.click(); a.remove(); }, /** * @param {string} text */ createItalic(text) { const italic = window.document.createElement("i"); italic.innerHTML = text; return italic; }, }, url: { chartParamsWhitelist: ["from", "to"], /** * @param {Object} args * @param {URLSearchParams} [args.urlParams] * @param {string} [args.pathname] */ replaceHistory({ urlParams, pathname }) { urlParams ||= new URLSearchParams(window.location.search); pathname ||= window.location.pathname; window.history.replaceState( null, "", `${pathname}?${urlParams.toString()}` ); }, /** * @param {Option} option */ resetParams(option) { const urlParams = new URLSearchParams(); if (option.kind === "chart") { [...new URLSearchParams(window.location.search).entries()] .filter(([key, _]) => this.chartParamsWhitelist.includes(key)) .forEach(([key, value]) => { urlParams.set(key, value); }); } this.replaceHistory({ urlParams, pathname: option.id }); }, /** * @param {string} key * @param {string | boolean | undefined} value */ writeParam(key, value) { const urlParams = new URLSearchParams(window.location.search); if (value !== undefined) { urlParams.set(key, String(value)); } else { urlParams.delete(key); } this.replaceHistory({ urlParams }); }, /** * @param {string} key */ removeParam(key) { this.writeParam(key, undefined); }, /** * * @param {string} key * @returns {boolean | null} */ readBoolParam(key) { const urlParams = new URLSearchParams(window.location.search); const parameter = urlParams.get(key); if (parameter) { return utils.isSerializedBooleanTrue(parameter); } return null; }, }, locale: { /** * @param {number} value * @param {number} [digits] * @param {Intl.NumberFormatOptions} [options] */ numberToUSFormat(value, digits, options) { return value.toLocaleString("en-us", { ...options, minimumFractionDigits: digits, maximumFractionDigits: digits, }); }, /** @param {number} value */ numberToShortUSFormat(value) { const absoluteValue = Math.abs(value); // value = absoluteValue; if (isNaN(value)) { return ""; // } else if (value === 0) { // return "0"; } else if (absoluteValue < 10) { return utils.locale.numberToUSFormat(value, 3); } else if (absoluteValue < 100) { return utils.locale.numberToUSFormat(value, 2); } else if (absoluteValue < 1_000) { return utils.locale.numberToUSFormat(value, 1); } else if (absoluteValue < 100_000) { return utils.locale.numberToUSFormat(value, 0); } else if (absoluteValue < 1_000_000) { return `${utils.locale.numberToUSFormat(value / 1_000, 1)}K`; } else if (absoluteValue >= 1_000_000_000_000_000_000) { return "Inf."; } const log = Math.floor(Math.log10(absoluteValue) - 6); const suffices = ["M", "B", "T", "Q"]; const letterIndex = Math.floor(log / 3); const letter = suffices[letterIndex]; const modulused = log % 3; if (modulused === 0) { return `${utils.locale.numberToUSFormat( value / (1_000_000 * 1_000 ** letterIndex), 3 )}${letter}`; } else if (modulused === 1) { return `${utils.locale.numberToUSFormat( value / (1_000_000 * 1_000 ** letterIndex), 2 )}${letter}`; } else { return `${utils.locale.numberToUSFormat( value / (1_000_000 * 1_000 ** letterIndex), 1 )}${letter}`; } }, }, storage: { /** * @param {string} key */ readBool(key) { const saved = localStorage.getItem(key); if (saved) { return utils.isSerializedBooleanTrue(saved); } return null; }, /** * @param {string} key * @param {string | boolean | undefined} value */ write(key, value) { value !== undefined && value !== null ? localStorage.setItem(key, String(value)) : localStorage.removeItem(key); }, /** * @param {string} key */ remove(key) { this.write(key, undefined); }, }, formatters: { dollars: new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2, maximumFractionDigits: 2, }), percentage: new Intl.NumberFormat("en-US", { style: "percent", minimumFractionDigits: 2, maximumFractionDigits: 2, }), }, /** * * @template {(...args: any[]) => any} F * @param {F} callback * @param {number} [wait=250] */ debounce(callback, wait = 250) { /** @type {number | undefined} */ let timeoutId; /** @type {Parameters} */ let latestArgs; return (/** @type {Parameters} */ ...args) => { latestArgs = args; if (!timeoutId) { timeoutId = window.setTimeout(async () => { await callback(...latestArgs); timeoutId = undefined; }, wait); } }; }, /** * @param {VoidFunction} callback * @param {number} [timeout = 1] */ runWhenIdle(callback, timeout = 1) { if ("requestIdleCallback" in window) { requestIdleCallback(callback); } else { setTimeout(callback, timeout); } }, /** * @param {Date} date * @returns {string} */ dateToString(date) { return date.toJSON().split("T")[0]; }, /** * @param {Time} time */ dateFromTime(time) { return typeof time === "string" ? new Date(time) : // @ts-ignore new Date(time.year, time.month, time.day); }, /** * @param {Date} oldest * @param {Date} youngest * @returns {number} */ getNumberOfDaysBetweenTwoDates(oldest, youngest) { return Math.round( Math.abs((youngest.getTime() - oldest.getTime()) / consts.ONE_DAY_IN_MS) ); }, }; function initEnv() { const standalone = "standalone" in window.navigator && !!window.navigator.standalone; const userAgent = navigator.userAgent.toLowerCase(); const isChrome = userAgent.includes("chrome"); const safari = userAgent.includes("safari"); const safariOnly = safari && !isChrome; const macOS = userAgent.includes("mac os"); const iphone = userAgent.includes("iphone"); const ipad = userAgent.includes("ipad"); const ios = iphone || ipad; return { standalone, userAgent, isChrome, safari, safariOnly, macOS, iphone, ipad, ios, localhost: window.location.hostname === "localhost", }; } const env = initEnv(); function createConstants() { const ONE_SECOND_IN_MS = 1_000; const FIVE_SECOND_IN_MS = 5 * ONE_SECOND_IN_MS; const TEN_SECOND_IN_MS = 2 * FIVE_SECOND_IN_MS; const ONE_MINUTE_IN_MS = 6 * TEN_SECOND_IN_MS; const FIVE_MINUTES_IN_MS = 5 * ONE_MINUTE_IN_MS; const TEN_MINUTES_IN_MS = 2 * FIVE_MINUTES_IN_MS; const ONE_HOUR_IN_MS = 6 * TEN_MINUTES_IN_MS; const ONE_DAY_IN_MS = 24 * ONE_HOUR_IN_MS; const HEIGHT_CHUNK_SIZE = 10_000; const MEDIUM_WIDTH = 768; return { ONE_SECOND_IN_MS, FIVE_SECOND_IN_MS, TEN_SECOND_IN_MS, ONE_MINUTE_IN_MS, FIVE_MINUTES_IN_MS, TEN_MINUTES_IN_MS, ONE_HOUR_IN_MS, ONE_DAY_IN_MS, HEIGHT_CHUNK_SIZE, MEDIUM_WIDTH, }; } const consts = createConstants(); const ids = { selectedId: `selected-id`, selectedFrameSelectorLabel: `selected-frame-selector-label`, foldersFilter: "folders-filter", chartRange: "chart-range", from: "from", to: "to", checkedFrameSelectorLabel: "checked-frame-selector-label", /** * @param {TimeScale} scale */ visibleTimeRange(scale) { return `${ids.chartRange}-${scale}`; }, /** * @param {string} s */ fromString(s) { return s.replace(/\W/g, " ").trim().replace(/ +/g, "-").toLowerCase(); }, }; const elements = { head: window.document.getElementsByTagName("head")[0], body: window.document.body, main: utils.dom.getElementById("main"), aside: utils.dom.getElementById("aside"), selectedLabel: utils.dom.getElementById(ids.selectedFrameSelectorLabel), foldersLabel: utils.dom.getElementById(`folders-frame-selector-label`), searchLabel: utils.dom.getElementById(`search-frame-selector-label`), searchFrame: utils.dom.getElementById("search-frame"), historyFrame: utils.dom.getElementById("history-frame"), settingsFrame: utils.dom.getElementById("settings-frame"), foldersFrame: utils.dom.getElementById("folders-frame"), selectedFrame: utils.dom.getElementById("selected-frame"), historyList: utils.dom.getElementById("history-list"), searchInput: /** @type {HTMLInputElement} */ ( utils.dom.getElementById("search-input") ), searchSmall: utils.dom.getElementById("search-small"), searchResults: utils.dom.getElementById("search-results"), selectedTitle: utils.dom.getElementById("selected-title"), selectedDescription: utils.dom.getElementById("selected-description"), selectors: utils.dom.getElementById("frame-selectors"), foldersFilterAllCount: utils.dom.getElementById("folders-filter-all-count"), foldersFilterFavoritesCount: utils.dom.getElementById( "folders-filter-favorites-count" ), foldersFilterNewCount: utils.dom.getElementById("folders-filter-new-count"), chartList: utils.dom.getElementById("chart-list"), legend: utils.dom.getElementById("legend"), style: getComputedStyle(window.document.documentElement), buttonFavorite: utils.dom.getElementById("button-favorite"), timeScaleDateButtons: utils.dom.getElementById("timescale-date-buttons"), timeScaleHeightButtons: utils.dom.getElementById("timescale-height-buttons"), selectedHeader: utils.dom.getElementById("selected-header"), selectedHr: utils.dom.getElementById("selected-hr"), home: utils.dom.getElementById("home"), charts: utils.dom.getElementById("charts"), dashboards: utils.dom.getElementById("dashboards"), }; const savedSelectedId = localStorage.getItem(ids.selectedId); const isFirstTime = !savedSelectedId; const urlSelected = window.document.location.pathname.substring(1); function initFrameSelectors() { let selectedFrameLabel = localStorage.getItem(ids.checkedFrameSelectorLabel); const selectors = elements.selectors; const children = Array.from(selectors.children); /** @type {HTMLElement | undefined} */ let focusedSection = undefined; for (let i = 0; i < children.length; i++) { const element = children[i]; switch (element.tagName) { case "LABEL": { element.addEventListener("click", () => { const id = element.id; selectedFrameLabel = id; localStorage.setItem(ids.checkedFrameSelectorLabel, id); const sectionId = element.id.split("-").splice(0, 2).join("-"); // Remove -selector const section = window.document.getElementById(sectionId); if (!section) { console.log(sectionId, section); throw "Section should exist"; } if (section === focusedSection) { return; } section.hidden = false; if (focusedSection) { focusedSection.hidden = true; } focusedSection = section; }); break; } } } if (selectedFrameLabel && (!urlSelected || urlSelected === savedSelectedId)) { const frameLabel = window.document.getElementById(selectedFrameLabel); if (!frameLabel) throw "Frame should exist"; frameLabel.click(); } else { elements.selectedLabel.click(); } // When going from mobile view to desktop view, if selected frame was open, go to the folders frame new IntersectionObserver((entries) => { for (let i = 0; i < entries.length; i++) { if ( !entries[i].isIntersecting && entries[i].target === elements.selectedLabel && selectedFrameLabel === ids.selectedFrameSelectorLabel ) { elements.foldersLabel.click(); } } }).observe(elements.selectedLabel); function setSelectedFrameParent() { const { clientWidth } = window.document.documentElement; if (clientWidth >= consts.MEDIUM_WIDTH) { elements.aside.append(elements.selectedFrame); } else { elements.main.append(elements.selectedFrame); } } setSelectedFrameParent(); window.addEventListener("resize", setSelectedFrameParent); } initFrameSelectors(); function createKeyDownEventListener() { window.document.addEventListener("keydown", (event) => { switch (event.key) { case "Escape": { event.stopPropagation(); event.preventDefault(); elements.foldersLabel.click(); break; } case "/": { if (window.document.activeElement === elements.searchInput) { return; } event.stopPropagation(); event.preventDefault(); elements.searchLabel.click(); elements.searchInput.focus(); break; } } }); } createKeyDownEventListener(); signalsPromise.then((signals) => { const dark = signals.createSignal(true); function createLastHeightResource() { const lastHeight = signals.createSignal(0); function fetchLastHeight() { fetch("/api/last-height").then((response) => { response.json().then((json) => { if (typeof json === "number") { lastHeight.set(json); } }); }); } fetchLastHeight(); setInterval(fetchLastHeight, consts.TEN_SECOND_IN_MS, {}); return lastHeight; } const lastHeight = createLastHeightResource(); function createColors() { function lightRed() { const tailwindRed300 = "#fca5a5"; const tailwindRed800 = "#991b1b"; return dark() ? tailwindRed300 : tailwindRed800; } function red() { return "#e63636"; // 550 } function darkRed() { const tailwindRed900 = "#7f1d1d"; const tailwindRed100 = "#fee2e2"; return dark() ? tailwindRed900 : tailwindRed100; } function orange() { return elements.style.getPropertyValue("--orange"); // 550 } function darkOrange() { const tailwindOrange900 = "#7c2d12"; const tailwindOrange100 = "#ffedd5"; return dark() ? tailwindOrange900 : tailwindOrange100; } function amber() { return "#e78a05"; // 550 } function yellow() { return "#db9e03"; // 550 } function lime() { return "#74b713"; // 550 } function green() { return "#1cb454"; } function darkGreen() { const tailwindGreen900 = "#14532d"; const tailwindGreen100 = "#dcfce7"; return dark() ? tailwindGreen900 : tailwindGreen100; } function emerald() { return "#0ba775"; } function darkEmerald() { const tailwindEmerald900 = "#064e3b"; const tailwindEmerald100 = "#d1fae5"; return dark() ? tailwindEmerald900 : tailwindEmerald100; } function teal() { return "#10a697"; // 550 } function cyan() { return "#06a3c3"; // 550 } function sky() { return "#0794d8"; // 550 } function blue() { return "#2f73f1"; // 550 } function indigo() { return "#5957eb"; } function violet() { return "#834cf2"; } function purple() { return "#9d45f0"; } function fuchsia() { return "#cc37e1"; } function pink() { return "#e53882"; } function rose() { return "#ea3053"; } function darkRose() { const tailwindRose900 = "#881337"; const tailwindRose100 = "#ffe4e6"; return dark() ? tailwindRose900 : tailwindRose100; } function off() { const _ = dark(); return elements.style.getPropertyValue("--off-color"); } function textColor() { const _ = dark(); return elements.style.getPropertyValue("--color"); } return { default: textColor, off, lightBitcoin: yellow, bitcoin: orange, darkBitcoin: darkOrange, lightDollars: lime, dollars: emerald, darkDollars: darkEmerald, yellow, orange, red, _1d: lightRed, _1w: red, _8d: orange, _13d: amber, _21d: yellow, _1m: lime, _34d: green, _55d: emerald, _89d: teal, _144d: cyan, _6m: sky, _1y: blue, _2y: indigo, _200w: violet, _4y: purple, _10y: fuchsia, p2pk: lime, p2pkh: violet, p2sh: emerald, p2wpkh: cyan, p2wsh: pink, p2tr: blue, crab: red, fish: lime, humpback: violet, plankton: emerald, shark: cyan, shrimp: pink, whale: blue, megalodon: purple, realizedPrice: orange, oneMonthHolders: cyan, threeMonthsHolders: lime, sth: yellow, sixMonthsHolder: red, oneYearHolders: pink, twoYearsHolders: purple, lth: fuchsia, balancedPrice: yellow, cointimePrice: yellow, trueMarketMeanPrice: blue, vaultedPrice: green, cvdd: lime, terminalPrice: red, loss: red, darkLoss: darkRed, profit: green, darkProfit: darkGreen, thermoCap: green, investorCap: rose, realizedCap: orange, darkLiveliness: darkRose, liveliness: rose, vaultedness: green, activityToVaultednessRatio: violet, up_to_1d: lightRed, up_to_1w: red, up_to_1m: orange, up_to_2m: orange, up_to_3m: orange, up_to_4m: orange, up_to_5m: orange, up_to_6m: orange, up_to_1y: orange, up_to_2y: orange, up_to_3y: orange, up_to_4y: orange, up_to_5y: orange, up_to_7y: orange, up_to_10y: orange, up_to_15y: orange, from_10y_to_15y: purple, from_7y_to_10y: violet, from_5y_to_7y: indigo, from_3y_to_5y: sky, from_2y_to_3y: teal, from_1y_to_2y: green, from_6m_to_1y: lime, from_3m_to_6m: yellow, from_1m_to_3m: amber, from_1w_to_1m: orange, from_1d_to_1w: red, from_1y: green, from_2y: teal, from_4y: indigo, from_10y: violet, from_15y: fuchsia, coinblocksCreated: purple, coinblocksDestroyed: red, coinblocksStored: green, momentum: [green, yellow, red], momentumGreen: green, momentumYellow: yellow, momentumRed: red, probability0_1p: red, probability0_5p: orange, probability1p: yellow, year_2009: yellow, year_2010: yellow, year_2011: yellow, year_2012: yellow, year_2013: yellow, year_2014: yellow, year_2015: yellow, year_2016: yellow, year_2017: yellow, year_2018: yellow, year_2019: yellow, year_2020: yellow, year_2021: yellow, year_2022: yellow, year_2023: yellow, year_2024: yellow, }; } const colors = createColors(); function importLightweightCharts() { return window.document.fonts.ready.then(() => import("./packages/lightweight-charts/v4.2.0/script.js").then( ({ createChart: createClassicChart, createChartEx: createCustomChart, }) => { /** * @class * @implements {IHorzScaleBehavior} */ class HorzScaleBehaviorHeight { options() { return /** @type {any} */ (undefined); } setOptions() {} preprocessData() {} updateFormatter() {} createConverterToInternalObj() { /** @type {(p: any) => any} */ return (price) => price; } /** @param {any} item */ key(item) { return item; } /** @param {any} item */ cacheKey(item) { return item; } /** @param {any} item */ convertHorzItemToInternal(item) { return item; } /** @param {any} item */ formatHorzItem(item) { return item; } /** @param {any} tickMark */ formatTickmark(tickMark) { return tickMark.time.toLocaleString("en-us"); } /** @param {any} tickMarks */ maxTickMarkWeight(tickMarks) { return tickMarks.reduce( this.getMarkWithGreaterWeight, tickMarks[0] ).weight; } /** * @param {any} sortedTimePoints * @param {number} startIndex */ fillWeightsForPoints(sortedTimePoints, startIndex) { for ( let index = startIndex; index < sortedTimePoints.length; ++index ) { sortedTimePoints[index].timeWeight = this.computeHeightWeight( sortedTimePoints[index].time ); } } /** * @param {any} a * @param {any} b */ getMarkWithGreaterWeight(a, b) { return a.weight > b.weight ? a : b; } /** @param {number} value */ computeHeightWeight(value) { // if (value === Math.ceil(value / 1000000) * 1000000) { // return 12; // } if (value === Math.ceil(value / 100000) * 100000) { return 11; } if (value === Math.ceil(value / 10000) * 10000) { return 10; } if (value === Math.ceil(value / 1000) * 1000) { return 9; } if (value === Math.ceil(value / 100) * 100) { return 8; } if (value === Math.ceil(value / 50) * 50) { return 7; } if (value === Math.ceil(value / 25) * 25) { return 6; } if (value === Math.ceil(value / 10) * 10) { return 5; } if (value === Math.ceil(value / 5) * 5) { return 4; } if (value === Math.ceil(value)) { return 3; } if (value * 2 === Math.ceil(value * 2)) { return 1; } return 0; } } /** * @param {Object} args * @param {TimeScale} args.scale * @param {HTMLElement} args.element */ function createChart({ scale, element }) { /** @satisfies {DeepPartial} */ const options = { autoSize: true, layout: { fontFamily: "Satoshi Chart", // fontSize: 13, background: { color: "transparent" }, attributionLogo: false, }, grid: { vertLines: { visible: false }, horzLines: { visible: false }, }, timeScale: { minBarSpacing: 0.05, shiftVisibleRangeOnNewBar: false, allowShiftVisibleRangeOnWhitespaceReplacement: false, }, handleScale: { axisDoubleClickReset: { time: false, }, }, crosshair: { mode: 0, }, localization: { priceFormatter: utils.locale.numberToShortUSFormat, locale: "en-us", ...(scale === "date" ? { // dateFormat: "EEEE, dd MMM 'yy", } : {}), }, }; /** @type {IChartApi} */ let chart; if (scale === "date") { chart = createClassicChart(element, options); } else { const horzScaleBehavior = new HorzScaleBehaviorHeight(); // @ts-ignore chart = createCustomChart(element, horzScaleBehavior, options); } chart.priceScale("right").applyOptions({ scaleMargins: { top: 0.075, bottom: 0.05, }, minimumWidth: 78, }); signals.createEffect(() => { const { default: _defaultColor, off: _offColor } = colors; const defaultColor = _defaultColor(); const offColor = _offColor(); chart.applyOptions({ layout: { textColor: offColor, }, rightPriceScale: { borderVisible: false, }, timeScale: { borderVisible: false, }, crosshair: { horzLine: { color: defaultColor, labelBackgroundColor: defaultColor, }, vertLine: { color: defaultColor, labelBackgroundColor: defaultColor, }, }, }); }); return chart; } /** * @type {DeepPartial} */ const defaultSeriesOptions = { // @ts-ignore lineWidth: 1.5, priceLineVisible: false, baseLineVisible: false, baseLineColor: "", }; /** * @param {SpecificSeriesBlueprintWithChart} args */ function createBaseLineSeries({ chart, color, options, owner }) { const topLineColor = color || colors.profit; const bottomLineColor = color || colors.loss; function computeColors() { return { topLineColor: topLineColor(), bottomLineColor: bottomLineColor(), }; } const transparent = "transparent"; /** @type {DeepPartial} */ const seriesOptions = { priceScaleId: "right", ...defaultSeriesOptions, ...options, topFillColor1: transparent, topFillColor2: transparent, bottomFillColor1: transparent, bottomFillColor2: transparent, ...computeColors(), }; const series = chart.addBaselineSeries(seriesOptions); signals.runWithOwner(owner, () => { signals.createEffect(() => { series.applyOptions(computeColors()); }); }); return series; } /** * @param {SpecificSeriesBlueprintWithChart} args */ function createCandlesticksSeries({ chart, options, owner }) { function computeColors() { const upColor = colors.profit(); const downColor = colors.loss(); return { upColor, wickUpColor: upColor, downColor, wickDownColor: downColor, }; } const candlestickSeries = chart.addCandlestickSeries({ baseLineVisible: false, borderVisible: false, priceLineVisible: false, baseLineColor: "", borderColor: "", borderDownColor: "", borderUpColor: "", ...options, ...computeColors(), }); signals.runWithOwner(owner, () => { signals.createEffect(() => { candlestickSeries.applyOptions(computeColors()); }); }); return candlestickSeries; } /** * @param {SpecificSeriesBlueprintWithChart} args */ function createLineSeries({ chart, color, options, owner }) { function computeColors() { return { color: color(), }; } const series = chart.addLineSeries({ ...defaultSeriesOptions, ...options, ...computeColors(), }); signals.runWithOwner(owner, () => { signals.createEffect(() => { series.applyOptions(computeColors()); }); }); return series; } function initWhitespace() { const whitespaceStartDate = new Date("1970-01-01"); const whitespaceStartDateYear = whitespaceStartDate.getUTCFullYear(); const whitespaceStartDateMonth = whitespaceStartDate.getUTCMonth(); const whitespaceStartDateDate = whitespaceStartDate.getUTCDate(); const whitespaceEndDate = new Date("2141-01-01"); let whitespaceDateDataset = /** @type {(WhitespaceData | SingleValueData)[]} */ ([]); function initDateWhitespace() { whitespaceDateDataset = new Array( utils.getNumberOfDaysBetweenTwoDates( whitespaceStartDate, whitespaceEndDate ) ); // Hack to be able to scroll freely // Setting them all to NaN is much slower for (let i = 0; i < whitespaceDateDataset.length; i++) { const date = new Date( whitespaceStartDateYear, whitespaceStartDateMonth, whitespaceStartDateDate + i ); const time = utils.dateToString(date); if (i === whitespaceDateDataset.length - 1) { whitespaceDateDataset[i] = { time, value: NaN, }; } else { whitespaceDateDataset[i] = { time, }; } } } const heightStart = -50_000; let whitespaceHeightDataset = /** @type {WhitespaceData[]} */ ([]); function initHeightWhitespace() { whitespaceHeightDataset = new Array( (new Date().getUTCFullYear() - 2009 + 1) * 60_000 ); for (let i = 0; i < whitespaceHeightDataset.length; i++) { const height = heightStart + i; whitespaceHeightDataset[i] = { time: /** @type {Time} */ (height), }; } } /** * @param {IChartApi} chart * @param {TimeScale} scale * @returns {ISeriesApi<'Line'>} */ function setWhitespace(chart, scale) { const whitespace = chart.addLineSeries(); if (scale === "date") { if (!whitespaceDateDataset.length) { initDateWhitespace(); } whitespace.setData(whitespaceDateDataset); } else { if (!whitespaceHeightDataset.length) { initHeightWhitespace(); } whitespace.setData(whitespaceHeightDataset); const time = whitespaceHeightDataset.length; whitespace.update({ time: /** @type {Time} */ (time), value: NaN, }); } return whitespace; } return { setWhitespace }; } const { setWhitespace } = initWhitespace(); /** * * @param {Parameters[0]} args */ function createChartWithWhitespace({ element, scale }) { const chart = /** @type {IChartApi & {whitespace: ISeriesApi<"Line">}} */ ( createChart({ scale, element, }) ); chart.whitespace = setWhitespace(chart, scale); return chart; } return { createChart, createChartWithWhitespace, createBaseLineSeries, createCandlesticksSeries, createLineSeries, }; } ) ); } /** @type {ReturnType | undefined} */ let lightweightChartsPromise = undefined; function initOptions() { /** @type {Signal