website: chart style changes

This commit is contained in:
nym21
2026-02-11 12:22:32 +01:00
parent 1d63b8901d
commit 121928bc57
11 changed files with 448 additions and 274 deletions
+167 -94
View File
@@ -190,11 +190,8 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
setup() {
elements.root.classList.add("chart");
elements.chart.classList.add("lightweight-chart");
parent.append(elements.root);
elements.root.append(legends.top.element);
elements.root.append(elements.chart);
elements.root.append(legends.bottom.element);
},
};
elements.setup();
@@ -220,6 +217,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
borderVisible: false,
},
timeScale: {
// borderColor: colors.border(),
borderVisible: false,
enableConflation: true,
...(fitContent
@@ -306,6 +304,9 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
separatorColor: borderColor,
},
},
timeScale: {
// borderColor: colors.border(),
},
crosshair: {
horzLine: {
color: offColor,
@@ -415,28 +416,6 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
seriesByHome: new Map(),
pendingVisibilityCheck: false,
/** @param {number} homePane */
isAllHidden(homePane) {
const map = this.seriesByHome.get(homePane);
return !map || [...map.keys()].every((s) => !s.active.value);
},
/**
* @param {number} homePane
* @param {number} targetPane
*/
moveTo(homePane, targetPane) {
const map = this.seriesByHome.get(homePane);
if (!map) return;
for (const iseries of map.values()) {
for (const is of iseries) {
if (is.getPane().paneIndex() !== targetPane) {
is.moveToPane(targetPane);
}
}
}
},
/**
* @param {number} paneIndex
* @param {VoidFunction} callback
@@ -454,26 +433,55 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
}
},
updateVisibility() {
const pane0Hidden = this.isAllHidden(0);
const pane1Hidden = this.isAllHidden(1);
const bothVisible = !pane0Hidden && !pane1Hidden;
/** @param {number} targetPaneIndex @param {Legend} legend */
injectLegend(targetPaneIndex, legend) {
const pane = ichart.panes().at(targetPaneIndex);
const parent = pane?.getHTMLElement()?.children?.item(1)?.firstChild;
if (!parent) return;
parent.appendChild(legend.element);
},
this.moveTo(1, bothVisible ? 1 : 0);
initialized: false,
if (bothVisible) {
setup() {
if (this.initialized) return;
this.initialized = true;
this.whenReady(0, () => {
const pane0 = ichart.panes().at(0);
fieldsets.createForPane(0);
this.injectLegend(0, legends.top);
if (pane0) injectScaleSelector(0, pane0);
this.updateSize(0);
});
if (this.seriesByHome.has(1)) {
this.whenReady(1, () => {
fieldsets.createForPane(0);
const pane1 = ichart.panes().at(1);
fieldsets.createForPane(1);
this.injectLegend(1, legends.bottom);
if (pane1) injectScaleSelector(1, pane1);
this.updateSize(1);
});
}
},
/** @param {number} homePane */
isAllHidden(homePane) {
const map = this.seriesByHome.get(homePane);
return !map || [...map.keys()].every((s) => !s.active.value);
},
/** @param {number} paneIndex */
updateSize(paneIndex) {
const pane = ichart.panes().at(paneIndex);
if (!pane) return;
const hidden = this.isAllHidden(paneIndex);
if (hidden) {
const chartHeight = ichart.chartElement().clientHeight;
pane.setStretchFactor(chartHeight > 0 ? 32 / (chartHeight - 32) : 0);
} else {
this.whenReady(0, () => {
if (pane0Hidden && !pane1Hidden) {
fieldsets.createForPane(1, 0);
} else {
fieldsets.createForPane(0);
}
});
pane.setStretchFactor(1);
}
},
@@ -494,7 +502,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
this.pendingVisibilityCheck = true;
requestAnimationFrame(() => {
this.pendingVisibilityCheck = false;
this.updateVisibility();
this.setup();
});
}
},
@@ -588,7 +596,7 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
if (value && !wasActive) {
state.fetch?.();
}
panes.updateVisibility();
panes.updateSize(paneIndex);
},
setOrder,
show,
@@ -759,7 +767,11 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
function tryProcess() {
if (seriesGeneration !== generation) return;
if (!timeData || !valuesData) return;
if (valuesStamp === state.lastStamp && timeData.stamp === state.lastTimeStamp) return;
if (
valuesStamp === state.lastStamp &&
timeData.stamp === state.lastTimeStamp
)
return;
state.lastStamp = valuesStamp;
state.lastTimeStamp = timeData.stamp;
if (timeData.data.length && valuesData.length) {
@@ -1490,47 +1502,76 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
},
};
/**
* @param {number} paneIndex
*/
/**
* @param {number} paneIndex
*/
function applyScaleForUnit(paneIndex) {
const pane = ichart.panes().at(paneIndex);
if (!pane) return;
const persisted = scalePersistedValues[paneIndex];
if (!persisted) return;
try {
pane.priceScale("right").applyOptions({
mode: persisted.value === "lin" ? 0 : 1,
});
} catch {}
}
/** @type {Record<number, ReturnType<typeof createPersistedValue<"lin" | "log">>>} */
const scalePersistedValues = {};
/**
* @param {number} paneIndex
* @param {IPaneApi<Time>} pane
*/
function injectScaleSelector(paneIndex, pane) {
const id = `${storageId}-scale`;
const defaultValue = paneIndex === 0 ? "log" : "lin";
const persisted = createPersistedValue({
defaultValue: /** @type {"lin" | "log"} */ (defaultValue),
storageKey: `${storageId}-p${paneIndex}-scale`,
urlKey: paneIndex === 0 ? "price_scale" : "unit_scale",
serialize: (v) => v,
deserialize: (s) => /** @type {"lin" | "log"} */ (s),
});
let persisted = scalePersistedValues[paneIndex];
if (!persisted) {
persisted = createPersistedValue({
defaultValue: /** @type {"lin" | "log"} */ (defaultValue),
storageKey: `${storageId}-p${paneIndex}-scale`,
urlKey: paneIndex === 0 ? "price_scale" : "unit_scale",
serialize: (v) => v,
deserialize: (s) => /** @type {"lin" | "log"} */ (s),
});
scalePersistedValues[paneIndex] = persisted;
}
/** @param {IPaneApi<Time>} pane @param {"lin" | "log"} value */
const applyScale = (pane, value) => {
try {
pane.priceScale("right").applyOptions({
mode: value === "lin" ? 0 : 1,
});
} catch {}
};
// Inject into the price scale td (last td in the pane's tr)
const paneEl = pane.getHTMLElement();
const tr = paneEl?.closest("tr");
const td = tr?.querySelector("td:last-child");
if (!td) return;
fieldsets.addIfNeeded({
id,
paneIndex,
position: "sw",
createChild(pane) {
applyScale(pane, persisted.value);
return createRadios({
choices: /** @type {const} */ (["lin", "log"]),
id: stringToId(`${id} ${paneIndex}`),
initialValue: persisted.value,
onChange(value) {
persisted.set(value);
applyScale(pane, value);
},
});
// Remove previous if any
td.querySelector(".scale-selector")?.remove();
/** @type {HTMLTableCellElement} */ (td).style.position = "relative";
const wrapper = window.document.createElement("div");
wrapper.classList.add("scale-selector");
const radios = createRadios({
choices: /** @type {const} */ (["lin", "log"]),
id: stringToId(`${id} ${paneIndex}`),
initialValue: persisted.value,
onChange(value) {
persisted.set(value);
try {
pane.priceScale("right").applyOptions({
mode: value === "lin" ? 0 : 1,
});
} catch {}
},
});
wrapper.append(radios);
td.append(wrapper);
}
const blueprints = {
@@ -1675,13 +1716,13 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
// Remove old series AFTER adding new ones to prevent pane collapse
oldSeries.forEach((s) => s.remove());
// Store scale config - it will be applied when createForPane runs after updateVisibility
applyScaleForUnit(paneIndex);
},
rebuild() {
generation++;
initialLoadComplete = false; // Reset to prevent saving stale ranges during load
panes.initialized = false;
time.setIndex(index.get());
time.fetch();
this.rebuildPane(0);
@@ -1692,9 +1733,46 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
// Rebuild when index changes
index.onChange.add(() => blueprints.rebuild());
// Index selector — injected into the last tr of the chart table
let preferredIndex = index.name.value;
/** @type {HTMLElement | null} */
let indexField = null;
const indexWrapper = window.document.createElement("div");
indexWrapper.classList.add("index-selector");
const lastTr = ichart.chartElement().querySelector("table > tr:last-child");
if (lastTr) {
lastTr.append(indexWrapper);
}
const chart = {
index,
/** @param {ChartableIndexName[]} choices */
setIndexChoices(choices) {
if (indexField) indexField.remove();
let currentValue = choices.includes(preferredIndex)
? preferredIndex
: (choices[0] ?? "date");
if (currentValue !== index.name.value) {
index.name.set(currentValue);
}
indexField = createSelect({
initialValue: currentValue,
onChange: (v) => {
preferredIndex = v;
index.name.set(v);
},
choices,
id: "index",
});
indexWrapper.append(indexField);
},
/**
* @param {Object} args
* @param {string} args.name
@@ -1730,27 +1808,22 @@ export function createChart({ parent, id: chartId, brk, fitContent }) {
units.find((u) => u.id === persistedUnit.value) ?? defaultUnit;
blueprints.panes[paneIndex].unit = initialUnit;
fieldsets.addIfNeeded({
id: `${chartId}-unit`,
paneIndex,
position: "nw",
createChild() {
return createSelect({
choices: units,
id: `pane-${paneIndex}-unit`,
initialValue: blueprints.panes[paneIndex].unit ?? defaultUnit,
toKey: (u) => u.id,
toLabel: (u) => u.name,
sorted: true,
onChange(unit) {
generation++;
persistedUnit.set(unit.id);
blueprints.panes[paneIndex].unit = unit;
blueprints.rebuildPane(paneIndex);
},
});
},
});
blueprints.panes[paneIndex].legend.setPrefix(
createSelect({
choices: units,
id: `pane-${paneIndex}-unit`,
initialValue: blueprints.panes[paneIndex].unit ?? defaultUnit,
toKey: (u) => u.id,
toLabel: (u) => u.name,
sorted: true,
onChange(unit) {
generation++;
persistedUnit.set(unit.id);
blueprints.panes[paneIndex].unit = unit;
blueprints.rebuildPane(paneIndex);
},
}),
);
});
blueprints.rebuild();
+24 -2
View File
@@ -3,6 +3,15 @@ import { stringToId } from "../utils/format.js";
export function createLegend() {
const element = window.document.createElement("legend");
const scroller = window.document.createElement("div");
const items = window.document.createElement("div");
scroller.append(items);
element.append(scroller);
scroller.addEventListener("wheel", (e) => e.stopPropagation());
scroller.addEventListener("touchstart", (e) => e.stopPropagation());
scroller.addEventListener("touchmove", (e) => e.stopPropagation());
/** @type {AnySeries | null} */
let hoveredSeries = null;
@@ -24,9 +33,22 @@ export function createLegend() {
/** @type {HTMLElement[]} */
const legends = [];
/** @type {HTMLElement | null} */
let prefix = null;
const separator = window.document.createElement("span");
separator.textContent = "|";
return {
element,
/**
* @param {HTMLElement} el
*/
setPrefix(el) {
if (prefix) prefix.replaceWith(el);
else scroller.insertBefore(el, items);
prefix = el;
el.after(separator);
},
/**
* @param {Object} args
* @param {AnySeries} args.series
@@ -41,11 +63,11 @@ export function createLegend() {
if (prev) {
prev.replaceWith(div);
} else {
const elementAtOrder = Array.from(element.children).at(order);
const elementAtOrder = Array.from(items.children).at(order);
if (elementAtOrder) {
elementAtOrder.before(div);
} else {
element.append(div);
items.append(div);
}
}
legends[order] = div;
+2 -2
View File
@@ -1,7 +1,7 @@
import { BrkClient } from "./modules/brk-client/index.js";
// const brk = new BrkClient("https://next.bitview.space");
const brk = new BrkClient("/");
const brk = new BrkClient("https://bitview.space");
// const brk = new BrkClient("/");
console.log(`VERSION = ${brk.VERSION}`);
+3 -3
View File
@@ -387,9 +387,9 @@ export function initOptions() {
details.append(summary);
summary.append(node.name);
const supCount = window.document.createElement("sup");
supCount.innerHTML = node.count.toLocaleString("en-us");
summary.append(supCount);
const count = window.document.createElement("small");
count.textContent = `(${node.count.toLocaleString("en-us")})`;
summary.append(count);
let built = false;
details.addEventListener("toggle", () => {
+2 -47
View File
@@ -1,4 +1,4 @@
import { createShadow, createRadios, createHeader } from "../utils/dom.js";
import { createShadow, createHeader } from "../utils/dom.js";
import { chartElement } from "../utils/elements.js";
import { serdeChartableIndex } from "../utils/serde.js";
import { Unit } from "../utils/units.js";
@@ -33,9 +33,7 @@ export function init() {
brk,
});
// Create index selector
const { fieldset, setChoices } = createIndexSelector(chart);
chartElement.append(fieldset);
const setChoices = chart.setIndexChoices;
/**
* Build top blueprints with price series prepended for each unit
@@ -147,46 +145,3 @@ function computeChoices(opt) {
);
}
/**
* @param {Chart} chart
*/
function createIndexSelector(chart) {
const fieldset = window.document.createElement("fieldset");
fieldset.id = "interval";
fieldset.dataset.size = "sm";
// Track user's preferred index (only updated on explicit selection)
let preferredIndex = chart.index.name.value;
/** @type {HTMLElement | null} */
let field = null;
/**
* @param {ChartableIndexName[]} newChoices
*/
function setChoices(newChoices) {
if (field) field.remove();
// Use preferred index if available, otherwise fall back to first choice
let currentValue = newChoices.includes(preferredIndex)
? preferredIndex
: (newChoices[0] ?? "date");
if (currentValue !== chart.index.name.value) {
chart.index.name.set(currentValue);
}
field = createRadios({
initialValue: currentValue,
onChange: (v) => {
preferredIndex = v; // User explicitly selected, update preference
chart.index.name.set(v);
},
choices: newChoices,
id: "index",
});
fieldset.append(field);
}
return { fieldset, setChoices };
}
+3
View File
@@ -287,6 +287,9 @@ export function createSelect({
const small = window.document.createElement("small");
small.textContent = `+${remaining}`;
field.append(small);
const arrow = window.document.createElement("span");
arrow.textContent = "↓";
field.append(arrow);
}
}
+222 -98
View File
@@ -19,6 +19,10 @@ nav {
color: var(--color);
}
> details > summary > small {
opacity: 1;
}
> a::after,
> details:not([open]) > summary::after {
color: var(--orange) !important;
@@ -66,6 +70,7 @@ nav {
position: relative;
padding-left: 0.75rem;
border-left: 1px;
/*border-style: dotted !important;*/
}
}
}
@@ -237,7 +242,6 @@ fieldset {
}
}
.chart > legend,
#chart > fieldset {
text-transform: lowercase;
flex-shrink: 0;
@@ -257,97 +261,251 @@ fieldset {
flex-direction: column;
min-height: 0;
flex: 1;
margin-top: 2rem;
margin-bottom: 1rem;
> legend {
gap: 1.5rem;
legend {
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;
&:empty {
display: 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 {
flex: 0;
height: 100%;
display: flex;
align-items: center;
gap: 0.75rem;
overflow-x: auto;
scrollbar-width: thin;
padding: 0 var(--main-padding);
padding-top: 0.25rem;
padding-bottom: 0.75rem;
pointer-events: auto;
> label {
margin: -0.375rem 0;
color: var(--color);
> span {
display: flex !important;
> div:has(> select) {
> select {
width: auto;
background: none;
}
&:has(input:not(:checked)) {
color: var(--off-color);
> span.main > span.name {
text-decoration-thickness: 1.5px;
text-decoration-color: var(--color);
text-decoration-line: line-through;
}
&:hover {
* {
color: var(--off-color) !important;
}
> span.main > span.name {
text-decoration-color: var(--orange) !important;
}
}
small {
flex-shrink: 0;
}
}
> a {
padding-left: 0.375rem;
padding-right: 0.375rem;
margin-left: -0.375rem;
margin-right: -0.375rem;
margin-top: 0.1rem;
> div:last-child {
display: flex;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
> div:last-child > div {
flex: 0;
height: 100%;
display: flex;
align-items: center;
> label {
margin: -0.375rem 0;
color: var(--color);
> span {
display: flex !important;
}
&:has(input:not(:checked)) {
color: var(--off-color);
> span.main > span.name {
text-decoration-thickness: 1.5px;
text-decoration-color: var(--color);
text-decoration-line: line-through;
}
&:hover {
* {
color: var(--off-color) !important;
}
> span.main > span.name {
text-decoration-color: var(--orange) !important;
}
}
}
}
> a {
padding-left: 0.375rem;
padding-right: 0.375rem;
margin-left: -0.375rem;
margin-right: -0.375rem;
margin-top: 0.1rem;
}
}
}
}
.lightweight-chart {
> div {
min-height: 0;
height: 100%;
margin-right: var(--negative-main-padding);
margin-left: var(--negative-main-padding);
margin-top: 0.25rem;
margin-bottom: 0.25rem;
fieldset {
padding: 0.5rem;
z-index: 10;
div:has(> select) {
display: flex;
flex-shrink: 0;
gap: 0.375rem;
}
table > tr:first-child {
position: relative;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: var(--main-padding);
background-image: linear-gradient(
to bottom,
var(--background-color),
transparent
);
z-index: 10;
pointer-events: none;
}
}
table > tr:last-child {
position: relative;
> td {
border-top: 1px;
}
}
.scale-selector {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
display: flex;
flex-direction: column;
pointer-events: none;
&[data-position="nw"] {
&::before {
content: "";
width: 100%;
height: 2rem;
background-image: linear-gradient(
to bottom,
transparent,
var(--background-color)
);
}
> div:has(label) {
display: flex;
font-size: var(--font-size-xs);
gap: 0.25rem;
background-color: var(--background-color);
align-content: center;
align-items: center;
text-transform: uppercase;
padding-left: 0.625rem;
padding-top: 0.125rem;
padding-bottom: 0.25rem;
label {
margin: -0.25rem;
padding: 0.25rem;
}
}
> fieldset {
pointer-events: auto;
background-color: var(--background-color);
width: 100%;
justify-content: center;
padding-bottom: 0.25rem;
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
}
}
.index-selector {
position: absolute;
left: 0;
top: 0;
bottom: 0;
z-index: 10;
display: flex;
align-items: center;
pointer-events: auto;
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
text-transform: uppercase;
> div {
background-color: var(--background-color);
padding-left: var(--main-padding);
top: 0;
left: 0;
padding-right: 0.25rem;
height: 100%;
margin-top: 1px;
display: flex;
align-items: center;
> select {
width: auto;
background: none;
}
}
&[data-position="ne"] {
top: 0;
right: 0;
&::after {
content: "";
height: 100%;
margin-top: 1px;
width: var(--main-padding);
background-image: linear-gradient(
to right,
var(--background-color),
transparent
);
}
&[data-position="se"] {
bottom: 0;
right: 0;
}
&[data-position="sw"] {
padding-left: var(--main-padding);
bottom: 0;
left: 0;
}
gap: 0;
}
button.capture {
@@ -360,38 +518,4 @@ fieldset {
color: var(--off-color);
}
}
> .panes {
position: relative;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
> .pane {
position: relative;
min-height: 0px;
width: 100%;
cursor: crosshair;
height: 100%;
&:has(+ .chart-wrapper:not([hidden])) {
height: calc(100% - 62px);
}
> fieldset {
pointer-events: none;
position: absolute;
left: 0px;
top: 0px;
z-index: 10;
}
}
> .shadow-bottom {
bottom: 1.75rem;
width: 80px;
left: auto;
}
}
}
+13 -3
View File
@@ -171,7 +171,6 @@ html {
scrollbar-color: var(--off-color) var(--background-color);
scrollbar-width: thin;
overflow: hidden;
interpolate-size: allow-keywords;
}
input {
@@ -218,7 +217,6 @@ label {
gap: 0.25rem;
> span.colors {
margin-top: 0.15rem;
display: flex;
width: 0.625rem;
height: 0.625rem;
@@ -375,6 +373,12 @@ sup {
font-weight: 500;
}
summary > small {
margin-left: 0.375rem;
opacity: 0.5;
font-weight: 300;
}
small {
color: var(--off-color);
font-weight: var(--font-weight-base);
@@ -387,7 +391,13 @@ small {
select + & {
font-weight: var(--font-weight-base);
font-size: var(--font-size-xs);
margin-left: -0.5rem !important;
+ span {
color: var(--off-color);
font-weight: 400;
font-size: 1rem;
line-height: 1;
}
}
}
+6 -21
View File
@@ -7,42 +7,27 @@
padding: var(--main-padding);
background-color: var(--background-color);
p#domain {
position: absolute;
left: 2rem;
top: 0.75rem;
color: var(--gray);
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
}
header {
flex-shrink: 0;
display: flex;
white-space: nowrap;
overflow-x: auto;
padding-bottom: 1rem;
margin-bottom: -1.5rem;
margin-bottom: -1.25rem;
padding-left: var(--main-padding);
margin-left: var(--negative-main-padding);
padding-right: var(--main-padding);
margin-right: var(--negative-main-padding);
margin-top: -0.5rem;
& > * {
flex: 1;
h1 {
font-size: 1.375rem;
letter-spacing: 0.05rem;
}
}
.chart {
flex: 1;
}
> .chart > legend,
> fieldset {
z-index: 20;
}
.lightweight-chart {
z-index: 40;
margin-bottom: -0.25rem;
}
}
+1 -1
View File
@@ -64,7 +64,7 @@
flex: none;
height: 400px;
.lightweight-chart {
> div {
margin-left: calc(var(--negative-main-padding) * 0.75);
fieldset {
+5 -3
View File
@@ -2,11 +2,13 @@
color-scheme: light dark;
/*--white: oklch(90% 0 0);*/
--white: oklch(95% 0.01 44);
--light-gray: oklch(90% 0.01 44);
--white: oklch(93.3% 0.006 75);
/*oklch(0.9333 0.0059 59.65)*/
--light-gray: oklch(85% 0.01 75);
--gray: oklch(60% 0.01 44);
--dark-gray: oklch(30% 0.01 44);
--black: oklch(16% 0.005 44);
--black: oklch(16.5% 0.006 90);
/*oklch(0.2038 0.0076 196.57)*/
--red: oklch(0.607 0.241 26.328);
--orange: oklch(67.64% 0.191 44.41);
--amber: oklch(0.7175 0.1835 64.199);