// @ts-check // Lightweight Charts download link: https://unpkg.com/browse/lightweight-charts@4.2.0/dist/ /** * @import { FilePath, PartialPreset, PartialPresetFolder, PartialPresetTree, Preset, PresetFolder, PresetTree, ResourceDataset, Scale, SerializedPresetsHistory, TimeRange } from "./types/self" * @import * as _ from "./libraries/uFuzzy/uFuzzy.d.ts" * @import { DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior } from "./libraries/lightweight-charts/types.d.ts" */ /* * Reactivity * https://gist.github.com/1Marc/09e739caa6a82cc176ab4c2abd691814 * Credit Ryan Carniato https://frontendmasters.com/courses/reactivity-solidjs/ */ /** * @typedef Effect * @type {object} * @property {VoidFunction} execute * @property {Set>} dependencies */ /** * @template T * @typedef {{(): T}} Accessor */ /** * @template T * @typedef {{ (): T; set: (newValue: T | ((old: T) => T)) => void }} Signal */ /** @type {Effect[]} */ let context = []; /** @param {VoidFunction} fn */ function untrack(fn) { const prevContext = context; context = []; const res = fn(); context = prevContext; return res; } /** @param {Effect} observer */ function cleanup(observer) { for (const dep of observer.dependencies) { dep.delete(observer); } observer.dependencies.clear(); } /** * @param {Effect} observer * @param {Set} subscriptions */ function subscribe(observer, subscriptions) { subscriptions.add(observer); observer.dependencies.add(subscriptions); } /** * @template T * @param {T} value * @param {{ equals?: boolean}} [options] */ function createSignal(value, options) { const subscriptions = new Set(); function read() { const observer = context[context.length - 1]; if (observer) subscribe(observer, subscriptions); return value; } /** @param {T | ((old: T) => T)} newValue */ function write(newValue) { if (options?.equals !== false && value === newValue) { return; } if (typeof newValue === "function") { // @ts-ignore value = newValue(value); } else { value = newValue; } for (const observer of [...subscriptions]) { observer.execute(); } } read.set = write; return read; } /** @param {VoidFunction} fn */ function createEffect(fn) { /** @type {Effect} */ const effect = { execute() { cleanup(effect); context.push(effect); fn(); context.pop(); }, dependencies: new Set(), }; effect.execute(); return effect; } /** * @template T * @param {() => T} fn */ function createMemo(fn) { /** @type {Signal} */ const signal = /** @type {any} */ (createSignal(undefined)); createEffect(() => signal.set(fn())); return signal; } function createScope() { /** @type {Effect[]} */ const observers = []; return { /** @param {VoidFunction} callback */ observe(callback) { this.add(createEffect(callback)); }, /** @param {Effect} effect */ add(effect) { observers.push(effect); }, reset() { for (let i = 0; i < observers.length; i++) { cleanup(observers[i]); } observers.length = 0; }, }; } /** * @param {string} id * @returns {HTMLElement} */ function getElementById(id) { const element = window.document.getElementById(id); if (!element) throw `Element with id = "${id}" should exist`; return element; } /** * @param {string} s * @returns {string} */ function stringToId(s) { return s.replace(/\W/g, " ").trim().replace(/ +/g, "-").toLowerCase(); } /** @param {VoidFunction} callback */ function runWhenIdle(callback) { if ("requestIdleCallback" in window) { requestIdleCallback(callback); } else { setTimeout(callback, 1); } } // TODO: Wrap all on dom object const mainFrames = getElementById("frames"); const chartFrameSelectorLabelId = `selected-frame-selector-label`; const chartLabel = getElementById(chartFrameSelectorLabelId); const foldersLabel = getElementById(`folders-frame-selector-label`); const searchLabel = getElementById(`search-frame-selector-label`); const foldersFrame = getElementById("folders-frame"); const searchFrame = getElementById("search-frame"); const historyFrame = getElementById("history-frame"); const historyList = getElementById("history-list"); const searchInput = /** @type {HTMLInputElement} */ ( getElementById("search-input") ); const searchSmall = getElementById("search-small"); const searchResults = getElementById("search-results"); const presetTitle = getElementById("preset-title"); const presetDescription = getElementById("preset-description"); const foldersFilterAllCount = getElementById("folders-filter-all-count"); const foldersFilterFavoritesCount = getElementById( "folders-filter-favorites-count" ); const foldersFilterNewCount = getElementById("folders-filter-new-count"); (function initFrames() { const localStorageKey = "checked-frame-selector-label"; let selectedFrameLabel = localStorage.getItem(localStorageKey); const fieldset = window.document.getElementById("frame-selectors"); if (!fieldset) throw "Fieldset should exist"; const children = Array.from(fieldset.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", (event) => { const id = element.id; event.stopPropagation(); event.preventDefault(); const forId = element.getAttribute("for") || ""; const input = /** @type {HTMLInputElement | undefined} */ ( window.document.getElementById(forId) ); if (!input) throw "Shouldn't be possible"; selectedFrameLabel = id; localStorage.setItem(localStorageKey, id); input.checked = true; const sectionId = element.id.split("-").splice(0, 2).join("-"); 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?.parentElement === mainFrames) { focusedSection.hidden = true; } focusedSection = section; }); break; } } } if (selectedFrameLabel) { const frameLabel = window.document.getElementById(selectedFrameLabel); if (!frameLabel) throw "Frame should exist"; frameLabel.click(); } else { chartLabel.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 === chartLabel && selectedFrameLabel === chartFrameSelectorLabelId ) { foldersLabel.click(); } } }).observe(chartLabel); document.addEventListener("keydown", (event) => { switch (event.key) { case "Escape": { event.stopPropagation(); event.preventDefault(); foldersLabel.click(); break; } case "/": { if (window.document.activeElement === searchInput) { return; } event.stopPropagation(); event.preventDefault(); searchLabel.click(); searchInput.focus(); break; } } }); })(); const dark = createSignal(true); (function initEverythingRelatedToPresets() { /** @type {Signal} */ const selected = createSignal(/** @type {any} */ (undefined)); const selectedLocalStorageKey = `selected-id`; const savedSelectedId = localStorage.getItem(selectedLocalStorageKey); const firstTime = !savedSelectedId; /** * @param {Scale} scale * @returns {PartialPresetFolder} */ function createMarketPresets(scale) { return { name: "Market", tree: [ { scale, icon: "πŸ’΅", name: "Price", title: "Market Price", description: "", unit: "US Dollars", }, { scale, icon: "♾️", name: "Capitalization", title: "Market Capitalization", description: "", unit: "US Dollars", bottom: [ { title: "Market Cap.", datasetPath: `${scale}-to-market-cap`, color: "#f97315", }, ], }, ], }; } /** @type {PartialPresetTree} */ const partialTree = [ { name: "Dashboards", tree: [], }, { name: "Charts", tree: [ { name: "By Block Date", tree: [createMarketPresets("date")], }, { name: "By Block Height - Desktop/Tablet Only", tree: [createMarketPresets("height")], }, ], }, ]; /** @type {string[]} */ const presetsIds = []; /** @type {Preset[]} */ const presetsList = []; /** @type {HTMLDetailsElement[]} */ const detailsList = []; /** @typedef {'all' | 'favorites' | 'new'} FoldersFilter */ const foldersFilterLocalStorageKey = "folders-filter"; const filter = createSignal( /** @type {FoldersFilter} */ ( localStorage.getItem(foldersFilterLocalStorageKey) || "all" ) ); const favoritesCount = createSignal(0); const newCount = createSignal(0); /** @param {Preset} preset */ function updateCounters(preset) { let firstFavoritesRun = true; createEffect(() => { if (preset.isFavorite()) { favoritesCount.set((c) => c + 1); } else if (!firstFavoritesRun) { favoritesCount.set((c) => c - 1); } firstFavoritesRun = false; }); let firstNewRun = true; createEffect(() => { if (!preset.visited()) { newCount.set((c) => c + 1); } else if (!firstNewRun) { newCount.set((c) => c - 1); } firstNewRun = false; }); } /** @param {string} id */ function createVisitedPresetLocalStorageKey(id) { return `${id}-visited`; } /** @param {{preset: Preset, frame: string, name?: string, top?: string, id?: string}} args */ function createPresetLabel({ preset, frame, name, top, id }) { const label = window.document.createElement("label"); const input = window.document.createElement("input"); input.type = "radio"; id = id ? `-${id}` : ""; input.name = `preset-${frame}${id}`; input.id = `${preset.id}-${frame}${id}-selector`; input.value = preset.id; label.append(input); label.id = `${input.id}-label`; // @ts-ignore label.for = input.id; label.title = preset.title; if (top) { const small = window.document.createElement("small"); small.innerHTML = top; label.append(small); } const spanMain = window.document.createElement("span"); spanMain.classList.add("main"); label.append(spanMain); const spanEmoji = window.document.createElement("span"); spanEmoji.classList.add("emoji"); spanEmoji.innerHTML = preset.icon; spanMain.append(spanEmoji); const spanName = window.document.createElement("span"); spanName.classList.add("name"); spanName.innerHTML = name || preset.name; spanMain.append(spanName); /** @type {HTMLSpanElement | undefined} */ let spanNew; if (!preset.visited()) { spanNew = window.document.createElement("span"); spanNew.classList.add("new"); spanMain.append(spanNew); } /** @type {HTMLSpanElement | undefined} */ let spanFavorite; if (preset.isFavorite()) { spanFavorite = window.document.createElement("span"); spanFavorite.classList.add("favorite"); spanMain.append(spanFavorite); } if (!selected() && (firstTime || savedSelectedId === preset.id)) { selected.set(preset); } label.addEventListener("click", (event) => { event.stopPropagation(); event.preventDefault(); selected.set(preset); }); const effect = createEffect(() => { if (selected()?.id === preset.id) { input.checked = true; spanNew?.remove(); preset.visited.set(true); localStorage.setItem( createVisitedPresetLocalStorageKey(preset.id), "1" ); localStorage.setItem(selectedLocalStorageKey, preset.id); } else { input.checked = false; } }); return { label, effect }; } /** * @param {PartialPresetTree} partialTree * @param {HTMLElement} parent * @param {FilePath | undefined} path * @returns {Signal} */ function processPartialTree(partialTree, parent, path = undefined) { const ul = window.document.createElement("ul"); parent.appendChild(ul); /** @type {Accessor[]} */ const listForSum = []; partialTree.forEach((anyPartial) => { const li = window.document.createElement("li"); ul.appendChild(li); if ("tree" in anyPartial) { const folderId = stringToId( `${(path || [])?.map(({ name }) => name).join(" ")} ${ anyPartial.name } folder` ); /** @type {Omit} */ const restFolder = { id: folderId, }; Object.assign(anyPartial, restFolder); presetsIds.push(restFolder.id); const details = window.document.createElement("details"); const folderOpenLocalStorageKey = `${folderId}-open`; details.open = !!localStorage.getItem(folderOpenLocalStorageKey); detailsList.push(details); li.appendChild(details); const summary = window.document.createElement("summary"); summary.id = folderId; const spanMarker = window.document.createElement("span"); spanMarker.classList.add("marker"); spanMarker.innerHTML = "●"; summary.append(spanMarker); const spanName = window.document.createElement("span"); spanName.classList.add("name"); spanName.innerHTML = anyPartial.name; summary.append(spanName); const smallCount = window.document.createElement("small"); smallCount.hidden = details.open; summary.append(smallCount); details.appendChild(summary); details.addEventListener("toggle", () => { const open = details.open; smallCount.hidden = open; if (open) { spanMarker.innerHTML = "β—‹"; localStorage.setItem(folderOpenLocalStorageKey, "1"); } else { spanMarker.innerHTML = "●"; localStorage.removeItem(folderOpenLocalStorageKey); } }); const childPresetsCount = processPartialTree(anyPartial.tree, details, [ ...(path || []), { name: anyPartial.name, id: restFolder.id, }, ]); listForSum.push(childPresetsCount); createEffect(() => { const count = childPresetsCount(); smallCount.innerHTML = count.toString(); if (!count) { li.hidden = true; } else { li.hidden = false; } }); } else { const id = `${anyPartial.scale}-to-${stringToId(anyPartial.title)}`; const favoriteLocalStorageKey = `${id}-favorite`; /** @type {Omit} */ const restPreset = { id, path: path || [], serializedPath: `/ ${[ ...(path || []).map(({ name }) => name), anyPartial.name, ].join(" / ")}`, isFavorite: createSignal(false), visited: createSignal( !!localStorage.getItem(createVisitedPresetLocalStorageKey(id)) ), }; Object.assign(anyPartial, restPreset); const preset = /** @type {Preset} */ (anyPartial); updateCounters(preset); const { label } = createPresetLabel({ preset, frame: "folders", }); const inDom = createSignal(true); li.append(label); // DOM effect createEffect(() => { switch (filter()) { case "all": { if (!inDom()) { ul.append(li); inDom.set(true); } break; } case "favorites": { if (preset.isFavorite()) { if (!inDom()) { ul.append(li); inDom.set(true); } } else if (inDom()) { inDom.set(false); ul.removeChild(li); } break; } case "new": { if (!preset.visited()) { if (!inDom()) { ul.append(li); inDom.set(true); } } else if (inDom()) { inDom.set(false); ul.removeChild(li); } break; } } }); const memo = createMemo(() => (inDom() ? 1 : 0)); listForSum.push(memo); presetsList.push(preset); presetsIds.push(id); } }); return createMemo(() => listForSum.reduce((acc, s) => acc + s(), 0)); } const tree = window.document.createElement("div"); tree.classList.add("tree"); foldersFrame.append(tree); const allCount = processPartialTree(partialTree, tree); (function checkUniqueIds() { if (presetsIds.length !== new Set(presetsIds).size) { /** @type {Map} */ const m = new Map(); presetsIds.forEach((id) => { m.set(id, (m.get(id) || 0) + 1); }); console.log( [...m.entries()] .filter(([_, value]) => value > 1) .map(([key, _]) => key) ); throw Error("ID duplicate"); } })(); (function createCountersDomUpdateEffect() { foldersFilterAllCount.innerHTML = allCount().toLocaleString(); createEffect(() => { foldersFilterFavoritesCount.innerHTML = favoritesCount().toLocaleString(); }); createEffect(() => { foldersFilterNewCount.innerHTML = newCount().toLocaleString(); }); })(); (function initFilterElements() { const filterAllInput = /** @type {HTMLInputElement} */ ( getElementById("folders-filter-all") ); const filterFavoritesInput = /** @type {HTMLInputElement} */ ( getElementById("folders-filter-favorites") ); const filterNewInput = /** @type {HTMLInputElement} */ ( getElementById("folders-filter-new") ); filterAllInput.addEventListener("change", () => { filter.set("all"); }); filterFavoritesInput.addEventListener("change", () => { filter.set("favorites"); }); filterNewInput.addEventListener("change", () => { filter.set("new"); }); createEffect(() => { const f = filter(); localStorage.setItem(foldersFilterLocalStorageKey, f); switch (f) { case "all": { filterAllInput.checked = true; break; } case "favorites": { filterFavoritesInput.checked = true; break; } case "new": { filterNewInput.checked = true; break; } } }); })(); (function initCloseAllButton() { getElementById("button-close-all-folders").addEventListener("click", () => { detailsList.forEach((details) => (details.open = false)); }); })(); (function initGoToSelectedButton() { function goToSelected() { filter.set("all"); if (!selected()) throw "Selected should be set by now"; const selectedId = selected().id; selected().path.forEach(({ id }) => { const summary = getElementById(id); const details = /** @type {HTMLDetailsElement | undefined} */ ( summary.parentElement ); if (!details) throw "Parent details should exist"; if (!details.open) { summary.click(); } }); setTimeout(() => { getElementById(`${selectedId}-folders-selector`).scrollIntoView({ behavior: "instant", block: "center", }); }, 0); } getElementById("button-go-to-selected").addEventListener( "click", goToSelected ); if (firstTime) { goToSelected(); } })(); createEffect(() => { const preset = selected(); presetTitle.innerHTML = preset.title; presetDescription.innerHTML = preset.serializedPath; }); const LOCAL_STORAGE_RANGE_KEY = "chart-range"; const URL_PARAMS_RANGE_FROM_KEY = "from"; const URL_PARAMS_RANGE_TO_KEY = "to"; const HEIGHT_CHUNK_SIZE = 10_000; /** * @param {Scale} scale * @returns {string} */ function getVisibleRangeLocalStorageKey(scale) { return `${LOCAL_STORAGE_RANGE_KEY}-${scale}`; } /** * @param {Scale} scale * @returns {TimeRange} */ function getInitialVisibleRange(scale) { const urlParams = new URLSearchParams(window.location.search); const urlFrom = urlParams.get(URL_PARAMS_RANGE_FROM_KEY); const urlTo = urlParams.get(URL_PARAMS_RANGE_TO_KEY); if (urlFrom && urlTo) { if (scale === "date" && urlFrom.includes("-") && urlTo.includes("-")) { return { from: new Date(urlFrom).toJSON().split("T")[0], to: new Date(urlTo).toJSON().split("T")[0], }; } else if ( scale === "height" && !urlFrom.includes("-") && !urlTo.includes("-") ) { return { from: Number(urlFrom), to: Number(urlTo), }; } } /** @type {TimeRange | null} */ const savedTimeRange = /** @type {any} */ ( JSON.parse( localStorage.getItem(getVisibleRangeLocalStorageKey(scale)) || "null" ) ); if (savedTimeRange) { return savedTimeRange; } switch (scale) { case "date": { const defaultTo = new Date(); const defaultFrom = new Date(); defaultFrom.setDate(defaultFrom.getUTCDate() - 6 * 30); return { from: defaultFrom.toJSON().split("T")[0], to: defaultTo.toJSON().split("T")[0], }; } case "height": { return { from: 800_000, to: 850_000, }; } } } runWhenIdle(() => import("./libraries/lightweight-charts/standalone.mjs").then( ({ createChart: createClassicChart }) => { const scale = createMemo(() => selected().scale); const activeDatasets = createSignal( /** @type {ResourceDataset[]} */ ([]), { equals: false, } ); const visibleRange = createSignal(getInitialVisibleRange(scale())); const visibleDatasetIds = createSignal(/** @type {number[]} */ ([]), { equals: false, }); function setActiveIds() { /** @type {number[]} */ let ids = []; const today = new Date(); const { from: rawFrom, to: rawTo } = visibleRange(); if (typeof rawFrom === "string" && typeof rawTo === "string") { const from = new Date(rawFrom).getUTCFullYear(); const to = new Date(rawTo).getUTCFullYear(); ids = Array.from( { length: to - from + 1 }, (_, i) => i + from ).filter((year) => year >= 2009 && year <= today.getUTCFullYear()); } else { const from = Math.floor(Number(rawFrom) / HEIGHT_CHUNK_SIZE); const to = Math.floor(Number(rawTo) / HEIGHT_CHUNK_SIZE); const length = to - from + 1; ids = Array.from( { length }, (_, i) => (from + i) * HEIGHT_CHUNK_SIZE ); } const old = visibleDatasetIds(); if ( old.length !== ids.length || old.at(0) !== ids.at(0) || old.at(-1) !== ids.at(-1) ) { console.log("range:", ids); visibleDatasetIds.set(ids); } } setActiveIds(); (function createFetchChunksOfVisibleDatasetsEffect() { // Fetch visible dataset createEffect(() => { const ids = visibleDatasetIds(); const datasets = activeDatasets(); for (let i = 0; i < ids.length; i++) { const id = ids[i]; for (let j = 0; j < datasets.length; j++) { datasets[j].fetch(id); } } }); })(); /** @param {number} value */ function numberToShortUSLocale(value) { const absoluteValue = Math.abs(value); // value = absoluteValue; if (isNaN(value)) { return ""; // } else if (value === 0) { // return "0"; } else if (absoluteValue < 10) { return numberToUSLocale(value, 3); } else if (absoluteValue < 100) { return numberToUSLocale(value, 2); } else if (absoluteValue < 1_000) { return numberToUSLocale(value, 1); } else if (absoluteValue < 100_000) { return numberToUSLocale(value, 0); } else if (absoluteValue < 1_000_000) { return `${numberToUSLocale(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 `${numberToUSLocale( value / (1_000_000 * 1_000 ** letterIndex), 3 )}${letter}`; } else if (modulused === 1) { return `${numberToUSLocale( value / (1_000_000 * 1_000 ** letterIndex), 2 )}${letter}`; } else { return `${numberToUSLocale( value / (1_000_000 * 1_000 ** letterIndex), 1 )}${letter}`; } } /** * @param {number} value * @param {number} [digits] * @param {Intl.NumberFormatOptions} [options] */ function numberToUSLocale(value, digits, options) { return value.toLocaleString("en-us", { ...options, minimumFractionDigits: digits, maximumFractionDigits: digits, }); } /** * @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; } } const colors = (function createColors() { /** @param {Accessor} dark */ function lightRed(dark) { const tailwindRed300 = "#fca5a5"; const tailwindRed800 = "#991b1b"; return dark() ? tailwindRed300 : tailwindRed800; } /** @param {Accessor} dark */ function red(dark) { return "#e63636"; // 550 } /** @param {Accessor} dark */ function darkRed(dark) { const tailwindRed900 = "#7f1d1d"; const tailwindRed100 = "#fee2e2"; return dark() ? tailwindRed900 : tailwindRed100; } /** @param {Accessor} dark */ function orange(dark) { return "#fa5c00"; // 550 } /** @param {Accessor} dark */ function darkOrange(dark) { const tailwindOrange900 = "#7c2d12"; const tailwindOrange100 = "#ffedd5"; return dark() ? tailwindOrange900 : tailwindOrange100; } /** @param {Accessor} dark */ function amber(dark) { return "#df9408"; // 550 } /** @param {Accessor} dark */ function yellow(dark) { return "#db9e03"; // 550 } /** @param {Accessor} dark */ function lime(dark) { return "#74b713"; // 550 } /** @param {Accessor} dark */ function green(dark) { return "#1cb454"; } /** @param {Accessor} dark */ function darkGreen(dark) { const tailwindGreen900 = "#14532d"; const tailwindGreen100 = "#dcfce7"; return dark() ? tailwindGreen900 : tailwindGreen100; } /** @param {Accessor} dark */ function emerald(dark) { return "#0ba775"; } /** @param {Accessor} dark */ function darkEmerald(dark) { const tailwindEmerald900 = "#064e3b"; const tailwindEmerald100 = "#d1fae5"; return dark() ? tailwindEmerald900 : tailwindEmerald100; } /** @param {Accessor} dark */ function teal(dark) { return "#10a697"; // 550 } /** @param {Accessor} dark */ function cyan(dark) { return "#06a3c3"; // 550 } /** @param {Accessor} dark */ function sky(dark) { return "#0794d8"; // 550 } /** @param {Accessor} dark */ function blue(dark) { return "#2f73f1"; // 550 } /** @param {Accessor} dark */ function indigo(dark) { return "#5957eb"; } /** @param {Accessor} dark */ function violet(dark) { return "#834cf2"; } /** @param {Accessor} dark */ function purple(dark) { return "#9d45f0"; } /** @param {Accessor} dark */ function fuchsia(dark) { return "#cc37e1"; } /** @param {Accessor} dark */ function pink(dark) { return "#e53882"; } /** @param {Accessor} dark */ function rose(dark) { return "#ea3053"; } /** @param {Accessor} dark */ function darkRose(dark) { const tailwindRose900 = "#881337"; const tailwindRose100 = "#ffe4e6"; return dark() ? tailwindRose900 : tailwindRose100; } /** @param {Accessor} dark */ function darkWhite(dark) { const tailwindGray400 = "#a3a3a3"; const tailwindGray600 = "#525252"; return dark() ? tailwindGray400 : tailwindGray600; } /** @param {Accessor} dark */ function gray(dark) { const tailwindGray400 = "#a3a3a3"; const tailwindGray600 = "#525252"; return dark() ? tailwindGray600 : tailwindGray400; } /** @param {Accessor} dark */ function white(dark) { return dark() ? "#ffffff" : "#000000"; } /** @param {Accessor} dark */ function black(dark) { return dark() ? "#000000" : "#ffffff"; } return { white, black, darkWhite, gray, lightBitcoin: yellow, bitcoin: orange, darkBitcoin: darkOrange, lightDollars: lime, dollars: emerald, darkDollars: darkEmerald, _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, ethereum: indigo, usdt: emerald, usdc: blue, ust: red, busd: yellow, usdd: emerald, frax: gray, dai: amber, tusd: indigo, pyusd: blue, 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, }; })(); /** @arg {{scale: Scale, element: HTMLElement}} args */ function createChart({ scale, element }) { /** @satisfies {DeepPartial} */ const options = { autoSize: true, layout: { fontFamily: "Satoshi Chart", 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: numberToShortUSLocale, locale: "en-us", }, }; /** @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, }); createEffect(() => { const { white } = colors; const color = white(dark); chart.applyOptions({ layout: { textColor: "#666666", }, rightPriceScale: { borderVisible: false, }, timeScale: { borderVisible: false, }, crosshair: { horzLine: { color: color, labelBackgroundColor: color, }, vertLine: { color: color, labelBackgroundColor: color, }, }, }); }); return chart; } createEffect(() => { const preset = selected(); const configs = [preset.top || [], preset.bottom].flatMap((list) => list ? [list] : [] ); const scale = preset.scale; configs.forEach((config, index) => { const chart = createChart({ scale, element: div, }); }); }); } ) ); (function initSearchFrame() { getElementById("reset-search").addEventListener("click", () => { searchInput.value = ""; searchInput.dispatchEvent(new Event("input")); searchInput.focus(); }); searchInput.addEventListener( "focus", () => { const haystack = presetsList.map( (preset) => `${preset.title}\t${preset.serializedPath}` ); const searchSmallOgInnerHTML = searchSmall.innerHTML; const RESULTS_PER_PAGE = 100; const scope = createScope(); import("./libraries/uFuzzy/uFuzzy.esm.js").then( ({ default: ufuzzy }) => { /** * @param {uFuzzy.SearchResult} searchResult * @param {number} pageIndex */ function computeResultPage(searchResult, pageIndex) { /** @type {{ preset: Preset, path: string, title: string }[]} */ let list = []; let [indexes, info, order] = searchResult || [null, null, null]; const minIndex = pageIndex * RESULTS_PER_PAGE; if (indexes?.length) { const maxIndex = Math.min( (order || indexes).length - 1, minIndex + RESULTS_PER_PAGE - 1 ); list = Array(maxIndex - minIndex + 1); if (info && order) { for (let i = minIndex; i <= maxIndex; i++) { let infoIdx = order[i]; const [title, path] = ufuzzy .highlight( haystack[info.idx[infoIdx]], info.ranges[infoIdx] ) .split("\t"); list[i % 100] = { preset: presetsList[info.idx[infoIdx]], path, title, }; } } else { for (let i = minIndex; i <= maxIndex; i++) { let index = indexes[i]; const [title, path] = haystack[index].split("\t"); list[i % 100] = { preset: presetsList[index], path, title, }; } } } return list; } /** @type {uFuzzy.Options} */ const config = { intraIns: Infinity, intraChars: `[a-z\d' ]`, }; const fuzzyMultiInsert = /** @type {uFuzzy} */ ( ufuzzy({ intraIns: 1, }) ); const fuzzyMultiInsertFuzzier = /** @type {uFuzzy} */ ( ufuzzy(config) ); const fuzzySingleError = /** @type {uFuzzy} */ ( ufuzzy({ intraMode: 1, ...config, }) ); /** @type {import("./libraries/uFuzzy/uFuzzy.d.ts")} */ const fuzzySingleErrorFuzzier = /** @type {uFuzzy} */ ( ufuzzy({ intraMode: 1, ...config, }) ); searchInput.addEventListener("input", () => { scope.reset(); searchResults.scrollTo({ top: 0, }); const needle = /** @type {string} */ (searchInput.value); if (!needle) { searchSmall.innerHTML = searchSmallOgInnerHTML; searchResults.innerHTML = ""; return; } const outOfOrder = 5; const infoThresh = 5_000; let result = fuzzyMultiInsert?.search( haystack, needle, undefined, infoThresh ); if (!result?.[0]?.length || !result?.[1]) { result = fuzzyMultiInsert?.search( haystack, needle, outOfOrder, infoThresh ); } if (!result?.[0]?.length || !result?.[1]) { result = fuzzySingleError?.search( haystack, needle, outOfOrder, infoThresh ); } if (!result?.[0]?.length || !result?.[1]) { result = fuzzySingleErrorFuzzier?.search( haystack, needle, outOfOrder, infoThresh ); } if (!result?.[0]?.length || !result?.[1]) { result = fuzzyMultiInsertFuzzier?.search( haystack, needle, undefined, infoThresh ); } if (!result?.[0]?.length || !result?.[1]) { result = fuzzyMultiInsertFuzzier?.search( haystack, needle, outOfOrder, infoThresh ); } searchSmall.innerHTML = `Found ${ result?.[0]?.length || 0 } preset(s)`; searchResults.innerHTML = ""; const list = computeResultPage(result, 0); list.forEach(({ preset, path, title }) => { const li = window.document.createElement("li"); searchResults.appendChild(li); const { label, effect } = createPresetLabel({ preset, frame: "search", name: title, top: path, }); scope.add(effect); li.append(label); }); }); } ); }, { once: true, } ); })(); (function initHistory() { const LOCAL_STORAGE_HISTORY_KEY = "history"; const MAX_HISTORY_LENGTH = 1_000; const history = /** @type {SerializedPresetsHistory} */ ( JSON.parse(localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY) || "[]") ).flatMap(([presetId, timestamp]) => { const preset = presetsList.find((preset) => preset.id === presetId); return preset ? [{ preset, date: new Date(timestamp) }] : []; }); /** @param {Date} date */ function dateToTestedString(date) { return date.toLocaleString().split(",")[0]; } /** @param {Date} date */ function dateToDisplayedString(date) { const formattedDate = dateToTestedString(date); const now = new Date(); if (dateToTestedString(now) === formattedDate) { return "Today"; } now.setUTCDate(now.getUTCDate() - 1); if (dateToTestedString(now) === formattedDate) { return "Yesterday"; } return date.toLocaleDateString(undefined, { weekday: "long", year: "numeric", month: "long", day: "numeric", }); } const grouped = history.reduce((grouped, { preset, date }) => { grouped[dateToTestedString(date)] ||= []; grouped[dateToTestedString(date)].push({ preset, date }); return grouped; }, /** @type {Record} */ ({})); /** @type {[string|undefined, string|undefined]} */ const firstTwo = [undefined, undefined]; (function initHistoryListInDom() { Object.entries(grouped).forEach(([key, tuples], index) => { if (index < 2) { firstTwo[index] = key; } const heading = window.document.createElement("h4"); heading.id = key; heading.innerHTML = dateToDisplayedString(tuples[0].date); historyList.append(heading); tuples.forEach(({ preset, date }) => { historyList.append( createPresetLabel({ preset, frame: "history", name: preset.title, id: date.valueOf().toString(), top: date.toLocaleTimeString(), }).label ); }); }); })(); createEffect(() => { const preset = selected(); const date = new Date(); const testedString = dateToTestedString(date); const { label } = createPresetLabel({ preset, frame: "history", name: preset.title, id: date.valueOf().toString(), top: date.toLocaleTimeString(), }); const li = window.document.createElement("li"); li.append(label); if (testedString === firstTwo[0]) { if (selected() === grouped[testedString].at(0)?.preset) { return; } grouped[testedString].unshift({ preset, date }); getElementById(testedString).after(li); } else { const [first, second] = firstTwo; /** @param {string | undefined} id */ function updateHeading(id) { if (!id) return; getElementById(id).innerHTML = dateToDisplayedString( grouped[id][0].date ); } updateHeading(first); updateHeading(second); const heading = window.document.createElement("h4"); heading.innerHTML = dateToDisplayedString(date); heading.id = testedString; historyList.prepend(li); historyList.prepend(heading); grouped[testedString] = [{ preset, date }]; firstTwo[1] = firstTwo[0]; firstTwo[0] = testedString; } history.unshift({ date: new Date(), preset, }); runWhenIdle(() => { /** @type {SerializedPresetsHistory} */ const serializedHistory = history.map(({ preset, date }) => [ preset.id, date.getTime(), ]); if (serializedHistory.length > MAX_HISTORY_LENGTH) { serializedHistory.length = MAX_HISTORY_LENGTH; } localStorage.setItem( LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(serializedHistory) ); }); }); })(); })();