website: snapshot

This commit is contained in:
nym21
2026-01-21 11:55:53 +01:00
parent 6c67dc4a98
commit a9b2da86ff
3 changed files with 259 additions and 125 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ function toRgba(color) {
*/ */
function tameColor(color) { function tameColor(color) {
if (color === "transparent") return color; if (color === "transparent") return color;
return `${color.slice(0, -1)} / 50%)`; return `${color.slice(0, -1)} / 25%)`;
} }
/** /**
+234 -100
View File
@@ -36,7 +36,11 @@ import { resources } from "../resources.js";
* @property {string} id * @property {string} id
* @property {number} paneIndex * @property {number} paneIndex
* @property {Signal<boolean>} active * @property {Signal<boolean>} active
* @property {Signal<boolean>} highlighted * @property {() => void} show
* @property {() => void} hide
* @property {(order: number) => void} setOrder
* @property {() => void} highlight
* @property {() => void} tame
* @property {() => boolean} hasData * @property {() => boolean} hasData
* @property {Signal<string | null>} url * @property {Signal<string | null>} url
* @property {() => readonly T[]} getData * @property {() => readonly T[]} getData
@@ -165,23 +169,19 @@ export function createChart({
}); });
}; };
const visibleBarsCount = signals.createSignal( /** @typedef {(visibleBarsCount: number) => void} ZoomChangeCallback */
initialVisibleBarsCount ?? Infinity,
); let visibleBarsCount = initialVisibleBarsCount ?? Infinity;
/** @type {() => 0 | 1 | 2 | 3} 0: <=200, 1: <=500, 2: <=1000, 3: >1000 */ /** @type {Set<ZoomChangeCallback>} */
const visibleBarsCountBucket = signals.createMemo(() => { const onZoomChange = new Set();
const count = visibleBarsCount();
return count > 1000 ? 3 : count > 500 ? 2 : count > 200 ? 1 : 0;
});
const shouldShowLine = signals.createMemo(
() => visibleBarsCountBucket() >= 2,
);
ichart.timeScale().subscribeVisibleLogicalRangeChange( ichart.timeScale().subscribeVisibleLogicalRangeChange(
throttle((range) => { throttle((range) => {
if (range) { if (!range) return;
visibleBarsCount.set(range.to - range.from); const count = range.to - range.from;
} if (count === visibleBarsCount) return;
visibleBarsCount = count;
onZoomChange.forEach((cb) => cb(count));
}, 100), }, 100),
); );
@@ -360,7 +360,11 @@ export function createChart({
* @param {Accessor<WhitespaceData[]>} [args.data] * @param {Accessor<WhitespaceData[]>} [args.data]
* @param {number} args.paneIndex * @param {number} args.paneIndex
* @param {boolean} [args.defaultActive] * @param {boolean} [args.defaultActive]
* @param {(ctx: { active: Signal<boolean>, highlighted: Signal<boolean>, zOrder: number }) => void} args.setup * @param {(order: number) => void} args.setOrder
* @param {() => void} args.show
* @param {() => void} args.hide
* @param {() => void} args.highlight
* @param {() => void} args.tame
* @param {() => readonly any[]} args.getData * @param {() => readonly any[]} args.getData
* @param {(data: any[]) => void} args.setData * @param {(data: any[]) => void} args.setData
* @param {(data: any) => void} args.update * @param {(data: any) => void} args.update
@@ -376,7 +380,11 @@ export function createChart({
defaultActive, defaultActive,
colors, colors,
data, data,
setup, setOrder,
show,
hide,
highlight,
tame,
getData, getData,
setData, setData,
update, update,
@@ -398,9 +406,12 @@ export function createChart({
sharedActiveSignals.set(urlId, active); sharedActiveSignals.set(urlId, active);
} }
const highlighted = signals.createSignal(true); setOrder(-order);
setup({ active, highlighted, zOrder: -order }); // Bridge signal to series methods
signals.createEffect(active, (isActive) => {
isActive ? show() : hide();
});
let hasData = false; let hasData = false;
let lastTime = -Infinity; let lastTime = -Infinity;
@@ -411,7 +422,11 @@ export function createChart({
/** @type {AnySeries} */ /** @type {AnySeries} */
const series = { const series = {
active, active,
highlighted, setOrder,
show,
hide,
highlight,
tame,
hasData: () => hasData, hasData: () => hasData,
id, id,
paneIndex, paneIndex,
@@ -693,13 +708,40 @@ export function createChart({
color: colors.default(), color: colors.default(),
lineWidth, lineWidth,
visible: false, visible: false,
priceLineVisible: false, priceLineVisible: true,
}, },
paneIndex, paneIndex,
) )
); );
let showLine = false; let active = true;
let highlighted = true;
let showLine = visibleBarsCount > 500;
function update() {
candlestickISeries.applyOptions({
visible: active && !showLine,
lastValueVisible: highlighted,
upColor: upColor.highlight(highlighted),
downColor: downColor.highlight(highlighted),
wickUpColor: upColor.highlight(highlighted),
wickDownColor: downColor.highlight(highlighted),
});
lineISeries.applyOptions({
visible: active && showLine,
lastValueVisible: highlighted,
color: colors.default.highlight(highlighted),
});
}
/** @type {ZoomChangeCallback} */
function handleZoom(count) {
const newShowLine = count > 500;
if (newShowLine === showLine) return;
showLine = newShowLine;
update();
}
onZoomChange.add(handleZoom);
const series = addSeries({ const series = addSeries({
colors: [upColor, downColor], colors: [upColor, downColor],
@@ -711,30 +753,29 @@ export function createChart({
data, data,
defaultActive, defaultActive,
metric, metric,
setup: ({ active, highlighted, zOrder }) => { setOrder(order) {
candlestickISeries.setSeriesOrder(zOrder); candlestickISeries.setSeriesOrder(order);
lineISeries.setSeriesOrder(zOrder); lineISeries.setSeriesOrder(order);
signals.createEffect( },
() => ({ show() {
shouldShow: shouldShowLine(), if (active) return;
active: active(), active = true;
highlighted: highlighted(), update();
}), },
({ shouldShow, active, highlighted }) => { hide() {
showLine = shouldShow; if (!active) return;
candlestickISeries.applyOptions({ active = false;
visible: active && !showLine, update();
upColor: upColor.highlight(highlighted), },
downColor: downColor.highlight(highlighted), highlight() {
wickUpColor: upColor.highlight(highlighted), if (highlighted) return;
wickDownColor: downColor.highlight(highlighted), highlighted = true;
}); update();
lineISeries.applyOptions({ },
visible: active && showLine, tame() {
color: colors.default.highlight(highlighted), if (!highlighted) return;
}); highlighted = false;
}, update();
);
}, },
setData: (data) => { setData: (data) => {
candlestickISeries.setData(data); candlestickISeries.setData(data);
@@ -747,6 +788,7 @@ export function createChart({
}, },
getData: () => candlestickISeries.data(), getData: () => candlestickISeries.data(),
onRemove: () => { onRemove: () => {
onZoomChange.delete(handleZoom);
ichart.removeSeries(candlestickISeries); ichart.removeSeries(candlestickISeries);
ichart.removeSeries(lineISeries); ichart.removeSeries(lineISeries);
}, },
@@ -794,6 +836,17 @@ export function createChart({
) )
); );
let active = true;
let highlighted = true;
function update() {
iseries.applyOptions({
visible: active,
lastValueVisible: highlighted,
color: positiveColor.highlight(highlighted),
});
}
const series = addSeries({ const series = addSeries({
colors: isDualColor ? [positiveColor, negativeColor] : [positiveColor], colors: isDualColor ? [positiveColor, negativeColor] : [positiveColor],
name, name,
@@ -804,17 +857,26 @@ export function createChart({
data, data,
defaultActive, defaultActive,
metric, metric,
setup: ({ active, highlighted, zOrder }) => { setOrder: (order) => iseries.setSeriesOrder(order),
iseries.setSeriesOrder(zOrder); show() {
signals.createEffect( if (active) return;
() => ({ active: active(), highlighted: highlighted() }), active = true;
({ active, highlighted }) => { update();
iseries.applyOptions({ },
visible: active, hide() {
color: positiveColor.highlight(highlighted), if (!active) return;
}); active = false;
}, update();
); },
highlight() {
if (highlighted) return;
highlighted = true;
update();
},
tame() {
if (!highlighted) return;
highlighted = false;
update();
}, },
setData: (data) => { setData: (data) => {
if (isDualColor) { if (isDualColor) {
@@ -854,13 +916,14 @@ export function createChart({
name, name,
unit, unit,
order, order,
color, color: _color,
paneIndex = 0, paneIndex = 0,
defaultActive, defaultActive,
data, data,
options, options,
}) { }) {
color ||= unit.id === "usd" ? colors.green : colors.orange; const color =
_color ?? (unit.id === "usd" ? colors.green : colors.orange);
/** @type {LineISeries} */ /** @type {LineISeries} */
const iseries = /** @type {any} */ ( const iseries = /** @type {any} */ (
@@ -877,6 +940,17 @@ export function createChart({
) )
); );
let active = true;
let highlighted = true;
function update() {
iseries.applyOptions({
visible: active,
lastValueVisible: highlighted,
color: color.highlight(highlighted),
});
}
const series = addSeries({ const series = addSeries({
colors: [color], colors: [color],
name, name,
@@ -887,17 +961,26 @@ export function createChart({
data, data,
defaultActive, defaultActive,
metric, metric,
setup: ({ active, highlighted, zOrder }) => { setOrder: (order) => iseries.setSeriesOrder(order),
iseries.setSeriesOrder(zOrder); show() {
signals.createEffect( if (active) return;
() => ({ active: active(), highlighted: highlighted() }), active = true;
({ active, highlighted }) => { update();
iseries.applyOptions({ },
visible: active, hide() {
color: color.highlight(highlighted), if (!active) return;
}); active = false;
}, update();
); },
highlight() {
if (highlighted) return;
highlighted = true;
update();
},
tame() {
if (!highlighted) return;
highlighted = false;
update();
}, },
setData: (data) => iseries.setData(data), setData: (data) => iseries.setData(data),
update: (data) => iseries.update(data), update: (data) => iseries.update(data),
@@ -923,13 +1006,14 @@ export function createChart({
name, name,
unit, unit,
order, order,
color, color: _color,
paneIndex = 0, paneIndex = 0,
defaultActive, defaultActive,
data, data,
options, options,
}) { }) {
color ||= unit.id === "usd" ? colors.green : colors.orange; const color =
_color ?? (unit.id === "usd" ? colors.green : colors.orange);
/** @type {LineISeries} */ /** @type {LineISeries} */
const iseries = /** @type {any} */ ( const iseries = /** @type {any} */ (
@@ -948,6 +1032,28 @@ export function createChart({
) )
); );
let active = true;
let highlighted = true;
let radius =
visibleBarsCount > 1000 ? 1 : visibleBarsCount > 200 ? 1.5 : 2;
function update() {
iseries.applyOptions({
visible: active,
lastValueVisible: highlighted,
color: color.highlight(highlighted),
});
}
/** @type {ZoomChangeCallback} */
function handleZoom(count) {
const newRadius = count > 1000 ? 1 : count > 200 ? 1.5 : 2;
if (newRadius === radius) return;
radius = newRadius;
iseries.applyOptions({ pointMarkersRadius: radius });
}
onZoomChange.add(handleZoom);
const series = addSeries({ const series = addSeries({
colors: [color], colors: [color],
name, name,
@@ -958,26 +1064,34 @@ export function createChart({
data, data,
defaultActive, defaultActive,
metric, metric,
setup: ({ active, highlighted, zOrder }) => { setOrder: (order) => iseries.setSeriesOrder(order),
iseries.setSeriesOrder(zOrder); show() {
signals.createEffect( if (active) return;
() => ({ active: active(), highlighted: highlighted() }), active = true;
({ active, highlighted }) => { update();
iseries.applyOptions({ },
visible: active, hide() {
color: color.highlight(highlighted), if (!active) return;
}); active = false;
}, update();
); },
signals.createEffect(visibleBarsCountBucket, (bucket) => { highlight() {
const radius = bucket === 3 ? 1 : bucket >= 1 ? 1.5 : 2; if (highlighted) return;
iseries.applyOptions({ pointMarkersRadius: radius }); highlighted = true;
}); update();
},
tame() {
if (!highlighted) return;
highlighted = false;
update();
}, },
setData: (data) => iseries.setData(data), setData: (data) => iseries.setData(data),
update: (data) => iseries.update(data), update: (data) => iseries.update(data),
getData: () => iseries.data(), getData: () => iseries.data(),
onRemove: () => ichart.removeSeries(iseries), onRemove: () => {
onZoomChange.delete(handleZoom);
ichart.removeSeries(iseries);
},
}); });
return series; return series;
}, },
@@ -1032,6 +1146,18 @@ export function createChart({
) )
); );
let active = true;
let highlighted = true;
function update() {
iseries.applyOptions({
visible: active,
lastValueVisible: highlighted,
topLineColor: topColor.highlight(highlighted),
bottomLineColor: bottomColor.highlight(highlighted),
});
}
const series = addSeries({ const series = addSeries({
colors: [topColor, bottomColor], colors: [topColor, bottomColor],
name, name,
@@ -1042,18 +1168,26 @@ export function createChart({
data, data,
defaultActive, defaultActive,
metric, metric,
setup: ({ active, highlighted, zOrder }) => { setOrder: (order) => iseries.setSeriesOrder(order),
iseries.setSeriesOrder(zOrder); show() {
signals.createEffect( if (active) return;
() => ({ active: active(), highlighted: highlighted() }), active = true;
({ active, highlighted }) => { update();
iseries.applyOptions({ },
visible: active, hide() {
topLineColor: topColor.highlight(highlighted), if (!active) return;
bottomLineColor: bottomColor.highlight(highlighted), active = false;
}); update();
}, },
); highlight() {
if (highlighted) return;
highlighted = true;
update();
},
tame() {
if (!highlighted) return;
highlighted = false;
update();
}, },
setData: (data) => iseries.setData(data), setData: (data) => iseries.setData(data),
update: (data) => iseries.update(data), update: (data) => iseries.update(data),
+24 -24
View File
@@ -7,7 +7,23 @@ import { stringToId } from "../utils/format.js";
export function createLegend(signals) { export function createLegend(signals) {
const element = window.document.createElement("legend"); const element = window.document.createElement("legend");
const hovered = signals.createSignal(/** @type {AnySeries | null} */ (null)); /** @type {AnySeries | null} */
let hoveredSeries = null;
/** @type {Map<AnySeries, { span: HTMLSpanElement, color: Color }[]>} */
const seriesColorSpans = new Map();
/** @param {AnySeries | null} series */
function setHovered(series) {
if (hoveredSeries === series) return;
hoveredSeries = series;
for (const [entrySeries, colorSpans] of seriesColorSpans) {
const shouldHighlight = !hoveredSeries || hoveredSeries === entrySeries;
shouldHighlight ? entrySeries.highlight() : entrySeries.tame();
for (const { span, color } of colorSpans) {
span.style.backgroundColor = color.highlight(shouldHighlight);
}
}
}
/** @type {HTMLElement[]} */ /** @type {HTMLElement[]} */
const legends = []; const legends = [];
@@ -62,37 +78,21 @@ export function createLegend(signals) {
spanMain.append(spanName); spanMain.append(spanName);
div.append(label); div.append(label);
label.addEventListener("mouseover", () => { label.addEventListener("mouseover", () => setHovered(series));
const h = hovered(); label.addEventListener("mouseleave", () => setHovered(null));
if (!h || h !== series) {
hovered.set(series);
}
});
label.addEventListener("mouseleave", () => {
hovered.set(null);
});
const shouldHighlight = () => !hovered() || hovered() === series;
// Update series highlighted state
signals.createEffect(shouldHighlight, (shouldHighlight) => {
series.highlighted.set(shouldHighlight);
});
const spanColors = window.document.createElement("span"); const spanColors = window.document.createElement("span");
spanColors.classList.add("colors"); spanColors.classList.add("colors");
spanMain.prepend(spanColors); spanMain.prepend(spanColors);
/** @type {{ span: HTMLSpanElement, color: Color }[]} */
const colorSpans = [];
colors.forEach((color) => { colors.forEach((color) => {
const spanColor = window.document.createElement("span"); const spanColor = window.document.createElement("span");
spanColor.style.backgroundColor = color.highlight(true);
spanColors.append(spanColor); spanColors.append(spanColor);
colorSpans.push({ span: spanColor, color });
signals.createEffect(
() => color.highlight(shouldHighlight()),
(c) => {
spanColor.style.backgroundColor = c;
},
);
}); });
seriesColorSpans.set(series, colorSpans);
const anchor = window.document.createElement("a"); const anchor = window.document.createElement("a");