global: snapshot

This commit is contained in:
nym21
2026-03-26 20:23:09 +01:00
parent 18bb4186a8
commit 259960b80b
9 changed files with 136 additions and 203 deletions

View File

@@ -6,7 +6,7 @@ import {
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";
import { createLegend } from "./legend.js";
import { createLegend, createSeriesLegend } from "./legend.js";
import { capture } from "./capture.js";
import { colors } from "../utils/colors.js";
import { createRadios, createSelect, getElementById } from "../utils/dom.js";
@@ -78,63 +78,34 @@ const MAX_SIZE = 10_000;
/** @returns {RangePreset[]} */
function getRangePresets() {
const nowSec = Math.floor(Date.now() / 1000);
const now = new Date();
const y = now.getUTCFullYear();
const m = now.getUTCMonth();
const d = now.getUTCDate();
/** @param {number} n */
const monthsAgo = (n) => Math.floor(Date.UTC(y, m - n, d) / 1000);
/** @param {number} months @param {number} [days] */
const ago = (months, days = 0) => Math.floor(Date.UTC(y, m - months, d - days) / 1000);
/** @type {RangePreset[]} */
const presets = [
{
label: "1w",
index: /** @type {IndexLabel} */ ("30mn"),
from: nowSec - 7 * 86_400,
},
{
label: "1m",
index: /** @type {IndexLabel} */ ("1h"),
from: monthsAgo(1),
},
{
label: "3m",
index: /** @type {IndexLabel} */ ("4h"),
from: monthsAgo(3),
},
{
label: "6m",
index: /** @type {IndexLabel} */ ("12h"),
from: monthsAgo(6),
},
{
label: "1y",
index: /** @type {IndexLabel} */ ("1d"),
from: monthsAgo(12),
},
{
label: "4y",
index: /** @type {IndexLabel} */ ("3d"),
from: monthsAgo(48),
},
{ label: "1w", index: /** @type {IndexLabel} */ ("30mn"), from: ago(0, 7) },
{ label: "1m", index: /** @type {IndexLabel} */ ("1h"), from: ago(1) },
{ label: "3m", index: /** @type {IndexLabel} */ ("4h"), from: ago(3) },
{ label: "6m", index: /** @type {IndexLabel} */ ("12h"), from: ago(6) },
{ label: "1y", index: /** @type {IndexLabel} */ ("1d"), from: ago(12) },
{ label: "4y", index: /** @type {IndexLabel} */ ("3d"), from: ago(48) },
{ label: "8y", index: /** @type {IndexLabel} */ ("1w"), from: ago(96) },
];
// Insert ytd at the right position
const ytdFrom = Math.floor(Date.UTC(y, 0, 1) / 1000);
const ri = presets.findIndex((e) => e.from <= ytdFrom);
const insertAt = ri === -1 ? presets.length : ri;
presets.splice(insertAt, 0, {
label: "ytd",
index: presets[ri === -1 ? presets.length - 1 : ri].index,
index: presets[Math.min(insertAt, presets.length - 1)].index,
from: ytdFrom,
});
presets.push({
label: "all",
index: /** @type {IndexLabel} */ ("1w"),
from: -Infinity,
});
presets.push({ label: "all", index: /** @type {IndexLabel} */ ("1w"), from: -Infinity });
return presets;
}
@@ -248,7 +219,7 @@ export function createChart({ parent, brk, fitContent }) {
range.set(value);
};
const legends = [createLegend(), createLegend()];
const legends = [createSeriesLegend(), createSeriesLegend()];
const root = document.createElement("div");
root.classList.add("chart");
@@ -352,7 +323,6 @@ export function createChart({ parent, brk, fitContent }) {
const offColor = colors.gray();
const borderColor = colors.border();
const offBorderColor = colors.offBorder();
console.log(borderColor);
ichart.applyOptions({
layout: {
textColor: offColor,
@@ -1540,7 +1510,7 @@ export function createChart({ parent, brk, fitContent }) {
// Rebuild when index changes
index.onChange.add(() => blueprints.rebuild());
// Index selector — injected into the last tr of the chart table
// Index selector + range presets
let preferredIndex = index.name.value;
/** @type {HTMLElement | null} */
let indexField = null;
@@ -1561,11 +1531,10 @@ export function createChart({ parent, brk, fitContent }) {
}
const data = blueprints.panes[0].series[0]?.getData();
if (!data?.length) return;
const from = isFinite(preset.from)
? (data.findIndex(
(d) => /** @type {number} */ (d.time) >= preset.from,
) ?? 0)
: 0;
const fi = data.findIndex(
(d) => /** @type {number} */ (d.time) >= preset.from,
);
const from = fi === -1 ? 0 : fi;
const padding = Math.round((data.length - from) * 0.025);
ichart.timeScale().setVisibleLogicalRange({
from: from - padding,
@@ -1592,33 +1561,28 @@ export function createChart({ parent, brk, fitContent }) {
index.name.set(currentValue);
}
indexField = window.document.createElement("div");
indexField.classList.add("index-bar");
const legend = createLegend();
indexField = legend.element;
const scroller = window.document.createElement("div");
indexField.append(scroller);
const selectField = createSelect({
initialValue: currentValue,
onChange: (v) => {
preferredIndex = v;
index.name.set(v);
},
choices,
groups,
id: "index",
});
scroller.append(selectField);
const sep = window.document.createElement("span");
sep.textContent = "|";
scroller.append(sep);
legend.setPrefix(
createSelect({
initialValue: currentValue,
onChange: (v) => {
preferredIndex = v;
index.name.set(v);
},
choices,
groups,
id: "index",
}),
);
for (const preset of getRangePresets()) {
const btn = window.document.createElement("button");
btn.textContent = preset.label;
btn.title = `${preset.label} at ${preset.index} interval`;
btn.addEventListener("click", () => applyPreset(preset));
scroller.append(btn);
legend.scroller.append(btn);
}
chartEl.append(indexField);

View File

@@ -1,20 +1,52 @@
import { createLabeledInput, createSpanName } from "../utils/dom.js";
import { stringToId } from "../utils/format.js";
/** @param {HTMLElement} el */
function captureScroll(el) {
el.addEventListener("wheel", (e) => e.stopPropagation(), { passive: true });
el.addEventListener("touchstart", (e) => e.stopPropagation(), { passive: true });
el.addEventListener("touchmove", (e) => e.stopPropagation(), { passive: true });
}
/**
* Creates a `<legend>` with a scrollable `<div>`.
* Call `setPrefix(el)` to insert a prefix element followed by a `|` separator.
* Append further content to `scroller`.
*/
export function createLegend() {
const element = window.document.createElement("legend");
const scroller = window.document.createElement("div");
const items = window.document.createElement("div");
scroller.append(items);
const element = /** @type {HTMLLegendElement} */ (
window.document.createElement("legend")
);
const scroller = /** @type {HTMLDivElement} */ (
window.document.createElement("div")
);
element.append(scroller);
captureScroll(scroller);
/** @param {HTMLElement} el */
function captureScroll(el) {
el.addEventListener("wheel", (e) => e.stopPropagation(), { passive: true });
el.addEventListener("touchstart", (e) => e.stopPropagation(), { passive: true });
el.addEventListener("touchmove", (e) => e.stopPropagation(), { passive: true });
}
const separator = window.document.createElement("span");
separator.textContent = "|";
captureScroll(separator);
return {
element,
scroller,
/** @param {HTMLElement} el */
setPrefix(el) {
const prev = separator.previousSibling;
if (prev) {
prev.replaceWith(el);
} else {
scroller.prepend(el, separator);
}
captureScroll(el);
},
};
}
export function createSeriesLegend() {
const legend = createLegend();
const items = window.document.createElement("div");
legend.scroller.append(items);
captureScroll(items);
/** @type {AnySeries | null} */
@@ -37,24 +69,10 @@ export function createLegend() {
/** @type {HTMLElement[]} */
const legends = [];
/** @type {HTMLElement | null} */
let prefix = null;
const separator = window.document.createElement("span");
separator.textContent = "|";
captureScroll(separator);
return {
element,
/**
* @param {HTMLElement} el
*/
setPrefix(el) {
if (prefix) prefix.replaceWith(el);
else scroller.insertBefore(el, items);
prefix = el;
captureScroll(el);
el.after(separator);
},
element: legend.element,
setPrefix: legend.setPrefix,
/**
* @param {Object} args
* @param {AnySeries} args.series

View File

@@ -1089,42 +1089,35 @@ export function createMarketSection() {
},
{
name: "Thermometer",
tree: [
{
name: "Bands",
title: "Thermometer",
top: priceBands(percentileBands(indicators.thermometer), { defaultActive: true }),
},
{
title: "Thermometer",
top: priceBands(percentileBands(indicators.thermometer), {
defaultActive: true,
}),
bottom: [
histogram({
series: indicators.thermometer.zone,
name: "Zone",
unit: Unit.count,
colorFn: (v) =>
/** @type {const} */ ([
colors.ratioPct._0_5,
colors.ratioPct._1,
colors.ratioPct._2,
colors.ratioPct._5,
colors.transparent,
colors.ratioPct._95,
colors.ratioPct._98,
colors.ratioPct._99,
colors.ratioPct._99_5,
])[v + 4],
}),
baseline({
series: indicators.thermometer.score,
name: "Score",
title: "Thermometer",
top: priceBands(percentileBands(indicators.thermometer)),
bottom: [
histogram({
series: indicators.thermometer.zone,
name: "Zone",
unit: Unit.count,
colorFn: (v) => /** @type {const} */ ([
colors.ratioPct._0_5,
colors.ratioPct._1,
colors.ratioPct._2,
colors.ratioPct._5,
colors.transparent,
colors.ratioPct._95,
colors.ratioPct._98,
colors.ratioPct._99,
colors.ratioPct._99_5,
])[v + 4],
}),
baseline({
series: indicators.thermometer.score,
name: "Score",
unit: Unit.count,
color: [colors.ratioPct._99, colors.ratioPct._1],
defaultActive: false,
}),
],
},
unit: Unit.count,
color: [colors.ratioPct._99, colors.ratioPct._1],
defaultActive: false,
}),
],
},
],

View File

@@ -114,7 +114,6 @@ export function init() {
/** @type {{ label: string, items: IndexLabel[] }[]} */
const ALL_GROUPS = [
{ label: "Height", items: ["blk", "halv", "diff"] },
{
label: "Time",
items: [
@@ -125,6 +124,7 @@ const ALL_GROUPS = [
"1y", "10y",
],
},
{ label: "Block", items: ["blk", "epch", "halv"] },
];
const ALL_CHOICES = /** @satisfies {IndexLabel[]} */ (

View File

@@ -26,7 +26,7 @@ export const INDEX_LABEL = /** @type {const} */ ({
day1: "1d", day3: "3d", week1: "1w",
month1: "1m", month3: "3m", month6: "6m",
year1: "1y", year10: "10y",
halving: "halv", epoch: "diff",
halving: "halv", epoch: "epch",
});
/** @typedef {typeof INDEX_LABEL} IndexLabelMap */