diff --git a/Cargo.lock b/Cargo.lock index 42d8cdfba..a43712413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/crates/brk_bindgen/Cargo.toml b/crates/brk_bindgen/Cargo.toml index b53a2dc81..c69ef9cb8 100644 --- a/crates/brk_bindgen/Cargo.toml +++ b/crates/brk_bindgen/Cargo.toml @@ -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 } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 76a06e6b8..9cf4a67c4 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.94.0" +channel = "1.94.1" diff --git a/website/scripts/chart/index.js b/website/scripts/chart/index.js index 8de1fb1bb..9ed4241f1 100644 --- a/website/scripts/chart/index.js +++ b/website/scripts/chart/index.js @@ -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); diff --git a/website/scripts/chart/legend.js b/website/scripts/chart/legend.js index 6a7961d8a..085a450d4 100644 --- a/website/scripts/chart/legend.js +++ b/website/scripts/chart/legend.js @@ -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 `` with a scrollable `
`. + * 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 diff --git a/website/scripts/options/market.js b/website/scripts/options/market.js index 3739eecfa..c40b71f4f 100644 --- a/website/scripts/options/market.js +++ b/website/scripts/options/market.js @@ -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, + }), ], }, ], diff --git a/website/scripts/panes/chart.js b/website/scripts/panes/chart.js index 40f2c3b14..552bd3021 100644 --- a/website/scripts/panes/chart.js +++ b/website/scripts/panes/chart.js @@ -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[]} */ ( diff --git a/website/scripts/utils/serde.js b/website/scripts/utils/serde.js index 114db44a3..77d12bc4e 100644 --- a/website/scripts/utils/serde.js +++ b/website/scripts/utils/serde.js @@ -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 */ diff --git a/website/styles/chart.css b/website/styles/chart.css index 6863d7f2c..de65ea014 100644 --- a/website/styles/chart.css +++ b/website/styles/chart.css @@ -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; } } }