diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cb282778..b310454d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,27 @@ # Changelog -## v. 0.1.2 | WIP +## v. 0.2.0 | WIP -![Image of the Satonomics Web App version 0.1.2](./assets/v0.1.2.jpg) +![Image of the Satonomics Web App version 0.2.0](./assets/v0.2.0.jpg) ### App - General - - Added a light theme ! -- Performance + - Added a light theme +- Charts - Added height datasets and many optimizations to make them usable - - Added split panes in order to always have the vertical axis visible + - Added split panes in order to have the vertical axis visible for all datasets + - Added min and max values on the charts + - Fixed legend hovering on mobile not resetting on touch end + - Added "3 months" and yearly time scale setters (from year 2009 to today) + - Hide scrollbar of timescale setters and instead added scroll buttons to the legend only visible on desktop + - Improved Share/QR Code screen + - Changed all Area series to Line series + - Fixed horizontal scrollable legend not updating on preset change +- Performance - Improved app's reactivity - Added some chunk splitting for a faster initial load - 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) - - Hide scrollbar of timescale setters - - Changed scroll buttons visibility by screen type (touchscreen or not) instead of screen size - - Added scroll buttons to the legend - - Tweaked scroll buttons background and gradient color from black to stone gray - - Improved Share/QR Code screen - Settings - Finally made a proper component where you can chose the app's theme, between a moving or static background and its text opacity - Misc diff --git a/app/index.html b/app/index.html index ab527b4ee..9e7b83e58 100644 --- a/app/index.html +++ b/app/index.html @@ -5,7 +5,7 @@ Satonomics =16'} cpu: [x64] os: [darwin] @@ -1396,8 +1396,8 @@ packages: dev: true optional: true - /@cloudflare/workerd-darwin-arm64@1.20240620.1: - resolution: {integrity: sha512-3rdND+EHpmCrwYX6hvxIBSBJ0f40tRNxond1Vfw7GiR1MJVi3gragiBx75UDFHCxfRw3J0GZ1qVlkRce2/Xbsg==} + /@cloudflare/workerd-darwin-arm64@1.20240701.0: + resolution: {integrity: sha512-w80ZVAgfH4UwTz7fXZtk7KmS2FzlXniuQm4ku4+cIgRTilBAuKqjpOjwUCbx5g13Gqcm9NuiHce+IDGtobRTIQ==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] @@ -1405,8 +1405,8 @@ packages: dev: true optional: true - /@cloudflare/workerd-linux-64@1.20240620.1: - resolution: {integrity: sha512-tURcTrXGeSbYqeM5ISVcofY20StKbVIcdxjJvNYNZ+qmSV9Fvn+zr7rRE+q64pEloVZfhsEPAlUCnFso5VV4XQ==} + /@cloudflare/workerd-linux-64@1.20240701.0: + resolution: {integrity: sha512-UWLr/Anxwwe/25nGv451MNd2jhREmPt/ws17DJJqTLAx6JxwGWA15MeitAIzl0dbxRFAJa+0+R8ag2WR3F/D6g==} engines: {node: '>=16'} cpu: [x64] os: [linux] @@ -1414,8 +1414,8 @@ packages: dev: true optional: true - /@cloudflare/workerd-linux-arm64@1.20240620.1: - resolution: {integrity: sha512-TThvkwNxaZFKhHZnNjOGqIYCOk05DDWgO+wYMuXg15ymN/KZPnCicRAkuyqiM+R1Fgc4kwe/pehjP8pbmcf6sg==} + /@cloudflare/workerd-linux-arm64@1.20240701.0: + resolution: {integrity: sha512-3kCnF9kYgov1ggpuWbgpXt4stPOIYtVmPCa7MO2xhhA0TWP6JDUHRUOsnmIgKrvDjXuXqlK16cdg3v+EWsaPJg==} engines: {node: '>=16'} cpu: [arm64] os: [linux] @@ -1423,8 +1423,8 @@ packages: dev: true optional: true - /@cloudflare/workerd-windows-64@1.20240620.1: - resolution: {integrity: sha512-Y/BA9Yj0r7Al1HK3nDHcfISgFllw6NR3XMMPChev57vrVT9C9D4erBL3sUBfofHU+2U9L+ShLsl6obBpe3vvUw==} + /@cloudflare/workerd-windows-64@1.20240701.0: + resolution: {integrity: sha512-6IPGITRAeS67j3BH1rN4iwYWDt47SqJG7KlZJ5bB4UaNAia4mvMBSy/p2p4vA89bbXoDRjMtEvRu7Robu6O7hQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -1887,8 +1887,8 @@ packages: - supports-color dev: true - /@iconify-json/tabler@1.1.115: - resolution: {integrity: sha512-nyD8OmtQhBl6FLptfVJe04fjoLIUT3sxe4sEChrXhVDuYQlb1DUPEQQkbwjAIzP4w9JcNYwdUpVbIWn60AjECw==} + /@iconify-json/tabler@1.1.116: + resolution: {integrity: sha512-p+dJ+3L/M2o10REG2lh179Blu5+AA51TFkwuUwY7F+vQsF5Z8DIjyNck3yoBBiCxWqhDhsLzC+p9YO7dWqISmw==} dependencies: '@iconify/types': 2.0.0 dev: true @@ -2385,11 +2385,11 @@ packages: resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} engines: {node: '>=0.4.0'} dependencies: - acorn: 8.12.0 + acorn: 8.12.1 dev: true - /acorn@8.12.0: - resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} + /acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} hasBin: true dev: true @@ -2507,7 +2507,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.23.1 - caniuse-lite: 1.0.30001639 + caniuse-lite: 1.0.30001640 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.1 @@ -2634,10 +2634,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001639 + caniuse-lite: 1.0.30001640 electron-to-chromium: 1.4.816 node-releases: 2.0.14 - update-browserslist-db: 1.0.16(browserslist@4.23.1) + update-browserslist-db: 1.1.0(browserslist@4.23.1) dev: true /buffer-crc32@0.2.13: @@ -2694,8 +2694,8 @@ packages: engines: {node: '>=6'} dev: true - /caniuse-lite@1.0.30001639: - resolution: {integrity: sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==} + /caniuse-lite@1.0.30001640: + resolution: {integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==} dev: true /capnp-ts@0.7.0: @@ -4104,7 +4104,7 @@ packages: engines: {node: '>=14'} dependencies: mlly: 1.7.1 - pkg-types: 1.1.2 + pkg-types: 1.1.3 dev: true /locate-path@5.0.0: @@ -4255,21 +4255,21 @@ packages: engines: {node: '>=4'} dev: true - /miniflare@3.20240620.0: - resolution: {integrity: sha512-NBMzqUE2mMlh/hIdt6U5MP+aFhEjKDq3l8CAajXAQa1WkndJdciWvzB2mfLETwoVFhMl/lphaVzyEN2AgwJpbQ==} + /miniflare@3.20240701.0: + resolution: {integrity: sha512-m9+I+7JNyqDGftCMKp9cK9pCZkK72hAL2mM9IWwhct+ZmucLBA8Uu6+rHQqA5iod86cpwOkrB2PrPA3wx9YNgw==} engines: {node: '>=16.13'} hasBin: true dependencies: '@cspotcode/source-map-support': 0.8.1 - acorn: 8.12.0 + acorn: 8.12.1 acorn-walk: 8.3.3 capnp-ts: 0.7.0 exit-hook: 2.2.1 glob-to-regexp: 0.4.1 stoppable: 1.1.0 undici: 5.28.4 - workerd: 1.20240620.1 - ws: 8.17.1 + workerd: 1.20240701.0 + ws: 8.18.0 youch: 3.3.3 zod: 3.23.8 transitivePeerDependencies: @@ -4326,9 +4326,9 @@ packages: /mlly@1.7.1: resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} dependencies: - acorn: 8.12.0 + acorn: 8.12.1 pathe: 1.1.2 - pkg-types: 1.1.2 + pkg-types: 1.1.3 ufo: 1.5.3 dev: true @@ -4611,8 +4611,8 @@ packages: find-up: 4.1.0 dev: true - /pkg-types@1.1.2: - resolution: {integrity: sha512-VEGf1he2DR5yowYRl0XJhWJq5ktm9gYIsH+y8sNJpHlxch7JPDaufgrsl4vYjd9hMUY8QVjoNncKbow9I7exyA==} + /pkg-types@1.1.3: + resolution: {integrity: sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==} dependencies: confbox: 0.1.7 mlly: 1.7.1 @@ -5524,7 +5524,7 @@ packages: hasBin: true dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.12.0 + acorn: 8.12.1 commander: 2.20.3 source-map-support: 0.5.21 dev: true @@ -5725,7 +5725,7 @@ packages: resolution: {integrity: sha512-91mxcZTadgXyj3lFWmrGT8GyoRHWuE5fqPOjg5RVtF6vj+OfM5G6WCzXjuYtSgELE5ggB34RY4oiCSEP8I3AHw==} dependencies: '@rollup/pluginutils': 5.1.0(rollup@2.79.1) - acorn: 8.12.0 + acorn: 8.12.1 escape-string-regexp: 5.0.0 estree-walker: 3.0.3 fast-glob: 3.3.2 @@ -5733,7 +5733,7 @@ packages: magic-string: 0.30.10 mlly: 1.7.1 pathe: 1.1.2 - pkg-types: 1.1.2 + pkg-types: 1.1.3 scule: 1.3.0 strip-literal: 2.1.0 unplugin: 1.11.0 @@ -5812,7 +5812,7 @@ packages: resolution: {integrity: sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==} engines: {node: '>=14.0.0'} dependencies: - acorn: 8.12.0 + acorn: 8.12.1 chokidar: 3.6.0 webpack-sources: 3.2.3 webpack-virtual-modules: 0.6.2 @@ -5823,8 +5823,8 @@ packages: engines: {node: '>=4'} dev: true - /update-browserslist-db@1.0.16(browserslist@4.23.1): - resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} + /update-browserslist-db@1.1.0(browserslist@4.23.1): + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -5855,7 +5855,7 @@ packages: spdx-expression-parse: 3.0.1 dev: true - /vite-plugin-pwa@0.20.0(vite@5.3.2)(workbox-build@7.1.1)(workbox-window@7.1.0): + /vite-plugin-pwa@0.20.0(vite@5.3.3)(workbox-build@7.1.1)(workbox-window@7.1.0): resolution: {integrity: sha512-/kDZyqF8KqoXRpMUQtR5Atri/7BWayW8Gp7Kz/4bfstsV6zSFTxjREbXZYL7zSuRL40HGA+o2hvUAFRmC+bL7g==} engines: {node: '>=16.0.0'} peerDependencies: @@ -5870,14 +5870,14 @@ packages: debug: 4.3.5 fast-glob: 3.3.2 pretty-bytes: 6.1.1 - vite: 5.3.2 + vite: 5.3.3 workbox-build: 7.1.1 workbox-window: 7.1.0 transitivePeerDependencies: - supports-color dev: true - /vite-plugin-solid@2.10.2(solid-js@1.8.18)(vite@5.3.2): + /vite-plugin-solid@2.10.2(solid-js@1.8.18)(vite@5.3.3): resolution: {integrity: sha512-AOEtwMe2baBSXMXdo+BUwECC8IFHcKS6WQV/1NEd+Q7vHPap5fmIhLcAzr+DUJ04/KHx/1UBU0l1/GWP+rMAPQ==} peerDependencies: '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* @@ -5893,14 +5893,14 @@ packages: merge-anything: 5.1.7 solid-js: 1.8.18 solid-refresh: 0.6.3(solid-js@1.8.18) - vite: 5.3.2 - vitefu: 0.2.5(vite@5.3.2) + vite: 5.3.3 + vitefu: 0.2.5(vite@5.3.3) transitivePeerDependencies: - supports-color dev: true - /vite@5.3.2: - resolution: {integrity: sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==} + /vite@5.3.3: + resolution: {integrity: sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -5934,7 +5934,7 @@ packages: fsevents: 2.3.3 dev: true - /vitefu@0.2.5(vite@5.3.2): + /vitefu@0.2.5(vite@5.3.3): resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} peerDependencies: vite: ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -5942,7 +5942,7 @@ packages: vite: optional: true dependencies: - vite: 5.3.2 + vite: 5.3.3 dev: true /webidl-conversions@3.0.1: @@ -6152,21 +6152,21 @@ packages: workbox-core: 7.1.0 dev: true - /workerd@1.20240620.1: - resolution: {integrity: sha512-Qoq+RrFNk4pvEO+kpJVn8uJ5TRE9YJx5jX5pC5LjdKlw1XeD8EdXt5k0TbByvWunZ4qgYIcF9lnVxhcDFo203g==} + /workerd@1.20240701.0: + resolution: {integrity: sha512-qSgNVqauqzNCij9MaJLF2c2ko3AnFioVSIxMSryGbRK+LvtGr9BKBt6JOxCb24DoJASoJDx3pe3DJHBVydUiBg==} engines: {node: '>=16'} hasBin: true requiresBuild: true optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20240620.1 - '@cloudflare/workerd-darwin-arm64': 1.20240620.1 - '@cloudflare/workerd-linux-64': 1.20240620.1 - '@cloudflare/workerd-linux-arm64': 1.20240620.1 - '@cloudflare/workerd-windows-64': 1.20240620.1 + '@cloudflare/workerd-darwin-64': 1.20240701.0 + '@cloudflare/workerd-darwin-arm64': 1.20240701.0 + '@cloudflare/workerd-linux-64': 1.20240701.0 + '@cloudflare/workerd-linux-arm64': 1.20240701.0 + '@cloudflare/workerd-windows-64': 1.20240701.0 dev: true - /wrangler@3.62.0: - resolution: {integrity: sha512-TM1Bd8+GzxFw/JzwsC3i/Oss4LTWvIEWXXo1vZhx+7PHcsxdbnQGBBwPurHNJDSu2Pw22+2pCZiUGKexmgJksw==} + /wrangler@3.63.1: + resolution: {integrity: sha512-fxMPNEyDc9pZNtQOuYqRikzv6lL5eP4S1zv7L/kw24uu1cCEmJ39j8bfJGzrAEqKDNsiFXVjEka0RjlpgEVWPg==} engines: {node: '>=16.17.0'} hasBin: true peerDependencies: @@ -6182,7 +6182,7 @@ packages: chokidar: 3.6.0 date-fns: 3.6.0 esbuild: 0.17.19 - miniflare: 3.20240620.0 + miniflare: 3.20240701.0 nanoid: 3.3.7 path-to-regexp: 6.2.2 resolve: 1.22.8 @@ -6221,8 +6221,8 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true - /ws@8.17.1: - resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + /ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 diff --git a/app/public/manifest.webmanifest b/app/public/manifest.webmanifest index db99fbfb2..ef4536323 100644 --- a/app/public/manifest.webmanifest +++ b/app/public/manifest.webmanifest @@ -1,7 +1,7 @@ { "name": "Satonomics", "short_name": "Satonomics", - "description": "Satoshi Economics", + "description": "A better, FOSS, Bitcoin-only, self-hostable Glassnode", "start_url": "/", "display": "standalone", "theme_color": "#0c0a09", diff --git a/app/src/app/components/background.tsx b/app/src/app/components/background.tsx index 1b7da9bd4..936aa7ea4 100644 --- a/app/src/app/components/background.tsx +++ b/app/src/app/components/background.tsx @@ -14,7 +14,7 @@ const texts = [ "hodl", `don't trust, verify`, "zap", - "bitcoin", + "â‚¿itcoin", "lightning", "nostr", "freedom tech", diff --git a/app/src/app/components/frames/chart/components/actions.tsx b/app/src/app/components/frames/chart/components/actions.tsx index 0c028d001..449e85668 100644 --- a/app/src/app/components/frames/chart/components/actions.tsx +++ b/app/src/app/components/frames/chart/components/actions.tsx @@ -1,6 +1,3 @@ -import { chartState } from "/src/scripts/lightweightCharts/chart/state"; -import { setTimeScale } from "/src/scripts/lightweightCharts/chart/time"; - import { Button } from "./button"; export function Actions({ @@ -28,11 +25,7 @@ export function Actions({ : IconTablerLayoutSidebarRightExpand } onClick={() => { - const range = chartState.range; - fullscreen().set((b) => !b); - - setTimeScale(range); }} classes="hidden md:block" /> diff --git a/app/src/app/components/frames/chart/components/chart.tsx b/app/src/app/components/frames/chart/components/chart.tsx index f828d22ab..09a11aee7 100644 --- a/app/src/app/components/frames/chart/components/chart.tsx +++ b/app/src/app/components/frames/chart/components/chart.tsx @@ -1,22 +1,52 @@ +import { createRWS } from "/src/solid/rws"; + export function Chart({ charts, parentDiv, presets, datasets, legendSetter, + dark: _dark, + activeRange, }: { charts: RWS; parentDiv: RWS; presets: Presets; datasets: Datasets; - legendSetter: Setter; + legendSetter: Setter; + dark: Accessor; + activeRange: RWS; }) { + const wasIdle = createRWS(false); + + if ("requestIdleCallback" in window) { + const idleCallback = requestIdleCallback(() => { + console.log("idle"); + wasIdle.set(true); + cancelIdleCallback(idleCallback); + }); + + onCleanup(() => { + cancelIdleCallback(idleCallback); + }); + } else { + const timeout = setTimeout(() => { + console.log("timeout"); + wasIdle.set(true); + }, 500); + + onCleanup(() => { + clearTimeout(timeout); + }); + } + onMount(() => { createEffect(() => { const preset = presets.selected(); const div = parentDiv(); + const dark = _dark(); - if (!div) return; + if (!wasIdle() || !div) return; untrack(() => { try { @@ -27,6 +57,8 @@ export function Chart({ datasets, preset, legendSetter, + dark, + activeRange, }); } catch (error) { console.error("chart: render: failed", error); diff --git a/app/src/app/components/frames/chart/components/legend.tsx b/app/src/app/components/frames/chart/components/legend.tsx index 8d417de22..115ef4740 100644 --- a/app/src/app/components/frames/chart/components/legend.tsx +++ b/app/src/app/components/frames/chart/components/legend.tsx @@ -1,3 +1,4 @@ +import { chunkIdToIndex } from "/src/scripts/datasets/resource"; import { createRWS } from "/src/solid/rws"; import { Scrollable } from "../../scrollable"; @@ -5,57 +6,90 @@ import { Scrollable } from "../../scrollable"; const transparency = "44"; export function Legend({ + scale, legend: legendList, + activeRange, }: { - legend: Accessor; + scale: Accessor; + legend: Accessor; + activeRange: Accessor; }) { - const hovering = createRWS(undefined); + const hovered = createRWS(undefined); let toggle = false; return ( - + {(legend) => { - const initialColors = {} as any; - const darkenColors = {} as any; - - Object.entries(legend.series.options()).forEach(([k, v]) => { - if (k.toLowerCase().includes("color") && v) { - initialColors[k] = v; - darkenColors[k] = `${v}${transparency}`; - } else if (k === "lastValueVisible" && v) { - initialColors[k] = v; - darkenColors[k] = !v; - } - }); - createEffect(() => { - if (hovering()) { - if (hovering()?.title !== legend.title) { - legend.series.applyOptions(darkenColors); - } - } else { - legend.series.applyOptions(initialColors); + const range = activeRange(); + + for (let i = 0; i < range.length; i++) { + const id = range[i]; + + const initialColors = {} as any; + const darkenColors = {} as any; + + const chunkIndex = chunkIdToIndex(scale(), id); + + const series = legend.seriesList.at(chunkIndex)?.(); + + if (!series) return; + + const seriesOptions = series.options(); + + if (!seriesOptions) continue; + + Object.entries(seriesOptions).forEach(([k, v]) => { + if (k.toLowerCase().includes("color") && v) { + if (typeof v === "string" && !v.startsWith("#")) { + return; + } + + v = (v as string).substring(0, 7); + initialColors[k] = v; + darkenColors[k] = `${v}${transparency}`; + } else if (k === "lastValueVisible" && v) { + initialColors[k] = true; + darkenColors[k] = false; + } + }); + + createEffect((wasHovering: boolean) => { + const hoveredLegend = hovered(); + const hovering = !!hovered(); + + if (wasHovering === hovering) { + return hovering; + } + + if (hoveredLegend) { + if (hoveredLegend.title !== legend.title) { + series.applyOptions(darkenColors); + } + } else { + series.applyOptions(initialColors); + } + + return hovering; + }, false); } }); - let previousClickValueOf: number = 0; + let previousClickTime: number = 0; return ( - - - - - - - - - - index + 2009) - .reverse()} - > - {(year) => ( + + - )} - + + + + + + + + + + index + 2009) + .reverse()} + > + {(year) => ( + + )} + + + + + + [i, i + 0.5]) + .reverse()} + > + {(i) => ( + + )} + + + ); @@ -122,29 +180,46 @@ function Button({ function setTimeScale({ charts, + scale, days, year, + range, }: { charts: RWS; + scale: ResourceScale; days?: number; year?: number; + range?: { from: number; to: number }; }) { - let from = new Date(); - let to = new Date(); + if (scale === "date") { + let from = new Date(); + let to = new Date(); - if (year) { - from = new Date(`${year}-01-01`); - to = new Date(`${year}-12-31`); - } else if (days) { - from.setDate(from.getUTCDate() - days); - } else { - from = new Date(GENESIS_DAY); + if (year) { + from = new Date(`${year}-01-01`); + to = new Date(`${year}-12-31`); + } else if (days) { + from.setDate(from.getUTCDate() - days); + } else { + from = new Date(GENESIS_DAY); + } + + charts() + .at(0) + ?.timeScale() + .setVisibleRange({ + from: (from.getTime() / 1000) as Time, + to: (to.getTime() / 1000) as Time, + }); + } else if (scale === "height") { + if (range) { + charts() + .at(0) + ?.timeScale() + .setVisibleRange({ + from: range.from as Time, + to: range.to as Time, + }); + } } - - charts()[0] - .timeScale() - .setVisibleRange({ - from: (from.getTime() / 1000) as Time, - to: (to.getTime() / 1000) as Time, - }); } diff --git a/app/src/app/components/frames/chart/index.tsx b/app/src/app/components/frames/chart/index.tsx index 346f474aa..274c1dac3 100644 --- a/app/src/app/components/frames/chart/index.tsx +++ b/app/src/app/components/frames/chart/index.tsx @@ -3,7 +3,6 @@ import { createRWS } from "/src/solid/rws"; import { Box } from "../box"; import { Actions } from "./components/actions"; -import { Chart } from "./components/chart"; import { Legend } from "./components/legend"; import { TimeScale } from "./components/timeScale"; import { Title } from "./components/title"; @@ -15,20 +14,26 @@ export function ChartFrame({ qrcode, standalone, fullscreen, + dark, }: { presets: Presets; hide?: Accessor; qrcode: RWS; datasets: Datasets; fullscreen?: RWS; + dark: Accessor; standalone: boolean; }) { - const legend = createRWS([]); + const legend = createRWS([]); const charts = createRWS([]); const div = createRWS(undefined); + const scale = createMemo(() => presets.selected().scale); + + const activeRange = createRWS([] as number[], { equals: false }); + const Chart = lazy(() => import("./components/chart").then((d) => ({ default: d.Chart })), ); @@ -50,7 +55,7 @@ export function ChartFrame({
- +
@@ -65,10 +70,12 @@ export function ChartFrame({ datasets={datasets} legendSetter={legend.set} presets={presets} + dark={dark} + activeRange={activeRange} />
- +
); } diff --git a/app/src/app/components/frames/folders/index.tsx b/app/src/app/components/frames/folders/index.tsx index cb2da1373..23032637f 100644 --- a/app/src/app/components/frames/folders/index.tsx +++ b/app/src/app/components/frames/folders/index.tsx @@ -31,7 +31,7 @@ export function FoldersFrame({
- presets.list.length} /> presets organized in a + presets.list.length} /> charts organized in a tree like structure.
diff --git a/app/src/app/components/frames/history.tsx b/app/src/app/components/frames/history.tsx index 0ac785e47..f1a9df549 100644 --- a/app/src/app/components/frames/history.tsx +++ b/app/src/app/components/frames/history.tsx @@ -18,7 +18,7 @@ export function HistoryFrame({ }} >
-
List of previously visited presets.
+
List of previously visited charts.
el.scrollWidth > el.clientWidth); - - checkArrows(); + checkScrollable(); }); }); + function checkScrollable() { + const div = maybeScrollable(); + + if (div) { + scrollable.set(() => div.scrollWidth > div.clientWidth); + } + + checkArrows(); + } + function checkArrows() { const target = maybeScrollable()!; @@ -38,6 +46,9 @@ export function Scrollable({ showRightArrow.set(() => right > 0); } + // @ts-ignore + createEffect(on(children, checkScrollable)); + return (
; backgroundOpacity: SL<{ text: string; value: number }>; }) { - createEffect(() => { - if ( - appTheme.selected() === "Dark" || - (appTheme.selected() === "System" && - window.matchMedia("(prefers-color-scheme: dark)").matches) - ) { - document.documentElement.classList.add("dark"); - } else { - document.documentElement.classList.remove("dark"); - } - }); - return (
diff --git a/app/src/app/index.tsx b/app/src/app/index.tsx index 1d38cb3d9..5e222ccd5 100644 --- a/app/src/app/index.tsx +++ b/app/src/app/index.tsx @@ -2,8 +2,6 @@ import { createRWS } from "/src/solid/rws"; import { standalone } from "../env"; import { createDatasets } from "../scripts/datasets"; -import { chartState } from "../scripts/lightweightCharts/chart/state"; -import { setTimeScale } from "../scripts/lightweightCharts/chart/time"; import { createPresets } from "../scripts/presets"; import { createSL } from "../scripts/utils/selectableList/static"; import { sleep } from "../scripts/utils/sleep"; @@ -41,6 +39,22 @@ export function App() { defaultIndex: 0, }); + const dark = createRWS(false); + + createEffect(() => { + if ( + appTheme.selected() === "Dark" || + (appTheme.selected() === "System" && + window.matchMedia("(prefers-color-scheme: dark)").matches) + ) { + dark.set(true); + document.documentElement.classList.add("dark"); + } else { + dark.set(false); + document.documentElement.classList.remove("dark"); + } + }); + const backgroundMode = createSL(["Scroll", "Static"] as const, { saveable: { key: "bg-mode", @@ -222,8 +236,6 @@ export function App() { windowWidth60p(), ), ); - - setTimeScale(resizeInitialRange()); } }} onMouseUp={() => resizingBarStart.set(undefined)} @@ -267,6 +279,7 @@ export function App() { qrcode={qrcode} standalone={false} datasets={datasets} + dark={dark} /> @@ -303,16 +316,12 @@ export function App() { diff --git a/app/src/scripts/datasets/consts/types.d.ts b/app/src/scripts/datasets/consts/types.d.ts index a9357aeeb..3a38ac7ae 100644 --- a/app/src/scripts/datasets/consts/types.d.ts +++ b/app/src/scripts/datasets/consts/types.d.ts @@ -9,7 +9,10 @@ type AddressCohortKeySplitByLiquidity = `${LiquidityKey}_${AddressCohortKey}`; type AnyCohortKey = AgeCohortKey | AddressCohortKey; -type AnyPossibleCohortKey = AnyCohortKey | AddressCohortKeySplitByLiquidity; +type AnyPossibleCohortKey = + | AnyCohortKey + | AddressCohortKeySplitByLiquidity + | LiquidityKey; type AverageName = (typeof import("./averages").averages)[number]["key"]; diff --git a/app/src/scripts/datasets/resource.ts b/app/src/scripts/datasets/resource.ts index 1c42a1efa..7751ed11e 100644 --- a/app/src/scripts/datasets/resource.ts +++ b/app/src/scripts/datasets/resource.ts @@ -1,12 +1,7 @@ -import { - ONE_DAY_IN_MS, - ONE_HOUR_IN_MS, - ONE_MINUTE_IN_MS, -} from "/src/scripts/utils/time"; +import { ONE_HOUR_IN_MS, ONE_MINUTE_IN_MS } from "/src/scripts/utils/time"; import { createRWS } from "/src/solid/rws"; import { HEIGHT_CHUNK_SIZE } from "."; -import { debounce } from "../utils/debounce"; export function createResourceDataset< Scale extends ResourceScale, @@ -49,21 +44,24 @@ export function createResourceDataset< const chunkId = json()?.chunk.id!; if (Array.isArray(map)) { - return map.map( - (value, index) => - ({ - number: chunkId + index, - time: (chunkId + index) as Time, - ...(typeof value !== "number" && value !== null - ? { ...(value as OHLC), value: value.close } - : { value: value === null ? NaN : (value as number) }), - }) as any as Value, - ); + const values = new Array(map.length); + + for (let i = 0; i < map.length; i++) { + const value = map[i]; + + values[i] = { + time: (chunkId + i) as Time, + ...(typeof value !== "number" && value !== null + ? { ...(value as OHLC), value: value.close } + : { value: value === null ? NaN : (value as number) }), + } as any as Value; + } + + return values; } else { return Object.entries(map).map( ([date, value]) => ({ - number: new Date(date).valueOf() / ONE_DAY_IN_MS, time: date, ...(typeof value !== "number" && value !== null ? { ...(value as OHLC), value: value.close } @@ -89,10 +87,18 @@ export function createResourceDataset< const fetched = fetchedJSONs.at(index); + if (scale === "height" && index > 0) { + const length = fetchedJSONs.at(index - 1)?.vec()?.length; + + if (length !== undefined && length < HEIGHT_CHUNK_SIZE) { + return; + } + } + if (!fetched || fetched.loading) { return; } else if (fetched.at) { - const diff = new Date().valueOf() - fetched.at.valueOf(); + const diff = new Date().getTime() - fetched.at.getTime(); if ( diff < ONE_MINUTE_IN_MS || @@ -127,6 +133,11 @@ export function createResourceDataset< } catch {} } + if (!navigator.onLine) { + fetched.loading = false; + return; + } + try { const fetchedResponse = await fetch(urlWithQuery); @@ -153,7 +164,7 @@ export function createResourceDataset< return; } - if (previousLength && previousLength <= newLength) { + if (previousLength && previousLength === newLength) { const previousLastValue = Object.values(previousMap || []).at(-1); const newLastValue = Object.values(newMap).at(-1); @@ -195,43 +206,11 @@ 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, drop() { fetchedJSONs.forEach((fetched) => { fetched.at = null; @@ -254,6 +233,6 @@ async function convertResponseToJSON< } } -function chunkIdToIndex(scale: ResourceScale, id: number) { +export 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 5230cfbfc..d1f8ac587 100644 --- a/app/src/scripts/datasets/types.d.ts +++ b/app/src/scripts/datasets/types.d.ts @@ -6,7 +6,7 @@ type AnyDatasets = DateDatasets | HeightDatasets; type ResourceScale = (typeof import("./index").scales)[index]; -type DatasetValue = T & Numbered & Valued; +type DatasetValue = T & Valued; interface ResourceDataset< Scale extends ResourceScale, @@ -24,7 +24,6 @@ interface ResourceDataset< url: string; fetch: (id: number) => void; fetchedJSONs: FetchedResult[]; - values: Accessor[]>; drop: VoidFunction; } diff --git a/app/src/scripts/lightweightCharts/chart/create.ts b/app/src/scripts/lightweightCharts/chart/create.ts index 7c943c2b6..2b4c94402 100644 --- a/app/src/scripts/lightweightCharts/chart/create.ts +++ b/app/src/scripts/lightweightCharts/chart/create.ts @@ -11,11 +11,20 @@ import { HorzScaleBehaviorHeight } from "./horzScaleBehavior"; export function createChart( scale: ResourceScale, element: HTMLElement, - priceScaleOptions?: DeepPartialPriceScaleOptions, + { + dark, + priceScaleOptions, + }: { + dark: boolean; + priceScaleOptions: DeepPartialPriceScaleOptions; + }, ) { console.log(`chart: create (scale: ${scale})`); - const { white } = colors; + const { white, black } = colors; + + const textColor = dark ? white : black; + const borderColor = dark ? "#332F24" : "#F1E4E0"; const options: DeepPartialChartOptions = { autoSize: true, @@ -23,18 +32,18 @@ export function createChart( fontFamily: "Lexend", background: { color: "transparent" }, fontSize: 14, - textColor: white, + textColor, }, grid: { vertLines: { visible: false }, horzLines: { visible: false }, }, rightPriceScale: { - borderColor: "#332F24", + borderColor, }, timeScale: { - borderColor: "#332F24", - minBarSpacing: scale === "date" ? 0.05 : 0.005, + borderColor, + minBarSpacing: 0.05, shiftVisibleRangeOnNewBar: false, allowShiftVisibleRangeOnWhitespaceReplacement: false, }, @@ -46,12 +55,12 @@ export function createChart( crosshair: { mode: CrosshairMode.Normal, horzLine: { - color: white, - labelBackgroundColor: white, + color: textColor, + labelBackgroundColor: textColor, }, vertLine: { - color: white, - labelBackgroundColor: white, + color: textColor, + labelBackgroundColor: textColor, }, }, localization: { diff --git a/app/src/scripts/lightweightCharts/chart/markers.ts b/app/src/scripts/lightweightCharts/chart/markers.ts index 6c1d5500d..e27309c23 100644 --- a/app/src/scripts/lightweightCharts/chart/markers.ts +++ b/app/src/scripts/lightweightCharts/chart/markers.ts @@ -1,121 +1,128 @@ import { colors } from "/src/scripts/utils/colors"; -import { ONE_DAY_IN_MS } from "/src/scripts/utils/time"; -import { chartState } from "./state"; -import { GENESIS_DAY } from "./whitespace"; +import { chunkIdToIndex } from "../../datasets/resource"; -export const setMinMaxMarkers = ({ +export function setMinMaxMarkers({ scale, - candlesticks, - range, - lowerOpacity, + visibleRange, + legendList, + activeRange, }: { scale: ResourceScale; - candlesticks: DatasetValue[]; - range: TimeRange; - lowerOpacity: boolean; -}) => { - const first = candlesticks.at(0); + visibleRange: TimeRange | undefined; + legendList: SeriesLegend[]; + activeRange: Accessor; +}) { + if (!visibleRange) return; - if (!first) return; + const { from, to } = visibleRange; - const offset = - scale === "date" - ? first.number - new Date(GENESIS_DAY).valueOf() / ONE_DAY_IN_MS - : 0; + const dateFrom = new Date(from as string); + const dateTo = new Date(to as string); - const slicedDataList = range - ? candlesticks.slice( - Math.ceil(range.from - offset < 0 ? 0 : range.from - offset), - Math.floor(range.to - offset) + 1, - ) - : []; + let max = undefined as [number, Time, number, ISeriesApi] | undefined; + let min = undefined as [number, Time, number, ISeriesApi] | undefined; - const series = chartState.priceSeries; + legendList.forEach(({ seriesList, dataset }) => { + activeRange().forEach((id) => { + const seriesIndex = chunkIdToIndex(scale, id); - if (!series) return; + const series = seriesList.at(seriesIndex)?.(); - if (slicedDataList.length) { - const markers: (SeriesMarker