web: filter possible index choices in charts

This commit is contained in:
nym21
2025-06-12 22:09:33 +02:00
parent 92f81b1493
commit c46006aacc
4 changed files with 214 additions and 125 deletions
@@ -988,18 +988,21 @@ function createPriceLine(series, options, colors) {
});
}
/** @param {number} value */
function numberToShortUSFormat(value) {
/**
* @param {number} value
* @param {0 | 2} [digits]
*/
function numberToShortUSFormat(value, digits) {
const absoluteValue = Math.abs(value);
if (isNaN(value)) {
return "";
} else if (absoluteValue < 10) {
return numberToUSFormat(value, 3);
return numberToUSFormat(value, Math.min(3, digits || 10));
} else if (absoluteValue < 1_000) {
return numberToUSFormat(value, 2);
return numberToUSFormat(value, Math.min(2, digits || 10));
} else if (absoluteValue < 10_000) {
return numberToUSFormat(value, 1);
return numberToUSFormat(value, Math.min(1, digits || 10));
} else if (absoluteValue < 1_000_000) {
return numberToUSFormat(value, 0);
} else if (absoluteValue >= 900_000_000_000_000_000) {
+51 -35
View File
@@ -10,7 +10,7 @@ const CANDLE = "candle";
* @param {Object} args
* @param {Colors} args.colors
* @param {LightweightCharts} args.lightweightCharts
* @param {Accessor<ChartOption>} args.selected
* @param {Accessor<ChartOption>} args.option
* @param {Signals} args.signals
* @param {Utilities} args.utils
* @param {WebSockets} args.webSockets
@@ -22,7 +22,7 @@ export function init({
colors,
elements,
lightweightCharts,
selected,
option,
signals,
utils,
webSockets,
@@ -35,7 +35,12 @@ export function init({
const { headerElement, headingElement } = utils.dom.createHeader();
elements.charts.append(headerElement);
const { index, fieldset } = createIndexSelector({ signals, utils });
const { index, fieldset } = createIndexSelector({
option,
vecIdToIndexes,
signals,
utils,
});
const TIMERANGE_LS_KEY = signals.createMemo(
() => `chart-timerange-${index()}`
@@ -231,7 +236,7 @@ export function init({
}
}
signals.createEffect(selected, (option) => {
signals.createEffect(option, (option) => {
headingElement.innerHTML = option.title;
const bottomUnits = /** @type {readonly Unit[]} */ (
@@ -449,25 +454,53 @@ export function init({
/**
* @param {Object} args
* @param {Accessor<ChartOption>} args.option
* @param {VecIdToIndexes} args.vecIdToIndexes
* @param {Signals} args.signals
* @param {Utilities} args.utils
*/
function createIndexSelector({ signals, utils }) {
function createIndexSelector({ option, vecIdToIndexes, signals, utils }) {
const choices_ = /** @type {const} */ ([
"timestamp",
"date",
"week",
// "difficulty epoch",
"month",
"quarter",
"year",
// "halving epoch",
"decade",
]);
/** @type {Accessor<typeof choices_>} */
const choices = signals.createMemo(() => {
const o = option();
if (!Object.keys(o.top).length && !Object.keys(o.bottom).length) {
return [...choices_];
}
const rawIndexes = new Set(
[Object.values(o.top), Object.values(o.bottom)]
.flat(2)
.map((blueprint) => vecIdToIndexes[blueprint.key])
.flat()
);
const serializedIndexes = [...rawIndexes].flatMap((index) => {
const c = utils.serde.chartableIndex.serialize(index);
return c ? [c] : [];
});
return /** @type {any} */ (
choices_.filter((choice) => serializedIndexes.includes(choice))
);
});
const { field, selected } = utils.dom.createHorizontalChoiceField({
defaultValue: "date",
keyPrefix,
key: "index",
choices: /**@type {const} */ ([
"timestamp",
"date",
"week",
// "difficulty epoch",
"month",
"quarter",
"year",
// "halving epoch",
"decade",
]),
choices,
id: "index",
signals,
});
@@ -476,25 +509,8 @@ function createIndexSelector({ signals, utils }) {
fieldset.append(field);
fieldset.dataset.size = "sm";
const index = signals.createMemo(
/** @returns {ChartableIndex} */ () => {
switch (selected()) {
case "timestamp":
return /** @satisfies {Height} */ (5);
case "date":
return /** @satisfies {DateIndex} */ (0);
case "week":
return /** @satisfies {WeekIndex} */ (22);
case "month":
return /** @satisfies {MonthIndex} */ (7);
case "quarter":
return /** @satisfies {QuarterIndex} */ (19);
case "year":
return /** @satisfies {YearIndex} */ (23);
case "decade":
return /** @satisfies {DecadeIndex} */ (1);
}
}
const index = signals.createMemo(() =>
utils.serde.chartableIndex.deserialize(selected())
);
return { fieldset, index };
+154 -84
View File
@@ -362,11 +362,11 @@ function createUtils() {
* @param {Object} args
* @param {T[number]} args.defaultValue
* @param {string} [args.id]
* @param {T} args.choices
* @param {T | Accessor<T>} args.choices
* @param {string} [args.keyPrefix]
* @param {string} args.key
* @param {boolean} [args.sorted]
* @param {{createEffect: CreateEffect, createSignal: Signals["createSignal"]}} args.signals
* @param {{createEffect: CreateEffect, createMemo: CreateMemo, createSignal: Signals["createSignal"]}} args.signals
*/
createHorizontalChoiceField({
id,
@@ -377,13 +377,21 @@ function createUtils() {
signals,
sorted,
}) {
const choices = sorted
? /** @type {T} */ (
/** @type {any} */ (
unsortedChoices.toSorted((a, b) => a.localeCompare(b))
const choices = signals.createMemo(() => {
/** @type {T} */
let c;
if (typeof unsortedChoices === "function") {
c = unsortedChoices();
} else {
c = unsortedChoices;
}
return sorted
? /** @type {T} */ (
/** @type {any} */ (c.toSorted((a, b) => a.localeCompare(b)))
)
)
: unsortedChoices;
: c;
});
/** @type {Signal<T[number]>} */
const selected = signals.createSignal(defaultValue, {
@@ -394,31 +402,39 @@ function createUtils() {
},
});
if (!choices.includes(selected())) {
console.log(choices, "don't include", selected());
selected.set(() => defaultValue);
}
const field = window.document.createElement("div");
field.classList.add("field");
const div = window.document.createElement("div");
field.append(div);
choices.forEach((choice) => {
const inputValue = choice;
const { label } = this.createLabeledInput({
inputId: `${id ?? key}-${choice.toLowerCase()}`,
inputName: id ?? key,
inputValue,
inputChecked: inputValue === selected(),
labelTitle: choice,
type: "radio",
});
signals.createEffect(choices, (choices) => {
const s = selected();
if (!choices.includes(s)) {
if (choices.includes(defaultValue)) {
selected.set(() => defaultValue);
} else if (choices.length) {
selected.set(() => choices[0]);
}
}
const text = window.document.createTextNode(choice);
label.append(text);
div.append(label);
div.innerHTML = "";
choices.forEach((choice) => {
const inputValue = choice;
const { label } = this.createLabeledInput({
inputId: `${id ?? key}-${choice.toLowerCase()}`,
inputName: id ?? key,
inputValue,
inputChecked: inputValue === selected(),
labelTitle: choice,
type: "radio",
});
const text = window.document.createTextNode(choice);
label.append(text);
div.append(label);
});
});
field.addEventListener("change", (event) => {
@@ -1093,6 +1109,116 @@ function createUtils() {
}
},
},
index: {
/**
* @param {Index} v
*/
serialize(v) {
switch (v) {
case /** @satisfies {DateIndex} */ (0):
return "dateindex";
case /** @satisfies {DecadeIndex} */ (1):
return "decadeindex";
case /** @satisfies {DifficultyEpoch} */ (2):
return "difficultyepoch";
case /** @satisfies {EmptyOutputIndex} */ (3):
return "emptyoutputindex";
case /** @satisfies {HalvingEpoch} */ (4):
return "halvingepoch";
case /** @satisfies {Height} */ (5):
return "height";
case /** @satisfies {InputIndex} */ (6):
return "inputindex";
case /** @satisfies {MonthIndex} */ (7):
return "monthindex";
case /** @satisfies {OpReturnIndex} */ (8):
return "opreturnindex";
case /** @satisfies {OutputIndex} */ (9):
return "outputindex";
case /** @satisfies {P2AIndex} */ (10):
return "p2aindex";
case /** @satisfies {P2MSIndex} */ (11):
return "p2msindex";
case /** @satisfies {P2PK33Index} */ (12):
return "p2pk33index";
case /** @satisfies {P2PK65Index} */ (13):
return "p2pk65index";
case /** @satisfies {P2PKHIndex} */ (14):
return "p2pkhindex";
case /** @satisfies {P2SHIndex} */ (15):
return "p2shindex";
case /** @satisfies {P2TRIndex} */ (16):
return "p2trindex";
case /** @satisfies {P2WPKHIndex} */ (17):
return "p2wpkhindex";
case /** @satisfies {P2WSHIndex} */ (18):
return "p2wshindex";
case /** @satisfies {QuarterIndex} */ (19):
return "quarterindex";
case /** @satisfies {TxIndex} */ (20):
return "txindex";
case /** @satisfies {UnknownOutputIndex} */ (21):
return "unknownoutputindex";
case /** @satisfies {WeekIndex} */ (22):
return "weekindex";
case /** @satisfies {YearIndex} */ (23):
return "yearindex";
}
},
},
chartableIndex: {
/**
* @param {Index} v
*/
serialize(v) {
switch (v) {
case /** @satisfies {DateIndex} */ (0):
return "date";
case /** @satisfies {DecadeIndex} */ (1):
return "decade";
// case /** @satisfies {DifficultyEpoch} */ (2):
// return "difficulty";
// case /** @satisfies {HalvingEpoch} */ (4):
// return "halving";
case /** @satisfies {Height} */ (5):
return "timestamp";
case /** @satisfies {MonthIndex} */ (7):
return "month";
case /** @satisfies {QuarterIndex} */ (19):
return "quarter";
case /** @satisfies {WeekIndex} */ (22):
return "week";
case /** @satisfies {YearIndex} */ (23):
return "year";
default:
return null;
}
},
/**
* @param {string} v
* @returns {ChartableIndex}
*/
deserialize(v) {
switch (v) {
case "timestamp":
return /** @satisfies {Height} */ (5);
case "date":
return /** @satisfies {DateIndex} */ (0);
case "week":
return /** @satisfies {WeekIndex} */ (22);
case "month":
return /** @satisfies {MonthIndex} */ (7);
case "quarter":
return /** @satisfies {QuarterIndex} */ (19);
case "year":
return /** @satisfies {YearIndex} */ (23);
case "decade":
return /** @satisfies {DecadeIndex} */ (1);
default:
throw Error("Unsupported");
}
},
},
};
const formatters = {
@@ -1333,62 +1459,6 @@ function createUtils() {
}
}
/**
* @param {Index} index
*/
function vecIndexToString(index) {
switch (index) {
case /** @satisfies {DateIndex} */ (0):
return "dateindex";
case /** @satisfies {DecadeIndex} */ (1):
return "decadeindex";
case /** @satisfies {DifficultyEpoch} */ (2):
return "difficultyepoch";
case /** @satisfies {EmptyOutputIndex} */ (3):
return "emptyoutputindex";
case /** @satisfies {HalvingEpoch} */ (4):
return "halvingepoch";
case /** @satisfies {Height} */ (5):
return "height";
case /** @satisfies {InputIndex} */ (6):
return "inputindex";
case /** @satisfies {MonthIndex} */ (7):
return "monthindex";
case /** @satisfies {OpReturnIndex} */ (8):
return "opreturnindex";
case /** @satisfies {OutputIndex} */ (9):
return "outputindex";
case /** @satisfies {P2AIndex} */ (10):
return "p2aindex";
case /** @satisfies {P2MSIndex} */ (11):
return "p2msindex";
case /** @satisfies {P2PK33Index} */ (12):
return "p2pk33index";
case /** @satisfies {P2PK65Index} */ (13):
return "p2pk65index";
case /** @satisfies {P2PKHIndex} */ (14):
return "p2pkhindex";
case /** @satisfies {P2SHIndex} */ (15):
return "p2shindex";
case /** @satisfies {P2TRIndex} */ (16):
return "p2trindex";
case /** @satisfies {P2WPKHIndex} */ (17):
return "p2wpkhindex";
case /** @satisfies {P2WSHIndex} */ (18):
return "p2wshindex";
case /** @satisfies {QuarterIndex} */ (19):
return "quarterindex";
case /** @satisfies {TxIndex} */ (20):
return "txindex";
case /** @satisfies {UnknownOutputIndex} */ (21):
return "unknownoutputindex";
case /** @satisfies {WeekIndex} */ (22):
return "weekindex";
case /** @satisfies {YearIndex} */ (23):
return "yearindex";
}
}
/**
* @param {Index} index
* @param {VecId} vecId
@@ -1396,7 +1466,7 @@ function createUtils() {
* @param {number} [to]
*/
function genPath(index, vecId, from, to) {
let path = `/query?index=${vecIndexToString(index)}&values=${vecId}`;
let path = `/query?index=${serde.index.serialize(index)}&values=${vecId}`;
if (from !== undefined) {
path += `&from=${from}`;
}
@@ -2144,7 +2214,7 @@ function main() {
colors,
elements,
lightweightCharts,
selected: /** @type {Accessor<ChartOption>} */ (
option: /** @type {Accessor<ChartOption>} */ (
chartOption
),
signals,
+1 -1
View File
@@ -1,7 +1,7 @@
// @ts-check
/**
* @typedef {Height | DateIndex | WeekIndex | DifficultyEpoch | MonthIndex | QuarterIndex | YearIndex | HalvingEpoch | DecadeIndex} ChartableIndex
* @typedef {Height | DateIndex | WeekIndex | MonthIndex | QuarterIndex | YearIndex | DecadeIndex} ChartableIndex
*/
/**
* @template {readonly unknown[]} T