mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-24 08:44:46 -07:00
kibo: part 5
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ interface BaseSeries {
|
||||
visible: Accessor<boolean>;
|
||||
}
|
||||
interface SingleSeries extends BaseSeries {
|
||||
iseries: ISeriesApi<any>;
|
||||
iseries: ISeriesApi<SeriesType>;
|
||||
dataset: Accessor<(SingleValueData | CandlestickData)[] | null>;
|
||||
}
|
||||
interface SplitSeries extends BaseSeries {
|
||||
@@ -108,7 +108,7 @@ interface Marker {
|
||||
weight: number;
|
||||
time: Time;
|
||||
value: number;
|
||||
seriesChunk: ISeriesApi<any>;
|
||||
seriesChunk: ISeriesApi<SeriesType>;
|
||||
}
|
||||
|
||||
interface HoveredLegend {
|
||||
|
||||
@@ -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<SeriesOptionsCommon>}
|
||||
*/
|
||||
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<any>} series
|
||||
* @param {ISeriesApi<SeriesType>} 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<SeriesType> | null} */ (null),
|
||||
);
|
||||
|
||||
return {
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {ISeriesApi<SeriesType>} 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<string, any>} */ ({});
|
||||
const darkenedColors = /** @type {Record<string, any>} */ ({});
|
||||
|
||||
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];
|
||||
};
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ const importSignals = import("./2024-11-02/script.js").then((_signals) => {
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} initialValue
|
||||
* @param {SignalOptions<T> & {save?: {keyPrefix: string; key: string; serialize: (v: NonNullable<T>) => string; deserialize: (v: string) => NonNullable<T>}}} [options]
|
||||
* @param {SignalOptions<T> & {save?: {keyPrefix: string; key: string; serialize: (v: T) => string; deserialize: (v: string) => T; serializeParam?: boolean}}} [options]
|
||||
* @returns {Signal<T>}
|
||||
*/
|
||||
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);
|
||||
|
||||
@@ -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<VecResource>} */ (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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user