mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-15 09:13:36 -07:00
website: reorg
This commit is contained in:
+20
-5
@@ -466,6 +466,20 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
div {
|
||||
&:has(> * + button[type="reset"]) {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
|
||||
button {
|
||||
color: var(--off-color);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
field,
|
||||
h1 {
|
||||
text-transform: capitalize;
|
||||
@@ -655,6 +669,7 @@
|
||||
appearance: none;
|
||||
background: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="gray"><path fill-rule="evenodd" d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>')
|
||||
100% 50% no-repeat transparent;
|
||||
flex: 1;
|
||||
|
||||
&:focus-visible {
|
||||
border: 0;
|
||||
@@ -769,8 +784,8 @@
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
border-radius: 9999px;
|
||||
align-self: center;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.125rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
@@ -953,14 +968,14 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chart-list {
|
||||
.chart > .panes {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
> .chart-wrapper {
|
||||
> .pane {
|
||||
z-index: 20;
|
||||
position: relative;
|
||||
min-height: 0px;
|
||||
@@ -1012,9 +1027,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
> .chart-div {
|
||||
> .lightweight-chart {
|
||||
height: 100%;
|
||||
margin-right: calc(var(--negative-main-padding) - 0.5rem);
|
||||
margin-right: var(--negative-main-padding);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+4
@@ -0,0 +1,4 @@
|
||||
import { Accessor, Setter } from "./2024-11-02/types/signals";
|
||||
|
||||
export type Signal<T> = Accessor<T> & { set: Setter<T>; reset: VoidFunction };
|
||||
export type Signals = Awaited<typeof import("./wrapper.js").default>;
|
||||
@@ -0,0 +1,138 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @import { SignalOptions } from "./2024-11-02/types/core/core"
|
||||
* @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "./2024-11-02/types/core/owner"
|
||||
* @import { createSignal as CreateSignal, createEffect as CreateEffect, Accessor, Setter, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner } from "./2024-11-02/types/signals";
|
||||
* @import { Signal } from "./types";
|
||||
*/
|
||||
|
||||
const importSignals = import("./2024-11-02/script.js").then((_signals) => {
|
||||
const signals = {
|
||||
createSolidSignal: /** @type {CreateSignal} */ (_signals.createSignal),
|
||||
createSolidEffect: /** @type {CreateEffect} */ (_signals.createEffect),
|
||||
createEffect: /** @type {CreateEffect} */ (compute, effect) => {
|
||||
let dispose = /** @type {VoidFunction | null} */ (null);
|
||||
// @ts-ignore
|
||||
_signals.createEffect(compute, (v) => {
|
||||
dispose?.();
|
||||
signals.createRoot((_dispose) => {
|
||||
dispose = _dispose;
|
||||
effect(v);
|
||||
});
|
||||
signals.onCleanup(() => dispose?.());
|
||||
});
|
||||
signals.onCleanup(() => dispose?.());
|
||||
},
|
||||
createMemo: /** @type {CreateMemo} */ (_signals.createMemo),
|
||||
createRoot: /** @type {CreateRoot} */ (_signals.createRoot),
|
||||
getOwner: /** @type {GetOwner} */ (_signals.getOwner),
|
||||
runWithOwner: /** @type {RunWithOwner} */ (_signals.runWithOwner),
|
||||
onCleanup: /** @type {OnCleanup} */ (_signals.onCleanup),
|
||||
flushSync: _signals.flushSync,
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} initialValue
|
||||
* @param {SignalOptions<T> & {save?: {id?: string; param?: string; serialize: (v: NonNullable<T>) => string; deserialize: (v: string) => NonNullable<T>}}} [options]
|
||||
* @returns {Signal<T>}
|
||||
*/
|
||||
createSignal(initialValue, options) {
|
||||
const [get, set] = this.createSolidSignal(
|
||||
/** @type {any} */ (initialValue),
|
||||
options,
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
get.set = set;
|
||||
|
||||
// @ts-ignore
|
||||
get.reset = () => set(initialValue);
|
||||
|
||||
if (options?.save) {
|
||||
const save = options.save;
|
||||
|
||||
let serialized = /** @type {string | null} */ (null);
|
||||
if (save.param) {
|
||||
serialized = new URLSearchParams(window.location.search).get(
|
||||
save.param,
|
||||
);
|
||||
}
|
||||
if (serialized === null && save.id) {
|
||||
serialized = localStorage.getItem(save.id);
|
||||
}
|
||||
if (serialized) {
|
||||
set(save.deserialize(serialized));
|
||||
}
|
||||
|
||||
let firstEffect = true;
|
||||
this.createEffect(get, (value) => {
|
||||
if (!save) return;
|
||||
|
||||
if (!firstEffect && save.id) {
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
(initialValue === undefined ||
|
||||
initialValue === null ||
|
||||
save.serialize(value) !== save.serialize(initialValue))
|
||||
) {
|
||||
localStorage.setItem(save.id, save.serialize(value));
|
||||
} else {
|
||||
localStorage.removeItem(save.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (save.param) {
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
(initialValue === undefined ||
|
||||
initialValue === null ||
|
||||
save.serialize(value) !== save.serialize(initialValue))
|
||||
) {
|
||||
writeParam(save.param, save.serialize(value));
|
||||
} else {
|
||||
removeParam(save.param);
|
||||
}
|
||||
}
|
||||
|
||||
firstEffect = false;
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return get;
|
||||
},
|
||||
};
|
||||
|
||||
return signals;
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string | undefined} value
|
||||
*/
|
||||
function writeParam(key, value) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (value !== undefined) {
|
||||
urlParams.set(key, String(value));
|
||||
} else {
|
||||
urlParams.delete(key);
|
||||
}
|
||||
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${window.location.pathname}?${urlParams.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
function removeParam(key) {
|
||||
writeParam(key, undefined);
|
||||
}
|
||||
|
||||
export default importSignals;
|
||||
+75
-705
@@ -1,32 +1,28 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @import { HoveredLegend, PriceSeriesType, Series } from "./types/self"
|
||||
* @import { ChartPane, HoveredLegend, PriceSeriesType, SplitSeries } from "./types/self"
|
||||
* @import { Options } from './options';
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {Consts} args.consts
|
||||
* @param {LightweightCharts} args.lightweightCharts
|
||||
* @param {Accessor<ChartOption>} args.selected
|
||||
* @param {Signals} args.signals
|
||||
* @param {Utilities} args.utils
|
||||
* @param {Options} args.options
|
||||
* @param {Datasets} args.datasets
|
||||
* @param {WebSockets} args.webSockets
|
||||
* @param {Elements} args.elements
|
||||
* @param {Ids} args.ids
|
||||
* @param {Accessor<boolean>} args.dark
|
||||
*/
|
||||
export function init({
|
||||
colors,
|
||||
consts,
|
||||
dark,
|
||||
datasets,
|
||||
elements,
|
||||
ids,
|
||||
lightweightCharts,
|
||||
options,
|
||||
selected,
|
||||
signals,
|
||||
utils,
|
||||
@@ -34,138 +30,29 @@ export function init({
|
||||
}) {
|
||||
console.log("init chart state");
|
||||
|
||||
/** @type {ChartPane[]} */
|
||||
let charts = [];
|
||||
|
||||
const scale = signals.createMemo(() => selected().scale);
|
||||
|
||||
elements.charts.append(utils.dom.createShadow("left"));
|
||||
elements.charts.append(utils.dom.createShadow("right"));
|
||||
|
||||
const { headerElement, titleElement, descriptionElement } =
|
||||
utils.dom.createHeader({
|
||||
title: selected().title,
|
||||
description: selected().serializedPath,
|
||||
});
|
||||
utils.dom.createHeader({});
|
||||
elements.charts.append(headerElement);
|
||||
signals.createEffect(selected, (option) => {
|
||||
titleElement.innerHTML = option.title;
|
||||
descriptionElement.innerHTML = option.serializedPath;
|
||||
});
|
||||
|
||||
// const div = window.document.createElement("div");
|
||||
// elements.charts.append(div);
|
||||
|
||||
// const legendElement = window.document.createElement("legend");
|
||||
// div.append(legendElement);
|
||||
|
||||
// const chartListElement = window.document.createElement("div");
|
||||
// chartListElement.classList.add("chart-list");
|
||||
// div.append(chartListElement);
|
||||
//
|
||||
const {
|
||||
chartListElement,
|
||||
legendElement,
|
||||
createPane: addChart,
|
||||
} = lightweightCharts.createChart({
|
||||
const chart = lightweightCharts.createChart({
|
||||
parent: elements.charts,
|
||||
signals,
|
||||
colors,
|
||||
id: "chart",
|
||||
scale: scale(),
|
||||
kind: "moveable",
|
||||
utils,
|
||||
});
|
||||
|
||||
/**
|
||||
* @returns {TimeRange}
|
||||
*/
|
||||
function getInitialVisibleTimeRange() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
const urlFrom = urlParams.get(ids.from);
|
||||
const urlTo = urlParams.get(ids.to);
|
||||
|
||||
if (urlFrom && urlTo) {
|
||||
if (scale() === "date" && urlFrom.includes("-") && urlTo.includes("-")) {
|
||||
console.log({
|
||||
from: new Date(urlFrom).toJSON().split("T")[0],
|
||||
to: new Date(urlTo).toJSON().split("T")[0],
|
||||
});
|
||||
return {
|
||||
from: new Date(urlFrom).toJSON().split("T")[0],
|
||||
to: new Date(urlTo).toJSON().split("T")[0],
|
||||
};
|
||||
} else if (
|
||||
scale() === "height" &&
|
||||
(!urlFrom.includes("-") || !urlTo.includes("-"))
|
||||
) {
|
||||
console.log({
|
||||
from: Number(urlFrom),
|
||||
to: Number(urlTo),
|
||||
});
|
||||
return {
|
||||
from: Number(urlFrom),
|
||||
to: Number(urlTo),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getSavedTimeRange() {
|
||||
return /** @type {TimeRange | null} */ (
|
||||
JSON.parse(
|
||||
localStorage.getItem(ids.visibleTimeRange(scale())) || "null",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const savedTimeRange = getSavedTimeRange();
|
||||
|
||||
console.log(savedTimeRange);
|
||||
|
||||
if (savedTimeRange) {
|
||||
return savedTimeRange;
|
||||
}
|
||||
|
||||
function getDefaultTimeRange() {
|
||||
switch (scale()) {
|
||||
case "date": {
|
||||
const defaultTo = new Date();
|
||||
const defaultFrom = new Date();
|
||||
defaultFrom.setDate(defaultFrom.getUTCDate() - 6 * 30);
|
||||
|
||||
return {
|
||||
from: defaultFrom.toJSON().split("T")[0],
|
||||
to: defaultTo.toJSON().split("T")[0],
|
||||
};
|
||||
}
|
||||
case "height": {
|
||||
return {
|
||||
from: 850_000,
|
||||
to: 900_000,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getDefaultTimeRange();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IChartApi} chart
|
||||
*/
|
||||
function setInitialVisibleTimeRange(chart) {
|
||||
const range = visibleTimeRange();
|
||||
|
||||
if (range) {
|
||||
chart.timeScale().setVisibleRange(/** @type {any} */ (range));
|
||||
|
||||
// On small screen it doesn't it might not set it in time
|
||||
setTimeout(() => {
|
||||
try {
|
||||
chart.timeScale().setVisibleRange(/** @type {any} */ (range));
|
||||
} catch {}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
const activeDatasets = signals.createSignal(
|
||||
/** @type {Set<ResourceDataset<any, any>>} */ (new Set()),
|
||||
{
|
||||
@@ -173,76 +60,16 @@ export function init({
|
||||
},
|
||||
);
|
||||
|
||||
const visibleTimeRange = signals.createSignal(getInitialVisibleTimeRange());
|
||||
|
||||
const visibleDatasetIds = signals.createSignal(/** @type {number[]} */ ([]), {
|
||||
equals: false,
|
||||
});
|
||||
|
||||
const lastVisibleDatasetIndex = signals.createMemo(() => {
|
||||
const last = visibleDatasetIds().at(-1);
|
||||
return last !== undefined ? utils.chunkIdToIndex(scale(), last) : undefined;
|
||||
});
|
||||
|
||||
const priceSeriesType = signals.createSignal(
|
||||
/** @type {PriceSeriesType} */ ("Candlestick"),
|
||||
);
|
||||
|
||||
function updateVisibleDatasetIds() {
|
||||
/** @type {number[]} */
|
||||
let ids = [];
|
||||
|
||||
const today = new Date();
|
||||
const { from: rawFrom, to: rawTo } = visibleTimeRange();
|
||||
|
||||
if (typeof rawFrom === "string" && typeof rawTo === "string") {
|
||||
const from = new Date(rawFrom).getUTCFullYear();
|
||||
const to = new Date(rawTo).getUTCFullYear();
|
||||
|
||||
ids = Array.from({ length: to - from + 1 }, (_, i) => i + from).filter(
|
||||
(year) => year >= 2009 && year <= today.getUTCFullYear(),
|
||||
);
|
||||
} else {
|
||||
const from = Math.floor(Number(rawFrom) / consts.HEIGHT_CHUNK_SIZE);
|
||||
const to = Math.floor(Number(rawTo) / consts.HEIGHT_CHUNK_SIZE);
|
||||
|
||||
const length = to - from + 1;
|
||||
|
||||
ids = Array.from(
|
||||
{ length },
|
||||
(_, i) => (from + i) * consts.HEIGHT_CHUNK_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
const old = visibleDatasetIds();
|
||||
|
||||
if (
|
||||
old.length !== ids.length ||
|
||||
old.at(0) !== ids.at(0) ||
|
||||
old.at(-1) !== ids.at(-1)
|
||||
) {
|
||||
console.log("range:", ids);
|
||||
|
||||
visibleDatasetIds.set(ids);
|
||||
}
|
||||
}
|
||||
updateVisibleDatasetIds();
|
||||
const debouncedUpdateVisibleDatasetIds = utils.debounce(
|
||||
updateVisibleDatasetIds,
|
||||
100,
|
||||
);
|
||||
|
||||
function saveVisibleRange() {
|
||||
const range = visibleTimeRange();
|
||||
utils.url.writeParam(ids.from, String(range.from));
|
||||
utils.url.writeParam(ids.to, String(range.to));
|
||||
localStorage.setItem(ids.visibleTimeRange(scale()), JSON.stringify(range));
|
||||
}
|
||||
const debouncedSaveVisibleRange = utils.debounce(saveVisibleRange, 250);
|
||||
|
||||
function createFetchChunksOfVisibleDatasetsEffect() {
|
||||
signals.createEffect(
|
||||
() => ({ ids: visibleDatasetIds(), activeDatasets: activeDatasets() }),
|
||||
() => ({
|
||||
ids: chart.visibleDatasetIds(),
|
||||
activeDatasets: activeDatasets(),
|
||||
}),
|
||||
({ ids, activeDatasets }) => {
|
||||
const datasets = Array.from(activeDatasets);
|
||||
|
||||
@@ -260,465 +87,37 @@ export function init({
|
||||
createFetchChunksOfVisibleDatasetsEffect();
|
||||
|
||||
/**
|
||||
* @param {IChartApi} chart
|
||||
* @param {Parameters<Chart['getTicksToWidthRatio']>[0]} args
|
||||
*/
|
||||
function subscribeVisibleTimeRangeChange(chart) {
|
||||
chart.timeScale().subscribeVisibleTimeRangeChange((range) => {
|
||||
if (!range) return;
|
||||
|
||||
visibleTimeRange.set(range);
|
||||
|
||||
debouncedUpdateVisibleDatasetIds();
|
||||
|
||||
debouncedSaveVisibleRange();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {IChartApi} args.chart
|
||||
* @param {LogicalRange} [args.visibleLogicalRange]
|
||||
* @param {TimeRange} [args.visibleTimeRange]
|
||||
*/
|
||||
function updateVisiblePriceSeriesType({
|
||||
chart,
|
||||
visibleLogicalRange,
|
||||
visibleTimeRange,
|
||||
}) {
|
||||
try {
|
||||
const width = chart.timeScale().width();
|
||||
|
||||
/** @type {number} */
|
||||
let ratio;
|
||||
|
||||
if (visibleLogicalRange) {
|
||||
ratio = (visibleLogicalRange.to - visibleLogicalRange.from) / width;
|
||||
} else if (visibleTimeRange) {
|
||||
if (scale() === "date") {
|
||||
const to = /** @type {Time} */ (visibleTimeRange.to);
|
||||
const from = /** @type {Time} */ (visibleTimeRange.from);
|
||||
|
||||
ratio =
|
||||
utils.getNumberOfDaysBetweenTwoDates(
|
||||
utils.date.fromTime(from),
|
||||
utils.date.fromTime(to),
|
||||
) / width;
|
||||
} else {
|
||||
const to = /** @type {number} */ (visibleTimeRange.to);
|
||||
const from = /** @type {number} */ (visibleTimeRange.from);
|
||||
|
||||
ratio = (to - from) / width;
|
||||
}
|
||||
} else {
|
||||
throw Error();
|
||||
}
|
||||
|
||||
function updateVisiblePriceSeriesType(args) {
|
||||
const ratio = chart.getTicksToWidthRatio(args);
|
||||
if (ratio) {
|
||||
if (ratio <= 0.5) {
|
||||
priceSeriesType.set("Candlestick");
|
||||
} else {
|
||||
priceSeriesType.set("Line");
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
const debouncedUpdateVisiblePriceSeriesType = utils.debounce(
|
||||
updateVisiblePriceSeriesType,
|
||||
50,
|
||||
);
|
||||
|
||||
const hoveredLegend = signals.createSignal(
|
||||
/** @type {HoveredLegend | undefined} */ (undefined),
|
||||
);
|
||||
const notHoveredLegendTransparency = "66";
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Series} args.series
|
||||
* @param {Accessor<boolean>} [args.disabled]
|
||||
* @param {string} [args.name]
|
||||
*/
|
||||
function createLegend({ series, disabled, name }) {
|
||||
const div = window.document.createElement("div");
|
||||
|
||||
if (disabled) {
|
||||
signals.createEffect(disabled, (disabled) => {
|
||||
div.hidden = disabled;
|
||||
});
|
||||
}
|
||||
|
||||
legendElement.prepend(div);
|
||||
|
||||
const { input, label } = utils.dom.createLabeledInput({
|
||||
inputId: `legend-${series.title}`,
|
||||
inputName: `selected-${series.title}${name}`,
|
||||
inputValue: "value",
|
||||
labelTitle: "Click to toggle",
|
||||
onClick: (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
input.checked = !input.checked;
|
||||
series.active.set(input.checked);
|
||||
},
|
||||
});
|
||||
|
||||
const spanMain = window.document.createElement("span");
|
||||
spanMain.classList.add("main");
|
||||
label.append(spanMain);
|
||||
|
||||
const spanName = utils.dom.createSpanName(series.title);
|
||||
spanMain.append(spanName);
|
||||
|
||||
div.append(label);
|
||||
label.addEventListener("mouseover", () => {
|
||||
const hovered = hoveredLegend();
|
||||
|
||||
if (!hovered || hovered.label !== label) {
|
||||
hoveredLegend.set({ label, series });
|
||||
}
|
||||
});
|
||||
label.addEventListener("mouseleave", () => {
|
||||
hoveredLegend.set(undefined);
|
||||
});
|
||||
|
||||
signals.createEffect(series.active, (checked) => {
|
||||
input.checked = checked;
|
||||
});
|
||||
|
||||
function shouldHighlight() {
|
||||
const hovered = hoveredLegend();
|
||||
return (
|
||||
!hovered ||
|
||||
(hovered.label === label && hovered.series.active()) ||
|
||||
(hovered.label !== label && !hovered.series.active())
|
||||
);
|
||||
}
|
||||
|
||||
const spanColors = window.document.createElement("span");
|
||||
spanColors.classList.add("colors");
|
||||
spanMain.prepend(spanColors);
|
||||
const colors = Array.isArray(series.color) ? series.color : [series.color];
|
||||
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 = `${color}${notHoveredLegendTransparency}`;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function createHoverEffect() {
|
||||
const initialColors = /** @type {Record<string, any>} */ ({});
|
||||
const darkenedColors = /** @type {Record<string, any>} */ ({});
|
||||
|
||||
/** @type {HoveredLegend | undefined} */
|
||||
let previouslyHovered = undefined;
|
||||
|
||||
signals.createEffect(
|
||||
() => ({ hovered: hoveredLegend(), ids: visibleDatasetIds() }),
|
||||
({ hovered, ids }) => {
|
||||
if (!hovered && !previouslyHovered) return hovered;
|
||||
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const chunkId = ids[i];
|
||||
const chunkIndex = utils.chunkIdToIndex(scale(), chunkId);
|
||||
const chunk = series.chunks[chunkIndex];
|
||||
|
||||
signals.createEffect(chunk, (chunk) => {
|
||||
if (!chunk) return;
|
||||
|
||||
if (hovered) {
|
||||
const seriesOptions = chunk.options();
|
||||
if (!seriesOptions) return;
|
||||
|
||||
initialColors[i] = {};
|
||||
darkenedColors[i] = {};
|
||||
|
||||
Object.entries(seriesOptions).forEach(([k, v]) => {
|
||||
if (k.toLowerCase().includes("color") && v) {
|
||||
if (typeof v === "string" && !v.startsWith("#")) {
|
||||
return;
|
||||
}
|
||||
|
||||
v = /** @type {string} */ (v).substring(0, 7);
|
||||
initialColors[i][k] = v;
|
||||
darkenedColors[i][k] =
|
||||
`${v}${notHoveredLegendTransparency}`;
|
||||
} else if (k === "lastValueVisible" && v) {
|
||||
initialColors[i][k] = true;
|
||||
darkenedColors[i][k] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
signals.createEffect(shouldHighlight, (shouldHighlight) => {
|
||||
if (shouldHighlight) {
|
||||
chunk.applyOptions(initialColors[i]);
|
||||
} else {
|
||||
chunk.applyOptions(darkenedColors[i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
previouslyHovered = hovered;
|
||||
},
|
||||
);
|
||||
}
|
||||
createHoverEffect();
|
||||
|
||||
const anchor = window.document.createElement("a");
|
||||
anchor.href = series.dataset.url;
|
||||
anchor.target = "_blank";
|
||||
anchor.rel = "noopener noreferrer";
|
||||
div.append(anchor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {TimeScale} S
|
||||
* @param {Object} args
|
||||
* @param {ResourceDataset<S>} args.dataset
|
||||
* @param {SeriesBlueprint} args.seriesBlueprint
|
||||
* @param {Option} args.option
|
||||
* @param {ChartPane} args.chart
|
||||
* @param {number} args.index
|
||||
* @param {Series[]} args.chartSeries
|
||||
* @param {Accessor<number | undefined>} args.lastVisibleDatasetIndex
|
||||
* @param {VoidFunction} args.setMinMaxMarkersWhenIdle
|
||||
* @param {Accessor<boolean>} [args.disabled]
|
||||
*/
|
||||
function createSeries({
|
||||
chart,
|
||||
option,
|
||||
index: seriesIndex,
|
||||
disabled: _disabled,
|
||||
lastVisibleDatasetIndex,
|
||||
setMinMaxMarkersWhenIdle,
|
||||
dataset,
|
||||
seriesBlueprint,
|
||||
chartSeries,
|
||||
}) {
|
||||
const {
|
||||
title,
|
||||
color,
|
||||
defaultActive,
|
||||
type,
|
||||
options: seriesOptions,
|
||||
} = seriesBlueprint;
|
||||
|
||||
/** @type {Signal<ISeriesApi<SeriesType> | undefined>[]} */
|
||||
const chunks = new Array(dataset.fetchedJSONs.length);
|
||||
|
||||
const id = ids.fromString(title);
|
||||
const storageId = options.optionAndSeriesToKey(option, seriesBlueprint);
|
||||
|
||||
const active = signals.createSignal(
|
||||
utils.url.readBoolParam(id) ??
|
||||
utils.storage.readBool(storageId) ??
|
||||
defaultActive ??
|
||||
true,
|
||||
);
|
||||
|
||||
const disabled = signals.createMemo(_disabled || (() => false));
|
||||
|
||||
const visible = signals.createMemo(() => active() && !disabled());
|
||||
|
||||
signals.createEffect(
|
||||
() => ({ disabled: disabled(), active: active() }),
|
||||
({ disabled, active }) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (active !== (defaultActive || true)) {
|
||||
utils.url.writeParam(id, active);
|
||||
utils.storage.write(storageId, active);
|
||||
} else {
|
||||
utils.url.removeParam(id);
|
||||
utils.storage.remove(storageId);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** @type {Series} */
|
||||
const series = {
|
||||
active,
|
||||
chunks,
|
||||
color: color || [colors.profit, colors.loss],
|
||||
dataset,
|
||||
disabled,
|
||||
id,
|
||||
title,
|
||||
visible,
|
||||
};
|
||||
|
||||
chartSeries.push(series);
|
||||
|
||||
const owner = signals.getOwner();
|
||||
|
||||
dataset.fetchedJSONs.forEach((json, index) => {
|
||||
const chunk = signals.createSignal(
|
||||
/** @type {ISeriesApi<SeriesType> | undefined} */ (undefined),
|
||||
);
|
||||
|
||||
chunks[index] = chunk;
|
||||
|
||||
const isMyTurn = signals.createMemo(() => {
|
||||
if (seriesIndex <= 0) return true;
|
||||
|
||||
const previousSeriesChunk = chartSeries.at(seriesIndex - 1)?.chunks[
|
||||
index
|
||||
];
|
||||
const isPreviousSeriesOnChart = previousSeriesChunk?.();
|
||||
|
||||
return !!isPreviousSeriesOnChart;
|
||||
});
|
||||
|
||||
signals.createEffect(
|
||||
() => ({ values: json.vec(), isMyTurn: isMyTurn() }),
|
||||
({ values, isMyTurn }) => {
|
||||
if (!values || !isMyTurn) return;
|
||||
|
||||
let s = chunk();
|
||||
|
||||
if (!s) {
|
||||
switch (type) {
|
||||
case "Baseline": {
|
||||
s = chart.createBaseLineSeries({
|
||||
color,
|
||||
options: seriesOptions,
|
||||
owner,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "Candlestick": {
|
||||
s = chart.createCandlesticksSeries({
|
||||
options: seriesOptions,
|
||||
owner,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
case "Line": {
|
||||
s = chart.createLineSeries({
|
||||
color,
|
||||
options: seriesOptions,
|
||||
owner,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
chunk.set(s);
|
||||
}
|
||||
|
||||
s.setData(values);
|
||||
|
||||
setMinMaxMarkersWhenIdle();
|
||||
},
|
||||
);
|
||||
|
||||
signals.createEffect(
|
||||
() => ({
|
||||
chunk: chunk(),
|
||||
currentVec: dataset.fetchedJSONs.at(index)?.vec(),
|
||||
nextVec: dataset.fetchedJSONs.at(index + 1)?.vec(),
|
||||
}),
|
||||
({ chunk, currentVec, nextVec }) => {
|
||||
if (chunk && currentVec?.length && nextVec?.length) {
|
||||
chunk.update(nextVec[0]);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
signals.createEffect(chunk, (chunk) => {
|
||||
const isChunkLastVisible = signals.createMemo(() => {
|
||||
const last = lastVisibleDatasetIndex();
|
||||
return last !== undefined && last === index;
|
||||
});
|
||||
|
||||
signals.createEffect(
|
||||
() => ({
|
||||
visible: series.visible(),
|
||||
isChunkLastVisible: isChunkLastVisible(),
|
||||
}),
|
||||
({ visible, isChunkLastVisible }) => {
|
||||
chunk?.applyOptions({
|
||||
lastValueVisible: visible && isChunkLastVisible,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const shouldChunkBeVisible = signals.createMemo(() => {
|
||||
if (visibleDatasetIds().length) {
|
||||
const start = utils.chunkIdToIndex(
|
||||
scale(),
|
||||
/** @type {number} */ (visibleDatasetIds().at(0)),
|
||||
);
|
||||
const end = utils.chunkIdToIndex(
|
||||
scale(),
|
||||
/** @type {number} */ (visibleDatasetIds().at(-1)),
|
||||
);
|
||||
|
||||
if (index >= start && index <= end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
let wasChunkVisible = false;
|
||||
const chunkVisible = signals.createMemo(() => {
|
||||
if (series.disabled()) {
|
||||
wasChunkVisible = false;
|
||||
} else {
|
||||
wasChunkVisible = wasChunkVisible || shouldChunkBeVisible();
|
||||
}
|
||||
return wasChunkVisible;
|
||||
});
|
||||
|
||||
signals.createEffect(chunk, (chunk) => {
|
||||
if (!chunk) return;
|
||||
|
||||
const visible = signals.createMemo(
|
||||
() => series.visible() && chunkVisible(),
|
||||
);
|
||||
|
||||
signals.createEffect(visible, (visible) => {
|
||||
chunk.applyOptions({
|
||||
visible,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
createLegend({ series, disabled, name: type });
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {PriceSeriesType} args.type
|
||||
* @param {VoidFunction} args.setMinMaxMarkersWhenIdle
|
||||
* @param {Option} args.option
|
||||
* @param {ChartPane} args.chart
|
||||
* @param {Series[]} args.chartSeries
|
||||
* @param {Accessor<number | undefined>} args.lastVisibleDatasetIndex
|
||||
* @param {ChartPane} args.chartPane
|
||||
* @param {SplitSeries[]} args.chartSeries
|
||||
*/
|
||||
function createPriceSeries({
|
||||
type,
|
||||
setMinMaxMarkersWhenIdle,
|
||||
option,
|
||||
chart,
|
||||
chartSeries,
|
||||
lastVisibleDatasetIndex,
|
||||
chartPane,
|
||||
chartSeries: splitSeries,
|
||||
}) {
|
||||
const s = scale();
|
||||
|
||||
@@ -751,14 +150,12 @@ export function init({
|
||||
|
||||
const disabled = signals.createMemo(() => priceSeriesType() !== type);
|
||||
|
||||
const priceSeries = createSeries({
|
||||
const priceSeries = chartPane.createSplitSeries({
|
||||
seriesBlueprint,
|
||||
dataset,
|
||||
option,
|
||||
index: -1,
|
||||
chart,
|
||||
chartSeries,
|
||||
lastVisibleDatasetIndex,
|
||||
splitSeries,
|
||||
disabled,
|
||||
setMinMaxMarkersWhenIdle,
|
||||
});
|
||||
@@ -788,7 +185,7 @@ export function init({
|
||||
*/
|
||||
function applyChartOption(option) {
|
||||
const scale = option.scale;
|
||||
visibleTimeRange.set(getInitialVisibleTimeRange());
|
||||
chart.visibleTimeRange.set(chart.getInitialVisibleTimeRange());
|
||||
|
||||
activeDatasets.set((s) => {
|
||||
s.clear();
|
||||
@@ -802,25 +199,22 @@ export function init({
|
||||
(list) => (list ? [list] : []),
|
||||
);
|
||||
|
||||
/** @type {Series[]} */
|
||||
/** @type {SplitSeries[]} */
|
||||
const allSeries = [];
|
||||
|
||||
charts = chartsBlueprints.map((seriesBlueprints, chartIndex) => {
|
||||
const chart = addChart({
|
||||
chartIndex,
|
||||
scale,
|
||||
unit: chartIndex ? option.unit : "US Dollars",
|
||||
chartsBlueprints.map((seriesBlueprints, paneIndex) => {
|
||||
const chartPane = chart.createPane({
|
||||
paneIndex,
|
||||
unit: paneIndex ? option.unit : "US Dollars",
|
||||
whitespace: true,
|
||||
});
|
||||
|
||||
setInitialVisibleTimeRange(chart);
|
||||
|
||||
/** @type {Series[]} */
|
||||
const chartSeries = [];
|
||||
/** @type {SplitSeries[]} */
|
||||
const splitSeries = [];
|
||||
|
||||
function setMinMaxMarkers() {
|
||||
try {
|
||||
const { from, to } = visibleTimeRange();
|
||||
const { from, to } = chart.visibleTimeRange();
|
||||
|
||||
const dateFrom = new Date(String(from));
|
||||
const dateTo = new Date(String(to));
|
||||
@@ -830,10 +224,10 @@ export function init({
|
||||
/** @type {Marker | undefined} */
|
||||
let min = undefined;
|
||||
|
||||
const ids = visibleDatasetIds();
|
||||
const ids = chart.visibleDatasetIds();
|
||||
|
||||
for (let i = 0; i < chartSeries.length; i++) {
|
||||
const { chunks, dataset } = chartSeries[i];
|
||||
for (let i = 0; i < splitSeries.length; i++) {
|
||||
const { chunks, dataset } = splitSeries[i];
|
||||
|
||||
for (let j = 0; j < ids.length; j++) {
|
||||
const id = ids[j];
|
||||
@@ -961,26 +355,22 @@ export function init({
|
||||
|
||||
function createSetMinMaxMarkersWhenIdleEffect() {
|
||||
signals.createEffect(
|
||||
() => [visibleTimeRange(), dark()],
|
||||
() => [chart.visibleTimeRange(), dark()],
|
||||
setMinMaxMarkersWhenIdle,
|
||||
);
|
||||
}
|
||||
createSetMinMaxMarkersWhenIdleEffect();
|
||||
|
||||
if (!chartIndex) {
|
||||
subscribeVisibleTimeRangeChange(chart);
|
||||
|
||||
if (!paneIndex) {
|
||||
updateVisiblePriceSeriesType({
|
||||
chart,
|
||||
visibleTimeRange: visibleTimeRange(),
|
||||
visibleTimeRange: chart.visibleTimeRange(),
|
||||
});
|
||||
|
||||
/** @param {PriceSeriesType} type */
|
||||
function _createPriceSeries(type) {
|
||||
return createPriceSeries({
|
||||
chart,
|
||||
chartSeries,
|
||||
lastVisibleDatasetIndex,
|
||||
chartPane,
|
||||
chartSeries: splitSeries,
|
||||
option,
|
||||
setMinMaxMarkersWhenIdle,
|
||||
type,
|
||||
@@ -1011,14 +401,12 @@ export function init({
|
||||
// Don't trigger reactivity by design
|
||||
activeDatasets().add(dataset);
|
||||
|
||||
createSeries({
|
||||
chartPane.createSplitSeries({
|
||||
index,
|
||||
seriesBlueprint,
|
||||
chart,
|
||||
option,
|
||||
lastVisibleDatasetIndex,
|
||||
setMinMaxMarkersWhenIdle,
|
||||
chartSeries,
|
||||
splitSeries,
|
||||
dataset,
|
||||
});
|
||||
});
|
||||
@@ -1027,7 +415,7 @@ export function init({
|
||||
|
||||
activeDatasets.set((s) => s);
|
||||
|
||||
chartSeries.forEach((series) => {
|
||||
splitSeries.forEach((series) => {
|
||||
allSeries.unshift(series);
|
||||
|
||||
signals.createEffect(series.active, () => {
|
||||
@@ -1036,26 +424,26 @@ export function init({
|
||||
});
|
||||
|
||||
const chartVisible = signals.createMemo(() =>
|
||||
chartSeries.some((series) => series.visible()),
|
||||
splitSeries.some((series) => series.visible()),
|
||||
);
|
||||
|
||||
function createChartVisibilityEffect() {
|
||||
signals.createEffect(chartVisible, (chartVisible) => {
|
||||
chart.setHidden(!chartVisible);
|
||||
chartPane.setHidden(!chartVisible);
|
||||
});
|
||||
}
|
||||
createChartVisibilityEffect();
|
||||
|
||||
function createTimeScaleVisibilityEffect() {
|
||||
signals.createEffect(chartVisible, (chartVisible) => {
|
||||
const visible = chartIndex === chartCount - 1 && chartVisible;
|
||||
const visible = paneIndex === chartCount - 1 && chartVisible;
|
||||
|
||||
chart.timeScale().applyOptions({
|
||||
chartPane.timeScale().applyOptions({
|
||||
visible,
|
||||
});
|
||||
|
||||
if (chartIndex === 1) {
|
||||
charts[0].timeScale().applyOptions({
|
||||
if (paneIndex === 1) {
|
||||
chart.panes[0].timeScale().applyOptions({
|
||||
visible: !visible,
|
||||
});
|
||||
}
|
||||
@@ -1063,31 +451,32 @@ export function init({
|
||||
}
|
||||
createTimeScaleVisibilityEffect();
|
||||
|
||||
chart.timeScale().subscribeVisibleLogicalRangeChange((logicalRange) => {
|
||||
if (!logicalRange) return;
|
||||
chartPane
|
||||
.timeScale()
|
||||
.subscribeVisibleLogicalRangeChange((logicalRange) => {
|
||||
if (!logicalRange) return;
|
||||
|
||||
// Must be the chart with the visible timeScale
|
||||
if (chartIndex === chartCount - 1) {
|
||||
debouncedUpdateVisiblePriceSeriesType({
|
||||
chart,
|
||||
visibleLogicalRange: logicalRange,
|
||||
});
|
||||
}
|
||||
|
||||
for (
|
||||
let otherChartIndex = 0;
|
||||
otherChartIndex <= chartCount - 1;
|
||||
otherChartIndex++
|
||||
) {
|
||||
if (chartIndex !== otherChartIndex) {
|
||||
charts[otherChartIndex]
|
||||
.timeScale()
|
||||
.setVisibleLogicalRange(logicalRange);
|
||||
// Must be the chart with the visible timeScale
|
||||
if (paneIndex === chartCount - 1) {
|
||||
debouncedUpdateVisiblePriceSeriesType({
|
||||
visibleLogicalRange: logicalRange,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chart.subscribeCrosshairMove(({ time, sourceEvent }) => {
|
||||
for (
|
||||
let otherChartIndex = 0;
|
||||
otherChartIndex <= chartCount - 1;
|
||||
otherChartIndex++
|
||||
) {
|
||||
if (paneIndex !== otherChartIndex) {
|
||||
chart.panes[otherChartIndex]
|
||||
.timeScale()
|
||||
.setVisibleLogicalRange(logicalRange);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chartPane.subscribeCrosshairMove(({ time, sourceEvent }) => {
|
||||
// Don't override crosshair position from scroll event
|
||||
if (time && !sourceEvent) return;
|
||||
|
||||
@@ -1096,9 +485,9 @@ export function init({
|
||||
otherChartIndex <= chartCount - 1;
|
||||
otherChartIndex++
|
||||
) {
|
||||
const otherChart = charts[otherChartIndex];
|
||||
const otherChart = chart.panes[otherChartIndex];
|
||||
|
||||
if (otherChart && chartIndex !== otherChartIndex) {
|
||||
if (otherChart && paneIndex !== otherChartIndex) {
|
||||
if (time) {
|
||||
otherChart.setCrosshairPosition(NaN, time, otherChart.whitespace);
|
||||
} else {
|
||||
@@ -1113,28 +502,9 @@ export function init({
|
||||
});
|
||||
}
|
||||
|
||||
function resetLegendElement() {
|
||||
legendElement.innerHTML = "";
|
||||
}
|
||||
|
||||
function resetChartListElement() {
|
||||
while (
|
||||
chartListElement.lastElementChild?.classList.contains("chart-wrapper")
|
||||
) {
|
||||
chartListElement.lastElementChild?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
charts.forEach((chart) => chart.remove());
|
||||
charts = [];
|
||||
resetLegendElement();
|
||||
resetChartListElement();
|
||||
}
|
||||
|
||||
function createApplyChartOptionEffect() {
|
||||
signals.createEffect(selected, (option) => {
|
||||
reset();
|
||||
chart.reset({ scale: option.scale });
|
||||
applyChartOption(option);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @import {Options} from './options';
|
||||
*/
|
||||
|
||||
+74
-758
@@ -1,7 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @import { Option, ResourceDataset, TimeScale, TimeRange, Unit, Marker, Weighted, DatasetPath, OHLC, FetchedJSON, DatasetValue, FetchedResult, AnyDatasetPath, SeriesBlueprint, BaselineSpecificSeriesBlueprint, CandlestickSpecificSeriesBlueprint, LineSpecificSeriesBlueprint, SpecificSeriesBlueprintWithChart, Signal, Color, DatasetCandlestickData, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, AnyPath, SimulationOption, Frequency, CreatePaneParameters, CreateBaselineSeriesParams, CreateCandlestickSeriesParams, CreateLineSeriesParams, LastValues } from "./types/self"
|
||||
* @import { Option, ResourceDataset, TimeScale, TimeRange, Unit, Marker, Weighted, DatasetPath, OHLC, FetchedJSON, DatasetValue, FetchedResult, AnyDatasetPath, SeriesBlueprint, BaselineSpecificSeriesBlueprint, CandlestickSpecificSeriesBlueprint, LineSpecificSeriesBlueprint, SpecificSeriesBlueprintWithChart, Color, DatasetCandlestickData, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, AnyPath, SimulationOption, Frequency, CreatePaneParameters, CreateBaselineSeriesParams, CreateCandlestickSeriesParams, CreateLineSeriesParams, LastValues, HoveredLegend, ChartPane, SplitSeries, SingleSeries, CreateSplitSeriesParameters } from "./types/self"
|
||||
* @import {createChart as CreateClassicChart, createChartEx as CreateCustomChart, LineStyleOptions} from "../packages/lightweight-charts/v4.2.0/types";
|
||||
* @import * as _ from "../packages/ufuzzy/v1.0.14/types"
|
||||
* @import { DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, SingleValueData, ISeriesApi, Time, LineData, LogicalRange, SeriesMarker, CandlestickData, SeriesType, BaselineStyleOptions, SeriesOptionsCommon } from "../packages/lightweight-charts/v4.2.0/types"
|
||||
@@ -9,729 +9,20 @@
|
||||
* @import { SignalOptions } from "../packages/solid-signals/2024-11-02/types/core/core"
|
||||
* @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "../packages/solid-signals/2024-11-02/types/core/owner"
|
||||
* @import { createSignal as CreateSignal, createEffect as CreateEffect, Accessor, Setter, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner } from "../packages/solid-signals/2024-11-02/types/signals";
|
||||
* @import {Signal, Signals} from "../packages/solid-signals/types";
|
||||
*/
|
||||
|
||||
function initPackages() {
|
||||
async function importSignals() {
|
||||
return import("../packages/solid-signals/2024-11-02/script.js").then(
|
||||
(_signals) => {
|
||||
const signals = {
|
||||
createSolidSignal: /** @type {CreateSignal} */ (
|
||||
_signals.createSignal
|
||||
),
|
||||
createSolidEffect: /** @type {CreateEffect} */ (
|
||||
_signals.createEffect
|
||||
),
|
||||
createEffect: /** @type {CreateEffect} */ (compute, effect) => {
|
||||
let dispose = /** @type {VoidFunction | null} */ (null);
|
||||
// @ts-ignore
|
||||
_signals.createEffect(compute, (v) => {
|
||||
dispose?.();
|
||||
signals.createRoot((_dispose) => {
|
||||
dispose = _dispose;
|
||||
effect(v);
|
||||
});
|
||||
signals.onCleanup(() => dispose?.());
|
||||
});
|
||||
signals.onCleanup(() => dispose?.());
|
||||
},
|
||||
createMemo: /** @type {CreateMemo} */ (_signals.createMemo),
|
||||
createRoot: /** @type {CreateRoot} */ (_signals.createRoot),
|
||||
getOwner: /** @type {GetOwner} */ (_signals.getOwner),
|
||||
runWithOwner: /** @type {RunWithOwner} */ (_signals.runWithOwner),
|
||||
onCleanup: /** @type {OnCleanup} */ (_signals.onCleanup),
|
||||
flushSync: _signals.flushSync,
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} initialValue
|
||||
* @param {SignalOptions<T> & {save?: {id?: string; param?: string; serialize: (v: NonNullable<T>) => string; deserialize: (v: string) => NonNullable<T>}}} [options]
|
||||
* @returns {Signal<T>}
|
||||
*/
|
||||
createSignal(initialValue, options) {
|
||||
const [get, set] = this.createSolidSignal(
|
||||
/** @type {any} */ (initialValue),
|
||||
options,
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
get.set = set;
|
||||
|
||||
// @ts-ignore
|
||||
get.reset = () => set(initialValue);
|
||||
|
||||
if (options?.save) {
|
||||
const save = options.save;
|
||||
|
||||
let serialized = null;
|
||||
if (save.param) {
|
||||
serialized = utils.url.readParam(save.param);
|
||||
}
|
||||
if (serialized === null && save.id) {
|
||||
serialized = utils.storage.read(save.id);
|
||||
}
|
||||
if (serialized) {
|
||||
set(save.deserialize(serialized));
|
||||
}
|
||||
|
||||
let firstEffect = true;
|
||||
this.createEffect(get, (value) => {
|
||||
if (!save) return;
|
||||
|
||||
if (!firstEffect && save.id) {
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
(initialValue === undefined ||
|
||||
initialValue === null ||
|
||||
save.serialize(value) !== save.serialize(initialValue))
|
||||
) {
|
||||
localStorage.setItem(save.id, save.serialize(value));
|
||||
} else {
|
||||
localStorage.removeItem(save.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (save.param) {
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
(initialValue === undefined ||
|
||||
initialValue === null ||
|
||||
save.serialize(value) !== save.serialize(initialValue))
|
||||
) {
|
||||
utils.url.writeParam(save.param, save.serialize(value));
|
||||
} else {
|
||||
utils.url.removeParam(save.param);
|
||||
}
|
||||
}
|
||||
|
||||
firstEffect = false;
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return get;
|
||||
},
|
||||
};
|
||||
|
||||
return signals;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** @typedef {Awaited<ReturnType<typeof importSignals>>} Signals */
|
||||
|
||||
const imports = {
|
||||
signals: importSignals,
|
||||
async signals() {
|
||||
return import("../packages/solid-signals/wrapper.js").then((d) =>
|
||||
d.default.then((d) => d),
|
||||
);
|
||||
},
|
||||
async lightweightCharts() {
|
||||
return window.document.fonts.ready.then(() =>
|
||||
import("../packages/lightweight-charts/v4.2.0/script.js").then(
|
||||
({
|
||||
createChart: createClassicChart,
|
||||
createChartEx: createCustomChart,
|
||||
}) => {
|
||||
/**
|
||||
* @class
|
||||
* @implements {IHorzScaleBehavior<number>}
|
||||
*/
|
||||
class HorzScaleBehaviorHeight {
|
||||
options() {
|
||||
return /** @type {any} */ (undefined);
|
||||
}
|
||||
setOptions() {}
|
||||
preprocessData() {}
|
||||
updateFormatter() {}
|
||||
|
||||
createConverterToInternalObj() {
|
||||
/** @type {(p: any) => any} */
|
||||
return (price) => price;
|
||||
}
|
||||
|
||||
/** @param {any} item */
|
||||
key(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
/** @param {any} item */
|
||||
cacheKey(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
/** @param {any} item */
|
||||
convertHorzItemToInternal(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
/** @param {any} item */
|
||||
formatHorzItem(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
/** @param {any} tickMark */
|
||||
formatTickmark(tickMark) {
|
||||
return tickMark.time.toLocaleString("en-us");
|
||||
}
|
||||
|
||||
/** @param {any} tickMarks */
|
||||
maxTickMarkWeight(tickMarks) {
|
||||
return tickMarks.reduce(
|
||||
this.getMarkWithGreaterWeight,
|
||||
tickMarks[0],
|
||||
).weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} sortedTimePoints
|
||||
* @param {number} startIndex
|
||||
*/
|
||||
fillWeightsForPoints(sortedTimePoints, startIndex) {
|
||||
for (
|
||||
let index = startIndex;
|
||||
index < sortedTimePoints.length;
|
||||
++index
|
||||
) {
|
||||
sortedTimePoints[index].timeWeight = this.computeHeightWeight(
|
||||
sortedTimePoints[index].time,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} a
|
||||
* @param {any} b
|
||||
*/
|
||||
getMarkWithGreaterWeight(a, b) {
|
||||
return a.weight > b.weight ? a : b;
|
||||
}
|
||||
|
||||
/** @param {number} value */
|
||||
computeHeightWeight(value) {
|
||||
// if (value === Math.ceil(value / 1000000) * 1000000) {
|
||||
// return 12;
|
||||
// }
|
||||
if (value === Math.ceil(value / 100000) * 100000) {
|
||||
return 11;
|
||||
}
|
||||
if (value === Math.ceil(value / 10000) * 10000) {
|
||||
return 10;
|
||||
}
|
||||
if (value === Math.ceil(value / 1000) * 1000) {
|
||||
return 9;
|
||||
}
|
||||
if (value === Math.ceil(value / 100) * 100) {
|
||||
return 8;
|
||||
}
|
||||
if (value === Math.ceil(value / 50) * 50) {
|
||||
return 7;
|
||||
}
|
||||
if (value === Math.ceil(value / 25) * 25) {
|
||||
return 6;
|
||||
}
|
||||
if (value === Math.ceil(value / 10) * 10) {
|
||||
return 5;
|
||||
}
|
||||
if (value === Math.ceil(value / 5) * 5) {
|
||||
return 4;
|
||||
}
|
||||
if (value === Math.ceil(value)) {
|
||||
return 3;
|
||||
}
|
||||
if (value * 2 === Math.ceil(value * 2)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {TimeScale} args.scale
|
||||
* @param {HTMLElement} args.element
|
||||
* @param {Signals} args.signals
|
||||
* @param {Colors} args.colors
|
||||
* @param {DeepPartial<ChartOptions>} [args.options]
|
||||
*/
|
||||
function createLightweightChart({
|
||||
scale,
|
||||
element,
|
||||
signals,
|
||||
colors,
|
||||
options: _options = {},
|
||||
}) {
|
||||
/** @satisfies {DeepPartial<ChartOptions>} */
|
||||
const options = {
|
||||
autoSize: true,
|
||||
layout: {
|
||||
fontFamily: "Satoshi Chart",
|
||||
fontSize: 13,
|
||||
background: { color: "transparent" },
|
||||
attributionLogo: false,
|
||||
},
|
||||
grid: {
|
||||
vertLines: { visible: false },
|
||||
horzLines: { visible: false },
|
||||
},
|
||||
timeScale: {
|
||||
minBarSpacing: 0.05,
|
||||
shiftVisibleRangeOnNewBar: false,
|
||||
allowShiftVisibleRangeOnWhitespaceReplacement: false,
|
||||
},
|
||||
handleScale: {
|
||||
axisDoubleClickReset: {
|
||||
time: false,
|
||||
},
|
||||
},
|
||||
localization: {
|
||||
priceFormatter: utils.locale.numberToShortUSFormat,
|
||||
locale: "en-us",
|
||||
...(scale === "date"
|
||||
? {
|
||||
// dateFormat: "EEEE, dd MMM 'yy",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
..._options,
|
||||
};
|
||||
|
||||
/** @type {IChartApi} */
|
||||
let chart;
|
||||
|
||||
if (scale === "date") {
|
||||
chart = createClassicChart(element, options);
|
||||
} else {
|
||||
const horzScaleBehavior = new HorzScaleBehaviorHeight();
|
||||
// @ts-ignore
|
||||
chart = createCustomChart(element, horzScaleBehavior, options);
|
||||
}
|
||||
|
||||
chart.priceScale("right").applyOptions({
|
||||
scaleMargins: {
|
||||
top: 0.075,
|
||||
bottom: 0.05,
|
||||
},
|
||||
minimumWidth: 78,
|
||||
});
|
||||
|
||||
signals.createEffect(
|
||||
() => ({
|
||||
defaultColor: colors.default(),
|
||||
offColor: colors.off(),
|
||||
}),
|
||||
({ defaultColor, offColor }) => {
|
||||
chart.applyOptions({
|
||||
layout: {
|
||||
textColor: offColor,
|
||||
},
|
||||
rightPriceScale: {
|
||||
borderVisible: false,
|
||||
},
|
||||
timeScale: {
|
||||
borderVisible: false,
|
||||
},
|
||||
crosshair: {
|
||||
horzLine: {
|
||||
color: defaultColor,
|
||||
labelBackgroundColor: defaultColor,
|
||||
},
|
||||
vertLine: {
|
||||
color: defaultColor,
|
||||
labelBackgroundColor: defaultColor,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {DeepPartial<SeriesOptionsCommon>}
|
||||
*/
|
||||
const defaultSeriesOptions = {
|
||||
// @ts-ignore
|
||||
lineWidth: 1.5,
|
||||
priceLineVisible: false,
|
||||
baseLineVisible: false,
|
||||
baseLineColor: "",
|
||||
};
|
||||
|
||||
function initWhitespace() {
|
||||
const whitespaceStartDate = new Date("1970-01-01");
|
||||
const whitespaceStartDateYear =
|
||||
whitespaceStartDate.getUTCFullYear();
|
||||
const whitespaceStartDateMonth =
|
||||
whitespaceStartDate.getUTCMonth();
|
||||
const whitespaceStartDateDate = whitespaceStartDate.getUTCDate();
|
||||
const whitespaceEndDate = new Date("2141-01-01");
|
||||
let whitespaceDateDataset =
|
||||
/** @type {(WhitespaceData | SingleValueData)[]} */ ([]);
|
||||
|
||||
function initDateWhitespace() {
|
||||
whitespaceDateDataset = new Array(
|
||||
utils.getNumberOfDaysBetweenTwoDates(
|
||||
whitespaceStartDate,
|
||||
whitespaceEndDate,
|
||||
),
|
||||
);
|
||||
// Hack to be able to scroll freely
|
||||
// Setting them all to NaN is much slower
|
||||
for (let i = 0; i < whitespaceDateDataset.length; i++) {
|
||||
const date = new Date(
|
||||
whitespaceStartDateYear,
|
||||
whitespaceStartDateMonth,
|
||||
whitespaceStartDateDate + i,
|
||||
);
|
||||
|
||||
const time = utils.date.toString(date);
|
||||
|
||||
if (i === whitespaceDateDataset.length - 1) {
|
||||
whitespaceDateDataset[i] = {
|
||||
time,
|
||||
value: NaN,
|
||||
};
|
||||
} else {
|
||||
whitespaceDateDataset[i] = {
|
||||
time,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const heightStart = -50_000;
|
||||
let whitespaceHeightDataset =
|
||||
/** @type {WhitespaceData[]} */ ([]);
|
||||
|
||||
function initHeightWhitespace() {
|
||||
whitespaceHeightDataset = new Array(
|
||||
(new Date().getUTCFullYear() - 2009 + 1) * 60_000,
|
||||
);
|
||||
for (let i = 0; i < whitespaceHeightDataset.length; i++) {
|
||||
const height = heightStart + i;
|
||||
|
||||
whitespaceHeightDataset[i] = {
|
||||
time: /** @type {Time} */ (height),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IChartApi} chart
|
||||
* @param {TimeScale} scale
|
||||
* @returns {ISeriesApi<'Line'>}
|
||||
*/
|
||||
function setWhitespace(chart, scale) {
|
||||
const whitespace = chart.addLineSeries();
|
||||
|
||||
if (scale === "date") {
|
||||
if (!whitespaceDateDataset.length) {
|
||||
initDateWhitespace();
|
||||
}
|
||||
|
||||
whitespace.setData(whitespaceDateDataset);
|
||||
} else {
|
||||
if (!whitespaceHeightDataset.length) {
|
||||
initHeightWhitespace();
|
||||
}
|
||||
|
||||
whitespace.setData(whitespaceHeightDataset);
|
||||
|
||||
const time = whitespaceHeightDataset.length;
|
||||
whitespace.update({
|
||||
time: /** @type {Time} */ (time),
|
||||
value: NaN,
|
||||
});
|
||||
}
|
||||
|
||||
return whitespace;
|
||||
}
|
||||
|
||||
return { setWhitespace };
|
||||
}
|
||||
const { setWhitespace } = initWhitespace();
|
||||
|
||||
/**
|
||||
* @typeof {Object} PaneParameters
|
||||
* @property {Unit} param.unit
|
||||
* @param {TimeScale} param.scale
|
||||
* @param {number} [param.chartIndex]
|
||||
* @param {true} [param.whitespace]
|
||||
* @param {DeepPartial<ChartOptions>} [param.options]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Object} param0
|
||||
* @param {string} param0.id
|
||||
* @param {HTMLElement} param0.parent
|
||||
* @param {Signals} param0.signals
|
||||
* @param {Colors} param0.colors
|
||||
* @param {"static" | "dynamic"} [param0.kind]
|
||||
* @param {CreatePaneParameters[]} [param0.config]
|
||||
*/
|
||||
function createChart({
|
||||
parent,
|
||||
signals,
|
||||
colors,
|
||||
id: chartId,
|
||||
kind,
|
||||
config,
|
||||
}) {
|
||||
const div = window.document.createElement("div");
|
||||
div.classList.add("charts");
|
||||
parent.append(div);
|
||||
|
||||
const legendElement = window.document.createElement("legend");
|
||||
div.append(legendElement);
|
||||
|
||||
const chartListElement = window.document.createElement("div");
|
||||
chartListElement.classList.add("chart-list");
|
||||
div.append(chartListElement);
|
||||
|
||||
/**
|
||||
* @param {CreatePaneParameters} param
|
||||
*/
|
||||
function createPane({
|
||||
chartIndex,
|
||||
whitespace,
|
||||
scale,
|
||||
unit,
|
||||
options,
|
||||
config,
|
||||
}) {
|
||||
const chartWrapper = window.document.createElement("div");
|
||||
chartWrapper.classList.add("chart-wrapper");
|
||||
chartListElement.append(chartWrapper);
|
||||
|
||||
const chartDiv = window.document.createElement("div");
|
||||
chartDiv.classList.add("chart-div");
|
||||
chartWrapper.append(chartDiv);
|
||||
|
||||
options = { ...options };
|
||||
if (kind === "static") {
|
||||
options.handleScale = false;
|
||||
options.handleScroll = false;
|
||||
} else {
|
||||
options.crosshair = {
|
||||
...options.crosshair,
|
||||
mode: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const _chart = createLightweightChart({
|
||||
scale,
|
||||
element: chartDiv,
|
||||
signals,
|
||||
colors,
|
||||
options,
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {CreateBaselineSeriesParams} args
|
||||
*/
|
||||
function createBaseLineSeries({ color, options, owner, data }) {
|
||||
const topLineColor = color || colors.profit;
|
||||
const bottomLineColor = color || colors.loss;
|
||||
|
||||
function computeColors() {
|
||||
return {
|
||||
topLineColor: topLineColor(),
|
||||
bottomLineColor: bottomLineColor(),
|
||||
};
|
||||
}
|
||||
|
||||
const transparent = "transparent";
|
||||
|
||||
/** @type {DeepPartial<BaselineStyleOptions & SeriesOptionsCommon>} */
|
||||
const seriesOptions = {
|
||||
priceScaleId: "right",
|
||||
...defaultSeriesOptions,
|
||||
...options,
|
||||
topFillColor1: transparent,
|
||||
topFillColor2: transparent,
|
||||
bottomFillColor1: transparent,
|
||||
bottomFillColor2: transparent,
|
||||
...computeColors(),
|
||||
};
|
||||
|
||||
const series = _chart.addBaselineSeries(seriesOptions);
|
||||
|
||||
signals.runWithOwner(owner, () => {
|
||||
signals.createEffect(computeColors, (computeColors) => {
|
||||
series.applyOptions(computeColors);
|
||||
});
|
||||
});
|
||||
|
||||
if (data) {
|
||||
series.setData(data);
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CreateCandlestickSeriesParams} args
|
||||
*/
|
||||
function createCandlestickSeries({ options, owner, data }) {
|
||||
function computeColors() {
|
||||
const upColor = colors.profit();
|
||||
const downColor = colors.loss();
|
||||
|
||||
return {
|
||||
upColor,
|
||||
wickUpColor: upColor,
|
||||
downColor,
|
||||
wickDownColor: downColor,
|
||||
};
|
||||
}
|
||||
|
||||
const series = _chart.addCandlestickSeries({
|
||||
baseLineVisible: false,
|
||||
borderVisible: false,
|
||||
priceLineVisible: false,
|
||||
baseLineColor: "",
|
||||
borderColor: "",
|
||||
borderDownColor: "",
|
||||
borderUpColor: "",
|
||||
...options,
|
||||
...computeColors(),
|
||||
});
|
||||
|
||||
signals.runWithOwner(owner, () => {
|
||||
signals.createEffect(computeColors, (computeColors) => {
|
||||
series.applyOptions(computeColors);
|
||||
});
|
||||
});
|
||||
|
||||
if (data) {
|
||||
series.setData(data);
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CreateLineSeriesParams} args
|
||||
*/
|
||||
function createLineSeries({ color, options, owner, data }) {
|
||||
function computeColors() {
|
||||
return {
|
||||
color: color(),
|
||||
};
|
||||
}
|
||||
|
||||
const series = _chart.addLineSeries({
|
||||
...defaultSeriesOptions,
|
||||
...options,
|
||||
...computeColors(),
|
||||
});
|
||||
|
||||
if (data) {
|
||||
series.setData(data);
|
||||
}
|
||||
|
||||
signals.runWithOwner(owner, () => {
|
||||
signals.createEffect(computeColors, (computeColors) => {
|
||||
series.applyOptions(computeColors);
|
||||
});
|
||||
});
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
const chart =
|
||||
/** @type {IChartApi & { whitespace: ISeriesApi<"Line">, createBaseLineSeries: typeof createBaseLineSeries, createCandlesticksSeries: typeof createCandlestickSeries, createLineSeries: typeof createLineSeries; setHidden: (b: boolean) => void }} */ (
|
||||
_chart
|
||||
);
|
||||
|
||||
if (whitespace) {
|
||||
chart.whitespace = setWhitespace(_chart, scale);
|
||||
}
|
||||
|
||||
chart.createBaseLineSeries = createBaseLineSeries;
|
||||
chart.createCandlesticksSeries = createCandlestickSeries;
|
||||
chart.createLineSeries = createLineSeries;
|
||||
chart.setHidden = (b) => {
|
||||
chartWrapper.hidden = b;
|
||||
};
|
||||
|
||||
function createUnitAndModeElements() {
|
||||
const fieldset = window.document.createElement("fieldset");
|
||||
fieldset.dataset.size = "sm";
|
||||
chartWrapper.append(fieldset);
|
||||
|
||||
const id = `chart-${chartId}-${chartIndex}-mode`;
|
||||
|
||||
const chartModes = /** @type {const} */ (["Lin", "Log"]);
|
||||
const chartMode = signals.createSignal(
|
||||
/** @type {Lowercase<typeof chartModes[number]>} */ (
|
||||
localStorage.getItem(id) || "lin"
|
||||
),
|
||||
);
|
||||
|
||||
const field = utils.dom.createHorizontalChoiceField({
|
||||
choices: chartModes,
|
||||
selected: chartMode(),
|
||||
id,
|
||||
title: unit,
|
||||
signals,
|
||||
});
|
||||
fieldset.append(field);
|
||||
|
||||
field.addEventListener("change", (event) => {
|
||||
// @ts-ignore
|
||||
const value = event.target.value;
|
||||
localStorage.setItem(id, value);
|
||||
chartMode.set(value);
|
||||
});
|
||||
|
||||
signals.createEffect(chartMode, (chartMode) =>
|
||||
_chart.priceScale("right").applyOptions({
|
||||
mode: chartMode === "lin" ? 0 : 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
createUnitAndModeElements();
|
||||
|
||||
config?.forEach((params) => {
|
||||
switch (params.kind) {
|
||||
case "line": {
|
||||
chart.createLineSeries(params);
|
||||
break;
|
||||
}
|
||||
case "candle": {
|
||||
chart.createCandlesticksSeries(params);
|
||||
break;
|
||||
}
|
||||
case "baseline": {
|
||||
chart.createBaseLineSeries(params);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (kind === "static") {
|
||||
chart.timeScale().fitContent();
|
||||
}
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
config?.forEach((params) => {
|
||||
createPane(params);
|
||||
});
|
||||
|
||||
return {
|
||||
legendElement,
|
||||
chartListElement,
|
||||
createPane,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createChart,
|
||||
};
|
||||
},
|
||||
import("../packages/lightweight-charts/wrapper.js").then((d) =>
|
||||
d.default.then((d) => d),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -778,9 +69,8 @@ function initPackages() {
|
||||
}
|
||||
const packages = initPackages();
|
||||
/**
|
||||
* @typedef {Awaited<ReturnType<typeof packages.signals>>} Signals
|
||||
* @typedef {Awaited<ReturnType<typeof packages.lightweightCharts>>} LightweightCharts
|
||||
* @typedef {ReturnType<ReturnType<Awaited<ReturnType<typeof packages.lightweightCharts>>['createChart']>['createPane']>} ChartPane
|
||||
* @typedef {ReturnType<LightweightCharts['createChart']>} Chart
|
||||
*/
|
||||
|
||||
const options = import("./options.js");
|
||||
@@ -1091,19 +381,33 @@ const utils = {
|
||||
* @param {Object} args
|
||||
* @param {string} args.id
|
||||
* @param {string} args.title
|
||||
* @param {string} args.placeholder
|
||||
* @param {Signal<number | null>} args.signal
|
||||
* @param {number} args.min
|
||||
* @param {number} args.max
|
||||
* @param {number} args.step
|
||||
* @param {number} [args.max]
|
||||
* @param {{createEffect: typeof CreateEffect}} args.signals
|
||||
*/
|
||||
createInputNumberElement({ id, title, signal, min, max, step, signals }) {
|
||||
createInputNumberElement({
|
||||
id,
|
||||
title,
|
||||
signal,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
placeholder,
|
||||
signals,
|
||||
}) {
|
||||
const input = window.document.createElement("input");
|
||||
if (!id || !title || !placeholder) throw Error("input attribute missing");
|
||||
input.id = id;
|
||||
input.title = title;
|
||||
input.placeholder = placeholder;
|
||||
input.type = "number";
|
||||
input.min = String(min);
|
||||
input.max = String(max);
|
||||
if (max) {
|
||||
input.max = String(max);
|
||||
}
|
||||
input.step = String(step);
|
||||
|
||||
let stateValue = /** @type {string | null} */ (null);
|
||||
@@ -1123,14 +427,32 @@ const utils = {
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const valueSer = input.value;
|
||||
stateValue = valueSer;
|
||||
const value = Number(valueSer);
|
||||
if (value >= min && value <= max) {
|
||||
stateValue = valueSer;
|
||||
if (value >= min && (max ? value <= max : true)) {
|
||||
signal.set(value);
|
||||
}
|
||||
});
|
||||
|
||||
return input;
|
||||
return { input, signal };
|
||||
},
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {string} args.id
|
||||
* @param {string} args.title
|
||||
* @param {Signal<number | null>} args.signal
|
||||
* @param {{createEffect: typeof CreateEffect}} args.signals
|
||||
*/
|
||||
createInputDollar({ id, title, signal, signals }) {
|
||||
return this.createInputNumberElement({
|
||||
id,
|
||||
placeholder: "US Dollars",
|
||||
min: 0,
|
||||
title,
|
||||
signal,
|
||||
signals,
|
||||
step: 1,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @param {Object} args
|
||||
@@ -1175,12 +497,12 @@ const utils = {
|
||||
}
|
||||
});
|
||||
|
||||
return input;
|
||||
return { input, signal };
|
||||
},
|
||||
/**
|
||||
* @param {Object} param0
|
||||
* @param {string} param0.title
|
||||
* @param {string} param0.description
|
||||
* @param {string} [param0.title]
|
||||
* @param {string} [param0.description]
|
||||
*/
|
||||
createHeader({ title, description }) {
|
||||
const headerElement = window.document.createElement("header");
|
||||
@@ -1194,12 +516,16 @@ const utils = {
|
||||
h1.style.flexDirection = "column";
|
||||
|
||||
const titleElement = window.document.createElement("span");
|
||||
titleElement.append(title);
|
||||
if (title) {
|
||||
titleElement.append(title);
|
||||
}
|
||||
h1.append(titleElement);
|
||||
titleElement.style.display = "block";
|
||||
|
||||
const descriptionElement = window.document.createElement("small");
|
||||
descriptionElement.append(description);
|
||||
if (description) {
|
||||
descriptionElement.append(description);
|
||||
}
|
||||
h1.append(descriptionElement);
|
||||
|
||||
return {
|
||||
@@ -1229,6 +555,7 @@ const utils = {
|
||||
createSelect({ id, list, signal }) {
|
||||
const select = window.document.createElement("select");
|
||||
select.name = id;
|
||||
select.id = id;
|
||||
|
||||
/** @type {Record<string, VoidFunction>} */
|
||||
const setters = {};
|
||||
@@ -1262,7 +589,7 @@ const utils = {
|
||||
|
||||
select.value = signal().value;
|
||||
|
||||
return select;
|
||||
return { select, signal };
|
||||
},
|
||||
/**
|
||||
* @param {'left' | 'bottom' | 'top' | 'right'} position
|
||||
@@ -1394,12 +721,8 @@ const utils = {
|
||||
numberToShortUSFormat(value) {
|
||||
const absoluteValue = Math.abs(value);
|
||||
|
||||
// value = absoluteValue;
|
||||
|
||||
if (isNaN(value)) {
|
||||
return "";
|
||||
// } else if (value === 0) {
|
||||
// return "0";
|
||||
} else if (absoluteValue < 10) {
|
||||
return utils.locale.numberToUSFormat(value, 3);
|
||||
} else if (absoluteValue < 100) {
|
||||
@@ -1410,7 +733,7 @@ const utils = {
|
||||
return utils.locale.numberToUSFormat(value, 0);
|
||||
} else if (absoluteValue < 1_000_000) {
|
||||
return `${utils.locale.numberToUSFormat(value / 1_000, 1)}K`;
|
||||
} else if (absoluteValue >= 1_000_000_000_000_000_000) {
|
||||
} else if (absoluteValue >= 9_000_000_000_000_000) {
|
||||
return "Inf.";
|
||||
}
|
||||
|
||||
@@ -1735,10 +1058,10 @@ const utils = {
|
||||
: Math.floor(id / consts.HEIGHT_CHUNK_SIZE);
|
||||
},
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {string} s
|
||||
*/
|
||||
stringToId(str) {
|
||||
return str.toLowerCase().replace(" ", "-");
|
||||
stringToId(s) {
|
||||
return s.replace(/\W/g, " ").trim().replace(/ +/g, "-").toLowerCase();
|
||||
},
|
||||
};
|
||||
/** @typedef {typeof utils} Utilities */
|
||||
@@ -1806,22 +1129,7 @@ const consts = createConstants();
|
||||
const ids = /** @type {const} */ ({
|
||||
selectedId: `selected-id`,
|
||||
asideSelectorLabel: `aside-selector-label`,
|
||||
chartRange: "chart-range",
|
||||
from: "from",
|
||||
to: "to",
|
||||
checkedFrameSelectorLabel: "checked-frame-selector-label",
|
||||
/**
|
||||
* @param {TimeScale} scale
|
||||
*/
|
||||
visibleTimeRange(scale) {
|
||||
return `${ids.chartRange}-${scale}`;
|
||||
},
|
||||
/**
|
||||
* @param {string} s
|
||||
*/
|
||||
fromString(s) {
|
||||
return s.replace(/\W/g, " ").trim().replace(/ +/g, "-").toLowerCase();
|
||||
},
|
||||
});
|
||||
/** @typedef {typeof ids} Ids */
|
||||
|
||||
@@ -2751,6 +2059,17 @@ packages.signals().then((signals) =>
|
||||
qrcode,
|
||||
});
|
||||
|
||||
function createWindowPopStateEvent() {
|
||||
window.addEventListener("popstate", (event) => {
|
||||
const urlSelected = utils.url.pathnameToSelectedId();
|
||||
const option = options.list.find((option) => urlSelected === option.id);
|
||||
if (option) {
|
||||
options.selected.set(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
// createWindowPopStateEvent();
|
||||
|
||||
function initSelected() {
|
||||
function initSelectedFrame() {
|
||||
console.log("selected: init");
|
||||
@@ -2808,13 +2127,10 @@ packages.signals().then((signals) =>
|
||||
signals.runWithOwner(owner, () =>
|
||||
initChartsElement({
|
||||
colors,
|
||||
consts,
|
||||
dark,
|
||||
datasets,
|
||||
elements,
|
||||
ids,
|
||||
lightweightCharts,
|
||||
options,
|
||||
selected: /** @type {any} */ (lastChartOption),
|
||||
signals,
|
||||
utils,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @import {Options} from './options';
|
||||
*/
|
||||
|
||||
+24
-16
@@ -1,5 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @import { AnySpecificSeriesBlueprint, CohortOption, CohortOptions, Color, DefaultCohortOption, DefaultCohortOptions, OptionPath, OptionsGroup, PartialChartOption, PartialOptionsGroup, PartialOptionsTree, RatioOption, RatioOptions, Series, SeriesBlueprint, SeriesBlueprintParam, SeriesBluePrintType, Signal, TimeScale } from "./types/self"
|
||||
* @import { AnySpecificSeriesBlueprint, CohortOption, CohortOptions, Color, DefaultCohortOption, DefaultCohortOptions, OptionPath, OptionsGroup, PartialChartOption, PartialOptionsGroup, PartialOptionsTree, RatioOption, RatioOptions, SplitSeries, SeriesBlueprint, SeriesBlueprintParam, SeriesBluePrintType, TimeScale } from "./types/self"
|
||||
*/
|
||||
|
||||
const DATE_TO_PREFIX = "date-to-";
|
||||
@@ -5113,6 +5115,10 @@ function createPartialOptions(colors) {
|
||||
name: "Geyser Leaderboard",
|
||||
url: () => "https://geyser.fund/project/kibo/leaderboard",
|
||||
},
|
||||
{
|
||||
name: "Donate to OpenSats",
|
||||
url: () => "https://opensats.org/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -5121,9 +5127,18 @@ function createPartialOptions(colors) {
|
||||
url: () => window.location.href,
|
||||
},
|
||||
{
|
||||
name: "Social",
|
||||
url: () =>
|
||||
"https://primal.net/p/npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44",
|
||||
name: "Socials",
|
||||
tree: [
|
||||
{
|
||||
name: "Bluesky",
|
||||
url: () => "https://bsky.app/profile/kibo.money",
|
||||
},
|
||||
{
|
||||
name: "Nostr",
|
||||
url: () =>
|
||||
"https://primal.net/p/npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Developers",
|
||||
@@ -5444,7 +5459,7 @@ export function initOptions({
|
||||
}, /** @type {HTMLLIElement | null} */ (null));
|
||||
|
||||
if ("tree" in anyPartial) {
|
||||
const folderId = ids.fromString(
|
||||
const folderId = utils.stringToId(
|
||||
`${(path || [])?.map(({ name }) => name).join(" ")} ${
|
||||
anyPartial.name
|
||||
} folder`,
|
||||
@@ -5537,16 +5552,16 @@ export function initOptions({
|
||||
title = anyPartial.title;
|
||||
} else if ("pdf" in anyPartial) {
|
||||
kind = "pdf";
|
||||
id = `${path?.at(-1)?.name || ""}-${ids.fromString(anyPartial.name)}-pdf`;
|
||||
id = `${path?.at(-1)?.name || ""}-${utils.stringToId(anyPartial.name)}-pdf`;
|
||||
title = anyPartial.name;
|
||||
anyPartial.pdf = `/assets/pdfs/${anyPartial.pdf}`;
|
||||
} else if ("url" in anyPartial) {
|
||||
kind = "url";
|
||||
id = `${ids.fromString(anyPartial.name)}-url`;
|
||||
id = `${utils.stringToId(anyPartial.name)}-url`;
|
||||
title = anyPartial.name;
|
||||
} else if ("scale" in anyPartial) {
|
||||
kind = "chart";
|
||||
id = `chart-${anyPartial.scale}-to-${ids.fromString(
|
||||
id = `chart-${anyPartial.scale}-to-${utils.stringToId(
|
||||
anyPartial.title,
|
||||
)}`;
|
||||
title = anyPartial.title;
|
||||
@@ -5554,7 +5569,7 @@ export function initOptions({
|
||||
kind = anyPartial.kind;
|
||||
title = "title" in anyPartial ? anyPartial.title : anyPartial.name;
|
||||
console.log("Unprocessed", anyPartial);
|
||||
id = `${kind}-${ids.fromString(title)}`;
|
||||
id = `${kind}-${utils.stringToId(title)}`;
|
||||
}
|
||||
|
||||
/** @type {ProcessedOptionAddons} */
|
||||
@@ -5651,13 +5666,6 @@ export function initOptions({
|
||||
tree: /** @type {OptionsTree} */ (partialOptions),
|
||||
treeElement,
|
||||
createOptionElement,
|
||||
/**
|
||||
* @param {Option} option
|
||||
* @param {Series | SeriesBlueprint} series
|
||||
*/
|
||||
optionAndSeriesToKey(option, series) {
|
||||
return `${option.id}-${ids.fromString(series.title)}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
/** @typedef {ReturnType<typeof initOptions>} Options */
|
||||
|
||||
+135
-105
@@ -1,3 +1,5 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @import { Options } from './options';
|
||||
* @import { ColorName, Frequencies, Frequency } from './types/self';
|
||||
@@ -63,8 +65,8 @@ export function init({
|
||||
),
|
||||
},
|
||||
},
|
||||
swap: {
|
||||
amount: {
|
||||
bitcoin: {
|
||||
investment: {
|
||||
initial: signals.createSignal(/** @type {number | null} */ (1000), {
|
||||
save: {
|
||||
...utils.serde.number,
|
||||
@@ -79,17 +81,17 @@ export function init({
|
||||
param: "recurrent-swap",
|
||||
},
|
||||
}),
|
||||
},
|
||||
frequency: signals.createSignal(
|
||||
/** @type {Frequency} */ (frequencies.list[0]),
|
||||
{
|
||||
save: {
|
||||
...frequencies.serde,
|
||||
id: `${storagePrefix}-swap-freq`,
|
||||
param: "swap-freq",
|
||||
frequency: signals.createSignal(
|
||||
/** @type {Frequency} */ (frequencies.list[0]),
|
||||
{
|
||||
save: {
|
||||
...frequencies.serde,
|
||||
id: `${storagePrefix}-swap-freq`,
|
||||
param: "swap-freq",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
interval: {
|
||||
start: signals.createSignal(
|
||||
@@ -156,10 +158,14 @@ export function init({
|
||||
}),
|
||||
description:
|
||||
"The amount of dollars you have ready on the exchange on day one.",
|
||||
input: createInputDollar({
|
||||
id: "simulation-dollars-initial",
|
||||
title: "Initial Dollar Amount",
|
||||
signal: settings.dollars.initial.amount,
|
||||
input: createResetableInput({
|
||||
...utils.dom.createInputDollar({
|
||||
id: "simulation-dollars-initial",
|
||||
title: "Initial Dollar Amount",
|
||||
signal: settings.dollars.initial.amount,
|
||||
signals,
|
||||
}),
|
||||
utils,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -173,10 +179,13 @@ export function init({
|
||||
}),
|
||||
description:
|
||||
"The frequency at which you'll top up your account at the exchange.",
|
||||
input: utils.dom.createSelect({
|
||||
id: "top-up-frequency",
|
||||
list: frequencies.list,
|
||||
signal: settings.dollars.topUp.frenquency,
|
||||
input: createResetableInput({
|
||||
...utils.dom.createSelect({
|
||||
id: "top-up-frequency",
|
||||
list: frequencies.list,
|
||||
signal: settings.dollars.topUp.frenquency,
|
||||
}),
|
||||
utils,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -190,10 +199,14 @@ export function init({
|
||||
}),
|
||||
description:
|
||||
"The recurrent amount of dollars you'll be transfering to said exchange.",
|
||||
input: createInputDollar({
|
||||
id: "simulation-dollars-later",
|
||||
title: "Top Up Dollar Amount",
|
||||
signal: settings.dollars.topUp.amount,
|
||||
input: createResetableInput({
|
||||
...utils.dom.createInputDollar({
|
||||
id: "simulation-dollars-top-up-amount",
|
||||
title: "Top Up Dollar Amount",
|
||||
signal: settings.dollars.topUp.amount,
|
||||
signals,
|
||||
}),
|
||||
utils,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -202,15 +215,19 @@ export function init({
|
||||
createFieldElement({
|
||||
title: createColoredTypeHTML({
|
||||
color: "orange",
|
||||
type: "Swap",
|
||||
text: "Initial Amount",
|
||||
type: "Bitcoin",
|
||||
text: "Initial Investment",
|
||||
}),
|
||||
description:
|
||||
"The amount, if available, of dollars that will be used to buy Bitcoin on day one.",
|
||||
input: createInputDollar({
|
||||
id: "simulation-dollars-later",
|
||||
title: "Initial Swap Amount",
|
||||
signal: settings.swap.amount.initial,
|
||||
input: createResetableInput({
|
||||
...utils.dom.createInputDollar({
|
||||
id: "simulation-bitcoin-initial-investment",
|
||||
title: "Initial Swap Amount",
|
||||
signal: settings.bitcoin.investment.initial,
|
||||
signals,
|
||||
}),
|
||||
utils,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -219,14 +236,17 @@ export function init({
|
||||
createFieldElement({
|
||||
title: createColoredTypeHTML({
|
||||
color: "orange",
|
||||
type: "Swap",
|
||||
text: "Frequency",
|
||||
type: "Bitcoin",
|
||||
text: "Investment Frequency",
|
||||
}),
|
||||
description: "The frequency at which you'll be buying Bitcoin.",
|
||||
input: utils.dom.createSelect({
|
||||
id: "top-up-frequency",
|
||||
list: frequencies.list,
|
||||
signal: settings.swap.frequency,
|
||||
input: createResetableInput({
|
||||
...utils.dom.createSelect({
|
||||
id: "investment-frequency",
|
||||
list: frequencies.list,
|
||||
signal: settings.bitcoin.investment.frequency,
|
||||
}),
|
||||
utils,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -235,15 +255,19 @@ export function init({
|
||||
createFieldElement({
|
||||
title: createColoredTypeHTML({
|
||||
color: "orange",
|
||||
type: "Swap",
|
||||
text: "Recurrent Amount",
|
||||
type: "Bitcoin",
|
||||
text: "Recurrent Investment",
|
||||
}),
|
||||
description:
|
||||
"The recurrent amount, if available, of dollars that will be used to buy Bitcoin.",
|
||||
input: createInputDollar({
|
||||
id: "simulation-dollars-later",
|
||||
title: "Recurrent Swap Amount",
|
||||
signal: settings.swap.amount.recurrent,
|
||||
input: createResetableInput({
|
||||
...utils.dom.createInputDollar({
|
||||
id: "simulation-bitcoin-recurrent-investment",
|
||||
title: "Bitcoin Recurrent Investment",
|
||||
signal: settings.bitcoin.investment.recurrent,
|
||||
signals,
|
||||
}),
|
||||
utils,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -256,9 +280,13 @@ export function init({
|
||||
text: "Start",
|
||||
}),
|
||||
description: "The first day of the simulation.",
|
||||
input: createInputDateField({
|
||||
signal: settings.interval.start,
|
||||
signals,
|
||||
input: createResetableInput({
|
||||
...utils.dom.createInputDate({
|
||||
id: "simulation-inverval-start",
|
||||
title: "First Simulation Date",
|
||||
signal: settings.interval.start,
|
||||
signals,
|
||||
}),
|
||||
utils,
|
||||
}),
|
||||
}),
|
||||
@@ -272,9 +300,13 @@ export function init({
|
||||
text: "End",
|
||||
}),
|
||||
description: "The last day of the simulation.",
|
||||
input: createInputDateField({
|
||||
signal: settings.interval.end,
|
||||
signals,
|
||||
input: createResetableInput({
|
||||
...utils.dom.createInputDate({
|
||||
id: "simulation-inverval-end",
|
||||
title: "Last Simulation Day",
|
||||
signal: settings.interval.end,
|
||||
signals,
|
||||
}),
|
||||
utils,
|
||||
}),
|
||||
}),
|
||||
@@ -288,14 +320,18 @@ export function init({
|
||||
text: "Exchange",
|
||||
}),
|
||||
description: "The amount of trading fees (in %) at the exchange.",
|
||||
input: utils.dom.createInputNumberElement({
|
||||
id: "",
|
||||
title: "",
|
||||
signal: settings.fees.percentage,
|
||||
min: 0,
|
||||
max: 50,
|
||||
step: 0.01,
|
||||
signals,
|
||||
input: createResetableInput({
|
||||
...utils.dom.createInputNumberElement({
|
||||
id: "simulation-fees",
|
||||
title: "Exchange Fees",
|
||||
signal: settings.fees.percentage,
|
||||
min: 0,
|
||||
max: 50,
|
||||
step: 0.01,
|
||||
signals,
|
||||
placeholder: "Fees",
|
||||
}),
|
||||
utils,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -315,9 +351,9 @@ export function init({
|
||||
initialDollarAmount: settings.dollars.initial.amount() || 0,
|
||||
topUpAmount: settings.dollars.topUp.amount() || 0,
|
||||
topUpFrequency: settings.dollars.topUp.frenquency(),
|
||||
initialSwap: settings.swap.amount.initial() || 0,
|
||||
recurrentSwap: settings.swap.amount.recurrent() || 0,
|
||||
swapFrequency: settings.swap.frequency(),
|
||||
initialSwap: settings.bitcoin.investment.initial() || 0,
|
||||
recurrentSwap: settings.bitcoin.investment.recurrent() || 0,
|
||||
swapFrequency: settings.bitcoin.investment.frequency(),
|
||||
start: settings.interval.start(),
|
||||
end: settings.interval.end(),
|
||||
fees: settings.fees.percentage(),
|
||||
@@ -333,8 +369,6 @@ export function init({
|
||||
end,
|
||||
fees,
|
||||
}) => {
|
||||
console.log({ start, end });
|
||||
|
||||
resultsElement.innerHTML = "";
|
||||
resultsElement.append(p1);
|
||||
resultsElement.append(p2);
|
||||
@@ -611,30 +645,35 @@ export function init({
|
||||
colors,
|
||||
id: `simulation-0`,
|
||||
kind: "static",
|
||||
scale: "date",
|
||||
utils,
|
||||
config: [
|
||||
{
|
||||
unit: "US Dollars",
|
||||
scale: "date",
|
||||
config: [
|
||||
{
|
||||
title: "Bitcoin Value",
|
||||
kind: "line",
|
||||
color: colors.amber,
|
||||
owner,
|
||||
data: bitcoinValueData,
|
||||
},
|
||||
{
|
||||
title: "Dollars Left",
|
||||
kind: "line",
|
||||
color: colors.offDollars,
|
||||
owner,
|
||||
data: dollarsLeftData,
|
||||
},
|
||||
{
|
||||
title: "Dollars Converted",
|
||||
kind: "line",
|
||||
color: colors.dollars,
|
||||
owner,
|
||||
data: totalInvestedAmountData,
|
||||
},
|
||||
{
|
||||
title: "Fees Paid",
|
||||
kind: "line",
|
||||
color: colors.rose,
|
||||
owner,
|
||||
@@ -650,13 +689,15 @@ export function init({
|
||||
signals,
|
||||
colors,
|
||||
id: `simulation-1`,
|
||||
scale: "date",
|
||||
kind: "static",
|
||||
utils,
|
||||
config: [
|
||||
{
|
||||
unit: "US Dollars",
|
||||
scale: "date",
|
||||
config: [
|
||||
{
|
||||
title: "Bitcoin Stack",
|
||||
kind: "line",
|
||||
color: colors.bitcoin,
|
||||
owner,
|
||||
@@ -672,19 +713,22 @@ export function init({
|
||||
signals,
|
||||
colors,
|
||||
id: `simulation-average-price`,
|
||||
scale: "date",
|
||||
kind: "static",
|
||||
utils,
|
||||
config: [
|
||||
{
|
||||
unit: "US Dollars",
|
||||
scale: "date",
|
||||
config: [
|
||||
{
|
||||
title: "Bitcoin Price",
|
||||
kind: "line",
|
||||
owner,
|
||||
color: colors.default,
|
||||
data: bitcoinPriceData,
|
||||
},
|
||||
{
|
||||
title: "Average Price Paid",
|
||||
kind: "line",
|
||||
owner,
|
||||
color: colors.lightDollars,
|
||||
@@ -700,13 +744,15 @@ export function init({
|
||||
signals,
|
||||
colors,
|
||||
id: `simulation-return-ratio`,
|
||||
scale: "date",
|
||||
kind: "static",
|
||||
utils,
|
||||
config: [
|
||||
{
|
||||
unit: "US Dollars",
|
||||
scale: "date",
|
||||
config: [
|
||||
{
|
||||
title: "Return Of Investment",
|
||||
kind: "baseline",
|
||||
owner,
|
||||
data: resultData,
|
||||
@@ -732,18 +778,21 @@ export function init({
|
||||
colors,
|
||||
id: `simulation-profitability-ratios`,
|
||||
kind: "static",
|
||||
scale: "date",
|
||||
utils,
|
||||
config: [
|
||||
{
|
||||
unit: "Percentage",
|
||||
scale: "date",
|
||||
config: [
|
||||
{
|
||||
title: "Unprofitable Days Ratio",
|
||||
kind: "line",
|
||||
owner,
|
||||
color: colors.red,
|
||||
data: unprofitableDaysRatioData,
|
||||
},
|
||||
{
|
||||
title: "Profitable Days Ratio",
|
||||
kind: "line",
|
||||
owner,
|
||||
color: colors.green,
|
||||
@@ -800,58 +849,39 @@ function createFieldElement({ title, description, input }) {
|
||||
|
||||
div.append(input);
|
||||
|
||||
const forId = input.id || input.firstElementChild?.id;
|
||||
|
||||
if (!forId) {
|
||||
console.log(input);
|
||||
throw `Input should've an ID`;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
label.for = forId;
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {string} args.id
|
||||
* @param {string} args.title
|
||||
* @param {Signal<number | null>} args.signal
|
||||
* @param {Object} param0
|
||||
* @param {Signal<any>} param0.signal
|
||||
* @param {HTMLInputElement} [param0.input]
|
||||
* @param {HTMLSelectElement} [param0.select]
|
||||
* @param {Utilities} param0.utils
|
||||
*/
|
||||
function createInputDollar({ id, title, signal }) {
|
||||
const input = window.document.createElement("input");
|
||||
input.id = id;
|
||||
input.type = "number";
|
||||
input.placeholder = "US Dollars";
|
||||
input.min = "0";
|
||||
input.title = title;
|
||||
|
||||
const value = signal();
|
||||
input.value = value !== null ? String(value) : "";
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const value = input.value;
|
||||
signal.set(value ? Number(value) : null);
|
||||
});
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} arg
|
||||
* @param {Signal<Date | null>} arg.signal
|
||||
* @param {Utilities} arg.utils
|
||||
* @param {Signals} arg.signals
|
||||
*/
|
||||
function createInputDateField({ signal, signals, utils }) {
|
||||
function createResetableInput({ input, select, signal, utils }) {
|
||||
const div = window.document.createElement("div");
|
||||
|
||||
div.append(
|
||||
utils.dom.createInputDate({
|
||||
id: "",
|
||||
title: "",
|
||||
signal,
|
||||
signals,
|
||||
}),
|
||||
);
|
||||
const element = input || select;
|
||||
if (!element) throw "createResetableField element missing";
|
||||
div.append(element);
|
||||
|
||||
const button = utils.dom.createButtonElement({
|
||||
onClick: signal.reset,
|
||||
text: "Reset",
|
||||
title: "Reset field",
|
||||
});
|
||||
button.type = "reset";
|
||||
|
||||
div.append(button);
|
||||
|
||||
|
||||
Vendored
+43
-11
@@ -19,6 +19,7 @@ import {
|
||||
import { DatePath, HeightPath, LastPath } from "./paths";
|
||||
import { Owner } from "../../packages/solid-signals/2024-11-02/types/core/owner";
|
||||
import { AnyPossibleCohortId } from "../options";
|
||||
import { Signal } from "../../packages/solid-signals/types";
|
||||
|
||||
type GrowToSize<T, N extends number, A extends T[]> = A["length"] extends N
|
||||
? A
|
||||
@@ -26,8 +27,6 @@ type GrowToSize<T, N extends number, A extends T[]> = A["length"] extends N
|
||||
|
||||
type FixedArray<T, N extends number> = GrowToSize<T, N, []>;
|
||||
|
||||
type Signal<T> = Accessor<T> & { set: Setter<T>; reset: VoidFunction };
|
||||
|
||||
type TimeScale = "date" | "height";
|
||||
|
||||
type TimeRange = Range<Time | number>;
|
||||
@@ -302,14 +301,21 @@ interface FetchedHeightDataset<Type> extends Versioned {
|
||||
|
||||
type PriceSeriesType = "Candlestick" | "Line";
|
||||
|
||||
interface Series {
|
||||
interface _Series {
|
||||
id: string;
|
||||
title: string;
|
||||
chunks: Array<Accessor<ISeriesApi<SeriesType> | undefined>>;
|
||||
color: Color | Color[];
|
||||
disabled: Accessor<boolean>;
|
||||
active: Signal<boolean>;
|
||||
visible: Accessor<boolean>;
|
||||
}
|
||||
|
||||
interface SingleSeries extends _Series {
|
||||
series: ISeriesApi<SeriesType>;
|
||||
}
|
||||
|
||||
interface SplitSeries extends _Series {
|
||||
disabled: Accessor<boolean>;
|
||||
chunks: Array<Accessor<ISeriesApi<SeriesType> | undefined>>;
|
||||
dataset: ResourceDataset<TimeScale, number>;
|
||||
}
|
||||
|
||||
@@ -334,7 +340,7 @@ declare global {
|
||||
|
||||
interface HoveredLegend {
|
||||
label: HTMLLabelElement;
|
||||
series: Series;
|
||||
series: SingleSeries | SplitSeries;
|
||||
}
|
||||
|
||||
type NotFunction<T> = T extends Function ? never : T;
|
||||
@@ -390,15 +396,41 @@ type Frequencies = { name: string; list: Frequency[] };
|
||||
|
||||
interface CreatePaneParameters {
|
||||
unit: Unit;
|
||||
scale: TimeScale;
|
||||
chartIndex?: number;
|
||||
paneIndex?: number;
|
||||
whitespace?: true;
|
||||
options?: DeepPartial<ChartOptions>;
|
||||
config?: (
|
||||
| ({ kind: "line" } & CreateLineSeriesParams)
|
||||
| ({ kind: "candle" } & CreateCandlestickSeriesParams)
|
||||
| ({ kind: "baseline" } & CreateBaselineSeriesParams)
|
||||
| ({ kind: "line"; title: string } & CreateLineSeriesParams)
|
||||
| ({ kind: "candle"; title: string } & CreateCandlestickSeriesParams)
|
||||
| ({ kind: "baseline"; title: string } & CreateBaselineSeriesParams)
|
||||
)[];
|
||||
}
|
||||
|
||||
interface CreateSplitSeriesParameters<S extends TimeScale> {
|
||||
dataset: ResourceDataset<S>;
|
||||
seriesBlueprint: SeriesBlueprint;
|
||||
option: Option;
|
||||
index: number;
|
||||
splitSeries: SplitSeries[];
|
||||
setMinMaxMarkersWhenIdle: VoidFunction;
|
||||
disabled?: Accessor<boolean>;
|
||||
}
|
||||
|
||||
type ChartPane = IChartApi & {
|
||||
whitespace: ISeriesApi<"Line">;
|
||||
createBaseLineSeries: (
|
||||
a: CreateBaselineSeriesParams,
|
||||
) => ISeriesApi<"Baseline">;
|
||||
createCandlesticksSeries: (
|
||||
a: CreateCandlestickSeriesParams,
|
||||
) => ISeriesApi<"Candlestick">;
|
||||
createLineSeries: (a: CreateLineSeriesParams) => ISeriesApi<"Line">;
|
||||
hidden: () => boolean;
|
||||
setHidden: (b: boolean) => void;
|
||||
setInitialVisibleTimeRange: VoidFunction;
|
||||
createSplitSeries: <S extends TimeScale>(
|
||||
a: CreateSplitSeriesParameters<S>,
|
||||
) => SplitSeries;
|
||||
};
|
||||
|
||||
type LastValues = Record<LastPath, number> | null;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.charts {
|
||||
.chart {
|
||||
flex: 1;
|
||||
margin-top: 2rem;
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chart-div {
|
||||
.lightweight-chart {
|
||||
margin-left: var(--negative-main-padding);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,18 +14,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
div:has(> input + button) {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
|
||||
button {
|
||||
color: var(--off-color);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -76,11 +64,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.charts {
|
||||
.chart {
|
||||
flex-shrink: 0;
|
||||
height: 400px;
|
||||
|
||||
.chart-div {
|
||||
.lightweight-chart {
|
||||
margin-left: calc(var(--negative-main-padding) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user