diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d1b186b4..8cb282778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,11 @@ - General - Added a light theme ! - Performance + - Added height datasets and many optimizations to make them usable + - Added split panes in order to always have the vertical axis visible - Improved app's reactivity - Added some chunk splitting for a faster initial load - - Global improvements that increased the Lighthouse's performance score from the low 30s to the high 70s -- Chart + - Global improvements that increased the Lighthouse's performance score - Fixed legend hovering on mobile not resetting on touch end - Updated legend padding so that the scrollbar, if visible, is less in the way - Added "3 months" and yearly time scale setters (from year 2009 to today) diff --git a/README.md b/README.md index 75cc7c37b..9645f9c6d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Description -Satonomics is a Bitcoin only on-chain data generator and visualizer that is fully free, open source, verifiable, local-first and self-hostable. +Satonomics is a better, FOSS, Bitcoin-only, self-hostable Glassnode. While [mempool.space](https://mempool.space) gives a very micro view of the network where you can follow the journey of any address, this tool on the other hand, is the exact opposite and very complimentary by giving you a much more global/macro view of the flow and various dynamics of the network. @@ -19,9 +19,11 @@ This project is in a very early stage. The web app will have bugs, the API might ## Instances Web App: + - [app.satonomics.xyz](https://app.satonomics.xyz) API: + - [api.satonomics.xyz](https://api.satonomics.xyz) ## Structure diff --git a/app/package.json b/app/package.json index f24d5dad5..f642f0aee 100644 --- a/app/package.json +++ b/app/package.json @@ -19,7 +19,6 @@ "@leeoniya/ufuzzy": "^1.0.14", "@solid-primitives/event-listener": "^2.3.3", "@solid-primitives/intersection-observer": "^2.1.6", - "@solid-primitives/memo": "^1.3.8", "@solid-primitives/resize-observer": "^2.0.25", "lean-qr": "^2.3.4", "lightweight-charts": "^4.1.6", @@ -36,7 +35,7 @@ "pwa-asset-generator": "^6.3.1", "rollup-plugin-visualizer": "^5.12.0", "tailwindcss": "^3.4.4", - "typescript": "^5.5.2", + "typescript": "^5.5.3", "unplugin-auto-import": "^0.17.6", "unplugin-icons": "^0.19.0", "vite": "^5.3.2", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index d64b6f197..ab317faad 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -14,9 +14,6 @@ dependencies: '@solid-primitives/intersection-observer': specifier: ^2.1.6 version: 2.1.6(solid-js@1.8.18) - '@solid-primitives/memo': - specifier: ^1.3.8 - version: 1.3.8(solid-js@1.8.18) '@solid-primitives/resize-observer': specifier: ^2.0.25 version: 2.0.25(solid-js@1.8.18) @@ -62,8 +59,8 @@ devDependencies: specifier: ^3.4.4 version: 3.4.4 typescript: - specifier: ^5.5.2 - version: 5.5.2 + specifier: ^5.5.3 + version: 5.5.3 unplugin-auto-import: specifier: ^0.17.6 version: 0.17.6(rollup@2.79.1) @@ -2251,16 +2248,6 @@ packages: solid-js: 1.8.18 dev: false - /@solid-primitives/memo@1.3.8(solid-js@1.8.18): - resolution: {integrity: sha512-U75pfLFSxFmM2xbx1+2XPPyWbaXrnUFF10spbFuOUgJ7azrC+4y+FnrVi4RKqHw9gftd8aKQuTiyMQq468YLQw==} - peerDependencies: - solid-js: ^1.6.12 - dependencies: - '@solid-primitives/scheduled': 1.4.3(solid-js@1.8.18) - '@solid-primitives/utils': 6.2.3(solid-js@1.8.18) - solid-js: 1.8.18 - dev: false - /@solid-primitives/resize-observer@2.0.25(solid-js@1.8.18): resolution: {integrity: sha512-jVDXkt2MiriYRaz4DYs62185d+6jQ+1DCsR+v7f6XMsIJJuf963qdBRFjtZtKXBaxdPNMyuPeDgf5XQe3EoDJg==} peerDependencies: @@ -2282,14 +2269,6 @@ packages: solid-js: 1.8.18 dev: false - /@solid-primitives/scheduled@1.4.3(solid-js@1.8.18): - resolution: {integrity: sha512-HfWN5w7b7FEc6VPLBKnnE302h90jsLMuR28Fcf7neRGGf8jBj6wm6/UFQ00VlKexHFMR6KQ2u4VBh5a1ZcqM8g==} - peerDependencies: - solid-js: ^1.6.12 - dependencies: - solid-js: 1.8.18 - dev: false - /@solid-primitives/static-store@0.0.8(solid-js@1.8.18): resolution: {integrity: sha512-ZecE4BqY0oBk0YG00nzaAWO5Mjcny8Fc06CdbXadH9T9lzq/9GefqcSe/5AtdXqjvY/DtJ5C6CkcjPZO0o/eqg==} peerDependencies: @@ -2528,7 +2507,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.23.1 - caniuse-lite: 1.0.30001638 + caniuse-lite: 1.0.30001639 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.1 @@ -2655,8 +2634,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001638 - electron-to-chromium: 1.4.815 + caniuse-lite: 1.0.30001639 + electron-to-chromium: 1.4.816 node-releases: 2.0.14 update-browserslist-db: 1.0.16(browserslist@4.23.1) dev: true @@ -2715,8 +2694,8 @@ packages: engines: {node: '>=6'} dev: true - /caniuse-lite@1.0.30001638: - resolution: {integrity: sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==} + /caniuse-lite@1.0.30001639: + resolution: {integrity: sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==} dev: true /capnp-ts@0.7.0: @@ -3122,8 +3101,8 @@ packages: jake: 10.9.1 dev: true - /electron-to-chromium@1.4.815: - resolution: {integrity: sha512-OvpTT2ItpOXJL7IGcYakRjHCt8L5GrrN/wHCQsRB4PQa1X9fe+X9oen245mIId7s14xvArCGSTIq644yPUKKLg==} + /electron-to-chromium@1.4.816: + resolution: {integrity: sha512-EKH5X5oqC6hLmiS7/vYtZHZFTNdhsYG5NVPRN6Yn0kQHNBlT59+xSM8HBy66P5fxWpKgZbPqb+diC64ng295Jw==} dev: true /emoji-regex@8.0.0: @@ -4125,7 +4104,7 @@ packages: engines: {node: '>=14'} dependencies: mlly: 1.7.1 - pkg-types: 1.1.1 + pkg-types: 1.1.2 dev: true /locate-path@5.0.0: @@ -4349,7 +4328,7 @@ packages: dependencies: acorn: 8.12.0 pathe: 1.1.2 - pkg-types: 1.1.1 + pkg-types: 1.1.2 ufo: 1.5.3 dev: true @@ -4632,8 +4611,8 @@ packages: find-up: 4.1.0 dev: true - /pkg-types@1.1.1: - resolution: {integrity: sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==} + /pkg-types@1.1.2: + resolution: {integrity: sha512-VEGf1he2DR5yowYRl0XJhWJq5ktm9gYIsH+y8sNJpHlxch7JPDaufgrsl4vYjd9hMUY8QVjoNncKbow9I7exyA==} dependencies: confbox: 0.1.7 mlly: 1.7.1 @@ -5671,8 +5650,8 @@ packages: possible-typed-array-names: 1.0.0 dev: true - /typescript@5.5.2: - resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} + /typescript@5.5.3: + resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -5754,10 +5733,10 @@ packages: magic-string: 0.30.10 mlly: 1.7.1 pathe: 1.1.2 - pkg-types: 1.1.1 + pkg-types: 1.1.2 scule: 1.3.0 strip-literal: 2.1.0 - unplugin: 1.10.2 + unplugin: 1.11.0 transitivePeerDependencies: - rollup dev: true @@ -5793,7 +5772,7 @@ packages: magic-string: 0.30.10 minimatch: 9.0.5 unimport: 3.7.2(rollup@2.79.1) - unplugin: 1.10.2 + unplugin: 1.11.0 transitivePeerDependencies: - rollup dev: true @@ -5824,13 +5803,13 @@ packages: debug: 4.3.5 kolorist: 1.8.0 local-pkg: 0.5.0 - unplugin: 1.10.2 + unplugin: 1.11.0 transitivePeerDependencies: - supports-color dev: true - /unplugin@1.10.2: - resolution: {integrity: sha512-KuPqnjU4HBcrSwmQatfdc5hU4xzaQrhoKqCKylwmLnbBvqj5udXL8cHrkOuYDoI4ESCwJIiAIKMujroIUKLgow==} + /unplugin@1.11.0: + resolution: {integrity: sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==} engines: {node: '>=14.0.0'} dependencies: acorn: 8.12.0 diff --git a/app/src/app/components/frames/chart/components/chart.tsx b/app/src/app/components/frames/chart/components/chart.tsx index a4172646a..f828d22ab 100644 --- a/app/src/app/components/frames/chart/components/chart.tsx +++ b/app/src/app/components/frames/chart/components/chart.tsx @@ -4,14 +4,12 @@ export function Chart({ presets, datasets, legendSetter, - activeResources, }: { charts: RWS; parentDiv: RWS; presets: Presets; datasets: Datasets; legendSetter: Setter; - activeResources: Accessor>>; }) { onMount(() => { createEffect(() => { @@ -28,7 +26,6 @@ export function Chart({ parentDiv: div, datasets, preset, - activeResources, legendSetter, }); } catch (error) { diff --git a/app/src/app/components/frames/chart/index.tsx b/app/src/app/components/frames/chart/index.tsx index 7781e0a8a..346f474aa 100644 --- a/app/src/app/components/frames/chart/index.tsx +++ b/app/src/app/components/frames/chart/index.tsx @@ -11,7 +11,6 @@ import { Title } from "./components/title"; export function ChartFrame({ presets, datasets, - activeResources, hide, qrcode, standalone, @@ -20,7 +19,6 @@ export function ChartFrame({ presets: Presets; hide?: Accessor; qrcode: RWS; - activeResources: Accessor>>; datasets: Datasets; fullscreen?: RWS; standalone: boolean; @@ -64,7 +62,6 @@ export function ChartFrame({ >>(new Set(), { - equals: false, - }); - - const datasets = createDatasets({ - setActiveResources: activeResources.set, - }); + const datasets = createDatasets(); onMount(() => { webSockets.openAll(); @@ -273,7 +267,6 @@ export function App() { qrcode={qrcode} standalone={false} datasets={datasets} - activeResources={activeResources} /> @@ -340,7 +333,6 @@ export function App() { presets={presets} qrcode={qrcode} fullscreen={fullscreen} - activeResources={activeResources} datasets={datasets} /> diff --git a/app/src/scripts/datasets/base.ts b/app/src/scripts/datasets/base.ts index f1edb948e..dd2259ecf 100644 --- a/app/src/scripts/datasets/base.ts +++ b/app/src/scripts/datasets/base.ts @@ -4,11 +4,9 @@ export { averages } from "./consts/averages"; export function createScaleDatasets({ scale, - setActiveResources, groupedKeysToURLPath, }: { scale: Scale; - setActiveResources: Setter>>; groupedKeysToURLPath: GroupedKeysToURLPath[Scale]; }) { type Key = keyof typeof groupedKeysToURLPath; @@ -23,7 +21,6 @@ export function createScaleDatasets({ datasets[key as unknown as Exclude] = createResourceDataset({ scale, path: groupedKeysToURLPath[key as Key] as any, - setActiveResources, }); } } @@ -31,7 +28,6 @@ export function createScaleDatasets({ const price = createResourceDataset({ scale, path: `/${scale}-to-ohlc`, - setActiveResources, }); Object.assign(datasets, { price }); diff --git a/app/src/scripts/datasets/date.ts b/app/src/scripts/datasets/date.ts index 2b0b6e2b6..fb08071c7 100644 --- a/app/src/scripts/datasets/date.ts +++ b/app/src/scripts/datasets/date.ts @@ -3,10 +3,8 @@ import { createResourceDataset } from "./resource"; export { averages } from "./consts/averages"; export function createDateDatasets({ - setActiveResources, groupedKeysToURLPath, }: { - setActiveResources: Setter>>; groupedKeysToURLPath: GroupedKeysToURLPath["date"]; }) { type Key = keyof typeof groupedKeysToURLPath; @@ -21,7 +19,6 @@ export function createDateDatasets({ datasets[key as Exclude] = createResourceDataset<"date">({ scale: "date", path: groupedKeysToURLPath[key as Key], - setActiveResources, }); } } @@ -29,7 +26,6 @@ export function createDateDatasets({ const price = createResourceDataset<"date", OHLC>({ scale: "date", path: "/date-to-ohlc", - setActiveResources, }); Object.assign(datasets, { price }); diff --git a/app/src/scripts/datasets/height.ts b/app/src/scripts/datasets/height.ts index e9b340b47..6d6512d5d 100644 --- a/app/src/scripts/datasets/height.ts +++ b/app/src/scripts/datasets/height.ts @@ -1,10 +1,8 @@ import { createResourceDataset } from "./resource"; export function createHeightDatasets({ - setActiveResources, groupedKeysToURLPath, }: { - setActiveResources: Setter>>; groupedKeysToURLPath: GroupedKeysToURLPath["height"]; }) { type Key = keyof typeof groupedKeysToURLPath; @@ -19,7 +17,6 @@ export function createHeightDatasets({ datasets[key as Exclude] = createResourceDataset<"height">({ scale: "height", path: groupedKeysToURLPath[key as Key], - setActiveResources, }); } } @@ -27,7 +24,6 @@ export function createHeightDatasets({ const price = createResourceDataset<"height", OHLC>({ scale: "height", path: "/height-to-ohlc", - setActiveResources, }); Object.assign(datasets, { price }); diff --git a/app/src/scripts/datasets/index.ts b/app/src/scripts/datasets/index.ts index 9944ae361..6b17d752d 100644 --- a/app/src/scripts/datasets/index.ts +++ b/app/src/scripts/datasets/index.ts @@ -7,18 +7,12 @@ export const scales = ["date" as const, "height" as const]; export const HEIGHT_CHUNK_SIZE = 10_000; -export function createDatasets({ - setActiveResources, -}: { - setActiveResources: Setter>>; -}) { +export function createDatasets() { const date = createDateDatasets({ - setActiveResources, groupedKeysToURLPath: groupedKeysToURLPath.date, }); const height = createHeightDatasets({ - setActiveResources, groupedKeysToURLPath: groupedKeysToURLPath.height, }); diff --git a/app/src/scripts/datasets/resource.ts b/app/src/scripts/datasets/resource.ts index c34816a64..1c42a1efa 100644 --- a/app/src/scripts/datasets/resource.ts +++ b/app/src/scripts/datasets/resource.ts @@ -1,5 +1,3 @@ -import { createLazyMemo } from "@solid-primitives/memo"; - import { ONE_DAY_IN_MS, ONE_HOUR_IN_MS, @@ -8,19 +6,12 @@ import { import { createRWS } from "/src/solid/rws"; import { HEIGHT_CHUNK_SIZE } from "."; +import { debounce } from "../utils/debounce"; export function createResourceDataset< Scale extends ResourceScale, Type extends OHLC | number = number, ->({ - scale, - path, - setActiveResources, -}: { - scale: Scale; - path: string; - setActiveResources: Setter>>; -}) { +>({ scale, path }: { scale: Scale; path: string }) { type Dataset = Scale extends "date" ? FetchedDateDataset : FetchedHeightDataset; @@ -85,8 +76,7 @@ export function createResourceDataset< }) as FetchedResult[]; const _fetch = async (id: number) => { - const index = - scale === "date" ? id - 2009 : Math.floor(id / HEIGHT_CHUNK_SIZE); + const index = chunkIdToIndex(scale, id); if ( index < 0 || @@ -205,25 +195,43 @@ export function createResourceDataset< fetched.loading = false; }; + const valuesCallback = (vecs: Value[][]) => { + let length = 0; + for (let i = 0; i < vecs.length; i++) { + length += vecs[i].length; + } + + if (!length) return; + + const array = new Array(length); + let k = 0; + for (let i = 0; i < vecs.length; i++) { + let vec = vecs[i]; + for (let j = 0; j < vec.length; j++) { + array[k++] = vec[j]; + } + } + + if (k !== length) throw Error("e"); + + values.set(array); + }; + + const debouncedValuesCallback = debounce(valuesCallback, 100); + + const values = createRWS([]); + + createEffect(() => { + const vecs = fetchedJSONs.map((fetched) => fetched.vec() || []); + debouncedValuesCallback(vecs); + }); + const resource: ResourceDataset = { scale, url: baseURL, fetch: _fetch, fetchedJSONs, - values: createLazyMemo(() => { - setActiveResources((resources) => resources.add(resource)); - - onCleanup(() => - setActiveResources((resources) => { - resources.delete(resource); - return resources; - }), - ); - - const flat = fetchedJSONs.flatMap((fetched) => fetched.vec() || []); - - return flat; - }), + values, drop() { fetchedJSONs.forEach((fetched) => { fetched.at = null; @@ -245,3 +253,7 @@ async function convertResponseToJSON< return null; } } + +function chunkIdToIndex(scale: ResourceScale, id: number) { + return scale === "date" ? id - 2009 : Math.floor(id / HEIGHT_CHUNK_SIZE); +} diff --git a/app/src/scripts/datasets/types.d.ts b/app/src/scripts/datasets/types.d.ts index 7ca681ed9..5230cfbfc 100644 --- a/app/src/scripts/datasets/types.d.ts +++ b/app/src/scripts/datasets/types.d.ts @@ -8,14 +8,6 @@ type ResourceScale = (typeof import("./index").scales)[index]; type DatasetValue = T & Numbered & Valued; -interface Dataset< - Scale extends ResourceScale, - Value extends SingleValueData | CandlestickData = SingleValueData, -> { - scale: Scale; - values: Accessor[]>; -} - interface ResourceDataset< Scale extends ResourceScale, Type extends OHLC | number = number, @@ -27,16 +19,15 @@ interface ResourceDataset< Value extends SingleValueData | CandlestickData = Type extends number ? SingleValueData : CandlestickData, -> extends Dataset { +> { + scale: Scale; url: string; fetch: (id: number) => void; fetchedJSONs: FetchedResult[]; + values: Accessor[]>; drop: VoidFunction; } -type AnyDataset = Dataset & - Partial>; - interface FetchedResult< Scale extends ResourceScale, Type extends number | OHLC, diff --git a/app/src/scripts/lightweightCharts/chart/price.ts b/app/src/scripts/lightweightCharts/chart/price.ts deleted file mode 100644 index 57b1f4a41..000000000 --- a/app/src/scripts/lightweightCharts/chart/price.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { colors } from "../../utils/colors"; -import { webSockets } from "../../ws"; -import { createCandlesticksSeries } from "../series/creators/candlesticks"; -import { createSeriesLegend } from "../series/creators/legend"; -import { createLineSeries } from "../series/creators/line"; -import { initTimeScale } from "./time"; - -export const PRICE_SCALE_MOMENTUM_ID = "momentum"; - -export const applyPriceSeries = < - Scale extends ResourceScale, - T extends SingleValueData, ->({ - chart, - preset, - dataset, - seriesType, - valuesSkipped, - options, - activeResources, -}: { - chart: IChartApi; - preset: Preset; - valuesSkipped: Accessor; - seriesType: Accessor<"Candlestick" | "Line">; - activeResources: Accessor>>; - dataset: Dataset; - options?: PriceSeriesOptions; -}) => { - const id = options?.id || "price"; - const title = options?.title || "Price"; - - const url = "url" in dataset ? (dataset as any).url : undefined; - - const priceScaleOptions: DeepPartialPriceScaleOptions = { - mode: 1, - ...options?.priceScaleOptions, - }; - - const [ohlcSeries, ohlcColors] = createCandlesticksSeries(chart, options); - - const ohlcLegend = createSeriesLegend({ - id, - presetId: preset.id, - title, - color: () => ohlcColors, - series: ohlcSeries, - disabled: () => seriesType() !== "Candlestick", - url, - }); - - ohlcSeries.priceScale().applyOptions(priceScaleOptions); - - // --- - - const lineColor = colors.white; - - const lineSeries = createLineSeries(chart, { - color: lineColor, - ...options?.seriesOptions, - }); - - const lineLegend = createSeriesLegend({ - id, - presetId: preset.id, - title, - color: () => lineColor, - series: lineSeries, - disabled: () => seriesType() !== "Line", - visible: ohlcLegend.visible, - url, - }); - - lineSeries.priceScale().applyOptions(priceScaleOptions); - - // --- - - // setMinMaxMarkers({ - // scale: preset.scale, - // candlesticks: - // dataset?.values() || datasets[preset.scale].price.values() || ([] as any), - // range: chartState.range, - // lowerOpacity, - // }); - - initTimeScale({ - activeResources, - }); - - createEffect(() => { - const values = dataset.values(); - // const values = computeDrawnSeriesValues(dataset.values(), valuesSkipped()); - - lineSeries.setData(values); - ohlcSeries.setData(values); - }); - - createEffect(() => { - if (preset.scale === "date") { - const latest = webSockets.liveKrakenCandle.latest(); - - if (latest) { - ohlcSeries.update(latest); - lineSeries.update(latest); - } - } - }); - - return { ohlcLegend, lineLegend }; -}; diff --git a/app/src/scripts/lightweightCharts/chart/time.ts b/app/src/scripts/lightweightCharts/chart/time.ts index bede3e188..6ebfe03f3 100644 --- a/app/src/scripts/lightweightCharts/chart/time.ts +++ b/app/src/scripts/lightweightCharts/chart/time.ts @@ -1,7 +1,6 @@ import { HEIGHT_CHUNK_SIZE } from "../../datasets"; import { debounce } from "../../utils/debounce"; import { writeURLParam } from "../../utils/urlParams"; -import { setMinMaxMarkers } from "./markers"; import { chartState, LOCAL_STORAGE_RANGE_KEY, @@ -20,9 +19,9 @@ const debouncedUpdateURLParams = debounce((range: TimeRange | null) => { }, 500); export function initTimeScale({ - activeResources, + activeDatasets, }: { - activeResources: Accessor>>; + activeDatasets: Set>; }) { setTimeScale(chartState.range); @@ -46,7 +45,7 @@ export function initTimeScale({ } ids.forEach((id) => { - activeResources().forEach((resource) => resource.fetch(id)); + activeDatasets.forEach((dataset) => dataset.fetch(id)); }); }, 100); @@ -63,6 +62,7 @@ export function initTimeScale({ chartState.range = range; }); }, 50); + onCleanup(() => clearTimeout(timeout)); } diff --git a/app/src/scripts/lightweightCharts/chart/whitespace.ts b/app/src/scripts/lightweightCharts/chart/whitespace.ts index e58995f26..782112a90 100644 --- a/app/src/scripts/lightweightCharts/chart/whitespace.ts +++ b/app/src/scripts/lightweightCharts/chart/whitespace.ts @@ -27,17 +27,16 @@ const whitespaceDateDataset: (SingleValueData & Numbered)[] = new Array( }; }); -const whitespaceHeightDataset: (WhitespaceData & Numbered)[] = new Array( - 840_000, +const heightStart = -100_000; +const whitespaceHeightDataset: (SingleValueData & Numbered)[] = new Array( + 1_200_000, ) .fill(0) - .map( - (_, index) => - ({ - time: index, - number: index, - }) as any, - ); + .map((_, index) => ({ + time: (heightStart + index) as any, + number: heightStart + index, + value: NaN, + })); export function setWhitespace(chart: IChartApi, scale: ResourceScale) { const whitespaceSeries = createLineSeries(chart); diff --git a/app/src/scripts/lightweightCharts/series/creators/histogram.ts b/app/src/scripts/lightweightCharts/series/creators/histogram.ts index b7be67c3b..66232c108 100644 --- a/app/src/scripts/lightweightCharts/series/creators/histogram.ts +++ b/app/src/scripts/lightweightCharts/series/creators/histogram.ts @@ -1,10 +1,11 @@ -import { PRICE_SCALE_MOMENTUM_ID } from "../../chart/price"; import { defaultSeriesOptions } from "./options"; type HistogramOptions = DeepPartial< HistogramStyleOptions & SeriesOptionsCommon >; +export const PRICE_SCALE_MOMENTUM_ID = "momentum"; + export const createHistogramSeries = ( chart: IChartApi, options?: HistogramOptions, diff --git a/app/src/scripts/presets/apply.ts b/app/src/scripts/presets/apply.ts index 1cb8be921..dbb9224a5 100644 --- a/app/src/scripts/presets/apply.ts +++ b/app/src/scripts/presets/apply.ts @@ -1,19 +1,22 @@ import { createRWS } from "/src/solid/rws"; import { createChart } from "../lightweightCharts/chart/create"; -import { applyPriceSeries } from "../lightweightCharts/chart/price"; import { chartState } from "../lightweightCharts/chart/state"; +import { initTimeScale } from "../lightweightCharts/chart/time"; import { setWhitespace } from "../lightweightCharts/chart/whitespace"; import { createAreaSeries } from "../lightweightCharts/series/creators/area"; import { createBaseLineSeries, DEFAULT_BASELINE_COLORS, } from "../lightweightCharts/series/creators/baseLine"; +import { createCandlesticksSeries } from "../lightweightCharts/series/creators/candlesticks"; import { createHistogramSeries } from "../lightweightCharts/series/creators/histogram"; import { createSeriesLegend } from "../lightweightCharts/series/creators/legend"; import { createLineSeries } from "../lightweightCharts/series/creators/line"; +import { colors } from "../utils/colors"; import { debounce } from "../utils/debounce"; import { stringToId } from "../utils/id"; +import { webSockets } from "../ws"; export enum SeriesType { Normal, @@ -24,7 +27,7 @@ export enum SeriesType { type SeriesConfig = | { - dataset: AnyDataset; + dataset: ResourceDataset; color?: string; colors?: undefined; seriesType: SeriesType.Based; @@ -33,7 +36,7 @@ type SeriesConfig = defaultVisible?: boolean; } | { - dataset: AnyDataset; + dataset: ResourceDataset; color?: string; colors?: string[]; seriesType: SeriesType.Histogram; @@ -42,7 +45,7 @@ type SeriesConfig = defaultVisible?: boolean; } | { - dataset: AnyDataset; + dataset: ResourceDataset; color: string; colors?: undefined; seriesType?: SeriesType.Normal | SeriesType.Area; @@ -61,20 +64,18 @@ export function applySeriesList({ datasets, priceDataset, priceOptions, - activeResources, legendSetter, }: { charts: RWS; parentDiv: HTMLDivElement; preset: Preset; legendSetter: Setter; - priceDataset?: AnyDataset; + priceDataset?: ResourceDataset; priceOptions?: PriceSeriesOptions; priceScaleOptions?: DeepPartialPriceScaleOptions; top?: SeriesConfig[]; bottom?: SeriesConfig[]; datasets: Datasets; - activeResources: Accessor>>; }) { reactiveChartList.set((charts) => { charts.forEach((chart) => { @@ -92,7 +93,9 @@ export function applySeriesList({ const priceSeriesType = createRWS<"Candlestick" | "Line">("Candlestick"); - const valuesSkipped = createRWS(0); + // const valuesSkipped = createRWS(0); + + const activeDatasets: Set> = new Set(); const charts = [top || [], bottom] .flatMap((list) => (list ? [list] : [])) @@ -136,17 +139,20 @@ export function applySeriesList({ const _legendList: PresetLegend = []; if (index === 0) { + const dataset = + priceDataset || + (datasets[preset.scale as Scale].price as unknown as NonNullable< + typeof priceDataset + >); + + activeDatasets.add(dataset); + const price = applyPriceSeries({ chart, preset, seriesType: priceSeriesType, - valuesSkipped, - dataset: - priceDataset || - (datasets[preset.scale as Scale].price as unknown as NonNullable< - typeof priceDataset - >), - activeResources, + // valuesSkipped, + dataset, options: priceOptions, }); @@ -168,6 +174,8 @@ export function applySeriesList({ options, defaultVisible, }) => { + activeDatasets.add(dataset); + let series: ISeriesApi< "Baseline" | "Line" | "Area" | "Histogram" >; @@ -216,10 +224,10 @@ export function applySeriesList({ ); createEffect(() => { - series.setData( - dataset?.values() || [], - // computeDrawnSeriesValues(dataset?.values(), valuesSkipped()), - ); + const values = dataset.values(); + console.log(values.length); + + series.setData(values); }); return series; @@ -275,11 +283,13 @@ export function applySeriesList({ const ratio = (range.to - range.from) / width; if (ratio <= 0.5) { + // valuesSkipped.set(0); + priceSeriesType.set("Candlestick"); } else { priceSeriesType.set("Line"); - valuesSkipped.set(Math.floor(ratio / 5)); + // valuesSkipped.set(Math.floor(ratio / 1.25)); } } catch {} } @@ -289,6 +299,10 @@ export function applySeriesList({ 50, ); + initTimeScale({ + activeDatasets, + }); + charts.forEach(({ chart }, index) => { chart.timeScale().subscribeVisibleLogicalRangeChange((timeRange) => { // Last chart otherwise length of timescale is Infinity @@ -327,31 +341,270 @@ export function applySeriesList({ reactiveChartList.set(() => charts.map(({ chart }) => chart)); } -export function computeDrawnSeriesValues( - values: DatasetValue[] | undefined, - valuesSkipped: number, -) { - values = values || []; +function applyPriceSeries< + Scale extends ResourceScale, + T extends OHLC | number, +>({ + chart, + preset, + dataset, + seriesType, + // valuesSkipped, + options, +}: { + chart: IChartApi; + preset: Preset; + // valuesSkipped: Accessor; + seriesType: Accessor<"Candlestick" | "Line">; + dataset: ResourceDataset; + options?: PriceSeriesOptions; +}) { + // console.time("series add"); - if (valuesSkipped === 0) { - return values; - } else { - const valuesSkippedPlus1 = valuesSkipped + 1; + const id = options?.id || "price"; + const title = options?.title || "Price"; - // console.log(_valuesSkippedPlus1); + const url = "url" in dataset ? (dataset as any).url : undefined; - let length = Math.floor(values.length / valuesSkippedPlus1); + const priceScaleOptions: DeepPartialPriceScaleOptions = { + mode: 1, + ...options?.priceScaleOptions, + }; - // console.log(length); + let [ohlcSeries, ohlcColors] = createCandlesticksSeries(chart, options); - const filteredValues = new Array(length); + const ohlcLegend = createSeriesLegend({ + id, + presetId: preset.id, + title, + color: () => ohlcColors, + series: ohlcSeries, + disabled: () => seriesType() !== "Candlestick", + url, + }); - for (let i = 0; i < length; i++) { - filteredValues[i] = values[i * valuesSkippedPlus1]; + ohlcSeries.priceScale().applyOptions(priceScaleOptions); + + // --- + + const lineColor = colors.white; + + let lineSeries = createLineSeries(chart, { + color: lineColor, + ...options?.seriesOptions, + }); + + const lineLegend = createSeriesLegend({ + id, + presetId: preset.id, + title, + color: () => lineColor, + series: lineSeries, + disabled: () => seriesType() !== "Line", + visible: ohlcLegend.visible, + url, + }); + + lineSeries.priceScale().applyOptions(priceScaleOptions); + + // console.timeEnd("series add"); + + // lineSeries.setData(whitespaceHeightDataset); + // ohlcSeries.setData({ time: 0, value: NaN }); + + // --- + + // setMinMaxMarkers({ + // scale: preset.scale, + // candlesticks: + // dataset?.values() || datasets[preset.scale].price.values() || ([] as any), + // range: chartState.range, + // lowerOpacity, + // }); + + // const startingValue = { + // number: -1, + // time: -1, + // open: NaN, + // high: NaN, + // low: NaN, + // close: NaN, + // value: NaN, + // }; + // lineSeries.update(startingValue); + // ohlcSeries.update(startingValue); + + // const callback = ( + // chunks: any[], + // valuesSkippedPlus1: number, + // length: number, + // ) => { + // console.time("t"); + // console.time("a"); + // // chart.removeSeries(ohlcSeries); + // // chart.removeSeries(lineSeries); + + // // ohlcSeries = createCandlesticksSeries(chart, options)[0]; + + // // ohlcSeries.priceScale().applyOptions(priceScaleOptions); + + // // lineSeries = createLineSeries(chart, { + // // color: lineColor, + // // ...options?.seriesOptions, + // // }); + + // // lineSeries.priceScale().applyOptions(priceScaleOptions); + + // const values = new Array(length); + + // let i = 0; + // for (let k = 0; k < chunks.length; k++) { + // const chunk = chunks[k]; + // // const chunk = + // // fetchedJSONs[chunkIdToIndex(dataset.scale, activeRange[k])]?.vec?.() || + // // []; + + // for (let j = 0; j < chunk.length; j += valuesSkippedPlus1) { + // values[i++] = chunk[j]; + // // console.log(chunk[j]); + // // callback(chunk[j]); + // // for (let i = 0; i < seriesList.length; i++) { + // // seriesList[i].update(chunk[j]); + // // } + // // const value = chunk[j]; + // // console.log(value.time); + // // lineSeries.update(value); + // // ohlcSeries.update(value); + + // // i++; + // } + // } + + // console.log(values.length); + // console.timeEnd("t"); + + // lineSeries.setData(values); + + // console.timeEnd("a"); + // }; + + // const debouncedCallback = debounce(callback, 200); + + createEffect(() => { + const values = dataset.values(); + console.log(values.length); + lineSeries.setData(values); + ohlcSeries.setData(values); + }); + // createEffect(() => + // computeDrawnSeriesValues( + // dataset, + // valuesSkipped(), + // debouncedCallback, + // // [lineSeries, ohlcSeries], + // // (value) => { + // // try { + // // console.log(value); + // // lineSeries.update(value); + // // ohlcSeries.update(value); + // // } catch {} + // // }), + // ), + // ); + + createEffect(() => { + if (preset.scale === "date") { + const latest = webSockets.liveKrakenCandle.latest(); + + if (latest) { + ohlcSeries.update(latest); + lineSeries.update(latest); + } } + }); - // console.log(filteredValues.length); - - return filteredValues; - } + return { ohlcLegend, lineLegend }; } + +// // const computeDrawnSeriesValues = debounce(_computeDrawnSeriesValues, 100); + +// function computeDrawnSeriesValues< +// S extends ResourceScale, +// T extends OHLC | number, +// >( +// dataset: ResourceDataset, +// valuesSkipped: number, +// callback: (chunks: any, v: number, l: number) => void, +// // seriesList: ISeriesApi[], +// ) { +// // console.time(dataset.url); + +// const { fetchedJSONs, activeRange: _activeRange } = dataset; + +// const activeRange = _activeRange(); + +// const valuesSkippedPlus1 = valuesSkipped + 1; + +// if (valuesSkippedPlus1 === 1) { +// console.log("todo valuesSkippedPlus1===1, skip for now"); +// } + +// // for (let i = 0; i < seriesList.length; i++) { +// // seriesList[i]. +// // } + +// const chunks = new Array(activeRange.length); +// let length = 0; + +// for (let i = 0; i < chunks.length; i++) { +// const chunk = +// fetchedJSONs[chunkIdToIndex(dataset.scale, activeRange[i])]?.vec?.() || +// []; + +// chunks[i] = chunk; + +// length += Math.ceil(chunk.length / valuesSkippedPlus1); +// } + +// callback(chunks, valuesSkippedPlus1, length); + +// // setValues(chunks, valuesSkippedPlus1, length, callback); +// // } + +// // // const debouncedSetValues = debounce(setValues, 50); +// // function setValues( +// // chunks: any[], +// // valuesSkippedPlus1: number, +// // length: number, +// // callback: (values: any[]) => void, +// // ) { +// // const values = new Array(length); + +// // let i = 0; +// // for (let k = 0; k < activeRange.length; k++) { +// // const chunk = chunks[k]; +// // // const chunk = +// // // fetchedJSONs[chunkIdToIndex(dataset.scale, activeRange[k])]?.vec?.() || +// // // []; + +// // for (let j = 0; j < chunk.length; j += valuesSkippedPlus1) { +// // // values[i++] = chunk[j]; +// // // console.log(chunk[j]); +// // // callback(chunk[j]); +// // for (let i = 0; i < seriesList.length; i++) { +// // seriesList[i].update(chunk[j]); +// // } + +// // // i++; +// // } +// // } + +// // console.log(i); + +// // if (i !== values.length) { +// // console.log({ n: i, values }); +// // throw Error("error"); +// // } + +// // console.timeEnd(dataset.url); +// } diff --git a/app/src/scripts/presets/types.d.ts b/app/src/scripts/presets/types.d.ts index 26d0898dc..117362bd5 100644 --- a/app/src/scripts/presets/types.d.ts +++ b/app/src/scripts/presets/types.d.ts @@ -24,7 +24,6 @@ type ApplyPreset = (params: { parentDiv: HTMLDivElement; datasets: Datasets; preset: Preset; - activeResources: Accessor>>; legendSetter: Setter; }) => void; diff --git a/app/src/scripts/utils/colors.ts b/app/src/scripts/utils/colors.ts index 8b7078246..3830d766c 100644 --- a/app/src/scripts/utils/colors.ts +++ b/app/src/scripts/utils/colors.ts @@ -110,6 +110,7 @@ export const convertCandleToVolumeColor = ( export const colors = { white, + black, darkWhite, gray, lightBitcoin: yellow, diff --git a/parser/Cargo.lock b/parser/Cargo.lock index b982f5e06..6547ae56b 100644 --- a/parser/Cargo.lock +++ b/parser/Cargo.lock @@ -1061,12 +1061,12 @@ dependencies = [ [[package]] name = "memory-stats" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34f79cf9964c5c9545493acda1263f1912f8d2c56c8a2ffee2606cb960acaacc" +checksum = "c73f5c649995a115e1a0220b35e4df0a1294500477f97a91d0660fb5abeb574a" dependencies = [ "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1209,9 +1209,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be" dependencies = [ "num-traits", ] @@ -1648,9 +1648,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", diff --git a/parser/Cargo.toml b/parser/Cargo.toml index f00300e65..a64ebe9e1 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -20,12 +20,12 @@ fastrand = "2.1.0" inferno = "0.11.19" itertools = "0.13.0" leveldb = "0.8.6" -memory-stats = "1.1.0" +memory-stats = "1.2.0" nohash = "0.2.0" -ordered-float = "4.2.0" +ordered-float = "4.2.1" par-iter-sync = "0.1.11" rayon = "1.10.0" reqwest = { version = "0.12.5", features = ["blocking", "json"] } sanakirja = "1.4.2" serde = { version = "1.0.203", features = ["derive"] } -serde_json = "1.0.117" +serde_json = "1.0.120" diff --git a/server/Cargo.lock b/server/Cargo.lock index dd469a9dd..fc01ca8fc 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1242,12 +1242,12 @@ dependencies = [ [[package]] name = "memory-stats" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34f79cf9964c5c9545493acda1263f1912f8d2c56c8a2ffee2606cb960acaacc" +checksum = "c73f5c649995a115e1a0220b35e4df0a1294500477f97a91d0660fb5abeb574a" dependencies = [ "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1390,9 +1390,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be" dependencies = [ "num-traits", ] @@ -1601,9 +1601,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -1874,9 +1874,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -2114,9 +2114,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -2133,9 +2133,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", diff --git a/server/Cargo.toml b/server/Cargo.toml index d1ff27616..82776dd0c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -7,12 +7,12 @@ edition = "2021" axum = "0.7.5" color-eyre = "0.6.3" itertools = "0.12.1" -regex = "1.10.4" +regex = "1.10.5" bincode = { git = "https://github.com/bincode-org/bincode.git" } -reqwest = { version = "0.12.4", features = ["json"] } -serde = { version = "1.0.199", features = ["derive"] } -serde_json = { version = "1.0.116" } -tokio = { version = "1.37.0", features = ["full"] } +reqwest = { version = "0.12.5", features = ["json"] } +serde = { version = "1.0.203", features = ["derive"] } +serde_json = { version = "1.0.120" } +tokio = { version = "1.38.0", features = ["full"] } tower-http = { version = "0.5.2", features = ["compression-full"] } parser = { path = "../parser" } derive_deref = "1.1.1"