diff --git a/websites/kibo.money/index.html b/websites/kibo.money/index.html
index 32814b3cc..2feb4a0ae 100644
--- a/websites/kibo.money/index.html
+++ b/websites/kibo.money/index.html
@@ -521,9 +521,10 @@
gap: 0.25rem;
> span.colors {
+ margin-top: 0.25rem;
display: flex;
- width: 0.75rem;
- height: 0.75rem;
+ width: 0.625rem;
+ height: 0.625rem;
flex-direction: column;
overflow: hidden;
border-radius: 9999px;
@@ -966,9 +967,13 @@
flex-direction: column;
min-height: 0;
z-index: 20;
- margin-bottom: 2rem;
+ flex: 1;
+ margin-top: 2rem;
+ margin-bottom: 1.5rem;
> legend {
+ text-transform: lowercase;
+ flex-shrink: 0;
display: flex;
align-items: center;
gap: 1.5rem;
@@ -976,7 +981,7 @@
margin-right: var(--negative-main-padding);
padding-left: var(--main-padding);
padding-right: var(--main-padding);
- padding-bottom: 1.25rem;
+ padding-bottom: 1.5rem;
overflow-x: auto;
min-width: 0;
@@ -1015,13 +1020,16 @@
}
> a {
- padding: 0.375rem;
- margin: -0.375rem;
+ padding-left: 0.375rem;
+ padding-right: 0.375rem;
+ margin-left: -0.375rem;
+ margin-right: -0.375rem;
}
}
}
.lightweight-chart {
+ min-height: 0;
height: 100%;
margin-right: var(--negative-main-padding);
}
diff --git a/websites/kibo.money/packages/lightweight-charts/types.d.ts b/websites/kibo.money/packages/lightweight-charts/types.d.ts
index 015e59f26..086f112aa 100644
--- a/websites/kibo.money/packages/lightweight-charts/types.d.ts
+++ b/websites/kibo.money/packages/lightweight-charts/types.d.ts
@@ -64,7 +64,7 @@ interface BaseSeries {
visible: Accessor;
}
interface SingleSeries extends BaseSeries {
- iseries: ISeriesApi;
+ iseries: ISeriesApi;
dataset: Accessor<(SingleValueData | CandlestickData)[] | null>;
}
interface SplitSeries extends BaseSeries {
@@ -108,7 +108,7 @@ interface Marker {
weight: number;
time: Time;
value: number;
- seriesChunk: ISeriesApi;
+ seriesChunk: ISeriesApi;
}
interface HoveredLegend {
diff --git a/websites/kibo.money/packages/lightweight-charts/wrapper.js b/websites/kibo.money/packages/lightweight-charts/wrapper.js
index 93f3f9e50..7bbe90ad8 100644
--- a/websites/kibo.money/packages/lightweight-charts/wrapper.js
+++ b/websites/kibo.money/packages/lightweight-charts/wrapper.js
@@ -1,8 +1,10 @@
// @ts-check
-/** @import {SeriesDefinition} from './v5.0.5/types' */
+/** @import {ISeriesApi, SeriesDefinition} from './v5.0.5/types' */
export default import("./v5.0.5/script.js").then((lc) => {
+ const oklchToRGBA = createOklchToRGBA();
+
/**
* @param {Object} args
* @param {HTMLElement} args.element
@@ -63,13 +65,6 @@ export default import("./v5.0.5/script.js").then((lc) => {
borderColor: colors.border(),
}),
({ defaultColor, offColor, borderColor }) => {
- console.log(
- defaultColor,
- offColor,
- borderColor,
- `rgba(${oklchToRGBA(borderColor).join(", ")})`,
- );
-
chart.applyOptions({
layout: {
textColor: offColor,
@@ -102,36 +97,25 @@ export default import("./v5.0.5/script.js").then((lc) => {
return chart;
}
- /**
- * @type {DeepPartial}
- */
- const defaultSeriesOptions = {
- // @ts-ignore
- lineWidth: 1.5,
- // priceLineVisible: false,
- // baseLineVisible: false,
- // baseLineColor: "",
- };
-
/**
* @param {Object} args
* @param {string} args.id
* @param {HTMLElement} args.parent
* @param {Signals} args.signals
* @param {Colors} args.colors
- * @param {"static" | "scrollable"} args.kind
* @param {Utilities} args.utils
* @param {VecsResources} args.vecsResources
* @param {Owner | null} [args.owner]
+ * @param {true} [args.fitContentOnResize]
*/
function createChartElement({
parent,
signals,
colors,
- kind,
utils,
vecsResources,
owner: _owner,
+ fitContentOnResize,
}) {
let owner = _owner || signals.getOwner();
@@ -141,6 +125,8 @@ export default import("./v5.0.5/script.js").then((lc) => {
const legend = createLegend({
parent: div,
+ utils,
+ signals,
});
const chartDiv = window.document.createElement("div");
@@ -150,89 +136,109 @@ export default import("./v5.0.5/script.js").then((lc) => {
let ichart = /** @type {IChartApi | null} */ (null);
let timeScaleSet = false;
- if (kind === "static") {
+ if (fitContentOnResize) {
new ResizeObserver(() => ichart?.timeScale().fitContent()).observe(
chartDiv,
);
}
/** @type {Index} */
- let index = 0;
+ let vecIndex = 0; // Default value, overwritten
let timeResource = /** @type {VecResource| null} */ (null);
+ let timeScaleSetCallback = /** @type {VoidFunction | null} */ (null);
+
/**
- * @param {ISeriesApi} series
+ * @param {ISeriesApi} series
* @param {VecResource} valuesResource
*/
function createSetDataEffect(series, valuesResource) {
- signals.createEffect(
- () => [timeResource?.fetched(), valuesResource.fetched()],
- ([indexes, _ohlcs]) => {
- if (!ichart) throw Error("IChart should be initialized");
+ signals.runWithOwner(owner, () =>
+ signals.createEffect(
+ () => [timeResource?.fetched(), valuesResource.fetched()],
+ ([indexes, _ohlcs]) => {
+ if (!ichart) throw Error("IChart should be initialized");
- if (!indexes || !_ohlcs) return;
- const ohlcs = /** @type {OHLCTuple[]} */ (_ohlcs);
- let length = Math.min(indexes.length, ohlcs.length);
- const data = new Array(length);
- let prevTime = null;
- let offset = 0;
- for (let i = 0; i < length; i++) {
- const time = indexes[i];
- if (prevTime && prevTime === time) {
- offset += 1;
+ if (!indexes || !_ohlcs) return;
+ const ohlcs = /** @type {OHLCTuple[]} */ (_ohlcs);
+ let length = Math.min(indexes.length, ohlcs.length);
+ const data = new Array(length);
+ let prevTime = null;
+ let offset = 0;
+ for (let i = 0; i < length; i++) {
+ const time = indexes[i];
+ if (prevTime && prevTime === time) {
+ offset += 1;
+ }
+ const v = ohlcs[i];
+ if (typeof v === "number") {
+ data[i - offset] = {
+ time,
+ value: v,
+ };
+ } else {
+ data[i - offset] = {
+ time,
+ open: v[0],
+ high: v[1],
+ low: v[2],
+ close: v[3],
+ };
+ }
+ prevTime = time;
}
- const v = ohlcs[i];
- if (typeof v === "number") {
- data[i - offset] = {
- time,
- value: v,
- };
- } else {
- data[i - offset] = {
- time,
- open: v[0],
- high: v[1],
- low: v[2],
- close: v[3],
- };
+ data.length -= offset;
+ series.setData(data);
+ timeScaleSetCallback?.();
+ if (
+ !timeScaleSet &&
+ (vecIndex === /** @satisfies {Yearindex} */ (15) ||
+ vecIndex === /** @satisfies {Decadeindex} */ (16))
+ ) {
+ ichart
+ .timeScale()
+ .setVisibleLogicalRange({ from: -1, to: data.length });
}
- prevTime = time;
- }
- data.length -= offset;
- series.setData(data);
- if (
- !timeScaleSet &&
- (index === /** @satisfies {Yearindex} */ (15) ||
- index === /** @satisfies {Decadeindex} */ (16))
- ) {
- ichart
- .timeScale()
- .setVisibleLogicalRange({ from: -1, to: data.length });
- }
- timeScaleSet = true;
- },
+ timeScaleSet = true;
+ },
+ ),
);
}
+ const activeResources = /** @type {VecResource[]} */ ([]);
+
+ ichart?.subscribeCrosshairMove(
+ utils.debounce(() => {
+ activeResources.forEach((v) => {
+ v.fetch();
+ });
+ }),
+ );
+
return {
+ inner: () => ichart,
/**
- * @param {Index} _index
+ * @param {Object} args
+ * @param {Index} args.index
+ * @param {VoidFunction} [args.timeScaleSetCallback]
*/
- create(_index) {
- index = _index;
+ create({ index: _index, timeScaleSetCallback: _timeScaleSetCallback }) {
+ vecIndex = _index;
+ timeScaleSetCallback = _timeScaleSetCallback || null;
+
if (ichart) throw Error("IChart shouldn't be initialized");
timeResource = vecsResources.getOrCreate(
- index,
- index === /** @satisfies {Height} */ (2)
+ vecIndex,
+ vecIndex === /** @satisfies {Height} */ (2)
? "fixed-timestamp"
: "timestamp",
);
timeResource.fetch();
ichart = createLightweightChart({
- index,
+ index: vecIndex,
element: chartDiv,
signals,
colors,
@@ -240,14 +246,18 @@ export default import("./v5.0.5/script.js").then((lc) => {
});
},
/**
- * @param {VecId} id
- * @param {number} [paneNumber]
+ * @param {Object} args
+ * @param {VecId} args.vecId
+ * @param {string} args.name
+ * @param {number} [args.paneNumber]
+ * @param {boolean} [args.defaultActive]
*/
- addCandlestickSeries(id, paneNumber) {
+ addCandlestickSeries({ vecId, name, paneNumber, defaultActive }) {
if (!ichart || !timeResource) throw Error("Chart not fully set");
- const valuesResource = vecsResources.getOrCreate(index, id);
+ const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
+ activeResources.push(valuesResource);
const green = colors.green();
const red = colors.red();
@@ -259,32 +269,61 @@ export default import("./v5.0.5/script.js").then((lc) => {
wickUpColor: green,
wickDownColor: red,
borderVisible: false,
+ visible: defaultActive !== false,
},
paneNumber,
);
+ legend.add({
+ series,
+ name,
+ id: vecId,
+ defaultActive,
+ colors: [colors.green, colors.red],
+ url: valuesResource.url,
+ });
+
createSetDataEffect(series, valuesResource);
return series;
},
/**
- * @param {VecId} id
- * @param {number} [paneNumber]
+ * @param {Object} args
+ * @param {VecId} args.vecId
+ * @param {string} args.name
+ * @param {Color} [args.color]
+ * @param {number} [args.paneNumber]
+ * @param {boolean} [args.defaultActive]
*/
- addLineSeries(id, paneNumber) {
+ addLineSeries({ vecId, name, color, paneNumber, defaultActive }) {
if (!ichart || !timeResource) throw Error("Chart not fully set");
- const valuesResource = vecsResources.getOrCreate(index, id);
+ const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
+ activeResources.push(valuesResource);
+
+ color ||= colors.orange;
const series = ichart.addSeries(
/** @type {SeriesDefinition<'Line'>} */ (lc.LineSeries),
{
lineWidth: /** @type {any} */ (1.5),
+ visible: defaultActive !== false,
+ priceLineVisible: false,
+ color: color(),
},
paneNumber,
);
+ legend.add({
+ series,
+ colors: [color],
+ id: vecId,
+ name,
+ defaultActive,
+ url: valuesResource.url,
+ });
+
createSetDataEffect(series, valuesResource);
return series;
@@ -299,6 +338,7 @@ export default import("./v5.0.5/script.js").then((lc) => {
ichart?.remove();
ichart = null;
timeScaleSet = false;
+ activeResources.length = 0;
legend.reset();
},
};
@@ -313,19 +353,152 @@ export default import("./v5.0.5/script.js").then((lc) => {
/**
* @param {Object} args
* @param {Element} args.parent
+ * @param {Signals} args.signals
+ * @param {Utilities} args.utils
*/
-function createLegend({ parent }) {
+function createLegend({ parent, signals, utils }) {
const legendElement = window.document.createElement("legend");
parent.append(legendElement);
+ const hovered = signals.createSignal(
+ /** @type {ISeriesApi | null} */ (null),
+ );
+
return {
+ /**
+ * @param {Object} args
+ * @param {ISeriesApi} args.series
+ * @param {string} args.id
+ * @param {string} args.name
+ * @param {Color[]} args.colors
+ * @param {boolean} [args.defaultActive]
+ * @param {string} [args.url]
+ */
+ add({ series, id, name, colors, defaultActive, url }) {
+ const div = window.document.createElement("div");
+
+ legendElement.append(div);
+
+ const nameId = utils.stringToId(name);
+
+ const active = signals.createSignal(defaultActive ?? true, {
+ save: {
+ keyPrefix: id,
+ key: nameId,
+ ...utils.serde.boolean,
+ },
+ });
+
+ signals.createEffect(active, (active) => {
+ series.applyOptions({
+ visible: active,
+ });
+ });
+
+ const { input, label } = utils.dom.createLabeledInput({
+ inputId: utils.stringToId(`legend-${id}-${nameId}`),
+ inputName: utils.stringToId(`selected-${id}-${nameId}`),
+ inputValue: "value",
+ labelTitle: "Click to toggle",
+ inputChecked: active(),
+ onClick: () => {
+ active.set(input.checked);
+ },
+ type: "checkbox",
+ });
+
+ const spanMain = window.document.createElement("span");
+ spanMain.classList.add("main");
+ label.append(spanMain);
+
+ const spanName = utils.dom.createSpanName(name);
+ spanMain.append(spanName);
+
+ div.append(label);
+ label.addEventListener("mouseover", () => {
+ const h = hovered();
+ if (!h || h !== series) {
+ hovered.set(series);
+ }
+ });
+ label.addEventListener("mouseleave", () => {
+ hovered.set(null);
+ });
+
+ function shouldHighlight() {
+ const h = hovered();
+ return !h || h === series;
+ }
+
+ /**
+ * @param {string} color
+ */
+ function tameColor(color) {
+ return `${color.slice(0, -1)} / 50%)`;
+ }
+
+ const spanColors = window.document.createElement("span");
+ spanColors.classList.add("colors");
+ spanMain.prepend(spanColors);
+ colors.forEach((color) => {
+ const spanColor = window.document.createElement("span");
+ spanColors.append(spanColor);
+
+ signals.createEffect(
+ () => ({
+ color: color(),
+ shouldHighlight: shouldHighlight(),
+ }),
+ ({ color, shouldHighlight }) => {
+ if (shouldHighlight) {
+ spanColor.style.backgroundColor = color;
+ } else {
+ spanColor.style.backgroundColor = tameColor(color);
+ }
+ },
+ );
+ });
+
+ const initialColors = /** @type {Record} */ ({});
+ const darkenedColors = /** @type {Record} */ ({});
+
+ const seriesOptions = series.options();
+ if (!seriesOptions) return;
+
+ Object.entries(seriesOptions).forEach(([k, v]) => {
+ if (k.toLowerCase().includes("color") && typeof v === "string") {
+ if (!v.startsWith("oklch")) return;
+ initialColors[k] = v;
+ darkenedColors[k] = tameColor(v);
+ } else if (k === "lastValueVisible" && v) {
+ initialColors[k] = true;
+ darkenedColors[k] = false;
+ }
+ });
+
+ signals.createEffect(shouldHighlight, (shouldHighlight) => {
+ if (shouldHighlight) {
+ series.applyOptions(initialColors);
+ } else {
+ series.applyOptions(darkenedColors);
+ }
+ });
+
+ if (url) {
+ const anchor = window.document.createElement("a");
+ anchor.href = url;
+ anchor.target = "_blank";
+ anchor.rel = "noopener noreferrer";
+ div.append(anchor);
+ }
+ },
reset() {
legendElement.innerHTML = "";
},
};
}
-const oklchToRGBA = (() => {
+function createOklchToRGBA() {
{
/**
*
@@ -401,7 +574,13 @@ const oklchToRGBA = (() => {
return function (oklch) {
oklch = oklch.replace("oklch(", "");
oklch = oklch.replace(")", "");
- const lch = oklch.split(" ").map((v, i) => {
+ let splitOklch = oklch.split(" / ");
+ let alpha = 1;
+ if (splitOklch.length === 2) {
+ alpha = Number(splitOklch.pop()?.replace("%", "")) / 100;
+ }
+ splitOklch = oklch.split(" ");
+ const lch = splitOklch.map((v, i) => {
if (!i && v.includes("%")) {
return Number(v.replace("%", "")) / 100;
} else {
@@ -415,7 +594,7 @@ const oklchToRGBA = (() => {
).map((v) => {
return Math.max(Math.min(Math.round(v * 255), 255), 0);
});
- return [...rgb, 1];
+ return [...rgb, alpha];
};
}
-})();
+}
diff --git a/websites/kibo.money/packages/solid-signals/wrapper.js b/websites/kibo.money/packages/solid-signals/wrapper.js
index c7153a4a9..351cce69c 100644
--- a/websites/kibo.money/packages/solid-signals/wrapper.js
+++ b/websites/kibo.money/packages/solid-signals/wrapper.js
@@ -33,7 +33,7 @@ const importSignals = import("./2024-11-02/script.js").then((_signals) => {
/**
* @template T
* @param {T} initialValue
- * @param {SignalOptions & {save?: {keyPrefix: string; key: string; serialize: (v: NonNullable) => string; deserialize: (v: string) => NonNullable}}} [options]
+ * @param {SignalOptions & {save?: {keyPrefix: string; key: string; serialize: (v: T) => string; deserialize: (v: string) => T; serializeParam?: boolean}}} [options]
* @returns {Signal}
*/
createSignal(initialValue, options) {
@@ -55,7 +55,11 @@ const importSignals = import("./2024-11-02/script.js").then((_signals) => {
const storageKey = `${save.keyPrefix}-${paramKey}`;
let serialized = /** @type {string | null} */ (null);
- serialized = new URLSearchParams(window.location.search).get(paramKey);
+ if (options.save.serializeParam !== false) {
+ serialized = new URLSearchParams(window.location.search).get(
+ paramKey,
+ );
+ }
if (serialized === null) {
serialized = localStorage.getItem(storageKey);
diff --git a/websites/kibo.money/scripts/chart.js b/websites/kibo.money/scripts/chart.js
index 94e83182d..adf80af60 100644
--- a/websites/kibo.money/scripts/chart.js
+++ b/websites/kibo.money/scripts/chart.js
@@ -36,29 +36,58 @@ export function init({
signals,
colors,
id: "chart",
- kind: "scrollable",
utils,
vecsResources,
});
- const index = createIndexSelector({ elements, signals, utils });
+ const index_ = createIndexSelector({ elements, signals, utils });
- // const vecs = signals.createSignal(
- // /** @type {Set} */ (new Set()),
- // {
- // equals: false,
- // },
- // );
+ let firstRun = true;
signals.createEffect(selected, (option) => {
titleElement.innerHTML = option.title;
- signals.createEffect(index, (index) => {
+ signals.createEffect(index_, (index) => {
+ utils.url.writeParam("index", String(index));
+
chart.reset({ owner: signals.getOwner() });
- chart.create(index);
+ const TIMERANGE_LS_KEY = `chart-timerange-${index}`;
- const candles = chart.addCandlestickSeries("ohlc");
+ const from = signals.createSignal(/** @type {number | null} */ (null), {
+ save: {
+ ...utils.serde.optNumber,
+ keyPrefix: TIMERANGE_LS_KEY,
+ key: "from",
+ serializeParam: firstRun,
+ },
+ });
+ const to = signals.createSignal(/** @type {number | null} */ (null), {
+ save: {
+ ...utils.serde.optNumber,
+ keyPrefix: TIMERANGE_LS_KEY,
+ key: "to",
+ serializeParam: firstRun,
+ },
+ });
+ chart.create({
+ index,
+ timeScaleSetCallback: () => {
+ const from_ = from();
+ const to_ = to();
+ if (from_ !== null && to_ !== null) {
+ chart.inner()?.timeScale().setVisibleLogicalRange({
+ from: from_,
+ to: to_,
+ });
+ }
+ },
+ });
+
+ const candles = chart.addCandlestickSeries({
+ vecId: "ohlc",
+ name: "Price",
+ });
signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
if (!latest) return;
const last = /** @type { CandlestickData | undefined} */ (
@@ -69,128 +98,35 @@ export function init({
});
[
- { blueprints: option.top, paneIndex: 0 },
- { blueprints: option.bottom, paneIndex: 1 },
- ].forEach(({ blueprints, paneIndex }) => {
+ { blueprints: option.top, paneNumber: 0 },
+ { blueprints: option.bottom, paneNumber: 1 },
+ ].forEach(({ blueprints, paneNumber }) => {
blueprints?.forEach((blueprint) => {
if (vecIdToIndexes[blueprint.key].includes(index)) {
- const series = chart.addLineSeries(blueprint.key, paneIndex);
- series.applyOptions({
- visible: blueprint.defaultActive !== false,
- color: blueprint.color?.(),
+ chart.addLineSeries({
+ vecId: blueprint.key,
+ color: blueprint.color,
+ name: blueprint.title,
+ defaultActive: blueprint.defaultActive,
+ paneNumber,
});
}
});
});
+
+ chart
+ .inner()
+ ?.timeScale()
+ .subscribeVisibleLogicalRangeChange(
+ utils.debounce((t) => {
+ from.set(t.from);
+ to.set(t.to);
+ }),
+ );
+
+ firstRun = false;
});
});
-
- // function createFetchChunksOfVisibleDatasetsEffect() {
- // signals.createEffect(
- // () => ({
- // ids: chart.visibleDatasetIds(),
- // activeDatasets: activeDatasets(),
- // }),
- // ({ ids, activeDatasets }) => {
- // const datasets = Array.from(activeDatasets);
-
- // if (ids.length === 0 || datasets.length === 0) return;
-
- // for (let i = 0; i < ids.length; i++) {
- // const id = ids[i];
- // for (let j = 0; j < datasets.length; j++) {
- // datasets[j].fetch(id);
- // }
- // }
- // },
- // );
- // }
- // createFetchChunksOfVisibleDatasetsEffect();
-
- // /**
- // * @param {ChartOption} option
- // */
- // function applyChartOption(option) {
- // chart.visibleTimeRange.set(chart.getInitialVisibleTimeRange());
-
- // activeDatasets.set((s) => {
- // s.clear();
- // return s;
- // });
-
- // const chartsBlueprints = [option.top || [], option.bottom].flatMap(
- // (list) => (list ? [list] : []),
- // );
-
- // chartsBlueprints.map((seriesBlueprints, paneIndex) => {
- // const chartPane = chart.createPane({
- // paneIndex,
- // unit: paneIndex ? option.unit : "US Dollars",
- // });
-
- // if (!paneIndex) {
- // /** @type {AnyDatasetPath} */
- // const datasetPath = `${scale}-to-price`;
-
- // const dataset = datasets.getOrCreate(scale, datasetPath);
-
- // // Don't trigger reactivity by design
- // activeDatasets().add(dataset);
-
- // const priceSeries = chartPane.createSplitSeries({
- // blueprint: {
- // datasetPath,
- // title: "BTC Price",
- // type: "Candlestick",
- // },
- // dataset,
- // id: option.id,
- // index: -1,
- // });
-
- // signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
- // if (!latest) return;
-
- // const index = utils.chunkIdToIndex(scale, latest.year);
-
- // priceSeries.forEach((splitSeries) => {
- // const series = splitSeries.chunks.at(index);
- // if (series) {
- // signals.createEffect(series, (series) => {
- // series?.update(latest);
- // });
- // }
- // });
- // });
- // }
-
- // [...seriesBlueprints].reverse().forEach((blueprint, index) => {
- // const dataset = datasets.getOrCreate(scale, blueprint.datasetPath);
-
- // // Don't trigger reactivity by design
- // activeDatasets().add(dataset);
-
- // chartPane.createSplitSeries({
- // index,
- // blueprint,
- // id: option.id,
- // dataset,
- // });
- // });
-
- // activeDatasets.set((s) => s);
-
- // return chart;
- // });
- // }
-
- // function createApplyChartOptionEffect() {
- // signals.createEffect(selected, (option) => {
- // chart.reset({ scale: option.scale, owner: signals.getOwner() });
- // applyChartOption(option);
- // });
- // }
- // createApplyChartOptionEffect();
}
/**
diff --git a/websites/kibo.money/scripts/main.js b/websites/kibo.money/scripts/main.js
index 25c89b34a..e8a83fbbb 100644
--- a/websites/kibo.money/scripts/main.js
+++ b/websites/kibo.money/scripts/main.js
@@ -866,6 +866,20 @@ function createUtils() {
return Number(v);
},
},
+ optNumber: {
+ /**
+ * @param {number | null} v
+ */
+ serialize(v) {
+ return v !== null ? String(v) : "";
+ },
+ /**
+ * @param {string} v
+ */
+ deserialize(v) {
+ return v ? Number(v) : null;
+ },
+ },
date: {
/**
* @param {Date} v
diff --git a/websites/kibo.money/scripts/options.js b/websites/kibo.money/scripts/options.js
index c631aad78..3c801b96b 100644
--- a/websites/kibo.money/scripts/options.js
+++ b/websites/kibo.money/scripts/options.js
@@ -4946,21 +4946,9 @@ function createPartialOptions(colors) {
color: colors.orange,
},
{
- key: "block-interval-max",
- title: "Max",
- color: colors.pink,
- defaultActive: false,
- },
- {
- key: "block-interval-min",
- title: "Min",
- color: colors.green,
- defaultActive: false,
- },
- {
- key: "block-interval-90p",
- title: "90p",
- color: colors.rose,
+ key: "block-interval-median",
+ title: "Median",
+ color: colors.amber,
defaultActive: false,
},
{
@@ -4969,24 +4957,36 @@ function createPartialOptions(colors) {
color: colors.red,
defaultActive: false,
},
- {
- key: "block-interval-median",
- title: "Median",
- color: colors.amber,
- defaultActive: false,
- },
{
key: "block-interval-25p",
title: "25p",
color: colors.yellow,
defaultActive: false,
},
+ {
+ key: "block-interval-90p",
+ title: "90p",
+ color: colors.rose,
+ defaultActive: false,
+ },
{
key: "block-interval-10p",
title: "10p",
color: colors.lime,
defaultActive: false,
},
+ {
+ key: "block-interval-max",
+ title: "Max",
+ color: colors.pink,
+ defaultActive: false,
+ },
+ {
+ key: "block-interval-min",
+ title: "Min",
+ color: colors.green,
+ defaultActive: false,
+ },
],
},
],