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

8
Cargo.lock generated
View File

@@ -2216,9 +2216,9 @@ dependencies = [
[[package]]
name = "oas3"
version = "0.20.1"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f67c885c7b19aaf652e84102035258ecb4e0425a4f71037e187798e367bd87"
checksum = "05ed0821ab10d7703415a06df039c2493f3a7667999d8b4e104731de0c53796f"
dependencies = [
"derive_more",
"http",
@@ -3350,9 +3350,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.1"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da36089a805484bcccfffe0739803392c8298778a2d2f09febf76fac5ad9025b"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]]
name = "unicode-xid"

View File

@@ -12,6 +12,6 @@ brk_cohort = { workspace = true }
brk_query = { workspace = true }
brk_types = { workspace = true }
indexmap = { workspace = true }
oas3 = "0.20"
oas3 = "0.21"
serde = { workspace = true }
serde_json = { workspace = true }

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "1.94.0"
channel = "1.94.1"

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 */

View File

@@ -17,21 +17,14 @@
}
legend {
padding: 0;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 20;
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
text-transform: lowercase;
pointer-events: none;
select {
text-transform: lowercase;
}
&::before,
&::after {
content: "";
@@ -68,14 +61,27 @@
scrollbar-width: thin;
padding: 0 var(--main-padding);
padding-top: 0.375rem;
padding-bottom: 0.75rem;
> * {
pointer-events: auto;
}
> span {
color: var(--gray);
padding: 0 0.75rem;
}
}
padding: 0;
top: 0;
text-transform: lowercase;
select {
text-transform: lowercase;
}
> div {
padding-bottom: 0.75rem;
small {
flex-shrink: 0;
@@ -152,7 +158,7 @@
top: 0;
left: 0;
right: 0;
height: var(--main-padding);
height: calc(var(--main-padding) * 2);
background-image: linear-gradient(
to bottom,
var(--background-color),
@@ -219,7 +225,7 @@
gap: 0.375rem;
background-color: var(--background-color);
padding-left: 0.625rem;
padding-top: 0.35rem;
padding-top: 0.375rem;
padding-bottom: 0.125rem;
&::after {
@@ -237,71 +243,23 @@
}
}
.index-bar {
position: absolute;
> legend {
top: auto;
bottom: 1.8rem;
left: calc(var(--main-padding) * -1);
right: 50px;
z-index: 20;
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
text-transform: uppercase;
pointer-events: none;
&::before,
&::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
width: var(--main-padding);
z-index: 1;
pointer-events: none;
}
&::before {
left: 0;
background-image: linear-gradient(
to left,
transparent,
var(--background-color)
);
}
&::after {
right: 0;
background-image: linear-gradient(
to right,
transparent,
var(--background-color)
);
}
> div {
display: flex;
align-items: center;
overflow-x: auto;
scrollbar-width: thin;
padding: 0 var(--main-padding);
padding-top: 0.375rem;
padding-bottom: 0.375rem;
> * {
pointer-events: auto;
flex-shrink: 0;
}
> span {
padding: 0 0.75rem;
}
button {
color: var(--off-color);
padding: 0.375rem;
margin: -0.375rem 0rem;
margin: -0.375rem 0.25rem;
&:hover {
color: var(--color);
&:first-of-type {
margin-left: -0.25rem;
}
}
}