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;
|
gap: 0.25rem;
|
||||||
|
|
||||||
> span.colors {
|
> span.colors {
|
||||||
|
margin-top: 0.25rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 0.75rem;
|
width: 0.625rem;
|
||||||
height: 0.75rem;
|
height: 0.625rem;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
@@ -966,9 +967,13 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
margin-bottom: 2rem;
|
flex: 1;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
> legend {
|
> legend {
|
||||||
|
text-transform: lowercase;
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
@@ -976,7 +981,7 @@
|
|||||||
margin-right: var(--negative-main-padding);
|
margin-right: var(--negative-main-padding);
|
||||||
padding-left: var(--main-padding);
|
padding-left: var(--main-padding);
|
||||||
padding-right: var(--main-padding);
|
padding-right: var(--main-padding);
|
||||||
padding-bottom: 1.25rem;
|
padding-bottom: 1.5rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
@@ -1015,13 +1020,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> a {
|
> a {
|
||||||
padding: 0.375rem;
|
padding-left: 0.375rem;
|
||||||
margin: -0.375rem;
|
padding-right: 0.375rem;
|
||||||
|
margin-left: -0.375rem;
|
||||||
|
margin-right: -0.375rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightweight-chart {
|
.lightweight-chart {
|
||||||
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-right: var(--negative-main-padding);
|
margin-right: var(--negative-main-padding);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ interface BaseSeries {
|
|||||||
visible: Accessor<boolean>;
|
visible: Accessor<boolean>;
|
||||||
}
|
}
|
||||||
interface SingleSeries extends BaseSeries {
|
interface SingleSeries extends BaseSeries {
|
||||||
iseries: ISeriesApi<any>;
|
iseries: ISeriesApi<SeriesType>;
|
||||||
dataset: Accessor<(SingleValueData | CandlestickData)[] | null>;
|
dataset: Accessor<(SingleValueData | CandlestickData)[] | null>;
|
||||||
}
|
}
|
||||||
interface SplitSeries extends BaseSeries {
|
interface SplitSeries extends BaseSeries {
|
||||||
@@ -108,7 +108,7 @@ interface Marker {
|
|||||||
weight: number;
|
weight: number;
|
||||||
time: Time;
|
time: Time;
|
||||||
value: number;
|
value: number;
|
||||||
seriesChunk: ISeriesApi<any>;
|
seriesChunk: ISeriesApi<SeriesType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HoveredLegend {
|
interface HoveredLegend {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// @ts-check
|
// @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) => {
|
export default import("./v5.0.5/script.js").then((lc) => {
|
||||||
|
const oklchToRGBA = createOklchToRGBA();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} args
|
* @param {Object} args
|
||||||
* @param {HTMLElement} args.element
|
* @param {HTMLElement} args.element
|
||||||
@@ -63,13 +65,6 @@ export default import("./v5.0.5/script.js").then((lc) => {
|
|||||||
borderColor: colors.border(),
|
borderColor: colors.border(),
|
||||||
}),
|
}),
|
||||||
({ defaultColor, offColor, borderColor }) => {
|
({ defaultColor, offColor, borderColor }) => {
|
||||||
console.log(
|
|
||||||
defaultColor,
|
|
||||||
offColor,
|
|
||||||
borderColor,
|
|
||||||
`rgba(${oklchToRGBA(borderColor).join(", ")})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
chart.applyOptions({
|
chart.applyOptions({
|
||||||
layout: {
|
layout: {
|
||||||
textColor: offColor,
|
textColor: offColor,
|
||||||
@@ -102,36 +97,25 @@ export default import("./v5.0.5/script.js").then((lc) => {
|
|||||||
return chart;
|
return chart;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {DeepPartial<SeriesOptionsCommon>}
|
|
||||||
*/
|
|
||||||
const defaultSeriesOptions = {
|
|
||||||
// @ts-ignore
|
|
||||||
lineWidth: 1.5,
|
|
||||||
// priceLineVisible: false,
|
|
||||||
// baseLineVisible: false,
|
|
||||||
// baseLineColor: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} args
|
* @param {Object} args
|
||||||
* @param {string} args.id
|
* @param {string} args.id
|
||||||
* @param {HTMLElement} args.parent
|
* @param {HTMLElement} args.parent
|
||||||
* @param {Signals} args.signals
|
* @param {Signals} args.signals
|
||||||
* @param {Colors} args.colors
|
* @param {Colors} args.colors
|
||||||
* @param {"static" | "scrollable"} args.kind
|
|
||||||
* @param {Utilities} args.utils
|
* @param {Utilities} args.utils
|
||||||
* @param {VecsResources} args.vecsResources
|
* @param {VecsResources} args.vecsResources
|
||||||
* @param {Owner | null} [args.owner]
|
* @param {Owner | null} [args.owner]
|
||||||
|
* @param {true} [args.fitContentOnResize]
|
||||||
*/
|
*/
|
||||||
function createChartElement({
|
function createChartElement({
|
||||||
parent,
|
parent,
|
||||||
signals,
|
signals,
|
||||||
colors,
|
colors,
|
||||||
kind,
|
|
||||||
utils,
|
utils,
|
||||||
vecsResources,
|
vecsResources,
|
||||||
owner: _owner,
|
owner: _owner,
|
||||||
|
fitContentOnResize,
|
||||||
}) {
|
}) {
|
||||||
let owner = _owner || signals.getOwner();
|
let owner = _owner || signals.getOwner();
|
||||||
|
|
||||||
@@ -141,6 +125,8 @@ export default import("./v5.0.5/script.js").then((lc) => {
|
|||||||
|
|
||||||
const legend = createLegend({
|
const legend = createLegend({
|
||||||
parent: div,
|
parent: div,
|
||||||
|
utils,
|
||||||
|
signals,
|
||||||
});
|
});
|
||||||
|
|
||||||
const chartDiv = window.document.createElement("div");
|
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 ichart = /** @type {IChartApi | null} */ (null);
|
||||||
let timeScaleSet = false;
|
let timeScaleSet = false;
|
||||||
|
|
||||||
if (kind === "static") {
|
if (fitContentOnResize) {
|
||||||
new ResizeObserver(() => ichart?.timeScale().fitContent()).observe(
|
new ResizeObserver(() => ichart?.timeScale().fitContent()).observe(
|
||||||
chartDiv,
|
chartDiv,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {Index} */
|
/** @type {Index} */
|
||||||
let index = 0;
|
let vecIndex = 0; // Default value, overwritten
|
||||||
|
|
||||||
let timeResource = /** @type {VecResource| null} */ (null);
|
let timeResource = /** @type {VecResource| null} */ (null);
|
||||||
|
|
||||||
|
let timeScaleSetCallback = /** @type {VoidFunction | null} */ (null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ISeriesApi<any>} series
|
* @param {ISeriesApi<SeriesType>} series
|
||||||
* @param {VecResource} valuesResource
|
* @param {VecResource} valuesResource
|
||||||
*/
|
*/
|
||||||
function createSetDataEffect(series, valuesResource) {
|
function createSetDataEffect(series, valuesResource) {
|
||||||
signals.createEffect(
|
signals.runWithOwner(owner, () =>
|
||||||
() => [timeResource?.fetched(), valuesResource.fetched()],
|
signals.createEffect(
|
||||||
([indexes, _ohlcs]) => {
|
() => [timeResource?.fetched(), valuesResource.fetched()],
|
||||||
if (!ichart) throw Error("IChart should be initialized");
|
([indexes, _ohlcs]) => {
|
||||||
|
if (!ichart) throw Error("IChart should be initialized");
|
||||||
|
|
||||||
if (!indexes || !_ohlcs) return;
|
if (!indexes || !_ohlcs) return;
|
||||||
const ohlcs = /** @type {OHLCTuple[]} */ (_ohlcs);
|
const ohlcs = /** @type {OHLCTuple[]} */ (_ohlcs);
|
||||||
let length = Math.min(indexes.length, ohlcs.length);
|
let length = Math.min(indexes.length, ohlcs.length);
|
||||||
const data = new Array(length);
|
const data = new Array(length);
|
||||||
let prevTime = null;
|
let prevTime = null;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
const time = indexes[i];
|
const time = indexes[i];
|
||||||
if (prevTime && prevTime === time) {
|
if (prevTime && prevTime === time) {
|
||||||
offset += 1;
|
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];
|
data.length -= offset;
|
||||||
if (typeof v === "number") {
|
series.setData(data);
|
||||||
data[i - offset] = {
|
timeScaleSetCallback?.();
|
||||||
time,
|
if (
|
||||||
value: v,
|
!timeScaleSet &&
|
||||||
};
|
(vecIndex === /** @satisfies {Yearindex} */ (15) ||
|
||||||
} else {
|
vecIndex === /** @satisfies {Decadeindex} */ (16))
|
||||||
data[i - offset] = {
|
) {
|
||||||
time,
|
ichart
|
||||||
open: v[0],
|
.timeScale()
|
||||||
high: v[1],
|
.setVisibleLogicalRange({ from: -1, to: data.length });
|
||||||
low: v[2],
|
|
||||||
close: v[3],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
prevTime = time;
|
timeScaleSet = true;
|
||||||
}
|
},
|
||||||
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;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeResources = /** @type {VecResource[]} */ ([]);
|
||||||
|
|
||||||
|
ichart?.subscribeCrosshairMove(
|
||||||
|
utils.debounce(() => {
|
||||||
|
activeResources.forEach((v) => {
|
||||||
|
v.fetch();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
inner: () => ichart,
|
||||||
/**
|
/**
|
||||||
* @param {Index} _index
|
* @param {Object} args
|
||||||
|
* @param {Index} args.index
|
||||||
|
* @param {VoidFunction} [args.timeScaleSetCallback]
|
||||||
*/
|
*/
|
||||||
create(_index) {
|
create({ index: _index, timeScaleSetCallback: _timeScaleSetCallback }) {
|
||||||
index = _index;
|
vecIndex = _index;
|
||||||
|
timeScaleSetCallback = _timeScaleSetCallback || null;
|
||||||
|
|
||||||
if (ichart) throw Error("IChart shouldn't be initialized");
|
if (ichart) throw Error("IChart shouldn't be initialized");
|
||||||
|
|
||||||
timeResource = vecsResources.getOrCreate(
|
timeResource = vecsResources.getOrCreate(
|
||||||
index,
|
vecIndex,
|
||||||
index === /** @satisfies {Height} */ (2)
|
vecIndex === /** @satisfies {Height} */ (2)
|
||||||
? "fixed-timestamp"
|
? "fixed-timestamp"
|
||||||
: "timestamp",
|
: "timestamp",
|
||||||
);
|
);
|
||||||
timeResource.fetch();
|
timeResource.fetch();
|
||||||
|
|
||||||
ichart = createLightweightChart({
|
ichart = createLightweightChart({
|
||||||
index,
|
index: vecIndex,
|
||||||
element: chartDiv,
|
element: chartDiv,
|
||||||
signals,
|
signals,
|
||||||
colors,
|
colors,
|
||||||
@@ -240,14 +246,18 @@ export default import("./v5.0.5/script.js").then((lc) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* @param {VecId} id
|
* @param {Object} args
|
||||||
* @param {number} [paneNumber]
|
* @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");
|
if (!ichart || !timeResource) throw Error("Chart not fully set");
|
||||||
|
|
||||||
const valuesResource = vecsResources.getOrCreate(index, id);
|
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
|
||||||
valuesResource.fetch();
|
valuesResource.fetch();
|
||||||
|
activeResources.push(valuesResource);
|
||||||
|
|
||||||
const green = colors.green();
|
const green = colors.green();
|
||||||
const red = colors.red();
|
const red = colors.red();
|
||||||
@@ -259,32 +269,61 @@ export default import("./v5.0.5/script.js").then((lc) => {
|
|||||||
wickUpColor: green,
|
wickUpColor: green,
|
||||||
wickDownColor: red,
|
wickDownColor: red,
|
||||||
borderVisible: false,
|
borderVisible: false,
|
||||||
|
visible: defaultActive !== false,
|
||||||
},
|
},
|
||||||
paneNumber,
|
paneNumber,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
legend.add({
|
||||||
|
series,
|
||||||
|
name,
|
||||||
|
id: vecId,
|
||||||
|
defaultActive,
|
||||||
|
colors: [colors.green, colors.red],
|
||||||
|
url: valuesResource.url,
|
||||||
|
});
|
||||||
|
|
||||||
createSetDataEffect(series, valuesResource);
|
createSetDataEffect(series, valuesResource);
|
||||||
|
|
||||||
return series;
|
return series;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* @param {VecId} id
|
* @param {Object} args
|
||||||
* @param {number} [paneNumber]
|
* @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");
|
if (!ichart || !timeResource) throw Error("Chart not fully set");
|
||||||
|
|
||||||
const valuesResource = vecsResources.getOrCreate(index, id);
|
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
|
||||||
valuesResource.fetch();
|
valuesResource.fetch();
|
||||||
|
activeResources.push(valuesResource);
|
||||||
|
|
||||||
|
color ||= colors.orange;
|
||||||
|
|
||||||
const series = ichart.addSeries(
|
const series = ichart.addSeries(
|
||||||
/** @type {SeriesDefinition<'Line'>} */ (lc.LineSeries),
|
/** @type {SeriesDefinition<'Line'>} */ (lc.LineSeries),
|
||||||
{
|
{
|
||||||
lineWidth: /** @type {any} */ (1.5),
|
lineWidth: /** @type {any} */ (1.5),
|
||||||
|
visible: defaultActive !== false,
|
||||||
|
priceLineVisible: false,
|
||||||
|
color: color(),
|
||||||
},
|
},
|
||||||
paneNumber,
|
paneNumber,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
legend.add({
|
||||||
|
series,
|
||||||
|
colors: [color],
|
||||||
|
id: vecId,
|
||||||
|
name,
|
||||||
|
defaultActive,
|
||||||
|
url: valuesResource.url,
|
||||||
|
});
|
||||||
|
|
||||||
createSetDataEffect(series, valuesResource);
|
createSetDataEffect(series, valuesResource);
|
||||||
|
|
||||||
return series;
|
return series;
|
||||||
@@ -299,6 +338,7 @@ export default import("./v5.0.5/script.js").then((lc) => {
|
|||||||
ichart?.remove();
|
ichart?.remove();
|
||||||
ichart = null;
|
ichart = null;
|
||||||
timeScaleSet = false;
|
timeScaleSet = false;
|
||||||
|
activeResources.length = 0;
|
||||||
legend.reset();
|
legend.reset();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -313,19 +353,152 @@ export default import("./v5.0.5/script.js").then((lc) => {
|
|||||||
/**
|
/**
|
||||||
* @param {Object} args
|
* @param {Object} args
|
||||||
* @param {Element} args.parent
|
* @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");
|
const legendElement = window.document.createElement("legend");
|
||||||
parent.append(legendElement);
|
parent.append(legendElement);
|
||||||
|
|
||||||
|
const hovered = signals.createSignal(
|
||||||
|
/** @type {ISeriesApi<SeriesType> | null} */ (null),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
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() {
|
reset() {
|
||||||
legendElement.innerHTML = "";
|
legendElement.innerHTML = "";
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const oklchToRGBA = (() => {
|
function createOklchToRGBA() {
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -401,7 +574,13 @@ const oklchToRGBA = (() => {
|
|||||||
return function (oklch) {
|
return function (oklch) {
|
||||||
oklch = oklch.replace("oklch(", "");
|
oklch = oklch.replace("oklch(", "");
|
||||||
oklch = oklch.replace(")", "");
|
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("%")) {
|
if (!i && v.includes("%")) {
|
||||||
return Number(v.replace("%", "")) / 100;
|
return Number(v.replace("%", "")) / 100;
|
||||||
} else {
|
} else {
|
||||||
@@ -415,7 +594,7 @@ const oklchToRGBA = (() => {
|
|||||||
).map((v) => {
|
).map((v) => {
|
||||||
return Math.max(Math.min(Math.round(v * 255), 255), 0);
|
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
|
* @template T
|
||||||
* @param {T} initialValue
|
* @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>}
|
* @returns {Signal<T>}
|
||||||
*/
|
*/
|
||||||
createSignal(initialValue, options) {
|
createSignal(initialValue, options) {
|
||||||
@@ -55,7 +55,11 @@ const importSignals = import("./2024-11-02/script.js").then((_signals) => {
|
|||||||
const storageKey = `${save.keyPrefix}-${paramKey}`;
|
const storageKey = `${save.keyPrefix}-${paramKey}`;
|
||||||
|
|
||||||
let serialized = /** @type {string | null} */ (null);
|
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) {
|
if (serialized === null) {
|
||||||
serialized = localStorage.getItem(storageKey);
|
serialized = localStorage.getItem(storageKey);
|
||||||
|
|||||||
@@ -36,29 +36,58 @@ export function init({
|
|||||||
signals,
|
signals,
|
||||||
colors,
|
colors,
|
||||||
id: "chart",
|
id: "chart",
|
||||||
kind: "scrollable",
|
|
||||||
utils,
|
utils,
|
||||||
vecsResources,
|
vecsResources,
|
||||||
});
|
});
|
||||||
|
|
||||||
const index = createIndexSelector({ elements, signals, utils });
|
const index_ = createIndexSelector({ elements, signals, utils });
|
||||||
|
|
||||||
// const vecs = signals.createSignal(
|
let firstRun = true;
|
||||||
// /** @type {Set<VecResource>} */ (new Set()),
|
|
||||||
// {
|
|
||||||
// equals: false,
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
|
|
||||||
signals.createEffect(selected, (option) => {
|
signals.createEffect(selected, (option) => {
|
||||||
titleElement.innerHTML = option.title;
|
titleElement.innerHTML = option.title;
|
||||||
signals.createEffect(index, (index) => {
|
signals.createEffect(index_, (index) => {
|
||||||
|
utils.url.writeParam("index", String(index));
|
||||||
|
|
||||||
chart.reset({ owner: signals.getOwner() });
|
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) => {
|
signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
|
||||||
if (!latest) return;
|
if (!latest) return;
|
||||||
const last = /** @type { CandlestickData | undefined} */ (
|
const last = /** @type { CandlestickData | undefined} */ (
|
||||||
@@ -69,128 +98,35 @@ export function init({
|
|||||||
});
|
});
|
||||||
|
|
||||||
[
|
[
|
||||||
{ blueprints: option.top, paneIndex: 0 },
|
{ blueprints: option.top, paneNumber: 0 },
|
||||||
{ blueprints: option.bottom, paneIndex: 1 },
|
{ blueprints: option.bottom, paneNumber: 1 },
|
||||||
].forEach(({ blueprints, paneIndex }) => {
|
].forEach(({ blueprints, paneNumber }) => {
|
||||||
blueprints?.forEach((blueprint) => {
|
blueprints?.forEach((blueprint) => {
|
||||||
if (vecIdToIndexes[blueprint.key].includes(index)) {
|
if (vecIdToIndexes[blueprint.key].includes(index)) {
|
||||||
const series = chart.addLineSeries(blueprint.key, paneIndex);
|
chart.addLineSeries({
|
||||||
series.applyOptions({
|
vecId: blueprint.key,
|
||||||
visible: blueprint.defaultActive !== false,
|
color: blueprint.color,
|
||||||
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);
|
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: {
|
date: {
|
||||||
/**
|
/**
|
||||||
* @param {Date} v
|
* @param {Date} v
|
||||||
|
|||||||
@@ -4946,21 +4946,9 @@ function createPartialOptions(colors) {
|
|||||||
color: colors.orange,
|
color: colors.orange,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "block-interval-max",
|
key: "block-interval-median",
|
||||||
title: "Max",
|
title: "Median",
|
||||||
color: colors.pink,
|
color: colors.amber,
|
||||||
defaultActive: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "block-interval-min",
|
|
||||||
title: "Min",
|
|
||||||
color: colors.green,
|
|
||||||
defaultActive: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "block-interval-90p",
|
|
||||||
title: "90p",
|
|
||||||
color: colors.rose,
|
|
||||||
defaultActive: false,
|
defaultActive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -4969,24 +4957,36 @@ function createPartialOptions(colors) {
|
|||||||
color: colors.red,
|
color: colors.red,
|
||||||
defaultActive: false,
|
defaultActive: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "block-interval-median",
|
|
||||||
title: "Median",
|
|
||||||
color: colors.amber,
|
|
||||||
defaultActive: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "block-interval-25p",
|
key: "block-interval-25p",
|
||||||
title: "25p",
|
title: "25p",
|
||||||
color: colors.yellow,
|
color: colors.yellow,
|
||||||
defaultActive: false,
|
defaultActive: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "block-interval-90p",
|
||||||
|
title: "90p",
|
||||||
|
color: colors.rose,
|
||||||
|
defaultActive: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "block-interval-10p",
|
key: "block-interval-10p",
|
||||||
title: "10p",
|
title: "10p",
|
||||||
color: colors.lime,
|
color: colors.lime,
|
||||||
defaultActive: false,
|
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