mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-11 15:33:33 -07:00
general: snapshot
This commit is contained in:
+13
-13
@@ -1,27 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## v. 0.1.2 | WIP
|
||||
## v. 0.2.0 | WIP
|
||||
|
||||

|
||||

|
||||
|
||||
### 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
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<title>Satonomics</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="An app to visualize Bitcoin on-chain data"
|
||||
content="A better, FOSS, Bitcoin-only, self-hostable Glassnode"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "satonomics",
|
||||
"description": "Satoshi Economics",
|
||||
"version": "0.1.1",
|
||||
"description": "A better, FOSS, Bitcoin-only, self-hostable Glassnode",
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@iconify-json/tabler": "^1.1.115",
|
||||
"@iconify-json/tabler": "^1.1.116",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.39",
|
||||
@@ -38,10 +38,10 @@
|
||||
"typescript": "^5.5.3",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-icons": "^0.19.0",
|
||||
"vite": "^5.3.2",
|
||||
"vite": "^5.3.3",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vite-plugin-solid": "^2.10.2",
|
||||
"workbox-window": "^7.1.0",
|
||||
"wrangler": "^3.62.0"
|
||||
"wrangler": "^3.63.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+65
-65
@@ -32,8 +32,8 @@ devDependencies:
|
||||
specifier: ^4.3.0
|
||||
version: 4.3.0(prettier@3.3.2)
|
||||
'@iconify-json/tabler':
|
||||
specifier: ^1.1.115
|
||||
version: 1.1.115
|
||||
specifier: ^1.1.116
|
||||
version: 1.1.116
|
||||
'@tailwindcss/container-queries':
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1(tailwindcss@3.4.4)
|
||||
@@ -68,20 +68,20 @@ devDependencies:
|
||||
specifier: ^0.19.0
|
||||
version: 0.19.0
|
||||
vite:
|
||||
specifier: ^5.3.2
|
||||
version: 5.3.2
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3
|
||||
vite-plugin-pwa:
|
||||
specifier: ^0.20.0
|
||||
version: 0.20.0(vite@5.3.2)(workbox-build@7.1.1)(workbox-window@7.1.0)
|
||||
version: 0.20.0(vite@5.3.3)(workbox-build@7.1.1)(workbox-window@7.1.0)
|
||||
vite-plugin-solid:
|
||||
specifier: ^2.10.2
|
||||
version: 2.10.2(solid-js@1.8.18)(vite@5.3.2)
|
||||
version: 2.10.2(solid-js@1.8.18)(vite@5.3.3)
|
||||
workbox-window:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
wrangler:
|
||||
specifier: ^3.62.0
|
||||
version: 3.62.0
|
||||
specifier: ^3.63.1
|
||||
version: 3.63.1
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1387,8 +1387,8 @@ packages:
|
||||
mime: 3.0.0
|
||||
dev: true
|
||||
|
||||
/@cloudflare/workerd-darwin-64@1.20240620.1:
|
||||
resolution: {integrity: sha512-YWeS2aE8jAzDefuus/3GmZcFGu3Ef94uCAoxsQuaEXNsiGM9NeAhPpKC1BJAlcv168U/Q1J+6hckcGtipf6ZcQ==}
|
||||
/@cloudflare/workerd-darwin-64@1.20240701.0:
|
||||
resolution: {integrity: sha512-XAZa4ZP+qyTn6JQQACCPH09hGZXP2lTnWKkmg5mPwT8EyRzCKLkczAf98vPP5bq7JZD/zORdFWRY0dOTap8zTQ==}
|
||||
engines: {node: '>=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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -14,7 +14,7 @@ const texts = [
|
||||
"hodl",
|
||||
`don't trust, verify`,
|
||||
"zap",
|
||||
"bitcoin",
|
||||
"₿itcoin",
|
||||
"lightning",
|
||||
"nostr",
|
||||
"freedom tech",
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -1,22 +1,52 @@
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
export function Chart({
|
||||
charts,
|
||||
parentDiv,
|
||||
presets,
|
||||
datasets,
|
||||
legendSetter,
|
||||
dark: _dark,
|
||||
activeRange,
|
||||
}: {
|
||||
charts: RWS<IChartApi[]>;
|
||||
parentDiv: RWS<HTMLDivElement | undefined>;
|
||||
presets: Presets;
|
||||
datasets: Datasets;
|
||||
legendSetter: Setter<PresetLegend>;
|
||||
legendSetter: Setter<SeriesLegend[]>;
|
||||
dark: Accessor<boolean>;
|
||||
activeRange: RWS<number[]>;
|
||||
}) {
|
||||
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);
|
||||
|
||||
@@ -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<PresetLegend>;
|
||||
scale: Accessor<ResourceScale>;
|
||||
legend: Accessor<SeriesLegend[]>;
|
||||
activeRange: Accessor<number[]>;
|
||||
}) {
|
||||
const hovering = createRWS<SeriesLegend | undefined>(undefined);
|
||||
const hovered = createRWS<SeriesLegend | undefined>(undefined);
|
||||
|
||||
let toggle = false;
|
||||
|
||||
return (
|
||||
<Scrollable classes="flex flex-1 items-center gap-1 p-1.5">
|
||||
<Scrollable classes="items-center gap-1 p-1.5">
|
||||
<For each={legendList()}>
|
||||
{(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 (
|
||||
<Show when={!legend.disabled()}>
|
||||
<button
|
||||
onMouseEnter={() => {
|
||||
if (legend.visible()) {
|
||||
hovering.set(legend);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => hovering.set(undefined)}
|
||||
onTouchEnd={() => hovering.set(undefined)}
|
||||
onMouseEnter={() => legend.visible() && hovered.set(legend)}
|
||||
onMouseLeave={() => hovered.set(undefined)}
|
||||
onTouchStart={() => legend.visible() && hovered.set(legend)}
|
||||
onTouchEnd={() => hovered.set(undefined)}
|
||||
onClick={() => {
|
||||
const currentClickValueOf = new Date().valueOf();
|
||||
const currentClickTime = new Date().getTime();
|
||||
|
||||
if (currentClickValueOf - previousClickValueOf > 300) {
|
||||
if (currentClickTime - previousClickTime > 300) {
|
||||
legend.visible.set((visible) => !visible);
|
||||
} else {
|
||||
legendList().forEach((_legend) => {
|
||||
@@ -69,12 +103,12 @@ export function Legend({
|
||||
toggle = !toggle;
|
||||
}
|
||||
|
||||
previousClickValueOf = currentClickValueOf;
|
||||
previousClickTime = currentClickTime;
|
||||
|
||||
if (legend.visible()) {
|
||||
hovering.set(legend);
|
||||
hovered.set(legend);
|
||||
} else {
|
||||
hovering.set(undefined);
|
||||
hovered.set(undefined);
|
||||
}
|
||||
}}
|
||||
class="flex flex-none items-center space-x-1.5 rounded-full py-1.5 pl-2 pr-2.5 hover:bg-orange-800/20 active:scale-[0.975] dark:hover:bg-orange-200/20"
|
||||
@@ -113,14 +147,13 @@ export function Legend({
|
||||
>
|
||||
{legend.title}
|
||||
</span>
|
||||
<Show when={legend.url}>
|
||||
<Show when={legend.dataset.url}>
|
||||
{(url) => (
|
||||
<a
|
||||
title="Dataset"
|
||||
class="border-superlight -my-0.5 !-mr-1 inline-flex size-6 flex-col overflow-hidden rounded-full border bg-white bg-opacity-5 p-1 pl-0.5 hover:bg-opacity-50 dark:bg-orange-200 dark:bg-opacity-5 dark:hover:bg-opacity-25"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
// event.preventDefault();
|
||||
}}
|
||||
href={url()}
|
||||
target={
|
||||
|
||||
@@ -5,7 +5,13 @@ import { classPropToString } from "/src/solid/classes";
|
||||
import { Box } from "../../box";
|
||||
import { Scrollable } from "../../scrollable";
|
||||
|
||||
export function TimeScale({ charts }: { charts: RWS<IChartApi[]> }) {
|
||||
export function TimeScale({
|
||||
scale,
|
||||
charts,
|
||||
}: {
|
||||
scale: Accessor<ResourceScale>;
|
||||
charts: RWS<IChartApi[]>;
|
||||
}) {
|
||||
const today = new Date();
|
||||
|
||||
const disabled = createMemo(() => charts().length === 0);
|
||||
@@ -13,89 +19,141 @@ export function TimeScale({ charts }: { charts: RWS<IChartApi[]> }) {
|
||||
return (
|
||||
<Box dark padded={false} classes="short:hidden">
|
||||
<Scrollable classes="p-1.5 space-x-2">
|
||||
<Button disabled={disabled} onClick={() => setTimeScale({ charts })}>
|
||||
All Time
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ charts, days: 7 })}
|
||||
>
|
||||
1 Week
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ charts, days: 30 })}
|
||||
>
|
||||
1 Month
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ charts, days: 3 * 30 })}
|
||||
>
|
||||
3 Months
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ charts, days: 6 * 30 })}
|
||||
>
|
||||
6 Months
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({
|
||||
charts,
|
||||
days: Math.ceil(
|
||||
(today.valueOf() -
|
||||
new Date(`${today.getUTCFullYear()}-01-01`).valueOf()) /
|
||||
ONE_DAY_IN_MS,
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
Year To Date
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ charts, days: 365 })}
|
||||
>
|
||||
1 Year
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ charts, days: 2 * 365 })}
|
||||
>
|
||||
2 Years
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ charts, days: 4 * 365 })}
|
||||
>
|
||||
4 Years
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ charts, days: 8 * 365 })}
|
||||
>
|
||||
8 Years
|
||||
</Button>
|
||||
<For
|
||||
each={new Array(
|
||||
new Date().getFullYear() - new Date("2009-01-01").getFullYear(),
|
||||
)
|
||||
.fill(0)
|
||||
.map((_, index) => index + 2009)
|
||||
.reverse()}
|
||||
>
|
||||
{(year) => (
|
||||
<Switch>
|
||||
<Match when={scale() === "date"}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ charts, year })}
|
||||
onClick={() => setTimeScale({ scale: scale(), charts })}
|
||||
>
|
||||
{year}
|
||||
All Time
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ scale: scale(), charts, days: 7 })}
|
||||
>
|
||||
1 Week
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ scale: scale(), charts, days: 30 })}
|
||||
>
|
||||
1 Month
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), charts, days: 3 * 30 })
|
||||
}
|
||||
>
|
||||
3 Months
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), charts, days: 6 * 30 })
|
||||
}
|
||||
>
|
||||
6 Months
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({
|
||||
scale: scale(),
|
||||
charts,
|
||||
days: Math.ceil(
|
||||
(today.getTime() -
|
||||
new Date(`${today.getUTCFullYear()}-01-01`).getTime()) /
|
||||
ONE_DAY_IN_MS,
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
Year To Date
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), charts, days: 365 })
|
||||
}
|
||||
>
|
||||
1 Year
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), charts, days: 2 * 365 })
|
||||
}
|
||||
>
|
||||
2 Years
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), charts, days: 4 * 365 })
|
||||
}
|
||||
>
|
||||
4 Years
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), charts, days: 8 * 365 })
|
||||
}
|
||||
>
|
||||
8 Years
|
||||
</Button>
|
||||
<For
|
||||
each={new Array(
|
||||
new Date().getFullYear() - new Date("2009-01-01").getFullYear(),
|
||||
)
|
||||
.fill(0)
|
||||
.map((_, index) => index + 2009)
|
||||
.reverse()}
|
||||
>
|
||||
{(year) => (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ scale: scale(), charts, year })}
|
||||
>
|
||||
{year}
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
<Match when={scale() === "height"}>
|
||||
<Button disabled={() => true} onClick={() => {}}>
|
||||
24h
|
||||
</Button>
|
||||
<Button disabled={() => true} onClick={() => {}}>
|
||||
48h
|
||||
</Button>
|
||||
<For
|
||||
each={new Array(9)
|
||||
.fill(0)
|
||||
.flatMap((_, i) => [i, i + 0.5])
|
||||
.reverse()}
|
||||
>
|
||||
{(i) => (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({
|
||||
scale: scale(),
|
||||
charts,
|
||||
range: {
|
||||
from: i * 100_000,
|
||||
to: (i + 0.5) * 100_000,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{`${100 * (i + 0.5)}k`}
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Scrollable>
|
||||
</Box>
|
||||
);
|
||||
@@ -122,29 +180,46 @@ function Button({
|
||||
|
||||
function setTimeScale({
|
||||
charts,
|
||||
scale,
|
||||
days,
|
||||
year,
|
||||
range,
|
||||
}: {
|
||||
charts: RWS<IChartApi[]>;
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<boolean>;
|
||||
qrcode: RWS<string>;
|
||||
datasets: Datasets;
|
||||
fullscreen?: RWS<boolean>;
|
||||
dark: Accessor<boolean>;
|
||||
standalone: boolean;
|
||||
}) {
|
||||
const legend = createRWS<PresetLegend>([]);
|
||||
const legend = createRWS<SeriesLegend[]>([]);
|
||||
|
||||
const charts = createRWS<IChartApi[]>([]);
|
||||
|
||||
const div = createRWS<HTMLDivElement | undefined>(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({
|
||||
<div class="border-lighter border-t" />
|
||||
|
||||
<div class="flex">
|
||||
<Legend legend={legend} />
|
||||
<Legend legend={legend} scale={scale} activeRange={activeRange} />
|
||||
|
||||
<div class="border-lighter border-l" />
|
||||
|
||||
@@ -65,10 +70,12 @@ export function ChartFrame({
|
||||
datasets={datasets}
|
||||
legendSetter={legend.set}
|
||||
presets={presets}
|
||||
dark={dark}
|
||||
activeRange={activeRange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TimeScale charts={charts} />
|
||||
<TimeScale charts={charts} scale={scale} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function FoldersFrame({
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="flex max-h-full min-h-0 flex-1 flex-col gap-4 p-4">
|
||||
<Header title="Folders">
|
||||
<Number number={() => presets.list.length} /> presets organized in a
|
||||
<Number number={() => presets.list.length} /> charts organized in a
|
||||
tree like structure.
|
||||
</Header>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export function HistoryFrame({
|
||||
}}
|
||||
>
|
||||
<div class="flex max-h-full min-h-0 flex-1 flex-col p-4">
|
||||
<Header title="History">List of previously visited presets.</Header>
|
||||
<Header title="History">List of previously visited charts.</Header>
|
||||
|
||||
<div
|
||||
class="space-y-0.5 pt-4"
|
||||
|
||||
@@ -21,12 +21,20 @@ export function Scrollable({
|
||||
return;
|
||||
}
|
||||
|
||||
scrollable.set(() => 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 (
|
||||
<div class="relative min-w-0 flex-1">
|
||||
<For
|
||||
|
||||
@@ -14,18 +14,6 @@ export function SettingsFrame({
|
||||
backgroundMode: SL<"Scroll" | "Static">;
|
||||
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 (
|
||||
<div
|
||||
class="flex-1 overflow-y-auto"
|
||||
@@ -34,10 +22,7 @@ export function SettingsFrame({
|
||||
}}
|
||||
>
|
||||
<div class="space-y-4 p-4">
|
||||
<Header title="Settings">
|
||||
And other stuff <strong class="italic">NOT</strong> transmitted by
|
||||
relays.
|
||||
</Header>
|
||||
<Header title="Settings">And other stuff</Header>
|
||||
|
||||
<div class="border-lighter -mx-4 border-t" />
|
||||
|
||||
@@ -74,7 +59,13 @@ export function SettingsFrame({
|
||||
|
||||
<hr class="border-lighter -mx-4 border-t" />
|
||||
<p class="text-center">
|
||||
<span class="opacity-50">Version:</span> {version}
|
||||
<span class="opacity-50">Version:</span>{" "}
|
||||
<a
|
||||
href="https://codeberg.org/satonomics/satonomics/src/branch/main/CHANGELOG.md"
|
||||
target="_blank"
|
||||
>
|
||||
{version}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+18
-10
@@ -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}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
@@ -303,16 +316,12 @@ export function App() {
|
||||
<div
|
||||
class="mx-[3px] my-8 hidden w-[6px] cursor-col-resize items-center justify-center rounded-full bg-orange-900 opacity-0 hover:opacity-50 dark:bg-orange-100 md:block short:hidden"
|
||||
onMouseDown={(event) => {
|
||||
resizeInitialRange.set(chartState.range);
|
||||
|
||||
if (resizingBarStart() === undefined) {
|
||||
resizingBarStart.set(event.clientX);
|
||||
resizingBarWidth.set(barWidth());
|
||||
}
|
||||
}}
|
||||
onTouchStart={(event) => {
|
||||
resizeInitialRange.set(chartState.range);
|
||||
|
||||
if (resizingBarStart() === undefined) {
|
||||
resizingBarStart.set(event.touches[0].clientX);
|
||||
resizingBarWidth.set(barWidth());
|
||||
@@ -320,8 +329,6 @@ export function App() {
|
||||
}}
|
||||
onDblClick={() => {
|
||||
barWidth.set(0);
|
||||
|
||||
setTimeScale(resizeInitialRange());
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
@@ -334,6 +341,7 @@ export function App() {
|
||||
qrcode={qrcode}
|
||||
fullscreen={fullscreen}
|
||||
datasets={datasets}
|
||||
dark={dark}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
+4
-1
@@ -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"];
|
||||
|
||||
|
||||
@@ -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<Value[]>([]);
|
||||
|
||||
createEffect(() => {
|
||||
const vecs = fetchedJSONs.map((fetched) => fetched.vec() || []);
|
||||
debouncedValuesCallback(vecs);
|
||||
});
|
||||
|
||||
const resource: ResourceDataset<Scale, Type> = {
|
||||
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);
|
||||
}
|
||||
|
||||
Vendored
+1
-2
@@ -6,7 +6,7 @@ type AnyDatasets = DateDatasets | HeightDatasets;
|
||||
|
||||
type ResourceScale = (typeof import("./index").scales)[index];
|
||||
|
||||
type DatasetValue<T> = T & Numbered & Valued;
|
||||
type DatasetValue<T> = T & Valued;
|
||||
|
||||
interface ResourceDataset<
|
||||
Scale extends ResourceScale,
|
||||
@@ -24,7 +24,6 @@ interface ResourceDataset<
|
||||
url: string;
|
||||
fetch: (id: number) => void;
|
||||
fetchedJSONs: FetchedResult<Scale, Type>[];
|
||||
values: Accessor<DatasetValue<Value>[]>;
|
||||
drop: VoidFunction;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<CandlestickData | SingleValueData>[];
|
||||
range: TimeRange;
|
||||
lowerOpacity: boolean;
|
||||
}) => {
|
||||
const first = candlesticks.at(0);
|
||||
visibleRange: TimeRange | undefined;
|
||||
legendList: SeriesLegend[];
|
||||
activeRange: Accessor<number[]>;
|
||||
}) {
|
||||
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<any>] | undefined;
|
||||
let min = undefined as [number, Time, number, ISeriesApi<any>] | 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<Time> & Numbered)[] = [];
|
||||
if (!series || !series?.options().visible) return;
|
||||
|
||||
const seriesIsCandlestick = series.seriesType() === "Candlestick";
|
||||
series.setMarkers([]);
|
||||
|
||||
[
|
||||
{
|
||||
mathFunction: "min" as const,
|
||||
placementAttribute: seriesIsCandlestick
|
||||
? ("low" as const)
|
||||
: ("close" as const),
|
||||
// valueAttribute: 'low' as const,
|
||||
markerOptions: {
|
||||
position: "belowBar" as const,
|
||||
shape: "arrowUp" as const,
|
||||
},
|
||||
},
|
||||
{
|
||||
mathFunction: "max" as const,
|
||||
placementAttribute: seriesIsCandlestick
|
||||
? ("high" as const)
|
||||
: ("close" as const),
|
||||
// valueAttribute: 'high' as const,
|
||||
markerOptions: {
|
||||
position: "aboveBar" as const,
|
||||
shape: "arrowDown" as const,
|
||||
},
|
||||
},
|
||||
].map(
|
||||
({
|
||||
mathFunction,
|
||||
placementAttribute,
|
||||
// valueAttribute,
|
||||
markerOptions,
|
||||
}) => {
|
||||
const value = Math[mathFunction](
|
||||
// ...slicedDataList.map((data) => data[valueAttribute] || 0),
|
||||
...slicedDataList.map(
|
||||
(data) =>
|
||||
(placementAttribute in data
|
||||
? data[placementAttribute]
|
||||
: data.value) || 0,
|
||||
),
|
||||
);
|
||||
const isCandlestick = series.seriesType() === "Candlestick";
|
||||
|
||||
const placement = Math[mathFunction](
|
||||
...slicedDataList.map(
|
||||
(data) =>
|
||||
(placementAttribute in data
|
||||
? data[placementAttribute]
|
||||
: data.value) || 0,
|
||||
),
|
||||
);
|
||||
const vec = dataset.fetchedJSONs.at(seriesIndex)?.vec();
|
||||
|
||||
const candle = slicedDataList.find(
|
||||
(data) =>
|
||||
(placementAttribute in data
|
||||
? data[placementAttribute]
|
||||
: data.value) === placement,
|
||||
);
|
||||
if (!vec) return;
|
||||
|
||||
return (
|
||||
candle &&
|
||||
markers.push({
|
||||
...markerOptions,
|
||||
number: candle.number,
|
||||
time: candle.time,
|
||||
color: lowerOpacity ? colors.darkWhite : colors.white,
|
||||
size: 0,
|
||||
text: value.toLocaleString("en-us"),
|
||||
})
|
||||
);
|
||||
},
|
||||
);
|
||||
for (let i = 0; i < vec.length; i++) {
|
||||
const data = vec[i];
|
||||
|
||||
series.setMarkers(sortWhitespaceDataArray(markers));
|
||||
let number;
|
||||
|
||||
if (scale === "date") {
|
||||
const date = new Date(
|
||||
typeof data.time === "string"
|
||||
? data.time
|
||||
: // @ts-ignore
|
||||
`${data.time.year}-${data.time.month}-${data.time.day}`,
|
||||
);
|
||||
|
||||
number = date.getTime();
|
||||
|
||||
if (date <= dateFrom || date >= dateTo) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
const height = data.time;
|
||||
|
||||
number = height as number;
|
||||
|
||||
if (height <= from || height >= to) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const high = isCandlestick ? data["high"] : data.value;
|
||||
// @ts-ignore
|
||||
const low = isCandlestick ? data["low"] : data.value;
|
||||
|
||||
if (!max || high > max[2]) {
|
||||
max = [number, data.time, high, series];
|
||||
}
|
||||
if (!min || low < min[2]) {
|
||||
min = [number, data.time, low, series];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let minMarker: (SeriesMarker<Time> & { weight: number }) | undefined;
|
||||
let maxMarker: (SeriesMarker<Time> & { weight: number }) | undefined;
|
||||
|
||||
if (min) {
|
||||
minMarker = {
|
||||
weight: min[0],
|
||||
time: min[1],
|
||||
color: colors.white,
|
||||
position: "belowBar" as const,
|
||||
shape: "arrowUp" as const,
|
||||
size: 0,
|
||||
text: min[2].toLocaleString("en-us"),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function sortWhitespaceDataArray<T extends WhitespaceData & Numbered>(
|
||||
array: T[],
|
||||
) {
|
||||
return array.sort(({ number: a }, { number: b }) => a - b);
|
||||
if (max) {
|
||||
maxMarker = {
|
||||
weight: max[0],
|
||||
time: max[1],
|
||||
color: colors.white,
|
||||
position: "aboveBar" as const,
|
||||
shape: "arrowDown" as const,
|
||||
size: 0,
|
||||
text: max[2].toLocaleString("en-us"),
|
||||
};
|
||||
}
|
||||
|
||||
if (min && max && min[3] === max[3] && minMarker && maxMarker) {
|
||||
const series = min[3];
|
||||
series.setMarkers(
|
||||
[minMarker, maxMarker].sort((a, b) => a.weight - b.weight),
|
||||
);
|
||||
} else {
|
||||
if (min && minMarker) {
|
||||
const series = min[3];
|
||||
series.setMarkers([minMarker]);
|
||||
}
|
||||
|
||||
if (max && maxMarker) {
|
||||
const series = max[3];
|
||||
series.setMarkers([maxMarker]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { getInitialRange } from "./time";
|
||||
|
||||
export const LOCAL_STORAGE_RANGE_KEY = "chart-range";
|
||||
export const URL_PARAMS_RANGE_FROM_KEY = "from";
|
||||
export const URL_PARAMS_RANGE_TO_KEY = "to";
|
||||
|
||||
export const chartState = {
|
||||
chart: null as IChartApi | null,
|
||||
range: getInitialRange(),
|
||||
};
|
||||
@@ -1,108 +1,157 @@
|
||||
import { HEIGHT_CHUNK_SIZE } from "../../datasets";
|
||||
import { debounce } from "../../utils/debounce";
|
||||
import { writeURLParam } from "../../utils/urlParams";
|
||||
import {
|
||||
chartState,
|
||||
LOCAL_STORAGE_RANGE_KEY,
|
||||
URL_PARAMS_RANGE_FROM_KEY,
|
||||
URL_PARAMS_RANGE_TO_KEY,
|
||||
} from "./state";
|
||||
|
||||
const debouncedUpdateURLParams = debounce((range: TimeRange | null) => {
|
||||
if (!range) return;
|
||||
|
||||
writeURLParam(URL_PARAMS_RANGE_FROM_KEY, String(range.from));
|
||||
|
||||
writeURLParam(URL_PARAMS_RANGE_TO_KEY, String(range.to));
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_RANGE_KEY, JSON.stringify(range));
|
||||
}, 500);
|
||||
const LOCAL_STORAGE_RANGE_KEY = "chart-range";
|
||||
const URL_PARAMS_RANGE_FROM_KEY = "from";
|
||||
const URL_PARAMS_RANGE_TO_KEY = "to";
|
||||
|
||||
export function initTimeScale({
|
||||
activeDatasets,
|
||||
scale,
|
||||
activeRange,
|
||||
exactRange,
|
||||
charts,
|
||||
}: {
|
||||
activeDatasets: Set<ResourceDataset<any, any>>;
|
||||
scale: ResourceScale;
|
||||
activeRange: RWS<number[]>;
|
||||
exactRange: RWS<TimeRange | undefined>;
|
||||
charts: ChartObject[];
|
||||
}) {
|
||||
setTimeScale(chartState.range);
|
||||
const firstChart = charts.at(0)?.chart;
|
||||
|
||||
const debouncedFetch = debounce((range: TimeRange | null) => {
|
||||
if (!firstChart) return;
|
||||
|
||||
firstChart.timeScale().subscribeVisibleTimeRangeChange((range) => {
|
||||
if (!range) return;
|
||||
|
||||
let ids: number[] = [];
|
||||
exactRange.set(range);
|
||||
|
||||
if (typeof range.from === "string" && typeof range.to === "string") {
|
||||
const from = new Date(range.from).getUTCFullYear();
|
||||
const to = new Date(range.to).getUTCFullYear();
|
||||
debouncedSetActiveRange({ range, activeRange });
|
||||
|
||||
ids = Array.from({ length: to - from + 1 }, (_, i) => i + from);
|
||||
} else {
|
||||
const from = Math.floor(Number(range.from) / HEIGHT_CHUNK_SIZE);
|
||||
const to = Math.floor(Number(range.to) / HEIGHT_CHUNK_SIZE);
|
||||
debouncedSaveTimeRange({ scale, range });
|
||||
});
|
||||
|
||||
const length = to - from + 1;
|
||||
|
||||
ids = Array.from({ length }, (_, i) => (from + i) * HEIGHT_CHUNK_SIZE);
|
||||
}
|
||||
|
||||
ids.forEach((id) => {
|
||||
activeDatasets.forEach((dataset) => dataset.fetch(id));
|
||||
});
|
||||
}, 100);
|
||||
|
||||
debouncedFetch(chartState.range);
|
||||
|
||||
let timeout = setTimeout(() => {
|
||||
chartState.chart?.timeScale().subscribeVisibleTimeRangeChange((range) => {
|
||||
debouncedFetch(range);
|
||||
|
||||
debouncedUpdateURLParams(range);
|
||||
|
||||
range = range || chartState.range;
|
||||
|
||||
chartState.range = range;
|
||||
});
|
||||
}, 50);
|
||||
|
||||
onCleanup(() => clearTimeout(timeout));
|
||||
setTimeScale(firstChart, getInitialRange(scale));
|
||||
}
|
||||
|
||||
export function getInitialRange(): TimeRange {
|
||||
function setTimeScale(chart: IChartApi, range: TimeRange | null) {
|
||||
if (range) {
|
||||
setTimeout(() => {
|
||||
chart.timeScale().setVisibleRange(range);
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function getInitialRange(scale: ResourceScale): TimeRange {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
const urlFrom = urlParams.get(URL_PARAMS_RANGE_FROM_KEY);
|
||||
const urlTo = urlParams.get(URL_PARAMS_RANGE_TO_KEY);
|
||||
|
||||
if (urlFrom && urlTo) {
|
||||
return {
|
||||
from: urlFrom,
|
||||
to: urlTo,
|
||||
} satisfies TimeRange;
|
||||
if (scale === "date" && urlFrom.includes("-") && urlTo.includes("-")) {
|
||||
return {
|
||||
from: new Date(urlFrom).toJSON().split("T")[0],
|
||||
to: new Date(urlTo).toJSON().split("T")[0],
|
||||
} satisfies TimeRange;
|
||||
} else if (
|
||||
scale === "height" &&
|
||||
!urlFrom.includes("-") &&
|
||||
!urlTo.includes("-")
|
||||
) {
|
||||
return {
|
||||
from: Number(urlFrom),
|
||||
to: Number(urlTo),
|
||||
} as any satisfies TimeRange;
|
||||
}
|
||||
}
|
||||
|
||||
const savedTimeRange = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_RANGE_KEY) || "null",
|
||||
localStorage.getItem(getLocalStorageKey(scale)) || "null",
|
||||
) as TimeRange | null;
|
||||
|
||||
if (savedTimeRange) {
|
||||
return savedTimeRange;
|
||||
}
|
||||
|
||||
const defaultTo = new Date();
|
||||
const defaultFrom = new Date();
|
||||
defaultFrom.setDate(defaultFrom.getUTCDate() - 6 * 30);
|
||||
switch (scale) {
|
||||
case "date": {
|
||||
const defaultTo = new Date();
|
||||
const defaultFrom = new Date();
|
||||
defaultFrom.setDate(defaultFrom.getUTCDate() - 6 * 30);
|
||||
|
||||
const defaultTimeRange = {
|
||||
from: defaultFrom.toJSON().split("T")[0],
|
||||
to: defaultTo.toJSON().split("T")[0],
|
||||
} satisfies TimeRange;
|
||||
|
||||
return defaultTimeRange;
|
||||
}
|
||||
|
||||
export function setTimeScale(range: TimeRange | null) {
|
||||
if (range) {
|
||||
setTimeout(() => {
|
||||
chartState.chart?.timeScale().setVisibleRange(range);
|
||||
}, 1);
|
||||
return {
|
||||
from: defaultFrom.toJSON().split("T")[0],
|
||||
to: defaultTo.toJSON().split("T")[0],
|
||||
} satisfies TimeRange;
|
||||
}
|
||||
case "height": {
|
||||
return {
|
||||
from: 800_000,
|
||||
to: 850_000,
|
||||
} as any satisfies TimeRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLocalStorageKey(scale: ResourceScale) {
|
||||
return `${LOCAL_STORAGE_RANGE_KEY}-${scale}`;
|
||||
}
|
||||
|
||||
function setActiveRange({
|
||||
range,
|
||||
activeRange,
|
||||
}: {
|
||||
range: TimeRange;
|
||||
activeRange: RWS<number[]>;
|
||||
}) {
|
||||
let ids: number[] = [];
|
||||
|
||||
const today = new Date();
|
||||
|
||||
if (typeof range.from === "string" && typeof range.to === "string") {
|
||||
const from = new Date(range.from).getUTCFullYear();
|
||||
const to = new Date(range.to).getUTCFullYear();
|
||||
|
||||
ids = Array.from({ length: to - from + 1 }, (_, i) => i + from).filter(
|
||||
(year) => year >= 2009 && year <= today.getUTCFullYear(),
|
||||
);
|
||||
} else {
|
||||
const from = Math.floor(Number(range.from) / HEIGHT_CHUNK_SIZE);
|
||||
const to = Math.floor(Number(range.to) / HEIGHT_CHUNK_SIZE);
|
||||
|
||||
const length = to - from + 1;
|
||||
|
||||
ids = Array.from({ length }, (_, i) => (from + i) * HEIGHT_CHUNK_SIZE);
|
||||
}
|
||||
|
||||
const old = activeRange();
|
||||
|
||||
if (
|
||||
old.length !== ids.length ||
|
||||
old.at(0) !== ids.at(0) ||
|
||||
old.at(-1) !== ids.at(-1)
|
||||
) {
|
||||
console.log("range:", ids);
|
||||
|
||||
activeRange.set(ids);
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedSetActiveRange = debounce(setActiveRange, 100);
|
||||
|
||||
function saveTimeRange({
|
||||
scale,
|
||||
range,
|
||||
}: {
|
||||
scale: ResourceScale;
|
||||
range: TimeRange;
|
||||
}) {
|
||||
writeURLParam(URL_PARAMS_RANGE_FROM_KEY, String(range.from));
|
||||
|
||||
writeURLParam(URL_PARAMS_RANGE_TO_KEY, String(range.to));
|
||||
|
||||
localStorage.setItem(getLocalStorageKey(scale), JSON.stringify(range));
|
||||
}
|
||||
|
||||
const debouncedSaveTimeRange = debounce(saveTimeRange, 500);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { dateToString, getNumberOfDaysBetweenTwoDates } from "../../utils/date";
|
||||
import { ONE_DAY_IN_MS } from "../../utils/time";
|
||||
import { createLineSeries } from "../series/creators/line";
|
||||
|
||||
export const DAY_BEFORE_GENESIS_DAY = "2009-01-02";
|
||||
@@ -9,41 +8,58 @@ export const GENESIS_DAY = "2009-01-03";
|
||||
|
||||
const whitespaceStartDate = "1970-01-01";
|
||||
const whitespaceEndDate = "2100-01-01";
|
||||
const whitespaceDateDataset: (SingleValueData & Numbered)[] = new Array(
|
||||
const whitespaceDateDataset: (WhitespaceData | SingleValueData)[] = new Array(
|
||||
getNumberOfDaysBetweenTwoDates(
|
||||
new Date(whitespaceStartDate),
|
||||
new Date(whitespaceEndDate),
|
||||
),
|
||||
)
|
||||
.fill(0)
|
||||
.map((_, index) => {
|
||||
const date = new Date(whitespaceStartDate);
|
||||
date.setUTCDate(date.getUTCDay() + index);
|
||||
);
|
||||
// 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(whitespaceStartDate);
|
||||
date.setUTCDate(date.getUTCDay() + i);
|
||||
|
||||
return {
|
||||
number: date.valueOf() / ONE_DAY_IN_MS,
|
||||
time: dateToString(date),
|
||||
const time = dateToString(date);
|
||||
|
||||
if (i === whitespaceDateDataset.length - 1) {
|
||||
whitespaceDateDataset[i] = {
|
||||
time,
|
||||
value: NaN,
|
||||
};
|
||||
});
|
||||
|
||||
const heightStart = -100_000;
|
||||
const whitespaceHeightDataset: (SingleValueData & Numbered)[] = new Array(
|
||||
1_200_000,
|
||||
)
|
||||
.fill(0)
|
||||
.map((_, index) => ({
|
||||
time: (heightStart + index) as any,
|
||||
number: heightStart + index,
|
||||
value: NaN,
|
||||
}));
|
||||
|
||||
export function setWhitespace(chart: IChartApi, scale: ResourceScale) {
|
||||
const whitespaceSeries = createLineSeries(chart);
|
||||
|
||||
if (scale === "date") {
|
||||
whitespaceSeries.setData(whitespaceDateDataset);
|
||||
} else {
|
||||
whitespaceSeries.setData(whitespaceHeightDataset);
|
||||
whitespaceDateDataset[i] = {
|
||||
time,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const heightStart = -50_000;
|
||||
const whitespaceHeightDataset: WhitespaceData[] = new Array(
|
||||
(new Date().getUTCFullYear() - 2009 + 1) * 60_000,
|
||||
);
|
||||
for (let i = 0; i < whitespaceHeightDataset.length; i++) {
|
||||
const height = heightStart + i;
|
||||
|
||||
whitespaceHeightDataset[i] = {
|
||||
time: height as any,
|
||||
};
|
||||
}
|
||||
|
||||
export function setWhitespace(chart: IChartApi, scale: ResourceScale) {
|
||||
const whitespace = createLineSeries(chart);
|
||||
|
||||
if (scale === "date") {
|
||||
whitespace.setData(whitespaceDateDataset);
|
||||
} else {
|
||||
whitespace.setData(whitespaceHeightDataset);
|
||||
|
||||
const time = whitespaceHeightDataset.length;
|
||||
whitespace.update({
|
||||
time: time as Time,
|
||||
value: NaN,
|
||||
});
|
||||
}
|
||||
|
||||
return whitespace;
|
||||
}
|
||||
|
||||
@@ -26,14 +26,14 @@ export const createBaseLineSeries = (
|
||||
} = options;
|
||||
|
||||
const allTopColor = topColor || color || DEFAULT_BASELINE_TOP_COLOR;
|
||||
const topFillColor = `${allTopColor}`;
|
||||
const topFillColor = `transparent`;
|
||||
const allBottomColor = bottomColor || color || DEFAULT_BASELINE_BOTTOM_COLOR;
|
||||
const bottomFillColor = `${allBottomColor}`;
|
||||
const bottomFillColor = `transparent`;
|
||||
|
||||
const seriesOptions: DeepPartialBaselineOptions = {
|
||||
priceScaleId: "right",
|
||||
...defaultSeriesOptions,
|
||||
lineWidth: 1,
|
||||
// lineWidth: 1,
|
||||
...options,
|
||||
...options.options,
|
||||
...(base ? { baseValue: { type: "price", price: base } } : {}),
|
||||
|
||||
@@ -8,26 +8,26 @@ import {
|
||||
} from "/src/scripts/utils/urlParams";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
export function createSeriesLegend({
|
||||
export function createSeriesLegend<Scale extends ResourceScale>({
|
||||
id,
|
||||
presetId,
|
||||
title,
|
||||
color,
|
||||
series,
|
||||
seriesList,
|
||||
defaultVisible = true,
|
||||
disabled: _disabled,
|
||||
visible: _visible,
|
||||
url,
|
||||
dataset,
|
||||
}: {
|
||||
id: string;
|
||||
presetId: string;
|
||||
title: string;
|
||||
color: Accessor<string | string[]>;
|
||||
series: ISeriesApi<SeriesType>;
|
||||
seriesList: Accessor<ISeriesApi<SeriesType> | undefined>[];
|
||||
defaultVisible?: boolean;
|
||||
disabled?: Accessor<boolean>;
|
||||
visible?: RWS<boolean>;
|
||||
url?: string;
|
||||
dataset: ResourceDataset<Scale>;
|
||||
}) {
|
||||
const storageID = `${presetId}-${id}`;
|
||||
|
||||
@@ -43,12 +43,6 @@ export function createSeriesLegend({
|
||||
|
||||
const drawn = createMemo(() => visible() && !disabled());
|
||||
|
||||
createEffect(() => {
|
||||
series.applyOptions({
|
||||
visible: drawn(),
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (disabled()) {
|
||||
return;
|
||||
@@ -68,11 +62,11 @@ export function createSeriesLegend({
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
series,
|
||||
seriesList,
|
||||
color,
|
||||
visible,
|
||||
disabled,
|
||||
drawn,
|
||||
url,
|
||||
dataset,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ export function createPresets({
|
||||
{
|
||||
title: `Total Non Empty Address`,
|
||||
color: colors.bitcoin,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset: params.datasets[scale].address_count,
|
||||
},
|
||||
],
|
||||
@@ -67,7 +66,6 @@ export function createPresets({
|
||||
{
|
||||
title: `Total Addresses Created`,
|
||||
color: colors.bitcoin,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset: params.datasets[scale].created_addresses,
|
||||
},
|
||||
],
|
||||
@@ -87,7 +85,6 @@ export function createPresets({
|
||||
{
|
||||
title: `Total Empty Addresses`,
|
||||
color: colors.darkWhite,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset: params.datasets[scale].empty_addresses,
|
||||
},
|
||||
],
|
||||
@@ -143,25 +140,46 @@ function createAddressPresetFolder<Scale extends ResourceScale>({
|
||||
color,
|
||||
datasetKey,
|
||||
}),
|
||||
{
|
||||
name: `Split By Liquidity`,
|
||||
tree: liquidities.map(
|
||||
(liquidity): PartialPresetFolder => ({
|
||||
name: liquidity.name,
|
||||
tree: createCohortPresetList({
|
||||
title: `${liquidity.name} ${name}`,
|
||||
name: `${liquidity.name} ${name}`,
|
||||
scale,
|
||||
color,
|
||||
datasetKey: `${liquidity.key}_${datasetKey}`,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
},
|
||||
createLiquidityFolder({
|
||||
scale,
|
||||
name,
|
||||
datasetKey,
|
||||
color,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function createLiquidityFolder<Scale extends ResourceScale>({
|
||||
scale,
|
||||
color,
|
||||
name,
|
||||
datasetKey,
|
||||
}: {
|
||||
scale: Scale;
|
||||
name: string;
|
||||
datasetKey: AddressCohortKey | "";
|
||||
color: string;
|
||||
}): PartialPresetFolder {
|
||||
return {
|
||||
name: `Split By Liquidity`,
|
||||
tree: liquidities.map(
|
||||
(liquidity): PartialPresetFolder => ({
|
||||
name: liquidity.name,
|
||||
tree: createCohortPresetList({
|
||||
title: `${liquidity.name} ${name}`,
|
||||
name: `${liquidity.name} ${name}`,
|
||||
scale,
|
||||
color,
|
||||
datasetKey: !datasetKey
|
||||
? liquidity.key
|
||||
: `${liquidity.key}_${datasetKey}`,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function createAddressCountPreset<Scale extends ResourceScale>({
|
||||
scale,
|
||||
color,
|
||||
|
||||
+362
-406
@@ -1,10 +1,10 @@
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { chunkIdToIndex } from "../datasets/resource";
|
||||
import { createChart } from "../lightweightCharts/chart/create";
|
||||
import { chartState } from "../lightweightCharts/chart/state";
|
||||
import { setMinMaxMarkers } from "../lightweightCharts/chart/markers";
|
||||
import { initTimeScale } from "../lightweightCharts/chart/time";
|
||||
import { setWhitespace } from "../lightweightCharts/chart/whitespace";
|
||||
import { createAreaSeries } from "../lightweightCharts/series/creators/area";
|
||||
import {
|
||||
createBaseLineSeries,
|
||||
DEFAULT_BASELINE_COLORS,
|
||||
@@ -16,13 +16,12 @@ 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,
|
||||
Line,
|
||||
Based,
|
||||
Area,
|
||||
Histogram,
|
||||
Candlestick,
|
||||
}
|
||||
|
||||
type SeriesConfig<Scale extends ResourceScale> =
|
||||
@@ -33,6 +32,7 @@ type SeriesConfig<Scale extends ResourceScale> =
|
||||
seriesType: SeriesType.Based;
|
||||
title: string;
|
||||
options?: BaselineSeriesOptions;
|
||||
priceScaleOptions?: DeepPartialPriceScaleOptions;
|
||||
defaultVisible?: boolean;
|
||||
}
|
||||
| {
|
||||
@@ -42,15 +42,27 @@ type SeriesConfig<Scale extends ResourceScale> =
|
||||
seriesType: SeriesType.Histogram;
|
||||
title: string;
|
||||
options?: DeepPartialHistogramOptions;
|
||||
priceScaleOptions?: DeepPartialPriceScaleOptions;
|
||||
defaultVisible?: boolean;
|
||||
}
|
||||
| {
|
||||
dataset: ResourceDataset<Scale>;
|
||||
seriesType: SeriesType.Candlestick;
|
||||
priceScaleOptions?: DeepPartialPriceScaleOptions;
|
||||
colors?: undefined;
|
||||
color?: undefined;
|
||||
options?: DeepPartialLineOptions;
|
||||
defaultVisible?: boolean;
|
||||
title: string;
|
||||
}
|
||||
| {
|
||||
dataset: ResourceDataset<Scale>;
|
||||
color: string;
|
||||
colors?: undefined;
|
||||
seriesType?: SeriesType.Normal | SeriesType.Area;
|
||||
seriesType?: SeriesType.Line;
|
||||
title: string;
|
||||
options?: DeepPartialLineOptions;
|
||||
priceScaleOptions?: DeepPartialPriceScaleOptions;
|
||||
defaultVisible?: boolean;
|
||||
};
|
||||
|
||||
@@ -65,18 +77,28 @@ export function applySeriesList<Scale extends ResourceScale>({
|
||||
priceDataset,
|
||||
priceOptions,
|
||||
legendSetter,
|
||||
dark,
|
||||
activeRange,
|
||||
}: {
|
||||
charts: RWS<IChartApi[]>;
|
||||
parentDiv: HTMLDivElement;
|
||||
preset: Preset;
|
||||
legendSetter: Setter<PresetLegend>;
|
||||
legendSetter: Setter<SeriesLegend[]>;
|
||||
priceDataset?: ResourceDataset<Scale>;
|
||||
priceOptions?: PriceSeriesOptions;
|
||||
priceScaleOptions?: DeepPartialPriceScaleOptions;
|
||||
top?: SeriesConfig<Scale>[];
|
||||
bottom?: SeriesConfig<Scale>[];
|
||||
datasets: Datasets;
|
||||
dark: boolean;
|
||||
activeRange: RWS<number[]>;
|
||||
}) {
|
||||
// ---
|
||||
// Reset states
|
||||
// ---
|
||||
|
||||
legendSetter([]);
|
||||
|
||||
reactiveChartList.set((charts) => {
|
||||
charts.forEach((chart) => {
|
||||
chart.remove();
|
||||
@@ -85,29 +107,36 @@ export function applySeriesList<Scale extends ResourceScale>({
|
||||
return [];
|
||||
});
|
||||
|
||||
activeRange.set([]);
|
||||
|
||||
parentDiv.replaceChildren();
|
||||
|
||||
// ---
|
||||
// Done
|
||||
// ---
|
||||
|
||||
const scale = preset.scale;
|
||||
|
||||
const legendList: PresetLegend = [];
|
||||
const presetLegend: SeriesLegend[] = [];
|
||||
|
||||
const priceSeriesType = createRWS<"Candlestick" | "Line">("Candlestick");
|
||||
const priceSeriesType = createRWS<PriceSeriesType>("Candlestick");
|
||||
|
||||
// const valuesSkipped = createRWS(0);
|
||||
const activeDatasets: ResourceDataset<any, any>[] = [];
|
||||
|
||||
const activeDatasets: Set<ResourceDataset<any, any>> = new Set();
|
||||
const lastActiveIndex = createMemo(() => {
|
||||
const last = activeRange().at(-1);
|
||||
return last !== undefined ? chunkIdToIndex(scale, last) : undefined;
|
||||
});
|
||||
|
||||
const exactRange = createRWS(undefined as TimeRange | undefined);
|
||||
|
||||
const charts = [top || [], bottom]
|
||||
.flatMap((list) => (list ? [list] : []))
|
||||
.flatMap((seriesConfigList, index, array) => {
|
||||
.flatMap((seriesConfigList, index) => {
|
||||
if (index !== 0 && seriesConfigList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isAnyArea = seriesConfigList.find(
|
||||
(config) => config.seriesType === SeriesType.Area,
|
||||
);
|
||||
|
||||
const div = document.createElement("div");
|
||||
|
||||
div.className = "w-full cursor-crosshair min-h-0 border-orange-200/10";
|
||||
@@ -115,28 +144,40 @@ export function applySeriesList<Scale extends ResourceScale>({
|
||||
parentDiv.appendChild(div);
|
||||
|
||||
const chart = createChart(scale, div, {
|
||||
...priceScaleOptions,
|
||||
...(isAnyArea
|
||||
? {
|
||||
scaleMargins: {
|
||||
bottom: 0,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
dark,
|
||||
priceScaleOptions: {
|
||||
...priceScaleOptions,
|
||||
},
|
||||
});
|
||||
|
||||
chartState.chart = chart;
|
||||
|
||||
if (!chart) {
|
||||
console.log("chart: undefined");
|
||||
return [];
|
||||
}
|
||||
|
||||
setWhitespace(chart, scale);
|
||||
const whitespace = setWhitespace(chart, scale);
|
||||
|
||||
const seriesList: ISeriesApi<any>[] = [];
|
||||
// return [
|
||||
// {
|
||||
// scale,
|
||||
// div,
|
||||
// chart,
|
||||
// whitespace,
|
||||
// legendList: [],
|
||||
// debouncedSetMinMaxMarkers: () => {},
|
||||
// },
|
||||
// ];
|
||||
|
||||
const _legendList: PresetLegend = [];
|
||||
const chartLegend: SeriesLegend[] = [];
|
||||
|
||||
const debouncedSetMinMaxMarkers = debounce(() => {
|
||||
setMinMaxMarkers({
|
||||
scale,
|
||||
visibleRange: exactRange(),
|
||||
legendList: chartLegend,
|
||||
activeRange,
|
||||
});
|
||||
}, 20);
|
||||
|
||||
if (index === 0) {
|
||||
const dataset =
|
||||
@@ -145,102 +186,93 @@ export function applySeriesList<Scale extends ResourceScale>({
|
||||
typeof priceDataset
|
||||
>);
|
||||
|
||||
activeDatasets.add(dataset);
|
||||
activeDatasets.push(dataset);
|
||||
|
||||
const price = applyPriceSeries({
|
||||
chart,
|
||||
preset,
|
||||
seriesType: priceSeriesType,
|
||||
// valuesSkipped,
|
||||
dataset,
|
||||
options: priceOptions,
|
||||
const title = priceOptions?.title || "Price";
|
||||
|
||||
const priceScaleOptions: DeepPartialPriceScaleOptions = {
|
||||
mode: 1,
|
||||
...priceOptions?.priceScaleOptions,
|
||||
};
|
||||
|
||||
function createPriceSeries(seriesType: PriceSeriesType) {
|
||||
let seriesConfig: SeriesConfig<Scale>;
|
||||
|
||||
if (seriesType === "Candlestick") {
|
||||
seriesConfig = {
|
||||
dataset,
|
||||
title,
|
||||
seriesType: SeriesType.Candlestick,
|
||||
options: priceOptions,
|
||||
priceScaleOptions,
|
||||
};
|
||||
} else {
|
||||
seriesConfig = {
|
||||
dataset,
|
||||
title,
|
||||
color: colors.white,
|
||||
options: priceOptions?.seriesOptions,
|
||||
priceScaleOptions,
|
||||
};
|
||||
}
|
||||
|
||||
return createSeriesGroup({
|
||||
activeRange,
|
||||
seriesConfig,
|
||||
chart,
|
||||
chartLegend,
|
||||
lastActiveIndex,
|
||||
preset,
|
||||
disabled: () => priceSeriesType() !== seriesType,
|
||||
debouncedSetMinMaxMarkers,
|
||||
});
|
||||
}
|
||||
|
||||
const priceCandlestickLegend = createPriceSeries("Candlestick");
|
||||
const priceLineLegend = createPriceSeries("Line");
|
||||
|
||||
createEffect(() => {
|
||||
priceCandlestickLegend.visible.set(priceLineLegend.visible());
|
||||
});
|
||||
|
||||
_legendList.push(price.lineLegend, price.ohlcLegend);
|
||||
|
||||
seriesList.push(price.lineLegend.series, price.ohlcLegend.series);
|
||||
createEffect(() => {
|
||||
priceLineLegend.visible.set(priceCandlestickLegend.visible());
|
||||
});
|
||||
}
|
||||
|
||||
seriesList.push(
|
||||
...seriesConfigList
|
||||
.reverse()
|
||||
.map(
|
||||
({
|
||||
dataset,
|
||||
color,
|
||||
colors,
|
||||
seriesType: type,
|
||||
title,
|
||||
options,
|
||||
defaultVisible,
|
||||
}) => {
|
||||
activeDatasets.add(dataset);
|
||||
seriesConfigList.reverse().forEach((seriesConfig) => {
|
||||
activeDatasets.push(seriesConfig.dataset);
|
||||
|
||||
let series: ISeriesApi<
|
||||
"Baseline" | "Line" | "Area" | "Histogram"
|
||||
>;
|
||||
|
||||
if (type === SeriesType.Based) {
|
||||
series = createBaseLineSeries(chart, {
|
||||
color,
|
||||
...options,
|
||||
});
|
||||
} else if (type === SeriesType.Area) {
|
||||
series = createAreaSeries(chart, {
|
||||
color,
|
||||
autoscaleInfoProvider: (
|
||||
getInfo: () => AutoscaleInfo | null,
|
||||
) => {
|
||||
const info = getInfo();
|
||||
if (info) {
|
||||
info.priceRange.minValue = 0;
|
||||
}
|
||||
return info;
|
||||
},
|
||||
...options,
|
||||
});
|
||||
} else if (type === SeriesType.Histogram) {
|
||||
series = createHistogramSeries(chart, {
|
||||
color,
|
||||
...options,
|
||||
});
|
||||
} else {
|
||||
series = createLineSeries(chart, {
|
||||
color,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
_legendList.push(
|
||||
createSeriesLegend({
|
||||
id: stringToId(title),
|
||||
presetId: preset.id,
|
||||
title,
|
||||
series,
|
||||
color: () => colors || color || DEFAULT_BASELINE_COLORS,
|
||||
defaultVisible,
|
||||
url: dataset.url,
|
||||
}),
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
const values = dataset.values();
|
||||
console.log(values.length);
|
||||
|
||||
series.setData(values);
|
||||
});
|
||||
|
||||
return series;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
_legendList.forEach((legend) => {
|
||||
legendList.splice(0, 0, legend);
|
||||
createSeriesGroup({
|
||||
activeRange,
|
||||
seriesConfig,
|
||||
chartLegend,
|
||||
chart,
|
||||
preset,
|
||||
lastActiveIndex,
|
||||
debouncedSetMinMaxMarkers,
|
||||
});
|
||||
});
|
||||
|
||||
return [{ div, chart, seriesList, legendList: _legendList }];
|
||||
});
|
||||
chartLegend.forEach((legend) => {
|
||||
presetLegend.splice(0, 0, legend);
|
||||
|
||||
createEffect(on(legend.visible, debouncedSetMinMaxMarkers));
|
||||
});
|
||||
|
||||
createEffect(on(exactRange, debouncedSetMinMaxMarkers));
|
||||
|
||||
return [
|
||||
{
|
||||
scale,
|
||||
div,
|
||||
chart,
|
||||
whitespace,
|
||||
legendList: chartLegend,
|
||||
debouncedSetMinMaxMarkers,
|
||||
},
|
||||
];
|
||||
}) satisfies ChartObject[];
|
||||
|
||||
createEffect(() => {
|
||||
const visibleCharts: typeof charts = [];
|
||||
@@ -267,344 +299,268 @@ export function applySeriesList<Scale extends ResourceScale>({
|
||||
});
|
||||
});
|
||||
|
||||
// const seriesType = createRWS(
|
||||
// checkIfUpClose(chart, chartState.range) || "Candlestick",
|
||||
// );
|
||||
|
||||
function updateVisibleRangeRatio(
|
||||
chart: IChartApi,
|
||||
range: LogicalRange | null,
|
||||
) {
|
||||
if (!range) return;
|
||||
|
||||
try {
|
||||
const width = chart.timeScale().width();
|
||||
|
||||
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 / 1.25));
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const debouncedUpdateVisibleRangeRatio = debounce(
|
||||
updateVisibleRangeRatio,
|
||||
const debouncedUpdateVisiblePriceSeriesType = debounce(
|
||||
updateVisiblePriceSeriesType,
|
||||
50,
|
||||
);
|
||||
|
||||
initTimeScale({
|
||||
activeDatasets,
|
||||
scale,
|
||||
charts,
|
||||
activeRange,
|
||||
exactRange,
|
||||
});
|
||||
|
||||
charts.forEach(({ chart }, index) => {
|
||||
const activeDatasetsLength = activeDatasets.length;
|
||||
createEffect(() => {
|
||||
const range = activeRange();
|
||||
|
||||
untrack(() => {
|
||||
for (let i = 0; i < range.length; i++) {
|
||||
const id = range[i];
|
||||
for (let j = 0; j < activeDatasetsLength; j++) {
|
||||
activeDatasets[j].fetch(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const lastChartIndex = charts.length - 1;
|
||||
|
||||
for (let i = 0; i < charts.length; i++) {
|
||||
const chart = charts[i].chart;
|
||||
|
||||
chart.timeScale().subscribeVisibleLogicalRangeChange((timeRange) => {
|
||||
// Last chart otherwise length of timescale is Infinity
|
||||
if (index === charts.length - 1) {
|
||||
debouncedUpdateVisibleRangeRatio(chart, timeRange);
|
||||
if (!timeRange) return;
|
||||
|
||||
// Must be the chart with the visible timeScale
|
||||
if (i === lastChartIndex) {
|
||||
debouncedUpdateVisiblePriceSeriesType(
|
||||
chart,
|
||||
timeRange,
|
||||
priceSeriesType,
|
||||
);
|
||||
}
|
||||
|
||||
charts.forEach(({ chart: _chart }, _index) => {
|
||||
if (timeRange && index !== _index) {
|
||||
_chart.timeScale().setVisibleLogicalRange(timeRange);
|
||||
for (let j = 0; j <= lastChartIndex; j++) {
|
||||
if (i !== j) {
|
||||
charts[j].chart.timeScale().setVisibleLogicalRange(timeRange);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
chart.subscribeCrosshairMove(({ time, sourceEvent }) => {
|
||||
// Don't override crosshair position from scroll event
|
||||
if (time && !sourceEvent) return;
|
||||
|
||||
charts.forEach(({ chart: _chart, seriesList }, _index) => {
|
||||
const first = seriesList.at(0);
|
||||
for (let j = 0; j <= lastChartIndex; j++) {
|
||||
const whitespace = charts[j].whitespace;
|
||||
const otherChart = charts[j].chart;
|
||||
|
||||
if (first && index !== _index) {
|
||||
if (whitespace && i !== j) {
|
||||
if (time) {
|
||||
_chart.setCrosshairPosition(NaN, time, first);
|
||||
otherChart.setCrosshairPosition(NaN, time, whitespace);
|
||||
} else {
|
||||
// No time when mouse goes outside the chart
|
||||
_chart.clearCrosshairPosition();
|
||||
otherChart.clearCrosshairPosition();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
legendSetter(legendList);
|
||||
legendSetter(presetLegend);
|
||||
|
||||
reactiveChartList.set(() => charts.map(({ chart }) => chart));
|
||||
}
|
||||
|
||||
function applyPriceSeries<
|
||||
Scale extends ResourceScale,
|
||||
T extends OHLC | number,
|
||||
>({
|
||||
chart,
|
||||
preset,
|
||||
dataset,
|
||||
seriesType,
|
||||
// valuesSkipped,
|
||||
options,
|
||||
}: {
|
||||
chart: IChartApi;
|
||||
preset: Preset;
|
||||
// valuesSkipped: Accessor<number>;
|
||||
seriesType: Accessor<"Candlestick" | "Line">;
|
||||
dataset: ResourceDataset<Scale, T>;
|
||||
options?: PriceSeriesOptions;
|
||||
}) {
|
||||
// console.time("series add");
|
||||
function updateVisiblePriceSeriesType(
|
||||
chart: IChartApi,
|
||||
range: LogicalRange,
|
||||
priceSeriesType: RWS<PriceSeriesType>,
|
||||
) {
|
||||
try {
|
||||
const width = chart.timeScale().width();
|
||||
|
||||
const id = options?.id || "price";
|
||||
const title = options?.title || "Price";
|
||||
const ratio = (range.to - range.from) / width;
|
||||
|
||||
const url = "url" in dataset ? (dataset as any).url : undefined;
|
||||
|
||||
const priceScaleOptions: DeepPartialPriceScaleOptions = {
|
||||
mode: 1,
|
||||
...options?.priceScaleOptions,
|
||||
};
|
||||
|
||||
let [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;
|
||||
|
||||
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);
|
||||
}
|
||||
if (ratio <= 0.5) {
|
||||
priceSeriesType.set("Candlestick");
|
||||
} else {
|
||||
priceSeriesType.set("Line");
|
||||
}
|
||||
});
|
||||
|
||||
return { ohlcLegend, lineLegend };
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// // const computeDrawnSeriesValues = debounce(_computeDrawnSeriesValues, 100);
|
||||
function createSeriesGroup<Scale extends ResourceScale>({
|
||||
activeRange,
|
||||
seriesConfig,
|
||||
preset,
|
||||
chartLegend,
|
||||
chart,
|
||||
disabled,
|
||||
lastActiveIndex,
|
||||
debouncedSetMinMaxMarkers,
|
||||
}: {
|
||||
activeRange: Accessor<number[]>;
|
||||
seriesConfig: SeriesConfig<Scale>;
|
||||
preset: Preset;
|
||||
chart: IChartApi;
|
||||
chartLegend: SeriesLegend[];
|
||||
lastActiveIndex: Accessor<number | undefined>;
|
||||
disabled?: Accessor<boolean>;
|
||||
debouncedSetMinMaxMarkers: VoidFunction;
|
||||
}) {
|
||||
const {
|
||||
dataset,
|
||||
title,
|
||||
colors,
|
||||
color,
|
||||
defaultVisible,
|
||||
seriesType: type,
|
||||
options,
|
||||
priceScaleOptions,
|
||||
} = seriesConfig;
|
||||
|
||||
// function computeDrawnSeriesValues<
|
||||
// S extends ResourceScale,
|
||||
// T extends OHLC | number,
|
||||
// >(
|
||||
// dataset: ResourceDataset<S, T>,
|
||||
// valuesSkipped: number,
|
||||
// callback: (chunks: any, v: number, l: number) => void,
|
||||
// // seriesList: ISeriesApi<any>[],
|
||||
// ) {
|
||||
// // console.time(dataset.url);
|
||||
const scale = preset.scale;
|
||||
|
||||
// const { fetchedJSONs, activeRange: _activeRange } = dataset;
|
||||
const seriesList: RWS<
|
||||
ISeriesApi<"Baseline" | "Line" | "Histogram" | "Candlestick"> | undefined
|
||||
>[] = new Array(dataset.fetchedJSONs.length);
|
||||
|
||||
// const activeRange = _activeRange();
|
||||
let defaultSeriesColor: string | string[] | undefined = undefined;
|
||||
|
||||
// const valuesSkippedPlus1 = valuesSkipped + 1;
|
||||
const legend = createSeriesLegend({
|
||||
id: stringToId(title),
|
||||
presetId: preset.id,
|
||||
title,
|
||||
seriesList,
|
||||
color: () =>
|
||||
colors || color || defaultSeriesColor || DEFAULT_BASELINE_COLORS,
|
||||
defaultVisible,
|
||||
disabled,
|
||||
dataset,
|
||||
});
|
||||
|
||||
// if (valuesSkippedPlus1 === 1) {
|
||||
// console.log("todo valuesSkippedPlus1===1, skip for now");
|
||||
// }
|
||||
chartLegend.push(legend);
|
||||
|
||||
// // for (let i = 0; i < seriesList.length; i++) {
|
||||
// // seriesList[i].
|
||||
// // }
|
||||
dataset.fetchedJSONs.forEach((json, index) => {
|
||||
const series: (typeof seriesList)[number] = createRWS(undefined);
|
||||
|
||||
// const chunks = new Array(activeRange.length);
|
||||
// let length = 0;
|
||||
seriesList[index] = series;
|
||||
|
||||
// for (let i = 0; i < chunks.length; i++) {
|
||||
// const chunk =
|
||||
// fetchedJSONs[chunkIdToIndex(dataset.scale, activeRange[i])]?.vec?.() ||
|
||||
// [];
|
||||
createEffect(() => {
|
||||
const values = json.vec();
|
||||
if (!values) return;
|
||||
|
||||
// chunks[i] = chunk;
|
||||
untrack(() => {
|
||||
let s = series();
|
||||
|
||||
// length += Math.ceil(chunk.length / valuesSkippedPlus1);
|
||||
// }
|
||||
if (!s) {
|
||||
switch (type) {
|
||||
case SeriesType.Based: {
|
||||
s = createBaseLineSeries(chart, {
|
||||
color,
|
||||
...options,
|
||||
});
|
||||
|
||||
// callback(chunks, valuesSkippedPlus1, length);
|
||||
break;
|
||||
}
|
||||
case SeriesType.Candlestick: {
|
||||
const candlestickSeries = createCandlesticksSeries(
|
||||
chart,
|
||||
options,
|
||||
);
|
||||
|
||||
// // setValues(chunks, valuesSkippedPlus1, length, callback);
|
||||
// // }
|
||||
s = candlestickSeries[0];
|
||||
defaultSeriesColor = candlestickSeries[1];
|
||||
|
||||
// // // const debouncedSetValues = debounce(setValues, 50);
|
||||
// // function setValues(
|
||||
// // chunks: any[],
|
||||
// // valuesSkippedPlus1: number,
|
||||
// // length: number,
|
||||
// // callback: (values: any[]) => void,
|
||||
// // ) {
|
||||
// // const values = new Array(length);
|
||||
break;
|
||||
}
|
||||
case SeriesType.Histogram: {
|
||||
s = createHistogramSeries(chart, {
|
||||
color,
|
||||
...options,
|
||||
});
|
||||
|
||||
// // let i = 0;
|
||||
// // for (let k = 0; k < activeRange.length; k++) {
|
||||
// // const chunk = chunks[k];
|
||||
// // // const chunk =
|
||||
// // // fetchedJSONs[chunkIdToIndex(dataset.scale, activeRange[k])]?.vec?.() ||
|
||||
// // // [];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
case SeriesType.Line: {
|
||||
s = createLineSeries(chart, {
|
||||
color,
|
||||
...options,
|
||||
});
|
||||
|
||||
// // 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]);
|
||||
// // }
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// // // i++;
|
||||
// // }
|
||||
// // }
|
||||
if (priceScaleOptions) {
|
||||
s.priceScale().applyOptions(priceScaleOptions);
|
||||
}
|
||||
|
||||
// // console.log(i);
|
||||
series.set(s);
|
||||
}
|
||||
|
||||
// // if (i !== values.length) {
|
||||
// // console.log({ n: i, values });
|
||||
// // throw Error("error");
|
||||
// // }
|
||||
s.setData(values);
|
||||
|
||||
// // console.timeEnd(dataset.url);
|
||||
// }
|
||||
untrack(() => {
|
||||
debouncedSetMinMaxMarkers();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const s = series();
|
||||
const currentVec = dataset.fetchedJSONs.at(index)?.vec();
|
||||
const nextVec = dataset.fetchedJSONs.at(index + 1)?.vec();
|
||||
|
||||
if (s && currentVec?.length && nextVec?.length) {
|
||||
s.update(nextVec[0]);
|
||||
}
|
||||
});
|
||||
|
||||
const isLast = createMemo(() => {
|
||||
const last = lastActiveIndex();
|
||||
return last !== undefined && last === index;
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
series()?.applyOptions({
|
||||
lastValueVisible: legend.drawn() && isLast(),
|
||||
});
|
||||
});
|
||||
|
||||
const inRange = createMemo(() => {
|
||||
const range = activeRange();
|
||||
|
||||
if (range.length) {
|
||||
const start = chunkIdToIndex(scale, range.at(0)!);
|
||||
const end = chunkIdToIndex(scale, range.at(-1)!);
|
||||
|
||||
if (index >= start && index <= end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const visible = createMemo((previous: boolean) => {
|
||||
if (legend.disabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return previous || inRange();
|
||||
}, false);
|
||||
|
||||
createEffect(() => {
|
||||
series()?.applyOptions({
|
||||
visible: legend.drawn() && visible(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return legend;
|
||||
}
|
||||
|
||||
@@ -162,7 +162,6 @@ export function createPresets() {
|
||||
{
|
||||
title: "Mined",
|
||||
color: colors.bitcoin,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset: params.datasets.date.total_blocks_mined,
|
||||
},
|
||||
],
|
||||
@@ -184,7 +183,6 @@ export function createPresets() {
|
||||
{
|
||||
title: "Size (MB)",
|
||||
color: colors.darkWhite,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset: params.datasets.date.cumulative_block_size,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -4,7 +4,10 @@ import { colors } from "../utils/colors";
|
||||
import { replaceHistory } from "../utils/history";
|
||||
import { stringToId } from "../utils/id";
|
||||
import { resetURLParams } from "../utils/urlParams";
|
||||
import { createPresets as createAddressesPresets } from "./addresses";
|
||||
import {
|
||||
createPresets as createAddressesPresets,
|
||||
createLiquidityFolder,
|
||||
} from "./addresses";
|
||||
import { createPresets as createBlocksPresets } from "./blocks";
|
||||
import { createPresets as createCoinblocksPresets } from "./coinblocks";
|
||||
import { createPresets as createHodlersPresets } from "./hodlers";
|
||||
@@ -31,7 +34,7 @@ export function createPresets(): Presets {
|
||||
{
|
||||
name: "By Date",
|
||||
tree: [
|
||||
createMarketPresets({ scale: "date" }),
|
||||
createMarketPresets("date"),
|
||||
createBlocksPresets(),
|
||||
createMinersPresets("date"),
|
||||
createTransactionsPresets("date"),
|
||||
@@ -42,6 +45,12 @@ export function createPresets(): Presets {
|
||||
name: "",
|
||||
title: "",
|
||||
}),
|
||||
createLiquidityFolder({
|
||||
scale: "date",
|
||||
color: colors.bitcoin,
|
||||
datasetKey: "",
|
||||
name: "",
|
||||
}),
|
||||
createHodlersPresets({ scale: "date" }),
|
||||
createAddressesPresets({ scale: "date" }),
|
||||
createCoinblocksPresets({ scale: "date" }),
|
||||
@@ -50,7 +59,7 @@ export function createPresets(): Presets {
|
||||
{
|
||||
name: "By Height",
|
||||
tree: [
|
||||
createMarketPresets({ scale: "height" }),
|
||||
createMarketPresets("height"),
|
||||
createMinersPresets("height"),
|
||||
createTransactionsPresets("height"),
|
||||
...createCohortPresetList({
|
||||
@@ -60,6 +69,12 @@ export function createPresets(): Presets {
|
||||
datasetKey: "",
|
||||
title: "",
|
||||
}),
|
||||
createLiquidityFolder({
|
||||
scale: "height",
|
||||
color: colors.bitcoin,
|
||||
datasetKey: "",
|
||||
name: "",
|
||||
}),
|
||||
createHodlersPresets({ scale: "height" }),
|
||||
createAddressesPresets({ scale: "height" }),
|
||||
createCoinblocksPresets({ scale: "height" }),
|
||||
@@ -101,7 +116,7 @@ export function createPresets(): Presets {
|
||||
const serializedHistory: SerializedPresetsHistory = history().map(
|
||||
({ preset, date }) => ({
|
||||
p: preset.id,
|
||||
d: date.valueOf(),
|
||||
d: date.getTime(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createPresets as createAveragesPresets } from "./averages";
|
||||
import { createPresets as createIndicatorsPresets } from "./indicators";
|
||||
import { createPresets as createReturnsPresets } from "./returns";
|
||||
|
||||
export function createPresets({ scale }: { scale: ResourceScale }) {
|
||||
export function createPresets(scale: ResourceScale) {
|
||||
return {
|
||||
name: "Market",
|
||||
tree: [
|
||||
@@ -18,25 +18,6 @@ export function createPresets({ scale }: { scale: ResourceScale }) {
|
||||
},
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
scale,
|
||||
icon: IconTablerPercentage,
|
||||
name: "Performance",
|
||||
title: "Market Performance",
|
||||
applyPreset(params) {
|
||||
return applySeriesList({
|
||||
...params,
|
||||
priceOptions: {
|
||||
id: "performance",
|
||||
title: "Performance",
|
||||
priceScaleOptions: {
|
||||
mode: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
scale,
|
||||
icon: IconTablerInfinity,
|
||||
|
||||
@@ -60,7 +60,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
{
|
||||
title: "Count",
|
||||
color,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset: params.datasets[scale][`${datasetPrefix}utxo_count`],
|
||||
},
|
||||
],
|
||||
@@ -105,7 +104,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
{
|
||||
title: `${name} Realized Cap.`,
|
||||
color,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset:
|
||||
params.datasets[scale][`${datasetPrefix}realized_cap`],
|
||||
},
|
||||
@@ -160,7 +158,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
dataset:
|
||||
params.datasets[scale][`${datasetPrefix}realized_profit`],
|
||||
color: colors.profit,
|
||||
seriesType: SeriesType.Area,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -181,7 +178,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
dataset:
|
||||
params.datasets[scale][`${datasetPrefix}realized_loss`],
|
||||
color: colors.loss,
|
||||
seriesType: SeriesType.Area,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -274,7 +270,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
{
|
||||
title: "Cumulative Realized Profit",
|
||||
color: colors.profit,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset:
|
||||
params.datasets[scale][
|
||||
`${datasetPrefix}cumulative_realized_profit`
|
||||
@@ -297,7 +292,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
{
|
||||
title: "Cumulative Realized Loss",
|
||||
color: colors.loss,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset:
|
||||
params.datasets[scale][
|
||||
`${datasetPrefix}cumulative_realized_loss`
|
||||
@@ -371,7 +365,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
dataset:
|
||||
params.datasets[scale][`${datasetPrefix}unrealized_profit`],
|
||||
color: colors.profit,
|
||||
seriesType: SeriesType.Area,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -393,7 +386,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
dataset:
|
||||
params.datasets[scale][`${datasetPrefix}unrealized_loss`],
|
||||
color: colors.loss,
|
||||
seriesType: SeriesType.Area,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -539,7 +531,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
{
|
||||
title: "Supply",
|
||||
color,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset: params.datasets[scale][`${datasetPrefix}supply`],
|
||||
},
|
||||
],
|
||||
@@ -558,7 +549,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
{
|
||||
title: "Supply",
|
||||
color: colors.profit,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset:
|
||||
params.datasets[scale][
|
||||
`${datasetPrefix}supply_in_profit`
|
||||
@@ -581,7 +571,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
{
|
||||
title: "Supply",
|
||||
color: colors.loss,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset:
|
||||
params.datasets[scale][
|
||||
`${datasetPrefix}supply_in_loss`
|
||||
@@ -658,7 +647,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
{
|
||||
title: "Supply",
|
||||
color,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset:
|
||||
params.datasets[scale][
|
||||
`${datasetPrefix}supply_to_circulating_supply_ratio`
|
||||
@@ -681,7 +669,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
{
|
||||
title: "Supply",
|
||||
color: colors.profit,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset:
|
||||
params.datasets[scale][
|
||||
`${datasetPrefix}supply_in_profit_to_circulating_supply_ratio`
|
||||
@@ -703,7 +690,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
bottom: [
|
||||
{
|
||||
title: "Supply",
|
||||
seriesType: SeriesType.Area,
|
||||
color: colors.loss,
|
||||
dataset:
|
||||
params.datasets[scale][
|
||||
@@ -779,7 +765,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
{
|
||||
title: "Supply",
|
||||
color: colors.profit,
|
||||
seriesType: SeriesType.Area,
|
||||
dataset:
|
||||
params.datasets[scale][
|
||||
`${datasetPrefix}supply_in_profit_to_own_supply_ratio`
|
||||
@@ -801,7 +786,6 @@ export function createCohortPresetList<Scale extends ResourceScale>({
|
||||
bottom: [
|
||||
{
|
||||
title: "Supply",
|
||||
seriesType: SeriesType.Area,
|
||||
color: colors.loss,
|
||||
dataset:
|
||||
params.datasets[scale][
|
||||
|
||||
Vendored
+13
-2
@@ -24,7 +24,9 @@ type ApplyPreset = (params: {
|
||||
parentDiv: HTMLDivElement;
|
||||
datasets: Datasets;
|
||||
preset: Preset;
|
||||
legendSetter: Setter<PresetLegend>;
|
||||
legendSetter: Setter<SeriesLegend[]>;
|
||||
dark: boolean;
|
||||
activeRange: RWS<number[]>;
|
||||
}) => void;
|
||||
|
||||
interface PartialPresetFolder {
|
||||
@@ -58,4 +60,13 @@ interface Presets {
|
||||
select(preset: Preset): void;
|
||||
}
|
||||
|
||||
type PresetLegend = SeriesLegend[];
|
||||
type PriceSeriesType = "Candlestick" | "Line";
|
||||
|
||||
interface ChartObject {
|
||||
scale: ResourceScale;
|
||||
div: HTMLDivElement;
|
||||
chart: IChartApi;
|
||||
whitespace: ISeriesApi<"Line">;
|
||||
legendList: SeriesLegend[];
|
||||
debouncedSetMinMaxMarkers: VoidFunction;
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ export const dateToString = (date: Date) => date.toJSON().split("T")[0];
|
||||
// export const FIVE_MONTHS_IN_DAYS = 30 * 5;
|
||||
|
||||
export const getNumberOfDaysBetweenTwoDates = (oldest: Date, youngest: Date) =>
|
||||
Math.round(Math.abs((youngest.valueOf() - oldest.valueOf()) / ONE_DAY_IN_MS));
|
||||
Math.round(Math.abs((youngest.getTime() - oldest.getTime()) / ONE_DAY_IN_MS));
|
||||
|
||||
@@ -31,7 +31,6 @@ export const krakenAPI = {
|
||||
|
||||
const candle: DatasetCandlestickData = {
|
||||
// date: dateStr,
|
||||
number: new Date(dateStr).valueOf() / ONE_DAY_IN_MS,
|
||||
time: dateStr,
|
||||
open: Number(open),
|
||||
high: Number(high),
|
||||
|
||||
+4
@@ -54,6 +54,10 @@ type DeepPartialLineOptions = DeepPartial<
|
||||
LineStyleOptions & SeriesOptionsCommon
|
||||
>;
|
||||
|
||||
type DeepPartialCandlestickOptions = DeepPartial<
|
||||
CandlestickStyleOptions & SeriesOptionsCommon
|
||||
>;
|
||||
|
||||
type DeepPartialHistogramOptions = DeepPartial<
|
||||
HistogramStyleOptions & SeriesOptionsCommon
|
||||
>;
|
||||
|
||||
Vendored
-4
@@ -6,10 +6,6 @@ interface Heighted {
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface Numbered {
|
||||
number: number;
|
||||
}
|
||||
|
||||
interface Valued {
|
||||
value: number;
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -1272,7 +1272,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parser"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"allocative",
|
||||
"bincode",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "parser"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -25,3 +25,4 @@ The backbone of the project, it does most of the work by parsing and then comput
|
||||
- Avoid floats as much as possible
|
||||
- Use structs like `WAmount` and `Price` for calculations
|
||||
- **Only** use `WAmount.to_btc()` when inserting or computing inside a dataset. It is **very** expensive.
|
||||
- No `Arc`, `Rc`, `Mutex` even from third party libraries, they're slower
|
||||
|
||||
Generated
+2
-2
@@ -1463,7 +1463,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parser"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"allocative",
|
||||
"bincode",
|
||||
@@ -1907,7 +1907,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "server"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"bincode",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "server"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,2 +1,16 @@
|
||||
# Restart cloudflared
|
||||
# Sometimes it's acting up
|
||||
if command -v cloudflared &> /dev/null; then
|
||||
# For Mac OS users
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
echo "Restarting cloudflared..."
|
||||
|
||||
sudo launchctl stop com.cloudflare.cloudflared
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.cloudflare.cloudflared.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.cloudflare.cloudflared.plist
|
||||
sudo launchctl start com.cloudflare.cloudflared
|
||||
fi
|
||||
fi
|
||||
|
||||
cargo watch -w "./src" -w "./run.sh" -x "run -r"
|
||||
# cargo watch -w "./src" -w "./run.sh" -w "../datasets/disk_path_to_type.json" -x "run -r"
|
||||
|
||||
Reference in New Issue
Block a user