Compare commits

...

68 Commits

Author SHA1 Message Date
k b68b016091 release: v0.3.0 2024-07-26 00:59:00 +02:00
k f1f4ad2188 global: fix: bugs 2024-07-26 00:44:17 +02:00
k d3d5e7f8d7 general: snapshot 2024-07-25 14:43:20 +02:00
k 0f8d7d5fe2 readme: add bkp api 2024-07-24 00:18:55 +02:00
k 63855e93a1 changelog: update v0.3.0 screenshot 2024-07-24 00:15:35 +02:00
k 07c1f5ab76 changelog: add v0.3.0 screenshot 2024-07-24 00:13:01 +02:00
k 4cd605fd34 global: update versions 2024-07-24 00:09:09 +02:00
k 8f5f28ede6 app: charts: add unit and price mode switch 2024-07-24 00:05:18 +02:00
k bf169d6954 app: add ivo to donators 2024-07-23 08:44:52 +02:00
k 1934c4bfda price: rm folder 2024-07-22 23:40:15 +02:00
k 5a7050df02 app: add chart scroll buttons 2024-07-22 19:06:58 +02:00
k 9871fdffc9 parser: add auto fetch price from main instance 2024-07-22 15:58:02 +02:00
k 232276d106 app: add height datasets 2024-07-22 11:08:58 +02:00
k 8b08a82f07 parser: add recap dataset 2024-07-21 22:59:54 +02:00
k 180d044f5d app: add SOPR 2024-07-21 00:51:36 +02:00
k 5611459f03 server: fix port 2024-07-20 23:18:49 +02:00
k 6eb4b51168 parser: cleanup 2024-07-20 23:15:36 +02:00
k a145b35ad1 general: snapshot 2024-07-20 23:13:41 +02:00
k d8a5b4a2e6 price: update 2024-07-20 13:32:58 +02:00
k 1f9d1542f1 parser: fix utxo panic after soft reset 2024-07-18 12:27:24 +02:00
k 4d23fdef61 general: snapshot 2024-07-18 09:16:18 +02:00
k fb978211ae price: push new 2024-07-16 11:48:41 +02:00
k 4fd67ebd99 app: add address size in sidebar when needed 2024-07-15 19:59:44 +02:00
k 0c899b2c16 parser: fix config file creation and remove panic 2024-07-15 19:43:37 +02:00
k 1be22713f9 parser: create config.toml if needed 2024-07-15 19:07:00 +02:00
k ad51edbe07 parser: setup clap 2024-07-15 18:52:29 +02:00
k 91f2427b44 app: add random chart button 2024-07-15 08:54:55 +02:00
k fbbb0920c5 parser: cohort ratio name changes 2024-07-13 00:40:47 +02:00
k 66bca200b4 parser: cointime forgot to add compute 2024-07-12 20:46:11 +02:00
k 5f11f15fe1 global: cointime ratios 2024-07-12 20:39:27 +02:00
k 96a50dd09a global: snapshot 2024-07-12 19:31:21 +02:00
k dcf605aa69 parser: percentiles fixed 2024-07-12 19:06:52 +02:00
k 6c7bd2a63a parser: percentiles 2024-07-12 18:08:27 +02:00
k 85835ac1d3 parser: add min date and height for percentile datasets 2024-07-12 17:38:22 +02:00
k 61038b07f9 parser: trying to fix ratio smas 2024-07-12 13:55:44 +02:00
k 68700925b0 parser: add 0 and 1 constant datasets 2024-07-12 12:55:46 +02:00
k 46f8e3bafd app: lazy load lean-qr 2024-07-12 12:47:05 +02:00
k 9077fee4d6 parser: add ratio for price smas 2024-07-12 12:02:57 +02:00
k 35fd5054aa general: snapshot 2024-07-12 08:35:41 +02:00
k 350c835873 parser: improve error message when price cannot be found 2024-07-10 21:34:55 +02:00
k 707ed7ec26 parser: update readme 2024-07-10 20:56:36 +02:00
k e159f18bfc parser: add node.args to git ignore 2024-07-10 20:51:27 +02:00
k 2308fa173a parser: allow node args 2024-07-10 20:51:03 +02:00
k 4a82ee0b05 parser: added ratio and co datasets 2024-07-10 18:34:01 +02:00
k 6976f5af0f general: snapshot 2024-07-10 18:33:24 +02:00
k 59cb524226 server: remove cloudflared restart from run script 2024-07-10 13:20:45 +02:00
k 37e1d2ba5b changelog: add mempool links 2024-07-08 20:47:36 +02:00
k 6c21e970aa changelog: update v0.2.0 date 2024-07-08 20:46:17 +02:00
k d3a4e917fb assets: add v0.2.0 screenshot 2024-07-08 20:43:56 +02:00
k 2481878892 release: v0.2.0 2024-07-08 19:57:30 +02:00
k 04359fbf31 general: snapshot 2024-07-08 17:31:51 +02:00
k 80ea12ed48 app: flatten lightweight-chart scripts 2024-07-06 12:05:11 +02:00
k 9d2d4b7d5f readme: update 2024-07-06 11:10:02 +02:00
k 3de2862655 readme: update 2024-07-06 10:23:54 +02:00
k abb4def848 readme: update 2024-07-06 09:08:11 +02:00
k 04decabc46 parser: fix ulimit only running in Mac OS 2024-07-05 22:23:00 +02:00
k 334ff52084 app: fix start date not being 1970-01-01 2024-07-05 18:28:01 +02:00
k a931ad7a1e general: snapshot 2024-07-05 18:03:53 +02:00
k 069311dcf3 general: snapshot 2024-07-03 20:40:35 +02:00
k b7e8cbea20 general: snapshot 2024-06-30 17:01:15 +02:00
k 9905eff383 readme: update 2024-06-25 22:29:27 +02:00
k 48320197f9 app: add geyser button 2024-06-25 22:06:48 +02:00
k 9c9a835f33 changelog: deleted image by mistake 2024-06-25 16:21:09 +02:00
k ec477b916b update: readme 2024-06-25 16:12:59 +02:00
k dc5a1fcb9a update: readme 2024-06-25 16:11:09 +02:00
k 20a51f980b general: snapshot 2024-06-25 14:46:23 +02:00
k 7604787fbb app: add mini window support 2024-06-24 06:50:23 +02:00
k e55b5195a9 release: v0.1.1 2024-06-24 05:14:52 +02:00
254 changed files with 12532 additions and 10032 deletions
+6
View File
@@ -1,7 +1,13 @@
.DS_Store
/app-next
/app-html
/datasets
/datasets2
/datasets_*
TODO.md
.stfolder
/charts
/price
+138 -3
View File
@@ -1,6 +1,123 @@
# Changelog
## v. 0.1.1 - WIP
## v. 0.3.0 | [853930](https://mempool.space/block/00000000000000000002eb5e9a7950ca2d5d98bd1ed28fc9098aa630d417985d) - 2024/07/26
![Image of the Satonomics Web App version 0.3.0](./assets/v0.3.0.jpg)
### Parser
- Global
- Improved self-hosting by:
- Fixing an incredibly annoying bug that made the program panic because of a wrong utxo/address durable state after a or many new datasets were added/changed after a first successful parse of the chain
- Fixing a bug that would crash the program if launched for the first time ever
- Auto fetch prices from the main Satonomics instance if missing instead of only trying Kraken's and Binance's API which are limited to the last 16 hours
- Merged the core of `HeightMap` and `DateMap` structs into `GenericMap`
- Added `Height` struct and many others
- Reorganized outputs of both the parser and the server for ease of use and easier sync compatibility
- CLI
- Added an argument parser for improved UX with several options
- Datasets
- Added the following datasets for all entities:
- Value destroyed
- Value created
- Spent Output Profit Ratio (SOPR)
- Added the following ratio datasets and their variations to all prices {realized, moving average, any cointime, etc}:
- Market Price to {X}
- Market Price to {X} Ratio
- Market Price to {X} Ratio 1 Week SMA
- Market Price to {X} Ratio 1 Month SMA
- Market Price to {X} Ratio 1 Year SMA
- Market Price to {X} Ratio 1 Year SMA Momentum Oscillator
- Market Price to {X} Ratio 99th Percentile
- Market Price to {X} Ratio 99.5th Percentile
- Market Price to {X} Ratio 99.9th Percentile
- Market Price to {X} Ratio 1st Percentile
- Market Price to {X} Ratio 0.5th Percentile
- {X} 1% Top Probability
- {X} 0.5% Top Probability
- {X} 0.1% Top Probability
- {X} 1% Bottom Probability
- {X} 0.5% Bottom Probability
- {X} 0.1% Bottom Probability
- Added block metadatasets and their variants (raw/sum/average/min/max/percentiles):
- Block size
- Block weight
- Block VBytes
- Block interval
- Price
- Improved error message when price cannot be found
### App
- General
- Added chart scroll button for nice animations à la Wicked
- Added scale mode switch (Linear/Logarithmic) at the bottom right of all charts
- Added unit at the top left of all charts
- Added a backup API in case the main one fails or is offline
- Complete redesign of the datasets object
- Removed import of routes in JSON in favor for hardcoded typed routes in string format which resulted in:
- \+ A much lighter app
- \+ Better Lighthouse score
- \- Slower Typescript server
- Fixed datasets with null values crashing their fetch function
- Added a 'Go to a random chart' button in several places
- Chart
- Fixed series color being set to default ones after hovering the legend
- Fixed chart starting showing candlesticks and quickly switching to a line when it should've started directly with the line
- Separated the QRCode generator library from the main chunk and made it imported on click
- Fixed timescale changing on small screen after changing charts
- Folders
- Added the size in the "filename" of address cohorts grouped by size
- Favorites
- Added a 'favorite' and 'unfavorite' button at the bottom
- Settings
- Removed the horizontal scroll bar which was unintended
### Server
- Run file
- Only run with a watcher if `cargo watch` is available
- Removed id_to_path file in favor for only `paths.d.ts` in `app/src/types`
## v. 0.2.0 | [851286](https://mempool.space/block/0000000000000000000281ca7f1bf8c50702bfca168c7af1bdc67c977c1ac8ed) - 2024/07/08
![Image of the Satonomics Web App version 0.2.0](./assets/v0.2.0.jpg)
### App
- General
- Added the height version of all datasets and many optimizations to make them usable but only available on desktop and tablets for now
- Added a light theme
- Charts
- 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
- Settings
- Finally made a proper component where you can chose the app's theme, between a moving or static background and its text opacity
- Added donations section with a leaderboard
- Added various links that are visible on the bottom side of the strip on desktop to mobile users
- Added install instructions when not installed for Apple users
- Misc
- Support mini window size, could be useful for embedded views
- Hopefully made scrollbars a little more subtle on WIndows and Linux, can't test
- Generale style updates
### Parser
- Fixed ulimit only being run in Mac OS instead of whenever the program is detected
## v. 0.1.1 | [849240](https://mempool.space/block/000000000000000000002b8653988655071c07bb5f7181c038f9326bc86db741) - 2024/06/24
![Image of the Satonomics Web App version 0.1.1](./assets/v0.1.1.jpg)
### Parser
@@ -9,7 +126,7 @@
### Server
- Added the chunk, date and time in the terminal logs
- Added the chunk, date and time of the request to the terminal logs
### App
@@ -18,6 +135,10 @@
- Added highlight effect to a legend by darkening the color of all the other series on the chart while hovering it with the mouse
- Added an API link in the legend for each dataset where applicable (when isn't generated locally)
- Save fullscreen preference in local storage and url
- Improved resize bar on desktop
- Changed resize button logo
- Changed the share button to visible on small screen too
- Improved share screen
- Fixed time range shifting not being the one in url params or saved in local storage
- Fixed time range shifting on series toggling via the legend
- Fixed time range shifting on fullscreen
@@ -27,13 +148,27 @@
- History
- Changed background for the sticky dates from blur to a solid color as it didn't appear properly in Firefox
- Build
- Added lazy loads to have split chunks after build
- Tried to add lazy loads to have split chunks after build, to have much faster load times and they worked great ! But they completely broke Safari on iOS, we can't have nice things
- Removed many libraries and did some things manually instead to improve build size
- Strip
- Temporarily removed the Home button on the strip bar on desktop as there is no landing page yet
- Settings
- Added version
- PWA
- Fixed background update
- Changed update check frequency to 1 minute (~1kb to fetch every minute which is very reasonable)
- Added a nice banner to ask the user to install the update
- Misc
- Removed tracker even though it was a very privacy friendly as it appeared to not be working properly
### Price
- Deleted old price datasets and their backups
## v. 0.1.0 | [848642](https://mempool.space/block/000000000000000000020be5761d70751252219a9557f55e91ecdfb86c4e026a) - 2024/06/19
![Image of the Satonomics Web App version 0.1.0](./assets/v0.1.0.jpg)
## v. 0.0.X | [835444](https://mempool.space/block/000000000000000000009f93907a0dd83c080d5585cc7ec82c076d45f6d7c872) - 2024/03/20
![Image of the Satonomics Web App version 0.0.X](./assets/v0.0.X.jpg)
+61 -17
View File
@@ -1,44 +1,88 @@
# SATONOMICS
![Image of the Satonomics Web App](./assets/latest.jpg)
## Description
TLDR: FOSS [glassnode](https://glassnode.com).
Satonomics is a better, FOSS, Bitcoin-only, self-hostable Glassnode.
Satonomics is an open-source suite of tools that computes, distributes, and displays on-chain data, making it freely available for anyone to use.
While [mempool.space](https://mempool.space) gives a very micro view of the network where you can follow the journey of any address, this tool is the exact opposite and very complimentary by giving you a much more global/macro view of the flow and various dynamics of the network via thousands of charts.
The generated datasets are incredibly diverse and can be used for a wide range of purposes. Whether you're looking to conduct a health check on the network, gain insights into its current or past state, or leverage the data for trading purposes, these tools offer various charts, dashboards (Soon TM), and an extensive API to help you achieve your goals.
To promote even more transparency and trust in the network, this project is committed to making on-chain data accessible and verifiable by all, no matter your intentions or financial situation. That is why, the whole project is completely free, from code to services, including a real-time API with thousands and thousands of routes which can be used at will.
To promote transparency and trust in the network, this project is committed to making on-chain data accessible and verifiable to all, without discrimination and is a great complimentary tool to [mempool.space](https://mempool.space).
**Having anyone be able to easily do a health-check of the network is incredibly important and should be wanted by every single bitcoiner.**
## Warning
This project is in a very early stage. The web app will have bugs, the API might break and the data can definitely to be false or slightly false.
## Instances
Web App:
- [app.satonomics.xyz](https://app.satonomics.xyz)
API:
- [api.satonomics.xyz](https://api.satonomics.xyz)
- [api-bkp.satonomics.xyz](https://api-bkp.satonomics.xyz)
## Structure
- `parser`: The backbone of the project, it does most of the work by parsing and then computing datasets from the timechain.
- `server`: A small server which automatically creates routes to access through an API all created datasets.
- `app`: A web app which displays the generated datasets in various charts.
- `app`: A web app which displays the generated datasets in various charts and dashboards.
## Git
## Goals / Philosophy
- [Repository](https://codeberg.org/satonomics/satonomics)
- [Issues](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/issues)
- [Proposals](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/proposals)
Adjectives that describe what this project is or strives to be, in no particular order:
## Goals
- **Best**: Replace Glassnode as the go to
- **Diverse**: Have as many charts/datasets as possible and something for everyone
- **Free**: Is and always will be completely free
- **Open**: With a very permissive license
- **Trustless**: You can verify and see exactly how each dataset is computed
- **Independent**: Only one, easily swappable, dependency (Price API)
- **Educational**: By providing many datasets that can be used to describe how Bitcoin works and why
- **Timeless**: Be relevant and usable 10 years from now by being independent and not do address tagging
- **Sovereign**: Be self-hostable on accessible hardware
- **Versatile**: You can view the data in charts, you can download the data, you can fetch the data via an API
- **Accessible**: Free Website and API with all the datasets for everyone
- Be the absolute best on-chain data source and app
- Have as many datasets and charts as possible
- Be self-hostable on cheap computers
- Be runnable on a machine with 8 GB RAM (16 GB RAM is already possible right now)
- Still being runnable 10 years from now
- By not relying on any third-party dependencies besides price APIs (which are and should be very common and easy to update)
- By **NOT** doing address labelling/tagging (which means **NO** exchange or any other individual address tracking), for that please use [mempool.space](https://mempool.space) or any other tool
## Milestones
Big features that are planned, in no particular order:
- **Homepage**: A landing page to explains the project and what it does
- **More Datasets/Charts**: If a dataset can be computed, it should exist and have its related charts
- **Dashboards**: For a quick and real-time view of the latest data of all the datasets
- **NOSTR integration**: First to save preferences, later to add some social functionnality
- **Datasets by block timestamp**: In addition to having datasets by block date and block height
- **Descriptions**: Add text to describe all charts and what they mean
- **Start9 Add-on**: By making the whole suite much easier to self-host, it's quite rough right now
- **API Documentation**: Highly needed to explain what's what
_Maybe_:
- A Desktop app
- A mobile app
## Brand
- **Name**: Willing to change if someone thinks of something better !
- **Logo**: Most likely a placeholder
## Collaboration
- Repositories:
- [Github](https://github.com/satonomics-org/satonomics)
- [Codeberg](https://codeberg.org/satonomics/satonomics)
- Issues:
- [Github](https://github.com/satonomics-org/satonomics/issues)
- [NOSTR](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/issues)
- Proposals:
- [Github](https://github.com/satonomics-org/satonomics/pulls)
- [NOSTR](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/proposals)
## Proof of Work
+2 -1
View File
@@ -6,4 +6,5 @@ dev-dist
visualizer
# Local Netlify folder
.netlify
.wrangler
.wrangler
paths.d.ts
+9 -2
View File
@@ -6,5 +6,12 @@ A web app to view the generated datasets in various charts.
## Requirements
- `node`
- `pnpm`
- Install `node`
- Install `pnpm`
- If using `cloudflare`, add cache rule to bypass the cache for `/sw.js`
## Deployment
```bash
pnpm deploy-prod
```
+6 -3
View File
@@ -1,11 +1,11 @@
<!doctype html>
<html lang="en" class="overflow-hidden bg-black text-white">
<html lang="en">
<head>
<meta charset="utf-8" />
<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"
@@ -362,7 +362,10 @@
media="(prefers-color-scheme: dark) and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
</head>
<body style="font-size: 15px; line-height: 22px">
<body
class="text-high-contrast overflow-hidden bg-white dark:bg-black"
style="font-size: 15px; line-height: 22px"
>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
+15 -16
View File
@@ -1,7 +1,7 @@
{
"name": "satonomics",
"description": "Satoshi Economics",
"version": "0.1.0",
"description": "A better, FOSS, Bitcoin-only, self-hostable Glassnode",
"version": "0.3.0",
"license": "MIT",
"type": "module",
"scripts": {
@@ -19,30 +19,29 @@
"@leeoniya/ufuzzy": "^1.0.14",
"@solid-primitives/event-listener": "^2.3.3",
"@solid-primitives/intersection-observer": "^2.1.6",
"@solid-primitives/memo": "^1.3.8",
"@solid-primitives/resize-observer": "^2.0.25",
"@solid-primitives/resize-observer": "^2.0.26",
"lean-qr": "^2.3.4",
"lightweight-charts": "^4.1.6",
"solid-js": "^1.8.17"
"lightweight-charts": "^4.1.7",
"solid-js": "^1.8.19"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
"@iconify-json/tabler": "^1.1.114",
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@iconify-json/tabler": "^1.1.118",
"@tailwindcss/container-queries": "^0.1.1",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"prettier": "^3.3.2",
"postcss": "^8.4.40",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.5",
"pwa-asset-generator": "^6.3.1",
"rollup-plugin-visualizer": "^5.12.0",
"tailwindcss": "^3.4.4",
"typescript": "^5.5.2",
"unplugin-auto-import": "^0.17.6",
"tailwindcss": "^3.4.6",
"typescript": "^5.5.4",
"unplugin-auto-import": "^0.18.2",
"unplugin-icons": "^0.19.0",
"vite": "^5.3.1",
"vite-plugin-pwa": "^0.20.0",
"vite": "^5.3.5",
"vite-plugin-pwa": "^0.20.1",
"vite-plugin-solid": "^2.10.2",
"workbox-window": "^7.1.0",
"wrangler": "^3.61.0"
"wrangler": "^3.66.0"
}
}
+806 -801
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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",
+67 -48
View File
@@ -4,6 +4,8 @@ const texts = [
"satonomics",
"satonomics",
"satonomics",
"satonomics",
"satonomics",
"stay humble, stack sats",
"21 million",
@@ -13,7 +15,7 @@ const texts = [
"hodl",
`don't trust, verify`,
"zap",
"bitcoin",
"itcoin",
"lightning",
"nostr",
"freedom tech",
@@ -29,52 +31,60 @@ const texts = [
"be your own bank",
"resistance money",
"foss",
"permissionless",
"great reset",
"orange pill",
"borderless",
"anonymous",
"nyknyc",
"low time preference",
"absolute scarcity",
"time is scarce",
"ride or die",
"cyberpunk",
];
export const LOCAL_STORAGE_MARQUEE_KEY = "bg-marquee";
export function Background({
marquee: on,
mode,
opacity,
focused,
}: {
marquee: Accessor<boolean>;
mode: SL<"Scroll" | "Static">;
opacity: SL<{ text: string; value: number }>;
focused: Accessor<boolean>;
}) {
createEffect(() => {
if (on()) {
localStorage.removeItem(LOCAL_STORAGE_MARQUEE_KEY);
} else {
localStorage.setItem(LOCAL_STORAGE_MARQUEE_KEY, "false");
}
});
return (
<>
<div class="absolute h-full w-full overflow-hidden opacity-[0.0333] will-change-auto">
<div
class="absolute h-full w-full overflow-hidden will-change-auto"
style={{
opacity: opacity.selected().value,
}}
>
<div class="-m-[2rem] -space-y-1 overflow-hidden md:-m-[1rem]">
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
<Line mode={mode} focused={focused} />
</div>
</div>
<div class="absolute h-full w-full opacity-10 mix-blend-multiply">
@@ -88,50 +98,59 @@ export function Background({
}
function Line({
on,
mode,
focused,
}: {
on: Accessor<boolean>;
mode: SL<"Scroll" | "Static">;
focused: Accessor<boolean>;
}) {
const shuffled = shuffle([...texts]);
shuffled.pop();
const shuffled = shuffle(texts).slice(0, 10);
const joined = shuffled.join(". ");
return (
<div class="select-none whitespace-nowrap">
<TextWrapper on={on} focused={focused} joined={joined} />
<TextWrapper mode={mode} focused={focused} joined={joined} />
</div>
);
}
function TextWrapper({
joined,
on,
mode,
focused,
}: {
on: Accessor<boolean>;
mode: SL<"Scroll" | "Static">;
focused: Accessor<boolean>;
joined: string;
}) {
const seconds = joined.length * 2;
const p = createRWS(undefined as HTMLParagraphElement | undefined);
const seconds = createRWS(joined.length * 2);
const wasOnceOn = createRWS(false);
createEffect(() => {
if (!wasOnceOn() && on()) {
if (!wasOnceOn() && mode.selected() === "Scroll") {
wasOnceOn.set(true);
}
});
onMount(() => {
seconds.set(Math.round(p()!.clientWidth / 20));
});
return (
<p
ref={p.set}
class="inline-block px-2 text-[5dvh] font-black uppercase leading-none"
style={{
...(wasOnceOn()
? {
animation: `marquee ${seconds}s linear infinite`,
"animation-play-state": focused() && on() ? "running" : "paused",
animation: `marquee ${seconds()}s linear infinite`,
"animation-play-state":
focused() && mode.selected() === "Scroll"
? "running"
: "paused",
}
: {}),
}}
+11 -111
View File
@@ -1,51 +1,21 @@
import { createResizeObserver } from "@solid-primitives/resize-observer";
import { classPropToString } from "/src/solid/classes";
import { createRWS } from "/src/solid/rws";
export function Box({
flex = true,
absolute,
padded = true,
spaced = true,
children,
dark,
overflowY,
classes,
}: {
flex?: boolean;
absolute?: "top" | "bottom";
padded?: boolean;
spaced?: boolean;
dark?: boolean;
overflowY?: boolean;
classes?: string;
} & ParentProps) {
const maybeScrollable = createRWS<HTMLDivElement | undefined>(undefined);
const scrollable = createRWS(false);
const showLeftArrow = createRWS(false);
const showRightArrow = createRWS(false);
onMount(() => {
createResizeObserver(maybeScrollable, (_, el) => {
if (el !== maybeScrollable()) {
return;
}
scrollable.set(() => el.scrollWidth > el.clientWidth);
checkArrows();
});
});
function checkArrows() {
const offset = 20;
const target = maybeScrollable()!;
const left = target.scrollLeft;
const right = target.scrollWidth - target.scrollLeft - target.clientWidth;
showLeftArrow.set(() => left > offset);
showRightArrow.set(() => right > offset);
}
return (
<div
class={classPropToString([
@@ -55,94 +25,24 @@ export function Box({
"absolute inset-x-0",
absolute === "top"
? "top-0"
: "pointer-events-none bottom-0 bg-gradient-to-b from-transparent to-black",
: "pointer-events-none bottom-0 bg-gradient-to-b from-transparent to-orange-100 dark:to-black",
]
: "relative",
classes,
])}
>
<div
class={classPropToString([
"pointer-events-auto relative overflow-hidden rounded-xl border border-orange-200/10 shadow-md",
"border-lighter pointer-events-auto relative overflow-hidden rounded-xl border shadow-md",
dark
? "bg-orange-100/5 backdrop-blur-sm"
: "bg-orange-200/10 backdrop-blur-md",
? "bg-white/40 backdrop-blur-sm dark:bg-orange-100/5"
: "bg-white/60 backdrop-blur-md dark:bg-orange-200/10",
])}
>
<For
each={[
{
showArrow: showLeftArrow,
side: "left-0",
order: "",
buttonPadding: "pl-3 pr-2",
iconPadding: "pr-0.5",
scrollMultiplier: -1,
chevronIcon: IconTablerChevronLeft,
gradientDirection: "bg-gradient-to-r",
},
{
showArrow: showRightArrow,
side: "right-0",
order: "order-2",
buttonPadding: "pl-2 pr-3",
iconPadding: "pl-0.5",
scrollMultiplier: 1,
chevronIcon: IconTablerChevronRight,
gradientDirection: "bg-gradient-to-l",
},
]}
>
{(obj) => (
<Show when={scrollable() && obj.showArrow()}>
<div
class={[
obj.side,
"pointer-events-none absolute bottom-0 top-0 z-20 flex transition-opacity duration-200 ease-in-out",
].join(" ")}
>
<div
class={[
obj.order,
obj.buttonPadding,
"pointer-events-auto hidden h-full items-center bg-black/90 md:flex",
].join(" ")}
>
<button
onClick={() => {
maybeScrollable()?.scrollBy({
left: Math.floor(
maybeScrollable()!.clientWidth *
obj.scrollMultiplier *
0.8,
),
behavior: "smooth",
});
}}
class="rounded-full border border-orange-200/20 bg-black p-0.5 transition hover:scale-110 active:scale-100"
>
<Dynamic
component={obj.chevronIcon}
class={[`size-5 ${obj.iconPadding}`]}
/>
</button>
</div>
<div
class={[
obj.gradientDirection,
"h-full w-10 from-black/90 to-transparent",
].join(" ")}
/>
</div>
</Show>
)}
</For>
<div
ref={maybeScrollable.set}
onScroll={checkArrows}
class={classPropToString([
flex && "flex w-full space-x-2",
overflowY && "overflow-y-auto",
flex && "flex w-full",
spaced && "space-x-2",
padded && "p-1.5",
])}
>
+18
View File
@@ -1,3 +1,5 @@
import { random } from "/src/scripts/utils/math/random";
export function Button({
onClick,
children,
@@ -11,3 +13,19 @@ export function Button({
</button>
);
}
export function ButtonRandomChart({ presets }: { presets: Presets }) {
return (
<button
class="inline-flex rounded-md bg-orange-700 bg-opacity-80 px-1.5 py-0.5 font-medium hover:bg-opacity-100 active:scale-95"
onClick={() => {
const randomPreset = random(presets.list);
if (randomPreset) {
presets.select(randomPreset);
}
}}
>
Open a random chart
</button>
);
}
@@ -1,9 +1,4 @@
import type { Generate } from "lean-qr";
import { chartState } from "/src/scripts/lightweightCharts/chart/state";
import { setTimeScale } from "/src/scripts/lightweightCharts/chart/time";
import { classPropToString } from "/src/solid/classes";
import { createRWS } from "/src/solid/rws";
import { Button } from "./button";
export function Actions({
presets,
@@ -14,45 +9,33 @@ export function Actions({
qrcode: RWS<string>;
fullscreen?: RWS<boolean>;
}) {
const leanQRGenerate = createRWS<Generate | undefined>(undefined);
onMount(() => {
import("lean-qr").then((leanQR) => {
leanQRGenerate.set(() => leanQR.generate);
});
});
const ButtonShare = lazy(() =>
import("./buttonShare").then((d) => ({ default: d.ButtonShare })),
);
return (
<div class="flex space-x-1">
<Button
icon={() => IconTablerMaximize}
onClick={() => {
const range = chartState.range;
<div class="flex space-x-1 p-1.5">
<Show when={fullscreen}>
{(fullscreen) => (
<Button
title="Toggle fullscreen"
icon={() =>
fullscreen()()
? IconTablerLayoutSidebarLeftExpand
: IconTablerLayoutSidebarRightExpand
}
onClick={() => {
fullscreen().set((b) => !b);
}}
classes="hidden md:block"
/>
)}
</Show>
fullscreen?.set((b) => !b);
<ButtonShare qrcode={qrcode} />
setTimeScale(range);
}}
classes="hidden md:block"
/>
<Button
icon={() => IconTablerShare}
disabled={() => !leanQRGenerate()}
onClick={() => {
let generate = leanQRGenerate();
if (generate) {
qrcode.set(() =>
generate(document.location.href).toDataURL({
on: [0xff, 0xff, 0xff, 0xff],
off: [0x00, 0x00, 0x00, 0x00],
}),
);
}
}}
classes="hidden md:block"
/>
<Button
title="Favorite"
colors={() =>
presets.selected().isFavorite()
? "text-amber-500 bg-amber-500/15 hover:bg-amber-500/30"
@@ -68,35 +51,3 @@ export function Actions({
</div>
);
}
function Button({
icon,
colors,
onClick,
disabled,
classes,
}: {
icon: () => ValidComponent;
colors?: () => string;
onClick: VoidFunction;
disabled?: () => boolean;
classes?: string;
}) {
return (
<button
disabled={disabled?.()}
class={classPropToString([
colors?.() || (disabled?.() ? "" : "hover:bg-orange-200/15"),
!disabled?.() && "group",
classes,
"flex-none rounded-lg p-2 disabled:opacity-50",
])}
onClick={onClick}
>
<Dynamic
component={icon()}
class="size-[1.125rem] group-active:scale-90"
/>
</button>
);
}
@@ -0,0 +1,36 @@
import { classPropToString } from "/src/solid/classes";
export function Button({
title,
icon,
colors,
onClick,
disabled,
classes,
}: {
title: string;
icon: () => ValidComponent;
colors?: () => string;
onClick: VoidFunction;
disabled?: () => boolean;
classes?: string;
}) {
return (
<button
title={title}
disabled={disabled?.()}
class={classPropToString([
colors?.() || (disabled?.() ? "" : "hover:bg-orange-200/15"),
!disabled?.() && "group",
classes,
"flex-none rounded-lg p-2 disabled:opacity-50",
])}
onClick={onClick}
>
<Dynamic
component={icon()}
class="size-[1.125rem] group-active:scale-90"
/>
</button>
);
}
@@ -0,0 +1,22 @@
import { Button } from "./button";
export function ButtonShare({ qrcode }: { qrcode: RWS<string> }) {
return (
<Button
title="Share"
icon={() => IconTablerShare}
onClick={() =>
import("lean-qr").then(({ generate }) =>
qrcode.set(() =>
generate(document.location.href).toDataURL({
on: [0xff, 0xff, 0xff, 0xff],
off: [0x00, 0x00, 0x00, 0x00],
padX: 0,
padY: 0,
}),
),
)
}
/>
);
}
@@ -1,33 +1,364 @@
import { cleanChart } from "/src/scripts/lightweightCharts/chart/clean";
import { renderChart } from "/src/scripts/lightweightCharts/chart/render";
import { requestIdleCallbackPossible } from "/src/env";
import { chunkIdToIndex } from "/src/scripts/datasets/resource";
import { createChart } from "/src/scripts/lightweightCharts/create";
import { createSeriesGroup } from "/src/scripts/lightweightCharts/group";
import { setMinMaxMarkers } from "/src/scripts/lightweightCharts/markers";
import {
debouncedUpdateVisiblePriceSeriesType,
updateVisiblePriceSeriesType,
} from "/src/scripts/lightweightCharts/price";
import {
initTimeScale,
setInitialTimeRange,
} from "/src/scripts/lightweightCharts/time";
import { setWhitespace } from "/src/scripts/lightweightCharts/whitespace";
import { SeriesType } from "/src/scripts/presets/enums";
import { colors } from "/src/scripts/utils/colors";
import { debounce } from "/src/scripts/utils/debounce";
import { createSL } from "/src/scripts/utils/selectableList/static";
import { webSockets } from "/src/scripts/ws";
import { classPropToString } from "/src/solid/classes";
import { createRWS } from "/src/solid/rws";
import { RadioGroup } from "../../settings";
export function Chart({
presets,
activeDatasets,
activeIds,
charts,
chartsDrawn,
dark,
datasets,
exactRange,
firstChartSetter,
index,
lastActiveIndex,
lastChartIndex,
legendSetter,
activeResources,
preset: presetAccessor,
priceSeriesType,
seriesConfigs,
seriesCount,
}: {
presets: Presets;
activeDatasets: ReadWriteSignal<ResourceDataset<any, any>[]>;
activeIds: RWS<number[]>;
charts: ReadWriteSignal<
{
chart: RWS<IChartApi | undefined>;
whitespace: RWS<ISeriesApiAny | undefined>;
}[]
>;
chartsDrawn: Accessor<ReadWriteSignal<boolean>[]>;
dark: Accessor<boolean>;
datasets: Datasets;
legendSetter: Setter<PresetLegend>;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
exactRange: ReadWriteSignal<TimeRange>;
firstChartSetter: Setter<IChartApi | undefined>;
index: Accessor<number>;
lastActiveIndex: Accessor<number | undefined>;
lastChartIndex: Accessor<number>;
legendSetter: Setter<SeriesLegend[]>;
preset: Accessor<Preset>;
priceSeriesType: ReadWriteSignal<PriceSeriesType>;
seriesConfigs: SeriesConfig[];
seriesCount: Accessor<number>;
}) {
onMount(() => {
createEffect(() => {
const preset = presets.selected();
const div = createRWS<HTMLDivElement | undefined>(undefined);
const chartIndex = index();
untrack(() =>
renderChart({
datasets,
preset,
legendSetter,
activeResources,
}),
);
});
const isDrawn = chartsDrawn()[chartIndex];
const isLastDrawn = createMemo(
() => chartsDrawn().findLastIndex((drawn) => drawn()) === chartIndex,
);
onCleanup(cleanChart);
const chartPriceModeKey = `chart-price-mode-${chartIndex}` as const;
const chartPriceMode = createSL(["Linear", "Log"] as const, {
saveable: {
key: chartPriceModeKey,
mode: "localStorage",
},
defaultValue: chartIndex === 0 ? "Log" : "Linear",
});
return <div id="chart" class="h-full w-full cursor-crosshair" />;
createEffect(
on([div, () => charts()[chartIndex]], ([div, chartConfig]) => {
if (!div || !chartConfig) return;
const preset = presetAccessor();
const scale = preset.scale;
const chart = createChart({
scale,
element: div,
dark,
});
if (!chart) {
console.log("chart: undefined");
return;
}
const whitespace = setWhitespace(chart, scale);
batch(() => {
chartConfig.chart.set(chart);
chartConfig.whitespace.set(whitespace);
if (chartIndex === 0) {
firstChartSetter(chart);
}
});
const range = exactRange();
setInitialTimeRange({ chart, range });
if (chartIndex === 0) {
initTimeScale({
scale,
chart,
activeIds,
exactRange,
});
if (range) {
updateVisiblePriceSeriesType({
scale,
chart,
priceSeriesType,
timeRange: range,
});
}
}
const chartLegend: SeriesLegend[] = [];
onCleanup(() => {
chartLegend.length = 0;
});
const markerCallback = () =>
setMinMaxMarkers({
scale,
visibleRange: exactRange(),
legendList: chartLegend,
dark,
activeIds: activeIds,
});
const debouncedSetMinMaxMarkers = requestIdleCallbackPossible
? () => requestIdleCallback(markerCallback)
: debounce(
markerCallback,
seriesCount() * 10 + scale === "date" ? 50 : 100,
);
createEffect(on([exactRange, dark], debouncedSetMinMaxMarkers));
if (chartIndex === 0) {
const datasetPath: AnyDatasetPath = `/${scale}-to-price`;
const dataset = datasets.getOrImport(scale, datasetPath);
// Don't trigger reactivity by design
activeDatasets().push(dataset);
const title = "Price";
function createPriceSeries(seriesType: PriceSeriesType) {
let seriesConfig: SeriesConfig;
if (seriesType === "Candlestick") {
seriesConfig = {
datasetPath,
title,
seriesType: SeriesType.Candlestick,
};
} else {
seriesConfig = {
datasetPath,
title,
color: colors.white,
};
}
const priceSeries = createSeriesGroup({
scale,
datasets,
index: -1,
activeIds,
seriesConfig,
chart,
chartLegend,
lastActiveIndex,
preset,
disabled: () => priceSeriesType() !== seriesType,
debouncedSetMinMaxMarkers,
dark,
});
createEffect(() => {
const latest = webSockets.liveKrakenCandle.latest();
if (!latest) return;
const index = chunkIdToIndex(scale, latest.year);
const series = priceSeries.seriesList.at(index)?.();
series?.update(latest);
});
return priceSeries;
}
const priceCandlestickLegend = createPriceSeries("Candlestick");
const priceLineLegend = createPriceSeries("Line");
createEffect(() => {
priceCandlestickLegend.visible.set(priceLineLegend.visible());
});
createEffect(() => {
priceLineLegend.visible.set(priceCandlestickLegend.visible());
});
}
[...seriesConfigs].reverse().forEach((seriesConfig, index) => {
const dataset = datasets.getOrImport(scale, seriesConfig.datasetPath);
// Don't trigger reactivity by design
activeDatasets().push(dataset);
createSeriesGroup({
scale,
datasets,
activeIds,
index,
seriesConfig,
chartLegend,
chart,
preset,
lastActiveIndex,
debouncedSetMinMaxMarkers,
dark,
});
});
chartLegend.forEach((legend) => {
createEffect(on(legend.visible, debouncedSetMinMaxMarkers));
});
legendSetter((l) => {
for (let i = 0; i < chartLegend.length; i++) {
l.splice(0, 0, chartLegend[i]);
}
return l;
});
createEffect(() =>
isDrawn.set(() => chartLegend.some((legend) => legend.drawn())),
);
createEffect(() =>
chart.timeScale().applyOptions({
visible: isLastDrawn(),
}),
);
createEffect(() =>
chart.priceScale("right").applyOptions({
mode: chartPriceMode.selected() === "Linear" ? 0 : 1,
}),
);
chart.timeScale().subscribeVisibleLogicalRangeChange((logicalRange) => {
if (!logicalRange) return;
// Must be the chart with the visible timeScale
if (chartIndex === lastChartIndex()) {
debouncedUpdateVisiblePriceSeriesType({
scale,
chart,
logicalRange,
priceSeriesType,
});
}
for (
let otherChartIndex = 0;
otherChartIndex <= lastChartIndex();
otherChartIndex++
) {
if (chartIndex !== otherChartIndex) {
const chart = charts()[otherChartIndex].chart();
chart?.timeScale().setVisibleLogicalRange(logicalRange);
}
}
});
chart.subscribeCrosshairMove(({ time, sourceEvent }) => {
// Don't override crosshair position from scroll event
if (time && !sourceEvent) return;
for (
let otherChartIndex = 0;
otherChartIndex <= lastChartIndex();
otherChartIndex++
) {
const { whitespace: _whitespace, chart: _otherChart } =
charts()[otherChartIndex];
const otherChart = _otherChart();
const whitespace = _whitespace();
if (otherChart && whitespace && chartIndex !== otherChartIndex) {
if (time) {
otherChart.setCrosshairPosition(NaN, time, whitespace);
} else {
// No time when mouse goes outside the chart
otherChart.clearCrosshairPosition();
}
}
}
});
// Trigger reactivity now
activeDatasets.set((l) => l);
}),
);
return (
<div
style={{
height: isLastDrawn() ? "100%" : "calc(100% - 62px)",
"margin-bottom": isLastDrawn() ? "" : "-2px",
}}
class={classPropToString([
isDrawn()
? ["max-h-full", !isLastDrawn() ? "border-b" : "mb-[-2px]"]
: "max-h-0",
"border-lighter relative h-full min-h-0 w-full cursor-crosshair",
])}
>
<div ref={div.set} class="size-full" />
<Show when={isDrawn()}>
<div class="text-low-contrast absolute left-0 top-0 px-2 py-1.5 text-xs">
{chartIndex === 0
? ("US Dollars" satisfies Unit)
: presetAccessor().unit}
</div>
<div
style={{
bottom: `${isLastDrawn() ? 32 : 0}px`,
right: `77px`,
}}
class="text-low-contrast absolute z-10 px-3 py-0.5"
>
<RadioGroup size="xs" title={chartPriceModeKey} sl={chartPriceMode} />
</div>
</Show>
</div>
);
}
@@ -0,0 +1,137 @@
import { chunkIdToIndex } from "/src/scripts/datasets/resource";
import {
getInitialTimeRange,
setActiveIds,
} from "/src/scripts/lightweightCharts/time";
import { createRWS } from "/src/solid/rws";
import { Chart } from "./chart";
export function Charts({
firstChartSetter,
preset,
datasets,
legendSetter,
dark,
activeIds,
}: {
firstChartSetter: Setter<IChartApi | undefined>;
preset: Accessor<Preset>;
datasets: Datasets;
legendSetter: Setter<SeriesLegend[]>;
dark: Accessor<boolean>;
activeIds: RWS<number[]>;
}) {
const scale = createMemo(() => preset().scale);
const exactRange = createRWS(getInitialTimeRange(scale()));
const priceSeriesType = createRWS<PriceSeriesType>("Candlestick");
const activeDatasets = createRWS([] as ResourceDataset<any, any>[], {
equals: false,
});
const chartSeriesConfigs = createRWS([] as SeriesConfig[][], {
equals: false,
});
const charts = createRWS(
[] as {
chart: RWS<IChartApi | undefined>;
whitespace: RWS<ISeriesApiAny | undefined>;
}[],
{
equals: false,
},
);
const lastChartIndex = createMemo(() => chartSeriesConfigs().length - 1);
const seriesCount = createMemo(() =>
chartSeriesConfigs().reduce(
(acc, l) => (acc += l.length),
1, // Because of price series
),
);
const lastActiveIndex = createMemo(() => {
const last = activeIds().at(-1);
return last !== undefined
? chunkIdToIndex(preset().scale, last)
: undefined;
});
const chartsDrawn = createMemo(() =>
chartSeriesConfigs().map((_) => createRWS(true)),
);
createEffect(
on([activeIds, activeDatasets], ([ids, activeDatasets]) => {
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
for (let j = 0; j < activeDatasets.length; j++) {
activeDatasets[j].fetch(id);
}
}
}),
);
createEffect(
on(preset, (preset) => {
const scale = preset.scale;
exactRange.set(getInitialTimeRange(scale));
chartSeriesConfigs.set(
[preset.top || [], preset.bottom].flatMap((list) =>
list ? [list] : [],
),
);
charts.set(() =>
new Array(chartSeriesConfigs().length).fill(undefined).map(() => ({
chart: createRWS(undefined as IChartApi | undefined),
whitespace: createRWS(undefined as ISeriesApiAny | undefined),
})),
);
setActiveIds({
exactRange: exactRange(),
activeIds,
});
legendSetter(() => []);
}),
);
onCleanup(() => {
firstChartSetter(undefined);
charts().map(({ chart, whitespace }) => {
chart()?.remove();
chart.set(undefined);
whitespace.set(undefined);
});
});
return (
<For
each={chartSeriesConfigs().filter(
(configs, index) => index === 0 || configs.length !== 0,
)}
>
{(seriesConfigs, index) => (
<Chart
activeDatasets={activeDatasets}
activeIds={activeIds}
charts={charts}
chartsDrawn={chartsDrawn}
dark={dark}
datasets={datasets}
exactRange={exactRange}
firstChartSetter={firstChartSetter}
index={index}
lastActiveIndex={lastActiveIndex}
lastChartIndex={lastChartIndex}
legendSetter={legendSetter}
preset={preset}
priceSeriesType={priceSeriesType}
seriesConfigs={seriesConfigs}
seriesCount={seriesCount}
/>
)}
</For>
);
}
@@ -1,56 +1,97 @@
import { chunkIdToIndex } from "/src/scripts/datasets/resource";
import { createRWS } from "/src/solid/rws";
const transparency = "66";
import { Scrollable } from "../../scrollable";
const transparency = "44";
export function Legend({
scale,
legend: legendList,
dark,
activeIds,
}: {
legend: Accessor<PresetLegend>;
scale: Accessor<ResourceScale>;
legend: Accessor<SeriesLegend[]>;
dark: Accessor<boolean>;
activeIds: Accessor<number[]>;
}) {
const hovering = createRWS<SeriesLegend | undefined>(undefined);
const hovered = createRWS<SeriesLegend | undefined>(undefined);
let toggle = false;
return (
<div class="flex flex-1 items-center gap-1 overflow-y-auto">
<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 = activeIds();
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={() => {
hovering.set(legend);
}}
onMouseLeave={() => 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) => {
@@ -64,9 +105,15 @@ export function Legend({
toggle = !toggle;
}
previousClickValueOf = currentClickValueOf;
previousClickTime = currentClickTime;
if (legend.visible()) {
hovered.set(legend);
} else {
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-200/20 active:scale-[0.975]"
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"
>
<span
class="flex size-4 flex-col overflow-hidden rounded-full"
@@ -76,9 +123,9 @@ export function Legend({
>
<For
each={
Array.isArray(legend.color())
? (legend.color() as string[])
: [legend.color() as string]
Array.isArray(legend.color)
? legend.color.map((c) => c(dark))
: [legend.color(dark)]
}
>
{(color) => (
@@ -92,7 +139,7 @@ export function Legend({
</For>
</span>
<span
class="text-white decoration-white decoration-wavy decoration-[1.5px]"
class="text-high-contrast decoration-high-contrast decoration-wavy decoration-[1.5px]"
style={{
"text-decoration-line": !legend.visible()
? "line-through"
@@ -102,16 +149,13 @@ export function Legend({
>
{legend.title}
</span>
<Show when={legend.url}>
<Show when={legend.dataset.url}>
{(url) => (
<a
class="-my-0.5 !-mr-1 inline-flex size-6 flex-col overflow-hidden rounded-full border border-orange-200/5 bg-orange-200 bg-opacity-5 p-1 pl-0.5 hover:bg-opacity-30"
style={{
opacity: legend.visible() ? 1 : 0.5,
}}
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={
@@ -120,7 +164,7 @@ export function Legend({
: undefined
}
>
<IconTablerExternalLink />
<IconTablerDownload />
</a>
)}
</Show>
@@ -129,6 +173,6 @@ export function Legend({
);
}}
</For>
</div>
</Scrollable>
);
}
@@ -1,67 +1,328 @@
import { chartState } from "/src/scripts/lightweightCharts/chart/state";
import { GENESIS_DAY } from "/src/scripts/lightweightCharts/chart/whitespace";
import { ONE_DAY_IN_MS } from "/src/scripts/utils/time";
import { classPropToString } from "/src/solid/classes";
import { createRWS } from "/src/solid/rws";
import { GENESIS_DAY } from "../../../../../scripts/lightweightCharts/whitespace";
import { Box } from "../../box";
import { Scrollable } from "../../scrollable";
const DELAY = 1;
const MULTIPLIER = DELAY / 1000;
const LEFT = -1;
const RIGHT = 1;
export function TimeScale({
scale,
firstChart,
}: {
scale: Accessor<ResourceScale>;
firstChart: RWS<IChartApi | undefined>;
}) {
const today = new Date();
const disabled = createMemo(() => !firstChart());
const scrollDirection = createRWS(0);
const timeScale = createMemo(() => {
const chart = firstChart();
if (!chart) return undefined;
return chart.timeScale();
});
let interval: number | undefined;
function createScrollLoop() {
clearInterval(interval);
const direction = scrollDirection();
if (!direction) return;
// @ts-ignore
interval = setInterval(() => {
const time = timeScale();
if (!time) return;
const range = time.getVisibleLogicalRange();
if (!range) return;
const speed = (range.to - range.from) * MULTIPLIER * direction;
// @ts-ignore
range.from += speed;
// @ts-ignore
range.to += speed;
time.setVisibleLogicalRange(range);
}, DELAY);
}
onCleanup(() => clearInterval(interval));
export function TimeScale() {
return (
<Box dark padded overflowY>
<Button onClick={() => setTimeScale()}>All Time</Button>
<Button onClick={() => setTimeScale(7)}>1 Week</Button>
<Button onClick={() => setTimeScale(30)}>1 Month</Button>
<Button onClick={() => setTimeScale(30 * 6)}>6 Months</Button>
<Button
onClick={() =>
setTimeScale(
Math.ceil(
(new Date().valueOf() -
new Date(`${new Date().getUTCFullYear()}-01-01`).valueOf()) /
ONE_DAY_IN_MS,
),
)
}
>
Year To Date
</Button>
<Button onClick={() => setTimeScale(365)}>1 Year</Button>
<Button onClick={() => setTimeScale(2 * 365)}>2 Years</Button>
<Button onClick={() => setTimeScale(4 * 365)}>4 Years</Button>
<Button onClick={() => setTimeScale(8 * 365)}>8 Years</Button>
<Box dark padded={false} spaced={false} classes="short:hidden">
<div class="flex items-center p-1.5">
<Button
square
disabled={disabled}
onClick={() => {
scrollDirection.set((v) => (v === LEFT ? 0 : LEFT));
createScrollLoop();
}}
>
<Show
when={scrollDirection() === LEFT}
fallback={<IconTablerPlayerTrackPrevFilled />}
>
<IconTablerPlayerPauseFilled />
</Show>
</Button>
</div>
<div class="border-lighter border-l" />
<Scrollable classes="p-1.5 space-x-2">
<Switch>
<Match when={scale() === "date"}>
<Button
minWidth
disabled={disabled}
onClick={() => setTimeScale({ scale: scale(), timeScale })}
>
All Time
</Button>
<Button
minWidth
disabled={disabled}
onClick={() =>
setTimeScale({ scale: scale(), timeScale, days: 7 })
}
>
1 Week
</Button>
<Button
minWidth
disabled={disabled}
onClick={() =>
setTimeScale({ scale: scale(), timeScale, days: 30 })
}
>
1 Month
</Button>
<Button
minWidth
disabled={disabled}
onClick={() =>
setTimeScale({ scale: scale(), timeScale, days: 3 * 30 })
}
>
3 Months
</Button>
<Button
minWidth
disabled={disabled}
onClick={() =>
setTimeScale({ scale: scale(), timeScale, days: 6 * 30 })
}
>
6 Months
</Button>
<Button
minWidth
disabled={disabled}
onClick={() =>
setTimeScale({
scale: scale(),
timeScale,
days: Math.ceil(
(today.getTime() -
new Date(`${today.getUTCFullYear()}-01-01`).getTime()) /
ONE_DAY_IN_MS,
),
})
}
>
Year To Date
</Button>
<Button
minWidth
disabled={disabled}
onClick={() =>
setTimeScale({ scale: scale(), timeScale, days: 365 })
}
>
1 Year
</Button>
<Button
minWidth
disabled={disabled}
onClick={() =>
setTimeScale({ scale: scale(), timeScale, days: 2 * 365 })
}
>
2 Years
</Button>
<Button
minWidth
disabled={disabled}
onClick={() =>
setTimeScale({ scale: scale(), timeScale, days: 4 * 365 })
}
>
4 Years
</Button>
<Button
minWidth
disabled={disabled}
onClick={() =>
setTimeScale({ scale: scale(), timeScale, 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
minWidth
disabled={disabled}
onClick={() =>
setTimeScale({ scale: scale(), timeScale, year })
}
>
{year}
</Button>
)}
</For>
</Match>
<Match when={scale() === "height"}>
<Button minWidth disabled={() => true} onClick={() => {}}>
24h
</Button>
<Button minWidth disabled={() => true} onClick={() => {}}>
48h
</Button>
<For
each={new Array(9)
.fill(0)
.flatMap((_, i) => [i, i + 0.5])
.reverse()}
>
{(i) => (
<Button
minWidth
disabled={disabled}
onClick={() =>
setTimeScale({
scale: scale(),
timeScale,
range: {
from: i * 100_000,
to: (i + 0.5) * 100_000,
},
})
}
>
{`${100 * (i + 0.5)}k`}
</Button>
)}
</For>
</Match>
</Switch>
</Scrollable>
<div class="border-lighter border-l" />
<div class="flex items-center p-1.5">
<Button
square
disabled={disabled}
onClick={() => {
scrollDirection.set((v) => (v === RIGHT ? 0 : RIGHT));
createScrollLoop();
}}
>
<Show
when={scrollDirection() === RIGHT}
fallback={<IconTablerPlayerTrackNextFilled />}
>
<IconTablerPlayerPauseFilled />
</Show>
</Button>
</div>
</Box>
);
}
function Button(props: ParentProps & { onClick: VoidFunction }) {
function Button({
onClick,
disabled,
children,
minWidth,
square,
}: ParentProps & {
onClick: VoidFunction;
disabled?: Accessor<boolean>;
minWidth?: boolean;
square?: boolean;
}) {
return (
<button
class="min-w-20 flex-shrink-0 flex-grow whitespace-nowrap rounded-lg px-2 py-1.5 hover:bg-white/20 active:scale-95"
onClick={props.onClick}
class={classPropToString([
minWidth && "min-w-20",
square ? "p-2" : "px-2 py-1.5",
disabled?.()
? "text-low-contrast"
: "hover:bg-orange-50/20 active:scale-95",
"flex-shrink-0 flex-grow whitespace-nowrap rounded-lg",
])}
onClick={onClick}
disabled={disabled?.()}
>
{props.children}
{children}
</button>
);
}
function setTimeScale(days?: number) {
const to = new Date();
function setTimeScale({
timeScale,
scale,
days,
year,
range,
}: {
timeScale: Accessor<ITimeScaleApi<Time> | undefined>;
scale: ResourceScale;
days?: number;
year?: number;
range?: { from: number; to: number };
}) {
if (scale === "date") {
let from = new Date();
let to = new Date();
if (days) {
const from = new Date();
from.setDate(from.getUTCDate() - days);
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);
}
chartState.chart?.timeScale().setVisibleRange({
timeScale()?.setVisibleRange({
from: (from.getTime() / 1000) as Time,
to: (to.getTime() / 1000) as Time,
});
} else {
// chartState.chart?.timeScale().fitContent();
chartState.chart?.timeScale().setVisibleRange({
from: (new Date(
// datasets.candlesticks.values()?.[0]?.date || "",
GENESIS_DAY,
).getTime() / 1000) as Time,
to: (to.getTime() / 1000) as Time,
});
} else if (scale === "height") {
if (range) {
timeScale()?.setVisibleRange({
from: range.from as Time,
to: range.to as Time,
});
}
}
}
@@ -1,11 +1,14 @@
export function Title({ presets }: { presets: Presets }) {
return (
<div class="flex flex-1 items-center overflow-y-auto pb-1.5 text-orange-100/50">
<div
class="flex flex-1 items-center overflow-y-auto p-1.5"
style={{
"scrollbar-width": "thin",
}}
>
<div class="flex-1 -space-y-1 whitespace-nowrap px-1 md:mt-0.5 md:-space-y-1.5">
<h3 class="text-xs">{`/ ${[...presets.selected().path.map(({ name }) => name), presets.selected().name].join(" / ")}`}</h3>
<h1 class="text-lg font-bold text-white md:text-xl">
{presets.selected().title}
</h1>
<h3 class="text-xs opacity-50">{`/ ${[...presets.selected().path.map(({ name }) => name), presets.selected().name].join(" / ")}`}</h3>
<h1 class="text-lg font-bold md:text-xl">{presets.selected().title}</h1>
</div>
</div>
);
+44 -22
View File
@@ -1,4 +1,5 @@
import { classPropToString } from "/src/solid/classes";
import { createWasIdleAccessor } from "/src/solid/idle";
import { createRWS } from "/src/solid/rws";
import { Box } from "../box";
@@ -10,63 +11,84 @@ import { Title } from "./components/title";
export function ChartFrame({
presets,
datasets,
activeResources,
hide,
qrcode,
standalone,
fullscreen,
dark,
}: {
presets: Presets;
hide?: Accessor<boolean>;
qrcode: RWS<string>;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
datasets: Datasets;
fullscreen?: RWS<boolean>;
dark: Accessor<boolean>;
standalone: boolean;
}) {
const legend = createRWS<PresetLegend>([]);
const legend = createRWS<SeriesLegend[]>([], { equals: false });
const Chart = lazy(() =>
import("./components/chart").then((d) => ({
default: d.Chart,
})),
const firstChart = createRWS<IChartApi | undefined>(undefined);
const scale = createMemo(() => presets.selected().scale);
const activeIds = createRWS([] as number[], { equals: false });
const wasIdle = createWasIdleAccessor();
const Charts = lazy(() =>
import("./components/charts").then((d) => ({ default: d.Charts })),
);
return (
<div
class={classPropToString([
standalone &&
"rounded-2xl border border-orange-200/15 bg-gradient-to-b from-orange-100/5 to-black/10 to-80%",
"border-lighter rounded-2xl border bg-gradient-to-b from-white/15 to-white/30 to-80% shadow-md dark:from-orange-100/5 dark:to-black/10",
"flex size-full min-h-0 flex-1 flex-col overflow-hidden",
])}
style={{
display: (hide ? !hide() : true) ? undefined : "none",
display: (hide ? hide() : false) ? "none" : undefined,
}}
>
<Box flex={false} dark>
<Box
flex={false}
dark
padded={false}
spaced={false}
classes="short:hidden"
>
<Title presets={presets} />
<div class="-mx-2 border-t border-orange-200/15" />
<div class="border-lighter border-t" />
<div class="flex pt-1.5">
<Legend legend={legend} />
<div class="flex">
<Legend
legend={legend}
scale={scale}
activeIds={activeIds}
dark={dark}
/>
<div class="-my-1.5 border-l border-orange-200/15 pr-1.5" />
<div class="border-lighter border-l" />
<Actions presets={presets} qrcode={qrcode} fullscreen={fullscreen} />
</div>
</Box>
<div class="-mt-2 min-h-0 flex-1">
<Chart
activeResources={activeResources}
datasets={datasets}
legendSetter={legend.set}
presets={presets}
/>
<div class="-mr-2 -mt-2 flex min-h-0 flex-1 flex-col">
<Show when={wasIdle()}>
<Charts
firstChartSetter={firstChart.set}
datasets={datasets}
legendSetter={legend.set}
preset={presets.selected}
dark={dark}
activeIds={activeIds}
/>
</Show>
</div>
<TimeScale />
<TimeScale firstChart={firstChart} scale={scale} />
</div>
);
}
+42 -17
View File
@@ -1,3 +1,5 @@
import { Box } from "./box";
import { Button, ButtonRandomChart } from "./button";
import { Header } from "./header";
import { Line } from "./line";
import { Number } from "./number";
@@ -11,8 +13,10 @@ export function FavoritesFrame({
}) {
return (
<div
class="flex-1 overflow-y-auto"
hidden={selectedFrame() !== "Favorites"}
class="relative flex-1 overflow-y-auto overflow-x-hidden"
style={{
display: selectedFrame() !== "Favorites" ? "none" : undefined,
}}
>
<div class="flex max-h-full min-h-0 flex-1 flex-col gap-4 p-4">
<Header title="Favorites">
@@ -20,29 +24,50 @@ export function FavoritesFrame({
favorites.
</Header>
<div class="-mx-4 border-t border-orange-200/10" />
<div class="border-lighter -mx-4 border-t" />
<div
class="space-y-0.5 py-1"
style={{
display: !presets.favorites().length ? "none" : undefined,
}}
// style={{
// display: !presets.favorites().length ? "none" : undefined,
// }}
>
<For each={presets.favorites()}>
{(preset) => (
<Line
id={`favorite-${preset.id}`}
name={preset.title}
onClick={() => presets.select(preset)}
active={() => presets.selected() === preset}
header={`/ ${[...preset.path.map(({ name }) => name), preset.name].join(" / ")}`}
/>
)}
</For>
<Show
when={presets.favorites().length}
fallback={
<p>
It seems like you couldn't find any interesting chart for your
favorites ! You might want to try to{" "}
<ButtonRandomChart presets={presets} />
</p>
}
>
<For each={presets.favorites()}>
{(preset) => (
<Line
id={`favorite-${preset.id}`}
name={preset.title}
onClick={() => presets.select(preset)}
active={() => presets.selected() === preset}
header={`/ ${[...preset.path.map(({ name }) => name), preset.name].join(" / ")}`}
/>
)}
</For>
</Show>
</div>
<div class="h-[25dvh] flex-none" />
</div>
<Box absolute="bottom">
<Button onClick={() => presets.selected().isFavorite.set((b) => !b)}>
<span>
{presets.selected().isFavorite()
? "Remove from favorites"
: "Add to favorites"}
</span>
</Button>
</Box>
</div>
);
}
@@ -26,10 +26,10 @@ export function Folder({
name={name}
icon={icon}
onClick={onClick}
classes={() => (open() ? "text-orange-100/75" : "")}
classes={() => (open() ? "opacity-60" : "")}
tail={() => (
<Show when={!open()}>
<span class="rounded-full bg-white bg-opacity-[0.075] px-2 py-0.5 text-xs text-neutral-400">
<span class="rounded-full bg-orange-50/10 px-2 py-0.5 text-xs text-neutral-400">
{children}
</span>
</Show>
@@ -0,0 +1,119 @@
import { File } from "./file";
import { Folder } from "./folder";
export function Tree({
tree,
selected,
openedFolders,
depth = 0,
visible,
selectPreset,
path = [],
favorites,
}: {
tree: PresetTree;
selected: Accessor<Preset>;
selectPreset(preset: Preset): void;
openedFolders: RWS<Set<string>>;
depth?: number;
visible?: Accessor<boolean>;
path?: FilePath;
favorites: Accessor<Preset[]>;
}) {
return (
<Show when={visible?.() || !visible}>
<div>
<For each={tree}>
{(thing) => {
const active = createMemo(() => thing.id === selected().id);
const favorite = createMemo(() =>
favorites().includes(thing as Preset),
);
const visited = (thing as Preset).visited;
if (!("tree" in thing)) {
return (
<File
id={thing.id}
name={thing.name}
active={active}
depth={depth}
icon={thing.icon || IconTablerFile}
favorite={favorite}
visited={visited}
onClick={() => {
const selectedId = selected().id;
if (selectedId === thing.id) {
return;
}
// Has been filled in createPresets
selectPreset(thing as Preset);
}}
/>
);
}
const childrenVisible = createMemo(() =>
openedFolders().has(thing.id),
);
const childCount = countChildren(thing);
return (
<div>
<Folder
id={thing.id}
name={thing.name}
depth={depth}
open={childrenVisible}
children={childCount}
onClick={() => {
openedFolders.set((s) => {
if (childrenVisible()) {
s.delete(thing.id);
} else {
s.add(thing.id);
}
return s;
});
}}
/>
<Tree
tree={thing.tree}
selected={selected}
depth={depth + 1}
openedFolders={openedFolders}
visible={childrenVisible}
path={[...path, { name: thing.name, id: thing.id }]}
selectPreset={selectPreset}
favorites={favorites}
/>
</div>
);
}}
</For>
</div>
</Show>
);
}
function countChildren(folder: PresetFolder) {
let count = 0;
function _countChildren(tree: PartialPresetTree) {
tree.forEach((anyPreset) => {
if ("tree" in anyPreset) {
_countChildren(anyPreset.tree);
} else {
count += 1;
}
});
}
_countChildren(folder.tree);
return count;
}
@@ -1,5 +1,6 @@
import { scrollIntoView } from "/src/scripts/utils/scroll";
import { sleep, tick } from "/src/scripts/utils/sleep";
import { sleep } from "/src/scripts/utils/sleep";
import { tick } from "/src/scripts/utils/tick";
import { createRWS } from "/src/solid/rws";
import { Box } from "../box";
@@ -8,7 +9,7 @@ import { Header } from "../header";
import { Number } from "../number";
import { Tree } from "./components/tree";
export function TreeFrame({
export function FoldersFrame({
presets,
selectedFrame,
}: {
@@ -25,17 +26,17 @@ export function TreeFrame({
<div
class="relative flex size-full flex-1 flex-col"
style={{
display: selectedFrame() !== "Tree" ? "none" : undefined,
display: selectedFrame() !== "Folders" ? "none" : undefined,
}}
>
<div class="flex-1 overflow-y-auto">
<div class="flex-1 overflow-y-auto overflow-x-hidden">
<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>
<div class="-mx-4 border-t border-orange-200/10" />
<div class="border-lighter -mx-4 border-t" />
<Tree
tree={presets.tree}
+1 -1
View File
@@ -2,7 +2,7 @@ export function Header({ title, children }: { title: string } & ParentProps) {
return (
<div>
<h3 class="text-lg font-bold md:text-xl">{title}</h3>
<p class="text-orange-100/75">{children}</p>
<p class="text-orange-950/60 dark:text-orange-100/75">{children}</p>
</div>
);
}
+95 -56
View File
@@ -1,5 +1,7 @@
import { run } from "/src/scripts/utils/run";
import { Box } from "./box";
import { Button, ButtonRandomChart } from "./button";
import { Header } from "./header";
import { Line } from "./line";
@@ -11,70 +13,107 @@ export function HistoryFrame({
selectedFrame: Accessor<FrameName>;
}) {
return (
<div class="flex-1 overflow-y-auto" hidden={selectedFrame() !== "History"}>
<div
class="flex-1 overflow-y-auto overflow-x-hidden"
style={{
display: selectedFrame() !== "History" ? "none" : undefined,
}}
>
<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"
style={{
display: !presets.history().length ? "none" : undefined,
}}
>
<For each={presets.history()}>
{({ preset, date }, index) => (
<div class="space-y-0.5 pt-4">
<Show
when={presets.history().length}
fallback={
<>
<Show
when={
index() === 0 ||
presets.history()[index()].date.toJSON().split("T")[0] !==
presets.history()[index() - 1].date.toJSON().split("T")[0]
}
>
<div class="sticky top-[-0.5rem] z-10 -mx-4 py-2">
<div class="border-y border-orange-200/10 bg-[rgb(25,15,15)] p-2">
<p class="ml-2">
<Switch fallback={date.toLocaleDateString()}>
<Match
when={
new Date().toJSON().split("T")[0] ===
date.toJSON().split("T")[0]
}
>
Today
</Match>
<Match
when={
run(() => {
const d = new Date();
d.setDate(d.getDate() - 1);
return d;
})
.toJSON()
.split("T")[0] === date.toJSON().split("T")[0]
}
>
Yesterday
</Match>
</Switch>
</p>
</div>
</div>
</Show>
<Line
id={`history-${preset.id}`}
name={preset.title}
onClick={() => presets.select(preset)}
active={() => presets.selected() === preset}
header={date.toLocaleTimeString()}
/>
<div class="border-lighter -mx-4 border-t" />
<p>
You somehow haven't visited by yourself a single chart.
Impressive ! You might want to try to{" "}
<ButtonRandomChart presets={presets} />
</p>
</>
)}
</For>
}
>
<For each={presets.history()}>
{({ preset, date }, index) => (
<>
<Show
when={
index() === 0 ||
presets.history()[index()].date.toJSON().split("T")[0] !==
presets
.history()
[index() - 1].date.toJSON()
.split("T")[0]
}
>
<div class="sticky top-[calc(-0.5rem-1px)] z-10 -mx-4 py-2">
<div class="border-lighter border-y bg-[#F4EAE3] p-2 dark:bg-[rgb(25,15,15)]">
<p class="ml-2">
<Switch fallback={date.toLocaleDateString()}>
<Match
when={
new Date().toJSON().split("T")[0] ===
date.toJSON().split("T")[0]
}
>
Today
</Match>
<Match
when={
run(() => {
const d = new Date();
d.setDate(d.getDate() - 1);
return d;
})
.toJSON()
.split("T")[0] === date.toJSON().split("T")[0]
}
>
Yesterday
</Match>
</Switch>
</p>
</div>
</div>
</Show>
<Line
id={`history-${preset.id}`}
name={preset.title}
onClick={() => presets.select(preset)}
active={() => presets.selected() === preset}
header={date.toLocaleTimeString()}
/>
</>
)}
</For>
</Show>
</div>
<div class="h-[25dvh] flex-none" />
</div>
{/* <Box absolute="bottom">
<Button
onClick={() => {
// search.set("");
// inputRef()?.focus();
}}
>
Previous day
</Button>
<Button
onClick={() => {
// search.set("");
// inputRef()?.focus();
}}
>
Next day
</Button>
</Box> */}
</div>
);
}
+2 -7
View File
@@ -45,9 +45,7 @@ export function Line({
title={name}
>
<For each={new Array(depth)}>
{() => (
<span class="ml-1 h-8 w-3 flex-none border-l border-orange-200/10" />
)}
{() => <span class="border-lighter ml-1 h-8 w-3 flex-none border-l" />}
</For>
<Show when={icon}>
{(icon) => (
@@ -68,10 +66,7 @@ export function Line({
])}
>
<Show when={header}>
<span
class="truncate text-xs text-white text-opacity-50"
innerHTML={header}
/>
<span class="truncate text-xs opacity-50" innerHTML={header} />
</Show>
<span class="space-x-1 truncate">
<span innerHTML={name} />
@@ -0,0 +1,137 @@
import { createResizeObserver } from "@solid-primitives/resize-observer";
import { touchScreen } from "/src/env";
import { classPropToString } from "/src/solid/classes";
import { createRWS } from "/src/solid/rws";
export function Scrollable({
children,
classes,
}: {
classes?: string;
} & ParentProps) {
const maybeScrollable = createRWS<HTMLDivElement | undefined>(undefined);
const scrollable = createRWS(false);
const showLeftArrow = createRWS(false);
const showRightArrow = createRWS(false);
onMount(() => {
createResizeObserver(maybeScrollable, (_, el) => {
if (el !== maybeScrollable()) {
return;
}
checkScrollable();
});
});
function checkScrollable() {
const div = maybeScrollable();
if (div) {
scrollable.set(() => div.scrollWidth > div.clientWidth);
}
checkArrows();
}
function checkArrows() {
const target = maybeScrollable()!;
const left = target.scrollLeft;
const right =
target.scrollWidth - Math.ceil(target.scrollLeft + target.clientWidth);
showLeftArrow.set(() => left > 0);
showRightArrow.set(() => right > 0);
}
// @ts-ignore
createEffect(on(children, checkScrollable));
return (
<div class="relative min-w-0 flex-1">
<For
each={[
{
showArrow: showLeftArrow,
side: "left-0",
order: "",
buttonPadding: "pl-2",
iconPadding: "pr-0.5",
scrollMultiplier: -1,
chevronIcon: IconTablerChevronLeft,
gradientDirection: "bg-gradient-to-r",
},
{
showArrow: showRightArrow,
side: "right-0",
order: "order-2",
buttonPadding: "pr-2",
iconPadding: "pl-0.5",
scrollMultiplier: 1,
chevronIcon: IconTablerChevronRight,
gradientDirection: "bg-gradient-to-l",
},
]}
>
{(obj) => (
<Show when={scrollable() && obj.showArrow()}>
<div
class={[
obj.side,
"pointer-events-none absolute bottom-0 top-0 flex transition-opacity duration-200 ease-in-out",
].join(" ")}
>
<Show when={!touchScreen}>
<div
class={[
obj.order,
obj.buttonPadding,
"pointer-events-auto flex h-full items-center bg-stone-100/75 dark:bg-stone-900/75",
].join(" ")}
>
<button
onClick={() => {
maybeScrollable()?.scrollBy({
left: Math.floor(
maybeScrollable()!.clientWidth *
obj.scrollMultiplier *
0.75,
),
behavior: "smooth",
});
}}
class="border-light rounded-full border bg-stone-100 p-0.5 shadow transition hover:scale-110 active:scale-100 dark:bg-stone-900"
>
<Dynamic
component={obj.chevronIcon}
class={[`size-5 ${obj.iconPadding}`]}
/>
</button>
</div>
</Show>
<div
class={[
obj.gradientDirection,
"h-full w-8 from-stone-100/75 to-transparent dark:from-stone-900/75",
].join(" ")}
/>
</div>
</Show>
)}
</For>
<div
ref={maybeScrollable.set}
onScroll={checkArrows}
class={classPropToString([
"no-scrollbar flex w-full overflow-x-auto",
classes,
])}
>
{children}
</div>
</div>
);
}
+25 -10
View File
@@ -6,7 +6,7 @@ import { createRWS } from "/src/solid/rws";
import { INPUT_PRESET_SEARCH_ID } from "../..";
import { Box } from "./box";
import { Button } from "./button";
import { Button, ButtonRandomChart } from "./button";
import { Line } from "./line";
const PER_PAGE = 100;
@@ -44,10 +44,16 @@ export function SearchFrame({
...config,
});
const haystack = presets.list.map(
(preset) =>
`${preset.title}\t/ ${[...preset.path.map(({ name }) => name), preset.name].join(" / ")}`,
);
let haystack = [] as string[];
function initHaystackIfNeeded() {
if (haystack.length) return;
haystack = presets.list.map(
(preset) =>
`${preset.title}\t/ ${[...preset.path.map(({ name }) => name), preset.name].join(" / ")}`,
);
}
const searchResult = createMemo(() => {
scrollIntoView(counterRef());
@@ -125,7 +131,15 @@ export function SearchFrame({
>
<div class="flex-1 space-y-1 overflow-y-auto p-4 pt-16">
<p class="py-2 text-orange-100/75">
<Show when={search()} fallback={"Write in the top bar to search."}>
<Show
when={search()}
fallback={
<p>
If you can't think of anything, you might want to try to{" "}
<ButtonRandomChart presets={presets} />
</p>
}
>
Found{" "}
<span class="font-medium text-orange-400/75">
{resultCount().toLocaleString("en-us")}
@@ -135,7 +149,7 @@ export function SearchFrame({
</p>
<Show when={search()}>
<div class="-mx-4 border-t border-orange-200/10" />
<div class="border-lighter -mx-4 border-t" />
<div
class="py-1"
@@ -174,9 +188,10 @@ export function SearchFrame({
class="w-full bg-transparent p-1 caret-orange-500 placeholder:text-orange-200/50 focus:outline-none"
placeholder="Search by name or path"
value={search()}
onFocus={initHaystackIfNeeded}
onInput={(event) => search.set(event.target.value)}
/>
<span class="-mx-1 flex size-5 flex-none items-center justify-center rounded-md border border-white text-xs font-bold">
<span class="-mx-1 flex size-5 flex-none items-center justify-center rounded-md border border-current text-xs font-bold">
<IconTablerSlash />
</span>
</div>
@@ -189,7 +204,7 @@ export function SearchFrame({
inputRef()?.focus();
}}
>
Clear search
Reset search
</Button>
</Box>
</div>
@@ -228,7 +243,7 @@ function ListSection({
);
return (
<div>
<div class="pb-16">
<For each={list()}>
{({ preset, path, title }) => (
<Line
+292 -20
View File
@@ -1,37 +1,309 @@
import { version } from "/src/../package.json";
import { ipad, iphone, macOS, safariOnly, standalone } from "/src/env";
import { classPropToString } from "/src/solid/classes";
import { AnchorAPI } from "../strip/components/anchorAPI";
import { AnchorGeyser } from "../strip/components/anchorGeyser";
import { AnchorGit } from "../strip/components/anchorGit";
import { AnchorNostr } from "../strip/components/anchorNostr";
import { Header } from "./header";
export function SettingsFrame({
marquee,
selectedFrame,
appTheme,
backgroundMode,
backgroundOpacity,
}: {
marquee: RWS<boolean>;
selectedFrame: Accessor<FrameName>;
appTheme: SL<"System" | "Dark" | "Light">;
backgroundMode: SL<"Scroll" | "Static">;
backgroundOpacity: SL<{ text: string; value: number }>;
}) {
const value = marquee();
return (
<div class="flex-1 overflow-y-auto" hidden={selectedFrame() !== "Settings"}>
<div
class="flex-1 overflow-y-auto overflow-x-hidden"
style={{
display: selectedFrame() !== "Settings" ? "none" : undefined,
}}
>
<div class="space-y-4 p-4">
<Header title="Settings" />
<Header title="Settings">And other stuff</Header>
<div class="-mx-4 border-t border-orange-200/10" />
<div class="border-lighter -mx-4 border-t" />
<div class="space-y-2">
<p>Background</p>
<div>Opacity</div>
<div>
<label class="switch">
Scroll
<input
type="checkbox"
checked={value}
onChange={(event) => marquee.set(event.target.checked || false)}
/>
<span class="slider"></span>
</label>
<div class="space-y-4">
<Title>General</Title>
<FieldRadioGroup
title="Theme"
ariaTitle="App's theme"
description="Options for the app's theme"
sl={appTheme}
/>
</div>
<div class="border-lighter -mx-4 border-t" />
<div class="space-y-4">
<Title>Background</Title>
<FieldRadioGroup
title="Mode"
ariaTitle="Background mode"
description="Options for how the background in displayed"
sl={backgroundMode}
/>
<FieldRadioGroup
title="Opacity"
ariaTitle="Background mode"
description="Options for the opacity of the text in the background"
sl={backgroundOpacity}
/>
</div>
<hr class="border-lighter -mx-4 border-t" />
<div class="space-y-4">
<Title>Donations</Title>
<p>
A <strong>massive thank you</strong> to everybody who sent their
hard earned sats. This project, by being completely free, is very
dependent and only founded by the goodwill of fellow itcoiners.
</p>
<p>Top 10 Leaderboard:</p>
<ol class="list-inside list-decimal">
<For
each={[
{
name: "_Checkɱate",
url: "https://primal.net/p/npub1qh5sal68c8swet6ut0w5evjmj6vnw29x3k967h7atn45unzjyeyq6ceh9r",
amount: 500_000,
},
{
name: "avvi |",
url: "https://primal.net/p/npub1md2q6fexrtmd5hx9gw2p5640vg662sjlpxyz3tdmu4j4g8hhkm6scn6hx3",
amount: 5_000,
},
{
name: "mutatrum",
url: "https://primal.net/p/npub1hklphk7fkfdgmzwclkhshcdqmnvr0wkfdy04j7yjjqa9lhvxuflsa23u2k",
amount: 5_000,
},
{
name: "Gunnar",
url: "https://primal.net/p/npub1rx9wg2d5lhah45xst3580sajcld44m0ll9u5dqhu2t74p6xwufaqwghtd4",
amount: 1_000,
},
{
name: "Blokchain Boog",
url: "https://x.com/BlokchainB",
amount: 1_500 + 1590,
},
{
name: "Josh",
url: "https://primal.net/p/npub1pc57ls4rad5kvsp733suhzl2d4u9y7h4upt952a2pucnalc59teq33dmza",
amount: 1_000,
},
{
name: "Alp",
url: "https://primal.net/p/npub175nul9cvufswwsnpy99lvyhg7ad9nkccxhkhusznxfkr7e0zxthql9g6w0",
amount: 1_000,
},
{
name: "Ulysses",
url: "https://primal.net/p/npub1n7n3dssm90hfsfjtamwh2grpzwjlvd2yffae9pqgg99583lxdypsnn9gtv",
amount: 1_000,
},
{
name: "btcschellingpt",
url: "https://primal.net/p/npub1nvfgglea9zlcs58tcqlc6j26rt50ngkgdk7699wfq4txrx37aqcsz4e7zd",
amount: 1_000 + 1_000,
},
{
name: "Coinatra",
url: "https://primal.net/p/npub1eut9kcejweegwp9waq3a4g03pvprdzkzvjjvl8fvj2a2wlx030eswzfna8",
amount: 1_000,
},
{
name: "Printer Go Brrrr",
url: "https://primal.net/p/npub1l5pxvjzhw77h86tu0sml2gxg8jpwxch7fsj6d05n7vuqpq75v34syk4q0n",
amount: 1_000,
},
{
name: "b81776c32d7b",
url: "https://primal.net/p/npub1hqthdsed0wpg57sqsc5mtyqxxgrh3s7493ja5h49v23v2nhhds4qk4w0kz",
amount: 17_509,
},
{
name: "DerGigi",
url: "https://primal.net/p/npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc",
amount: 6001,
},
{
name: "Adarnit",
url: "https://primal.net/p/npub17armdveqy42uhuuuwjc5m2dgjkz7t7epgvwpuccqw8jusm8m0g4sn86n3s",
amount: 17_726,
},
{
name: "Auburn Citadel",
url: "https://primal.net/p/npub1730y5k2s9u82w9snx3hl37r8gpsrmqetc2y3xyx9h65yfpf28rtq0y635y",
amount: 17_471,
},
{
name: "Anon",
amount: 210_000,
},
{
name: "Daniel ∞/21M",
url: "https://twitter.com/DanielAngelovBG",
amount: 21_000,
},
{
name: "Ivo",
url: "https://primal.net/p/npub1mnwjn40hr042rsmzu64rsnwsw07uegg4tjkv620c94p6e797wkvq3qeujc",
amount: 5_000,
},
]
.sort((a, b) =>
b.amount !== a.amount
? b.amount - a.amount
: a.name.localeCompare(b.name),
)
.slice(0, 10)}
>
{({ name, url, amount }) => (
<li>
<a href={url} target="_blank">
{name}
</a>{" "}
- {amount.toLocaleString("en-us")} sats
</li>
)}
</For>
</ol>
</div>
<Show when={!standalone && safariOnly && (macOS || ipad || iphone)}>
<hr class="border-lighter -mx-4 border-t" />
<div class="space-y-4">
<Title>Install</Title>
<p>
<Show when={macOS}>
This app can be installed by clicking on the "File" tab on the
menu bar and then on "Add to dock".
</Show>
<Show when={iphone || ipad}>
This app can be installed by tapping on the "Share" button tab
of Safari and then on "Add to Home Screen".
</Show>
</p>
</div>
</Show>
</div>
<hr class="border-lighter -mx-4 border-t" />
<div class="pt-4 md:hidden">
<div class="flex items-center justify-center gap-8 py-1">
<AnchorAPI />
<AnchorGit />
<AnchorNostr />
<AnchorGeyser />
</div>
</div>
<p class="pb-[10vh] pt-4 text-center">
<span class="opacity-50">Version:</span>{" "}
<a
href="https://github.com/satonomics-org/satonomics/blob/main/CHANGELOG.md"
target="_blank"
>
{version}
</a>
</p>
</div>
);
}
function Title({ children }: ParentProps) {
return <p class="text-base font-medium">{children}</p>;
}
export function FieldRadioGroup<
T extends
| string
| {
text: string;
value: number;
},
>({
title,
sl,
ariaTitle,
description,
}: {
title: string;
ariaTitle: string;
description: string;
sl: SL<T>;
}) {
return (
<fieldset aria-label={`Choose an option for: ${ariaTitle}`}>
<p class="pb-0.5">{title}</p>
<p class="pb-1 text-sm opacity-50">{description}</p>
<RadioGroup sl={sl} title={title} />
</fieldset>
);
}
export function RadioGroup<
T extends
| string
| {
text: string;
value: number;
},
>({ title, sl, size }: { title: string; sl: SL<T>; size?: Size }) {
return (
<div
class={classPropToString([
size === "xs" && "gap-0.5 rounded-md border p-0.5 text-xs",
size === "sm" && "gap-1 rounded-md border p-1 text-sm",
(!size || size === "base") && "gap-1.5 rounded-lg border p-1.5",
"border-superlight -mx-2 mt-2 flex bg-stone-400/30 backdrop-blur-[2px] dark:bg-stone-950/75",
])}
>
<For each={sl.list()}>
{(value) => (
<label
class={classPropToString([
size === "xs" && "rounded px-1.5 py-0",
size === "sm" && "rounded px-2 py-1",
(!size || size === "base") && "rounded-md px-3 py-1.5",
value === sl.selected()
? "border-lighter bg-orange-50/75 shadow dark:bg-orange-200/10"
: "border-transparent",
"flex flex-1 cursor-pointer select-none items-center justify-center border font-medium hover:bg-orange-50 focus:outline-none active:scale-95 active:bg-orange-50 dark:hover:bg-orange-200/20 dark:active:bg-orange-200/10",
])}
>
<input
type="radio"
name={`${title}-option`}
value={typeof value === "object" ? value.value : value}
class="sr-only"
onClick={() => {
sl.select(value);
}}
/>
<span>{typeof value === "object" ? value.text : value}</span>
</label>
)}
</For>
</div>
);
}
@@ -1,117 +0,0 @@
import { File } from "./file";
import { Folder } from "./folder";
export function Tree({
tree,
selected,
openedFolders,
depth = 0,
visible,
selectPreset,
path = [],
favorites,
}: {
tree: PresetTree;
selected: Accessor<Preset>;
selectPreset(preset: Preset): void;
openedFolders: RWS<Set<string>>;
depth?: number;
visible?: Accessor<boolean>;
path?: FilePath;
favorites: Accessor<Preset[]>;
}) {
return (
<div style={{ display: visible?.() === false ? "none" : undefined }}>
<For each={tree}>
{(thing) => {
const active = createMemo(() => thing.id === selected().id);
const favorite = createMemo(() =>
favorites().includes(thing as Preset),
);
const visited = (thing as Preset).visited;
if (!("tree" in thing)) {
return (
<File
id={thing.id}
name={thing.name}
active={active}
depth={depth}
icon={thing.icon || IconTablerFile}
favorite={favorite}
visited={visited}
onClick={() => {
const selectedId = selected().id;
if (selectedId === thing.id) {
return;
}
// Has been filled in createPresets
selectPreset(thing as Preset);
}}
/>
);
}
const childrenVisible = createMemo(() =>
openedFolders().has(thing.id),
);
const childCount = countChildren(thing);
return (
<div>
<Folder
id={thing.id}
name={thing.name}
depth={depth}
open={childrenVisible}
children={childCount}
onClick={() => {
openedFolders.set((s) => {
if (childrenVisible()) {
s.delete(thing.id);
} else {
s.add(thing.id);
}
return s;
});
}}
/>
<Tree
tree={thing.tree}
selected={selected}
depth={depth + 1}
openedFolders={openedFolders}
visible={childrenVisible}
path={[...path, { name: thing.name, id: thing.id }]}
selectPreset={selectPreset}
favorites={favorites}
/>
</div>
);
}}
</For>
</div>
);
}
function countChildren(folder: PresetFolder) {
let count = 0;
function _countChildren(tree: PartialPresetTree) {
tree.forEach((anyPreset) => {
if ("tree" in anyPreset) {
_countChildren(anyPreset.tree);
} else {
count += 1;
}
});
}
_countChildren(folder.tree);
return count;
}
+42 -13
View File
@@ -1,18 +1,47 @@
import { touchScreen } from "/src/env";
export function Qrcode({ qrcode }: { qrcode: RWS<string> }) {
return (
<Show when={qrcode()}>
<div
class="absolute inset-0 z-50 flex h-full w-full items-center justify-center bg-black"
onClick={() => {
qrcode.set("");
}}
>
<img
class="aspect-square max-h-full grow object-contain"
src={qrcode()}
style={{ "image-rendering": "pixelated" }}
/>
<div
class="absolute inset-0 z-50 flex size-full items-center justify-center bg-black/50 backdrop-blur-md"
onClick={() => {
qrcode.set("");
}}
>
<div class="flex size-full max-h-[80dvh] max-w-md flex-col justify-center space-y-8 px-8 pb-8 text-base">
<p class="pb-4 text-center text-3xl font-bold">Share</p>
<p>
To share this page, you can either send the following QR Code with a
phone:
</p>
<div class="flex min-h-0 w-full flex-1 flex-col items-center justify-center">
<img
class="aspect-square min-h-0 flex-1 grow object-contain"
onClick={(event) => {
event?.stopPropagation();
}}
src={qrcode()}
style={{ "image-rendering": "pixelated" }}
/>
</div>
<div>
<p>Or if you prefer you can share this link instead:</p>
<a
onClick={(event) => {
event.stopPropagation();
}}
href={location.href}
>
{location.href}
</a>
</div>
<p>
{touchScreen ? "Touch" : "Click"} anywhere but on the QR Code to exit.
</p>
</div>
</Show>
</div>
);
}
@@ -4,22 +4,25 @@ export function AnchorAPI() {
return (
<Anchor
title="API"
icon={() => () => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.13468 2.41153C3.88395 3.0478 3.37143 3.79772 3.37143 4.4186C3.37143 5.03949 3.88395 5.78941 5.13468 6.42568C6.3444 7.04109 8.06359 7.44186 10 7.44186C11.9364 7.44186 13.6556 7.04109 14.8653 6.42568C16.1161 5.78941 16.6286 5.03949 16.6286 4.4186C16.6286 3.79772 16.1161 3.0478 14.8653 2.41153C13.6556 1.79612 11.9364 1.39535 10 1.39535C8.06359 1.39535 6.3444 1.79612 5.13468 2.41153ZM16.6286 6.93694C16.2841 7.21648 15.8934 7.46274 15.4786 7.67372C14.0411 8.40502 12.1032 8.83721 10 8.83721C7.89684 8.83721 5.95889 8.40502 4.52136 7.67372C4.10664 7.46274 3.71588 7.21648 3.37143 6.93694V10C3.37143 10.6209 3.88395 11.3708 5.13468 12.0071C6.3444 12.6225 8.06359 13.0233 10 13.0233C11.9364 13.0233 13.6556 12.6225 14.8653 12.0071C16.1161 11.3708 16.6286 10.6209 16.6286 10V6.93694ZM18 4.4186C18 2.98447 16.8752 1.87393 15.4786 1.16349C14.0411 0.432186 12.1032 0 10 0C7.89684 0 5.95889 0.432186 4.52136 1.16349C3.12484 1.87393 2 2.98447 2 4.4186V15.5814C2 17.0155 3.12484 18.1261 4.52136 18.8365C5.95889 19.5678 7.89684 20 10 20C12.1032 20 14.0411 19.5678 15.4786 18.8365C16.8752 18.1261 18 17.0155 18 15.5814V4.4186ZM16.6286 12.5183C16.2841 12.7979 15.8934 13.0441 15.4786 13.2551C14.0411 13.9864 12.1032 14.4186 10 14.4186C7.89684 14.4186 5.95889 13.9864 4.52136 13.2551C4.10664 13.0441 3.71588 12.7979 3.37143 12.5183V15.5814C3.37143 16.2023 3.88395 16.9522 5.13468 17.5885C6.3444 18.2039 8.06359 18.6047 10 18.6047C11.9364 18.6047 13.6556 18.2039 14.8653 17.5885C16.1161 16.9522 16.6286 16.2023 16.6286 15.5814V12.5183ZM6.34285 10C6.34285 10.5138 5.93351 10.9302 5.42857 10.9302C4.92362 10.9302 4.51428 10.5138 4.51428 10C4.51428 9.48625 4.92362 9.06977 5.42857 9.06977C5.93351 9.06977 6.34285 9.48625 6.34285 10ZM9.0857 11.8605C9.59065 11.8605 9.99999 11.444 9.99999 10.9302C9.99999 10.4165 9.59065 10 9.0857 10C8.58076 10 8.17142 10.4165 8.17142 10.9302C8.17142 11.444 8.58076 11.8605 9.0857 11.8605ZM6.34285 15.5814C6.34285 16.0951 5.93351 16.5116 5.42857 16.5116C4.92362 16.5116 4.51428 16.0951 4.51428 15.5814C4.51428 15.0676 4.92362 14.6512 5.42857 14.6512C5.93351 14.6512 6.34285 15.0676 6.34285 15.5814ZM9.0857 17.4419C9.59065 17.4419 9.99999 17.0254 9.99999 16.5116C9.99999 15.9979 9.59065 15.5814 9.0857 15.5814C8.58076 15.5814 8.17142 15.9979 8.17142 16.5116C8.17142 17.0254 8.58076 17.4419 9.0857 17.4419Z"
fill="currentColor"
></path>
</svg>
)}
icon={
() => IconTablerApi
// () => (
// <svg
// width="20"
// height="20"
// viewBox="0 0 20 20"
// fill="none"
// xmlns="http://www.w3.org/2000/svg"
// >
// <path
// fill-rule="evenodd"
// clip-rule="evenodd"
// d="M5.13468 2.41153C3.88395 3.0478 3.37143 3.79772 3.37143 4.4186C3.37143 5.03949 3.88395 5.78941 5.13468 6.42568C6.3444 7.04109 8.06359 7.44186 10 7.44186C11.9364 7.44186 13.6556 7.04109 14.8653 6.42568C16.1161 5.78941 16.6286 5.03949 16.6286 4.4186C16.6286 3.79772 16.1161 3.0478 14.8653 2.41153C13.6556 1.79612 11.9364 1.39535 10 1.39535C8.06359 1.39535 6.3444 1.79612 5.13468 2.41153ZM16.6286 6.93694C16.2841 7.21648 15.8934 7.46274 15.4786 7.67372C14.0411 8.40502 12.1032 8.83721 10 8.83721C7.89684 8.83721 5.95889 8.40502 4.52136 7.67372C4.10664 7.46274 3.71588 7.21648 3.37143 6.93694V10C3.37143 10.6209 3.88395 11.3708 5.13468 12.0071C6.3444 12.6225 8.06359 13.0233 10 13.0233C11.9364 13.0233 13.6556 12.6225 14.8653 12.0071C16.1161 11.3708 16.6286 10.6209 16.6286 10V6.93694ZM18 4.4186C18 2.98447 16.8752 1.87393 15.4786 1.16349C14.0411 0.432186 12.1032 0 10 0C7.89684 0 5.95889 0.432186 4.52136 1.16349C3.12484 1.87393 2 2.98447 2 4.4186V15.5814C2 17.0155 3.12484 18.1261 4.52136 18.8365C5.95889 19.5678 7.89684 20 10 20C12.1032 20 14.0411 19.5678 15.4786 18.8365C16.8752 18.1261 18 17.0155 18 15.5814V4.4186ZM16.6286 12.5183C16.2841 12.7979 15.8934 13.0441 15.4786 13.2551C14.0411 13.9864 12.1032 14.4186 10 14.4186C7.89684 14.4186 5.95889 13.9864 4.52136 13.2551C4.10664 13.0441 3.71588 12.7979 3.37143 12.5183V15.5814C3.37143 16.2023 3.88395 16.9522 5.13468 17.5885C6.3444 18.2039 8.06359 18.6047 10 18.6047C11.9364 18.6047 13.6556 18.2039 14.8653 17.5885C16.1161 16.9522 16.6286 16.2023 16.6286 15.5814V12.5183ZM6.34285 10C6.34285 10.5138 5.93351 10.9302 5.42857 10.9302C4.92362 10.9302 4.51428 10.5138 4.51428 10C4.51428 9.48625 4.92362 9.06977 5.42857 9.06977C5.93351 9.06977 6.34285 9.48625 6.34285 10ZM9.0857 11.8605C9.59065 11.8605 9.99999 11.444 9.99999 10.9302C9.99999 10.4165 9.59065 10 9.0857 10C8.58076 10 8.17142 10.4165 8.17142 10.9302C8.17142 11.444 8.58076 11.8605 9.0857 11.8605ZM6.34285 15.5814C6.34285 16.0951 5.93351 16.5116 5.42857 16.5116C4.92362 16.5116 4.51428 16.0951 4.51428 15.5814C4.51428 15.0676 4.92362 14.6512 5.42857 14.6512C5.93351 14.6512 6.34285 15.0676 6.34285 15.5814ZM9.0857 17.4419C9.59065 17.4419 9.99999 17.0254 9.99999 16.5116C9.99999 15.9979 9.59065 15.5814 9.0857 15.5814C8.58076 15.5814 8.17142 15.9979 8.17142 16.5116C8.17142 17.0254 8.58076 17.4419 9.0857 17.4419Z"
// fill="currentColor"
// ></path>
// </svg>
// )
}
href="https://api.satonomics.xyz"
/>
);
@@ -0,0 +1,26 @@
import { Anchor } from "./anchor";
export function AnchorGeyser() {
return (
<Anchor
title="Geyser"
icon={() => (props: { class?: string }) => (
<svg
viewBox="0 0 365 365"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
class={props.class}
style={{
padding: "0.125rem",
}}
>
<path
d="M346.396 125.679C346.193 124.459 345.986 123.24 345.755 122.028C345.017 118.123 345.017 118.123 345.017 118.123C343.98 113.895 339.304 110.436 334.628 110.435L221.265 110.425C216.587 110.424 211.581 114.065 210.14 118.514L186.054 192.873C185.343 195.069 184.408 197.774 185.441 199.236C186.502 200.735 189.57 200.963 191.939 200.963H238.093C238.093 200.963 240.249 200.945 240.536 202.442C240.843 204.031 239.19 205.743 239.19 205.743L157.833 291.25C154.987 294.242 150.017 299.46 146.792 302.845L125.923 324.742C125.923 324.742 125.497 325.221 124.999 325.68C124.712 325.946 124.436 326.178 124.173 326.383C121.278 328.632 119.909 327.374 121.11 323.26L131.729 286.87C133.04 282.381 135.182 275.033 136.493 270.544L142.575 249.702C143.595 246.206 144.753 242.242 145.481 239.743C145.688 239.033 146.17 236.844 145.442 235.737C144.751 234.686 143.587 234.487 142.28 234.487C140.864 234.487 139.196 234.487 137.442 234.487H93.4839C88.8059 234.487 87.2359 232.119 89.9939 229.225C92.7519 226.331 97.6479 221.193 100.875 217.807L135.051 181.949C138.278 178.563 143.246 173.345 146.094 170.352C148.942 167.359 153.91 162.139 157.133 158.75L162.904 152.684C166.129 149.295 171.404 143.75 174.627 140.362L186.471 127.916C189.694 124.527 194.758 119.205 197.721 116.089C200.686 112.974 203.889 109.608 204.838 108.609C205.787 107.611 209.201 104.022 212.426 100.633L278.317 31.3812C281.542 27.9922 280.911 23.2282 276.919 20.7932C276.919 20.7932 267.599 15.1122 258.755 11.4192C235.769 1.81919 210.816 -1.59681 185.61 0.678194C178.503 1.32019 171.393 2.41119 164.321 3.94219C145.237 8.07619 126.546 15.3542 108.954 25.4422C97.9699 31.7382 87.4599 39.1132 77.5829 47.4572C54.3189 67.1072 34.7959 91.9432 20.9249 120.702C14.0339 134.988 8.95193 149.42 5.51493 163.704C-1.69007 193.646 -1.63107 223.046 4.49993 249.499C8.57593 267.102 15.3589 283.516 24.5589 298.092C25.1199 298.982 25.6939 299.867 26.2739 300.743C31.2249 308.225 36.8479 315.217 43.0999 321.638C52.6369 331.432 63.6919 339.937 76.2069 346.811C82.4469 350.24 88.8519 353.149 95.3729 355.575C99.0779 356.951 102.824 358.176 106.605 359.24C113.087 361.068 119.679 362.438 126.337 363.378C184.333 371.547 251.78 346.032 298.382 290.078C307.3 279.37 315.068 268.043 321.671 256.283C330.341 240.845 336.993 224.657 341.544 208.154C345.339 194.392 347.665 180.431 348.483 166.541C349.298 152.728 348.617 139.014 346.396 125.679Z"
fill="currentColor"
></path>
</svg>
)}
href="https://geyser.fund/project/satonomics/"
/>
);
}
@@ -4,8 +4,8 @@ export function AnchorGit() {
return (
<Anchor
title="Git"
icon={() => IconTablerGitMerge}
href="https://codeberg.org/satonomics/satonomics"
icon={() => IconTablerBrandGithubFilled}
href="https://github.com/satonomics-org/satonomics"
/>
);
}
@@ -1,7 +1,7 @@
export function AnchorLogo() {
return (
<a
class="inline-flex justify-center rounded-lg bg-gradient-to-br from-orange-500 to-orange-800 p-4"
class="inline-flex justify-center rounded-lg bg-gradient-to-br from-orange-300 to-orange-600 p-4 text-orange-50 shadow dark:from-orange-500 dark:to-orange-800 dark:text-orange-50"
href="https://app.satonomics.xyz"
title="Reload"
>
@@ -1,13 +1,13 @@
import { Button } from "./button";
export function ButtonTree({
export function ButtonFolders({
selected,
setSelected,
}: {
selected: Accessor<FrameName>;
setSelected: Setter<FrameName>;
}) {
const frameName: FrameName = "Tree";
const frameName: FrameName = "Folders";
return (
<Button
@@ -1,10 +0,0 @@
import { Button } from "./button";
export function ButtonRefresh() {
return (
<Button title="Refresh" onClick={() => document.location.reload()}>
<IconTablerRefreshAlert class="absolute size-5 animate-ping text-orange-400" />
<IconTablerRefreshAlert class="relative size-5 text-orange-300" />
</Button>
);
}
@@ -18,8 +18,12 @@ export function Clickable({
<Dynamic
component={onClick ? "button" : href ? "a" : "span"}
class={classPropToString([
selected?.() ? "bg-orange-200/10" : "opacity-50 hover:bg-orange-200/10",
"select-none rounded-lg p-3.5 hover:text-orange-400 hover:opacity-100 active:scale-90",
!href
? selected?.()
? "bg-orange-800/10 dark:bg-orange-200/10"
: "text-orange-900/50 dark:text-orange-100/50"
: "text-opacity-70 dark:text-opacity-70",
"inline-flex select-none rounded-lg p-3.5 hover:bg-orange-800/10 hover:text-orange-600 hover:opacity-100 active:scale-90 dark:hover:bg-orange-200/10 dark:hover:text-orange-400",
])}
title={title}
onClick={onClick}
+5 -11
View File
@@ -1,30 +1,27 @@
import { AnchorAPI } from "./components/anchorAPI";
import { AnchorGeyser } from "./components/anchorGeyser";
import { AnchorGit } from "./components/anchorGit";
import { AnchorHome } from "./components/anchorHome";
import { AnchorLogo } from "./components/anchorLogo";
import { AnchorNostr } from "./components/anchorNostr";
import { ButtonChart } from "./components/buttonChart";
import { ButtonFavorites } from "./components/buttonFavorites";
import { ButtonFolders } from "./components/buttonFolders";
import { ButtonHistory } from "./components/buttonHistory";
import { ButtonRefresh } from "./components/buttonRefresh";
import { ButtonSearch } from "./components/buttonSearch";
import { ButtonSettings } from "./components/buttonSettings";
import { ButtonTree } from "./components/buttonTree";
export function StripDesktop({
selected,
setSelected,
needsRefresh,
}: {
selected: Accessor<FrameName>;
setSelected: Setter<FrameName>;
needsRefresh: Accessor<boolean>;
}) {
return (
<>
<AnchorLogo />
<ButtonTree selected={selected} setSelected={setSelected} />
<ButtonFolders selected={selected} setSelected={setSelected} />
<ButtonFavorites selected={selected} setSelected={setSelected} />
<ButtonSearch selected={selected} setSelected={setSelected} />
<ButtonHistory selected={selected} setSelected={setSelected} />
@@ -33,13 +30,10 @@ export function StripDesktop({
<div class="size-full" />
<Show when={needsRefresh()}>
<ButtonRefresh />
</Show>
<AnchorAPI />
<AnchorGit />
<AnchorNostr />
<AnchorGeyser />
{/* <AnchorHome /> */}
</>
);
@@ -55,7 +49,7 @@ export function StripMobile({
return (
<>
<ButtonChart selected={selected} setSelected={setSelected} />
<ButtonTree selected={selected} setSelected={setSelected} />
<ButtonFolders selected={selected} setSelected={setSelected} />
<ButtonFavorites selected={selected} setSelected={setSelected} />
<ButtonSearch selected={selected} setSelected={setSelected} />
<ButtonHistory selected={selected} setSelected={setSelected} />
+82
View File
@@ -0,0 +1,82 @@
import { useRegisterSW } from "virtual:pwa-register/solid";
export function Update() {
// if ("serviceWorker" in navigator) {
// navigator.serviceWorker.getRegistrations().then((l) => {
// console.log(l);
// if (!l.length) return;
// wasAlreadySW.set(true);
// // navigator.serviceWorker.addEventListener("controllerchange", () => {
// // // Will show up in safari and chrome
// // console.log("sw: controller change");
// // setTimeout(() => {
// // needsRefresh.set(true);
// // }, 1000);
// // });
// });
// setTimeout(() => {
// navigator.serviceWorker
// .register("/sw.js", { scope: "/" })
// .then((registration) => {
// // Will show up on safari sw update but not on chrome
// console.log("sw: registration succeeded");
// console.log(registration);
// registration.addEventListener("updatefound", () => {
// if (wasAlreadySW()) {
// setTimeout(() => {
// needsRefresh.set(true);
// }, 1000);
// }
// // will show up on chrome sw update
// console.log("sw: update found");
// });
// })
// .catch((error) => {
// // registration failed
// console.error(`sw: registration failed with ${error}`);
// });
// }, 5000);
// }
// useRegisterSW can also return an offlineReady thingy
const {
needRefresh: [needRefresh, setNeedRefresh],
updateServiceWorker,
} = useRegisterSW({
immediate: true,
onRegisteredSW(swUrl, r) {
console.log("sw: registered: " + r?.scope);
},
onRegisterError(error: Error) {
console.log("sw: registration error", error);
},
});
return (
<Show when={needRefresh()}>
<div class="absolute inset-x-1.5 top-1.5 z-[99999] flex items-center justify-between rounded-lg bg-orange-700/75 p-1.5 shadow backdrop-blur-sm">
<div>
<span class="truncate px-1">New version available,</span>
<button
class="mr-2 rounded-md bg-orange-50 bg-opacity-60 px-1.5 py-0.5 font-medium text-orange-950 hover:bg-opacity-100"
onClick={async () => await updateServiceWorker()}
>
Install
</button>
</div>
<button
class="rounded-md bg-black/25 p-1 hover:bg-black/50"
onClick={() => setNeedRefresh(false)}
>
<IconTablerX />
</button>
</div>
</Show>
);
}
+156 -84
View File
@@ -1,11 +1,9 @@
import { createRWS } from "/src/solid/rws";
import { env } from "../env";
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 { priceToUSLocale } from "../scripts/utils/locale";
import { createSL } from "../scripts/utils/selectableList/static";
import { sleep } from "../scripts/utils/sleep";
import {
readBooleanFromStorage,
@@ -14,11 +12,14 @@ import {
import { readBooleanURLParam, writeURLParam } from "../scripts/utils/urlParams";
import { webSockets } from "../scripts/ws";
import { classPropToString } from "../solid/classes";
import { Background, LOCAL_STORAGE_MARQUEE_KEY } from "./components/background";
import { Background } from "./components/background";
import { ChartFrame } from "./components/frames/chart";
import { TreeFrame } from "./components/frames/tree";
import { FavoritesFrame } from "./components/frames/favorites";
import { FoldersFrame } from "./components/frames/folders";
import { HistoryFrame } from "./components/frames/history";
import { SettingsFrame } from "./components/frames/settings";
import { StripDesktop, StripMobile } from "./components/strip";
import { registerServiceWorker } from "./scripts/register";
import { Update } from "./components/update";
const LOCAL_STORAGE_BAR_KEY = "bar-width";
const LOCAL_STORAGE_FULLSCREEN = "fullscrenn";
@@ -26,37 +27,111 @@ const LOCAL_STORAGE_FULLSCREEN = "fullscrenn";
export const INPUT_PRESET_SEARCH_ID = "input-search-preset";
export function App() {
const needRefresh = registerServiceWorker().needRefresh[0];
const tabFocused = createRWS(true);
const qrcode = createRWS("");
const appTheme = createSL(["System", "Dark", "Light"] as const, {
saveable: {
key: "app-theme",
mode: "localStorage",
},
defaultIndex: 0,
});
const dark = createRWS(false);
const preferredColorSchemeMatchMedia = window.matchMedia(
"(prefers-color-scheme: dark)",
);
const preferredSystemTheme = createRWS<"light" | "dark">(
preferredColorSchemeMatchMedia.matches ? "dark" : "light",
);
function preferredColorSchemeListener(event: MediaQueryListEvent) {
return preferredSystemTheme.set(event.matches ? "dark" : "light");
}
preferredColorSchemeMatchMedia.addEventListener(
"change",
preferredColorSchemeListener,
);
onCleanup(() => {
preferredColorSchemeMatchMedia.removeEventListener(
"change",
preferredColorSchemeListener,
);
});
createEffect(() => {
if (
appTheme.selected() === "Dark" ||
(appTheme.selected() === "System" && preferredSystemTheme() === "dark")
) {
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",
mode: "localStorage",
},
defaultIndex: 0,
});
const backgroundOpacity = createSL(
[
{
text: "Strong",
value: 0.0444,
},
{
text: "Normal",
value: 0.0333,
},
{
text: "Light",
value: 0.0222,
},
{
text: "Subtle",
value: 0.0111,
},
] as const,
{
saveable: {
key: "bg-text-opacity",
mode: "localStorage",
},
defaultIndex: 2,
},
);
const fullscreen = createRWS(
readBooleanURLParam(LOCAL_STORAGE_FULLSCREEN) ||
readBooleanFromStorage(LOCAL_STORAGE_FULLSCREEN) ||
false,
);
const activeResources = createRWS<Set<ResourceDataset<any, any>>>(new Set(), {
equals: false,
});
const datasets = createDatasets({
setActiveResources: activeResources.set,
});
const windowWidth = createRWS(window.innerWidth);
const windowWidth60p = createMemo(() => windowWidth() * 0.6);
const windowResizeCallback = () => {
windowWidth.set(window.innerWidth);
};
window.addEventListener("resize", windowResizeCallback);
onCleanup(() => window.removeEventListener("resize", windowResizeCallback));
const windowSizeIsAtLeastMedium = createMemo(() => windowWidth() >= 720);
const windowSizeIsAtLeastMedium = createMemo(() => windowWidth() >= 768);
const minBarWidth = 384;
const barWidth = createRWS(
Number(localStorage.getItem(LOCAL_STORAGE_BAR_KEY)),
Number(localStorage.getItem(LOCAL_STORAGE_BAR_KEY)) || minBarWidth,
);
createEffect(() => {
@@ -77,15 +152,14 @@ export function App() {
const selectedFrame = createMemo(() =>
windowSizeIsAtLeastMedium() && _selectedFrame() === "Chart"
? "Tree"
? "Folders"
: _selectedFrame(),
);
const presets = createPresets(datasets);
const marquee = createRWS(!localStorage.getItem(LOCAL_STORAGE_MARQUEE_KEY));
const presets = createPresets();
const resizingBarStart = createRWS<number | undefined>(undefined);
const resizingBarWidth = createRWS<number>(0);
createEffect(
() => {
@@ -98,6 +172,8 @@ export function App() {
},
);
const datasets = createDatasets();
onMount(() => {
webSockets.openAll();
@@ -109,37 +185,11 @@ export function App() {
console.log("close:", close);
document.title = `${priceToUSLocale(latest.close, false)} | Satonomics`;
document.title = `${latest.close.toLocaleString("en-us")} | Satonomics`;
}
});
});
const FavoritesFrame = lazy(() =>
import("./components/frames/favorites").then((d) => ({
default: d.FavoritesFrame,
})),
);
const HistoryFrame = lazy(() =>
import("./components/frames/history").then((d) => ({
default: d.HistoryFrame,
})),
);
const SearchFrame = lazy(() =>
import("./components/frames/search").then((d) => ({
default: d.SearchFrame,
})),
);
const SettingsFrame = lazy(() =>
import("./components/frames/settings").then((d) => ({
default: d.SettingsFrame,
})),
);
const Qrcode = lazy(() =>
import("./components/qrcode").then((d) => ({
default: d.Qrcode,
})),
);
const documentVisibilityChange = () =>
tabFocused.set(document.visibilityState === "visible");
document.addEventListener("visibilitychange", documentVisibilityChange);
@@ -174,11 +224,25 @@ export function App() {
document.addEventListener("keydown", documentOnKeyDown);
onCleanup(() => document.removeEventListener("keydown", documentOnKeyDown));
const resizeInitialRange = createRWS<TimeRange | null>(null);
const SearchFrame = lazy(() =>
import("./components/frames/search").then((d) => ({
default: d.SearchFrame,
})),
);
const Qrcode = lazy(() =>
import("./components/qrcode").then((d) => ({
default: d.Qrcode,
})),
);
return (
<>
<Background marquee={marquee} focused={tabFocused} />
<Background
focused={tabFocused}
mode={backgroundMode}
opacity={backgroundOpacity}
/>
<div
class="relative h-dvh selection:bg-orange-800"
@@ -186,12 +250,18 @@ export function App() {
"user-select": resizingBarStart() !== undefined ? "none" : undefined,
}}
onMouseMove={(event) => {
const start = resizingBarStart();
const startingClientX = resizingBarStart();
if (start !== undefined) {
barWidth.set(event.x - start + 384);
setTimeScale(resizeInitialRange());
if (startingClientX !== undefined) {
barWidth.set(
Math.min(
Math.max(
resizingBarWidth() + event.clientX - startingClientX,
minBarWidth,
),
windowWidth60p(),
),
);
}
}}
onMouseUp={() => resizingBarStart.set(undefined)}
@@ -199,29 +269,34 @@ export function App() {
onTouchEnd={() => resizingBarStart.set(undefined)}
onTouchCancel={() => resizingBarStart.set(undefined)}
>
<Qrcode qrcode={qrcode} />
<Show when={qrcode()}>
<Qrcode qrcode={qrcode} />
</Show>
<div class="flex size-full flex-col md:flex-row md:p-3">
<Update />
<div class="flex size-full flex-col md:flex-row md:p-3 md:short:p-0">
<Show when={!windowSizeIsAtLeastMedium() || !fullscreen()}>
<div
class={classPropToString([
env.standalone && "border-t",
"flex h-full flex-col overflow-hidden border-white/10 bg-gradient-to-b from-orange-500/10 to-orange-950/10 md:flex-row md:rounded-2xl md:border",
standalone && "border-t md:border-t-0",
"border-lighter flex h-full flex-col overflow-hidden bg-gradient-to-b from-orange-300/15 to-orange-400/15 dark:from-orange-500/10 dark:to-orange-950/10 md:flex-row md:rounded-2xl md:border md:shadow-md md:short:hidden",
])}
>
<div class="hidden flex-col gap-2 border-r border-white/10 bg-black/30 p-3 backdrop-blur-sm md:flex">
<div class="border-lighter hidden flex-col gap-2 border-r bg-orange-300/30 p-3 backdrop-blur-sm dark:bg-black/30 md:flex">
<StripDesktop
selected={selectedFrame}
setSelected={_selectedFrame.set}
needsRefresh={needRefresh}
/>
</div>
<div
class="flex h-full min-h-0 md:min-w-[384px]"
class="flex h-full min-h-0"
style={{
...(windowSizeIsAtLeastMedium()
? {
width: `min(${barWidth()}px, 75dvw)`,
"min-width": `${minBarWidth}px`,
width: `${barWidth()}px`,
"max-width": `${windowWidth60p()}px`,
}
: {}),
}}
@@ -233,11 +308,11 @@ export function App() {
qrcode={qrcode}
standalone={false}
datasets={datasets}
activeResources={activeResources}
dark={dark}
/>
</Show>
<TreeFrame presets={presets} selectedFrame={selectedFrame} />
<FoldersFrame presets={presets} selectedFrame={selectedFrame} />
<FavoritesFrame
presets={presets}
selectedFrame={selectedFrame}
@@ -245,15 +320,17 @@ export function App() {
<SearchFrame presets={presets} selectedFrame={selectedFrame} />
<HistoryFrame presets={presets} selectedFrame={selectedFrame} />
<SettingsFrame
marquee={marquee}
selectedFrame={selectedFrame}
appTheme={appTheme}
backgroundMode={backgroundMode}
backgroundOpacity={backgroundOpacity}
/>
</div>
<div
class={classPropToString([
env.standalone && "pb-6",
"flex justify-between gap-3 border-t border-white/10 bg-black/30 p-2 backdrop-blur-sm md:hidden",
standalone && "pb-6",
"border-lighter flex justify-between gap-3 border-t bg-black/30 p-2 backdrop-blur-sm sm:justify-around md:hidden short:hidden",
])}
>
<StripMobile
@@ -266,26 +343,21 @@ export function App() {
<Show when={!fullscreen()}>
<div
class="mx-[3px] my-8 hidden w-[6px] cursor-col-resize items-center justify-center rounded-full bg-orange-100 opacity-0 hover:opacity-50 md:block"
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);
resizingBarStart() === undefined &&
// TODO: set size of bar instead
if (resizingBarStart() === undefined) {
resizingBarStart.set(event.clientX);
resizingBarWidth.set(barWidth());
}
}}
onTouchStart={(event) => {
resizeInitialRange.set(chartState.range);
resizingBarStart() === undefined &&
if (resizingBarStart() === undefined) {
resizingBarStart.set(event.touches[0].clientX);
resizingBarWidth.set(barWidth());
}
}}
onDblClick={() => {
resizeInitialRange.set(chartState.range);
barWidth.set(0);
setTimeScale(resizeInitialRange());
}}
/>
</Show>
@@ -297,8 +369,8 @@ export function App() {
presets={presets}
qrcode={qrcode}
fullscreen={fullscreen}
activeResources={activeResources}
datasets={datasets}
dark={dark}
/>
</div>
</Show>
-67
View File
@@ -1,67 +0,0 @@
import { useRegisterSW } from "virtual:pwa-register/solid";
import { FIVE_MINUTES_IN_MS } from "/src/scripts/utils/time";
export function registerServiceWorker() {
return useRegisterSW({
onRegisteredSW(swUrl, registered) {
console.log("sw: registered", registered);
if (registered) {
const callback = async () => {
if (!(!registered.installing && navigator)) return;
if ("connection" in navigator && !navigator.onLine) return;
const resp = await fetch(swUrl, {
cache: "no-store",
headers: {
cache: "no-store",
"cache-control": "no-cache",
},
});
if (resp?.status === 200) {
await registered.update();
}
};
callback();
setInterval(callback, FIVE_MINUTES_IN_MS);
}
},
onRegisterError(error) {
console.log("sw: registration error", error);
},
onNeedRefresh() {
console.log("sw: needs refresh");
},
});
}
// From update.tsx
// onMount(async () => {
// if ('serviceWorker' in navigator) {
// try {
// const registration = await navigator.serviceWorker.register('/sw.js')
// registration.addEventListener('updatefound', () => {
// const worker = registration.installing
// worker?.addEventListener('statechange', () => {
// if (
// worker.state === 'activated' &&
// navigator.serviceWorker.controller
// ) {
// ;(Object.entries(props.resources) as Entries<ResourcesHTTP>)
// .map(([_, value]) => value.fetch)
// .forEach((fetch) => fetch())
// setTimeout(() => updateAvailable.set(true), FIVE_SECOND_IN_MS)
// }
// })
// })
// } catch {}
// }
// })
+3 -1
View File
@@ -1,7 +1,9 @@
type FrameName =
| "Chart"
| "Tree"
| "Folders"
| "Favorites"
| "Search"
| "History"
| "Settings";
type Size = "xs" | "sm" | "base" | "lg" | "xl";
+31 -3
View File
@@ -1,3 +1,31 @@
export const env = {
standalone: "standalone" in window.navigator && !!window.navigator.standalone,
};
export const standalone =
"standalone" in window.navigator && !!window.navigator.standalone;
export const touchScreen =
"ontouchstart" in window ||
navigator.maxTouchPoints > 0 ||
(navigator as any).msMaxTouchPoints > 0;
export const requestIdleCallbackPossible = "requestIdleCallback" in window;
console.log(navigator.userAgent);
export const macOS = navigator.userAgent.toLowerCase().includes("mac os");
export const iphone = navigator.userAgent.toLowerCase().includes("iphone");
export const ipad = navigator.userAgent.toLowerCase().includes("ipad");
export const chrome = navigator.userAgent.toLowerCase().includes("chrome");
export const firefox = navigator.userAgent.toLowerCase().includes("firefox");
export const gecko = navigator.userAgent.toLowerCase().includes("gecko");
export const safari = navigator.userAgent.toLowerCase().includes("safari");
export const safariOnly = safari && !chrome;
export const phone =
/Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent,
);
+3 -5
View File
@@ -3,6 +3,8 @@ import { render } from "solid-js/web";
import "./styles.css";
import { App } from "./app";
const root = document.getElementById("root");
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -11,8 +13,4 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
);
}
render(() => {
const App = lazy(() => import("./app").then((d) => ({ default: d.App })));
return <App />;
}, root!);
render(App, root!);
+8 -6
View File
@@ -2,17 +2,19 @@ export const addressCohortsBySize = [
{
key: "plankton",
name: "Plankton",
size: "1 sat to 0.1 BTC",
},
{
key: "shrimp",
name: "Shrimp",
size: "0.1 sat to 1 BTC",
},
{ key: "crab", name: "Crab" },
{ key: "fish", name: "Fish" },
{ key: "shark", name: "Shark" },
{ key: "whale", name: "Whale" },
{ key: "humpback", name: "Humpback" },
{ key: "megalodon", name: "Megalodon" },
{ key: "crab", name: "Crab", size: "1 BTC to 10 BTC" },
{ key: "fish", name: "Fish", size: "10 BTC to 100 BTC" },
{ key: "shark", name: "Shark", size: "100 BTC to 1000 BTC" },
{ key: "whale", name: "Whale", size: "1000 BTC to 10 000 BTC" },
{ key: "humpback", name: "Humpback", size: "10 000 BTC to 100 000 BTC" },
{ key: "megalodon", name: "Megalodon", size: "More than 100 000 BTC" },
] as const;
export const addressCohortsByType = [
+50 -31
View File
@@ -1,10 +1,12 @@
export const xthCohorts = [
{
id: "sth",
key: "sth",
name: "Short Term Holders",
legend: "STH",
},
{
id: "lth",
key: "lth",
name: "Long Term Holders",
legend: "LTH",
@@ -12,75 +14,86 @@ export const xthCohorts = [
] as const;
export const upToCohorts = [
{ key: "up_to_1d", name: "Up To 1 Day", legend: "1D" },
{ key: "up_to_1w", name: "Up To 1 Week", legend: "1W" },
{ key: "up_to_1m", name: "Up To 1 Month", legend: "1M" },
{ key: "up_to_2m", name: "Up To 2 Months", legend: "2M" },
{ key: "up_to_3m", name: "Up To 3 Months", legend: "3M" },
{ key: "up_to_4m", name: "Up To 4 Months", legend: "4M" },
{ key: "up_to_5m", name: "Up To 5 Months", legend: "5M" },
{ key: "up_to_6m", name: "Up To 6 Months", legend: "6M" },
{ key: "up_to_1y", name: "Up To 1 Year", legend: "1Y" },
{ key: "up_to_2y", name: "Up To 2 Years", legend: "2Y" },
{ key: "up_to_3y", name: "Up To 3 Years", legend: "3Y" },
{ key: "up_to_5y", name: "Up To 5 Years", legend: "5Y" },
{ key: "up_to_7y", name: "Up To 7 Years", legend: "7Y" },
{ key: "up_to_10y", name: "Up To 10 Years", legend: "10Y" },
{ key: "up_to_15y", name: "Up To 15 Years", legend: "15Y" },
{ id: "up-to-1d", key: "up_to_1d", name: "Up To 1 Day", legend: "1D" },
{ id: "up-to-1w", key: "up_to_1w", name: "Up To 1 Week", legend: "1W" },
{ id: "up-to-1m", key: "up_to_1m", name: "Up To 1 Month", legend: "1M" },
{ id: "up-to-2m", key: "up_to_2m", name: "Up To 2 Months", legend: "2M" },
{ id: "up-to-3m", key: "up_to_3m", name: "Up To 3 Months", legend: "3M" },
{ id: "up-to-4m", key: "up_to_4m", name: "Up To 4 Months", legend: "4M" },
{ id: "up-to-5m", key: "up_to_5m", name: "Up To 5 Months", legend: "5M" },
{ id: "up-to-6m", key: "up_to_6m", name: "Up To 6 Months", legend: "6M" },
{ id: "up-to-1y", key: "up_to_1y", name: "Up To 1 Year", legend: "1Y" },
{ id: "up-to-2y", key: "up_to_2y", name: "Up To 2 Years", legend: "2Y" },
{ id: "up-to-3y", key: "up_to_3y", name: "Up To 3 Years", legend: "3Y" },
{ id: "up-to-5y", key: "up_to_5y", name: "Up To 5 Years", legend: "5Y" },
{ id: "up-to-7y", key: "up_to_7y", name: "Up To 7 Years", legend: "7Y" },
{ id: "up-to-10y", key: "up_to_10y", name: "Up To 10 Years", legend: "10Y" },
{ id: "up-to-15y", key: "up_to_15y", name: "Up To 15 Years", legend: "15Y" },
] as const;
export const fromXToYCohorts = [
{
id: "from-1d-to-1w",
key: "from_1d_to_1w",
name: "From 1 Day To 1 Week",
legend: "1D - 1W",
},
{
id: "from-1w-to-1m",
key: "from_1w_to_1m",
name: "From 1 Week To 1 Month",
legend: "1W - 1M",
},
{
id: "from-1m-to-3m",
key: "from_1m_to_3m",
name: "From 1 Month To 3 Months",
legend: "1M - 3M",
},
{
id: "from-3m-to-6m",
key: "from_3m_to_6m",
name: "From 3 Months To 6 Months",
legend: "3M - 6M",
},
{
id: "from-6m-to-1y",
key: "from_6m_to_1y",
name: "From 6 Months To 1 Year",
legend: "6M - 1Y",
},
{
id: "from-1y-to-2y",
key: "from_1y_to_2y",
name: "From 1 Year To 2 Years",
legend: "1Y - 2Y",
},
{
id: "from-2y-to-3y",
key: "from_2y_to_3y",
name: "From 2 Years To 3 Years",
legend: "2Y - 3Y",
},
{
id: "from-3y-to-5y",
key: "from_3y_to_5y",
name: "From 3 Years To 5 Years",
legend: "3Y - 5Y",
},
{
id: "from-5y-to-7y",
key: "from_5y_to_7y",
name: "From 5 Years To 7 Years",
legend: "5Y - 7Y",
},
{
id: "from-7y-to-10y",
key: "from_7y_to_10y",
name: "From 7 Years To 10 Years",
legend: "7Y - 10Y",
},
{
id: "from-10y-to-15y",
key: "from_10y_to_15y",
name: "From 10 Years To 15 Years",
legend: "10Y - 15Y",
@@ -89,26 +102,31 @@ export const fromXToYCohorts = [
export const fromXCohorts = [
{
id: "from-1y",
key: "from_1y",
name: "From 1 Year",
legend: "1Y+",
},
{
id: "from-2y",
key: "from_2y",
name: "From 2 Years",
legend: "2Y+",
},
{
id: "from-4y",
key: "from_4y",
name: "From 4 Years",
legend: "4Y+",
},
{
id: "from-10y",
key: "from_10y",
name: "From 10 Years",
legend: "10Y+",
},
{
id: "from-15y",
key: "from_15y",
name: "From 15 Years",
legend: "15Y+",
@@ -116,27 +134,28 @@ export const fromXCohorts = [
] as const;
export const yearCohorts = [
{ key: "year_2009", name: "2009" },
{ key: "year_2010", name: "2010" },
{ key: "year_2011", name: "2011" },
{ key: "year_2012", name: "2012" },
{ key: "year_2013", name: "2013" },
{ key: "year_2014", name: "2014" },
{ key: "year_2015", name: "2015" },
{ key: "year_2016", name: "2016" },
{ key: "year_2017", name: "2017" },
{ key: "year_2018", name: "2018" },
{ key: "year_2019", name: "2019" },
{ key: "year_2020", name: "2020" },
{ key: "year_2021", name: "2021" },
{ key: "year_2022", name: "2022" },
{ key: "year_2023", name: "2023" },
{ key: "year_2024", name: "2024" },
{ id: "year-2009", key: "year_2009", name: "2009" },
{ id: "year-2010", key: "year_2010", name: "2010" },
{ id: "year-2011", key: "year_2011", name: "2011" },
{ id: "year-2012", key: "year_2012", name: "2012" },
{ id: "year-2013", key: "year_2013", name: "2013" },
{ id: "year-2014", key: "year_2014", name: "2014" },
{ id: "year-2015", key: "year_2015", name: "2015" },
{ id: "year-2016", key: "year_2016", name: "2016" },
{ id: "year-2017", key: "year_2017", name: "2017" },
{ id: "year-2018", key: "year_2018", name: "2018" },
{ id: "year-2019", key: "year_2019", name: "2019" },
{ id: "year-2020", key: "year_2020", name: "2020" },
{ id: "year-2021", key: "year_2021", name: "2021" },
{ id: "year-2022", key: "year_2022", name: "2022" },
{ id: "year-2023", key: "year_2023", name: "2023" },
{ id: "year-2024", key: "year_2024", name: "2024" },
] as const;
export const ageCohorts = [
{
key: "",
id: "",
name: "",
},
...xthCohorts,
@@ -1,11 +1,13 @@
export const liquidities = [
{
key: "illiquid",
id: "illiquid",
name: "Illiquid",
},
{ key: "liquid", name: "Liquid" },
{ key: "liquid", id: "liquid", name: "Liquid" },
{
key: "highly_liquid",
id: "highly-liquid",
name: "Highly Liquid",
},
] as const;
@@ -1,114 +1,133 @@
export const percentiles = [
{
key: "median_price_paid",
id: "median-price-paid",
name: "Median",
title: "Median Paid",
value: 50,
},
{
key: "95p_price_paid",
id: "95p-price-paid",
name: `95%`,
title: `95th Percentile Paid`,
value: 95,
},
{
key: "90p_price_paid",
id: "90p-price-paid",
name: `90%`,
title: `90th Percentile Paid`,
value: 90,
},
{
key: "85p_price_paid",
id: "85p-price-paid",
name: `85%`,
title: `85th Percentile Paid`,
value: 85,
},
{
key: "80p_price_paid",
id: "80p-price-paid",
name: `80%`,
title: `80th Percentile Paid`,
value: 80,
},
{
key: "75p_price_paid",
id: "75p-price-paid",
name: `75%`,
title: `75th Percentile Paid`,
value: 75,
},
{
key: "70p_price_paid",
id: "70p-price-paid",
name: `70%`,
title: `70th Percentile Paid`,
value: 70,
},
{
key: "65p_price_paid",
id: "65p-price-paid",
name: `65%`,
title: `65th Percentile Paid`,
value: 65,
},
{
key: "60p_price_paid",
id: "60p-price-paid",
name: `60%`,
title: `60th Percentile Paid`,
value: 60,
},
{
key: "55p_price_paid",
id: "55p-price-paid",
name: `55%`,
title: `55th Percentile Paid`,
value: 55,
},
{
key: "45p_price_paid",
id: "45p-price-paid",
name: `45%`,
title: `45th Percentile Paid`,
value: 45,
},
{
key: "40p_price_paid",
id: "40p-price-paid",
name: `40%`,
title: `40th Percentile Paid`,
value: 40,
},
{
key: "35p_price_paid",
id: "35p-price-paid",
name: `35%`,
title: `35th Percentile Paid`,
value: 35,
},
{
key: "30p_price_paid",
id: "30p-price-paid",
name: `30%`,
title: `30th Percentile Paid`,
value: 30,
},
{
key: "25p_price_paid",
id: "25p-price-paid",
name: `25%`,
title: `25th Percentile Paid`,
value: 25,
},
{
key: "20p_price_paid",
id: "20p-price-paid",
name: `20%`,
title: `20th Percentile Paid`,
value: 20,
},
{
key: "15p_price_paid",
id: "15p-price-paid",
name: `15%`,
title: `15th Percentile Paid`,
value: 15,
},
{
key: "10p_price_paid",
id: "10p-price-paid",
name: `10%`,
title: `10th Percentile Paid`,
value: 10,
},
{
key: "05p_price_paid",
id: "05p-price-paid",
name: `5%`,
title: `5th Percentile Paid`,
value: 5,
+17 -6
View File
@@ -1,15 +1,24 @@
type AgeCohortKey = (typeof import("./age").ageCohorts)[number]["key"];
type AgeCohortId = (typeof import("./age").ageCohorts)[number]["id"];
type AddressCohortKey =
type AgeCohortIdSub = Exclude<AgeCohortId, "">;
type AddressCohortId =
(typeof import("./address").addressCohorts)[number]["key"];
type LiquidityKey = (typeof import("./liquidities").liquidities)[number]["key"];
type LiquidityId = (typeof import("./liquidities").liquidities)[number]["id"];
type AddressCohortKeySplitByLiquidity = `${LiquidityKey}_${AddressCohortKey}`;
type AddressCohortIdSplitByLiquidity = `${LiquidityId}-${AddressCohortId}`;
type AnyCohortKey = AgeCohortKey | AddressCohortKey;
type AnyCohortId = AgeCohortId | AddressCohortId;
type AnyPossibleCohortKey = AnyCohortKey | AddressCohortKeySplitByLiquidity;
type AnyPossibleCohortId =
| AnyCohortId
| AddressCohortIdSplitByLiquidity
| LiquidityId;
type AnyDatasetPrefix =
| ""
| `${AgeCohortIdSub | AddressCohortId | AddressCohortIdSplitByLiquidity | LiquidityId}-`;
type AverageName = (typeof import("./averages").averages)[number]["key"];
@@ -17,3 +26,5 @@ type TotalReturnKey = (typeof import("./returns").totalReturns)[number]["key"];
type CompoundReturnKey =
(typeof import("./returns").compoundReturns)[number]["key"];
type PercentileId = (typeof import("./percentiles").percentiles)[number]["id"];
-41
View File
@@ -1,41 +0,0 @@
import groupedKeysToPath from "/src/../../datasets/grouped_keys_to_url_path.json";
import { createResourceDataset } from "./resource";
export { averages } from "./consts/averages";
export function createDateDatasets({
setActiveResources,
}: {
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
type Key = keyof typeof groupedKeysToPath.date;
type ResourceData = ReturnType<typeof createResourceDataset<"date">>;
const resourceDatasets = {} as Record<Exclude<Key, "ohlc">, ResourceData>;
Object.entries(groupedKeysToPath.date).forEach(([_key, path]) => {
const key = _key as Key;
if (key !== "ohlc") {
resourceDatasets[key] = createResourceDataset<"date">({
scale: "date",
path,
setActiveResources,
});
}
});
const price = createResourceDataset<"date", OHLC>({
scale: "date",
path: "/date-to-ohlc",
setActiveResources,
});
const datasets = {
price,
...resourceDatasets,
};
return datasets;
}
-36
View File
@@ -1,36 +0,0 @@
import groupedKeysToPath from "/src/../../datasets/grouped_keys_to_url_path.json";
import { createResourceDataset } from "./resource";
export function createHeightDatasets({
setActiveResources,
}: {
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
type Key = keyof typeof groupedKeysToPath.height;
type ResourceData = ReturnType<typeof createResourceDataset<"height">>;
const resourceDatasets = {} as Record<Exclude<Key, "ohlc">, ResourceData>;
Object.keys(groupedKeysToPath.height).forEach(([_key, path]) => {
const key = _key as Key;
if (key !== "ohlc") {
resourceDatasets[key] = createResourceDataset<"height">({
scale: "height",
path,
setActiveResources,
});
}
});
const price = createResourceDataset<"height", OHLC>({
scale: "height",
path: "/height-to-ohlc",
setActiveResources,
});
return {
...resourceDatasets,
price,
};
}
+42 -10
View File
@@ -1,17 +1,49 @@
import { createDateDatasets } from "./date";
import { createHeightDatasets } from "./height";
import { createResourceDataset } from "./resource";
export const scales = ["date" as const, "height" as const];
export const HEIGHT_CHUNK_SIZE = 10_000;
export function createDatasets({
setActiveResources,
}: {
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
export function createDatasets() {
const date = new Map<DatePath, ResourceDataset<"date">>();
const height = new Map<HeightPath, ResourceDataset<"height">>();
function getOrImport<Scale extends ResourceScale>(
scale: Scale,
path: DatasetPath<Scale>,
): ResourceDataset<Scale> {
if (scale === "date") {
const found = date.get(path as any);
if (found) return found as ResourceDataset<Scale>;
} else {
const found = height.get(path as any);
if (found) return found as ResourceDataset<Scale>;
}
let dataset: ResourceDataset<Scale, any>;
if (path === `/${scale}-to-price`) {
dataset = createResourceDataset<Scale, OHLC>({
scale,
path,
});
} else {
dataset = createResourceDataset<Scale>({
scale,
path,
});
}
if (scale === "date") {
date.set(path as any, dataset as any);
} else {
height.set(path as any, dataset as any);
}
return dataset;
}
return {
date: createDateDatasets({ setActiveResources }),
height: createHeightDatasets({ setActiveResources }),
} satisfies Record<ResourceScale, any>;
getOrImport,
};
}
+215 -183
View File
@@ -1,237 +1,265 @@
import { createLazyMemo } from "@solid-primitives/memo";
import {
ONE_DAY_IN_MS,
ONE_HOUR_IN_MS,
ONE_MINUTE_IN_MS,
} from "/src/scripts/utils/time";
import { requestIdleCallbackPossible } from "/src/env";
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 ".";
const USE_LOCAL_URL = true;
const LOCAL_URL = "http://localhost:3110";
const WEB_URL = "https://api.satonomics.xyz";
const BACKUP_WEB_URL = "https://api-bkp.satonomics.xyz";
export function createResourceDataset<
Scale extends ResourceScale,
Type extends OHLC | number = number,
>({
scale,
path,
setActiveResources,
}: {
scale: Scale;
path: string;
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
const baseURL = `${
location.hostname === "localhost"
? "http://localhost:3110"
: "https://api.satonomics.xyz"
}${path}`;
type Dataset = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>;
>({ scale, path }: { scale: Scale; path: string }) {
type Value = DatasetValue<
Type extends number ? SingleValueData : CandlestickData
>;
const fetchedJSONs = new Array(
(new Date().getFullYear() - new Date("2009-01-01").getFullYear()) *
(scale === "date" ? 2 : 8),
)
.fill(null)
.map((): FetchedResult<Scale, Type> => {
const json = createRWS<FetchedJSON<Scale, Type, Dataset> | null>(null);
const baseURL = `${
USE_LOCAL_URL && location.hostname === "localhost" ? LOCAL_URL : WEB_URL
}${path}`;
return {
at: null,
json,
loading: false,
vec: createMemo(() => {
const map = json()?.dataset.map || null;
const backupURL = `${
USE_LOCAL_URL && location.hostname === "localhost"
? LOCAL_URL
: BACKUP_WEB_URL
}${path}`;
const chunkId = json()?.chunk.id!;
return createRoot((dispose) => {
const fetchedJSONs = new Array(
(new Date().getFullYear() - new Date("2009-01-01").getFullYear() + 2) *
(scale === "date" ? 1 : 6),
)
.fill(null)
.map((): FetchedResult<Scale, Type> => {
const json = createRWS<FetchedJSON<Scale, Type> | null>(null);
if (!map) {
return null;
}
return {
at: null,
json,
loading: false,
vec: createMemo(() => {
const map = json()?.dataset.map;
if (Array.isArray(map)) {
return map.map(
(value, index) =>
({
number: chunkId + index,
time: (chunkId + index) as Time,
if (!map) {
return null;
}
const chunkId = json()?.chunk.id!;
if (Array.isArray(map)) {
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,
);
} 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 }
: { value: value === null ? NaN : (value as number) }),
}) as any as Value,
);
}
}),
};
}) as FetchedResult<Scale, Type>[];
} as any as Value;
}
const _fetch = async (id: number) => {
const index =
scale === "date" ? id - 2009 : Math.floor(id / HEIGHT_CHUNK_SIZE);
return values;
} else {
return Object.entries(map).map(
([date, value]) =>
({
time: date,
...(typeof value !== "number" && value !== null
? { ...(value as OHLC), value: value.close }
: { value: value === null ? NaN : (value as number) }),
}) as any as Value,
);
}
}),
};
}) as FetchedResult<Scale, Type>[];
if (
index < 0 ||
(scale === "date" && id > new Date().getUTCFullYear()) ||
(scale === "height" &&
id > 165 * 365 * (new Date().getUTCFullYear() - 2009))
) {
return;
}
const fetched = fetchedJSONs.at(index);
if (!fetched || fetched.loading) {
return;
} else if (fetched.at) {
const diff = new Date().valueOf() - fetched.at.valueOf();
const _fetch = async (id: number) => {
const index = chunkIdToIndex(scale, id);
if (
diff < ONE_MINUTE_IN_MS ||
(index < fetchedJSONs.findLastIndex((json) => json.at) &&
diff < ONE_HOUR_IN_MS)
index < 0 ||
(scale === "date" && id > new Date().getUTCFullYear()) ||
(scale === "height" &&
id > 165 * 365 * (new Date().getUTCFullYear() - 2009))
) {
return;
}
}
fetched.loading = true;
const fetched = fetchedJSONs.at(index);
let cache: Cache | undefined;
if (scale === "height" && index > 0) {
const length = fetchedJSONs.at(index - 1)?.vec()?.length;
const urlWithQuery = `${baseURL}?chunk=${id}`;
if (!fetched.json()) {
try {
cache = await caches.open("resources");
const cachedResponse = await cache.match(urlWithQuery);
if (cachedResponse) {
const json = await convertResponseToJSON<Scale, Type>(cachedResponse);
if (json) {
console.log(`cache: ${path}?chunk=${id}`);
fetched.json.set(() => json);
}
if (length !== undefined && length < HEIGHT_CHUNK_SIZE) {
return;
}
} catch {}
}
}
try {
const fetchedResponse = await fetch(urlWithQuery);
if (!fetched || fetched.loading) {
return;
} else if (fetched.at) {
const diff = new Date().getTime() - fetched.at.getTime();
if (!fetchedResponse.ok) {
if (
diff < ONE_MINUTE_IN_MS ||
(index < fetchedJSONs.findLastIndex((json) => json.at) &&
diff < ONE_HOUR_IN_MS)
) {
return;
}
}
fetched.loading = true;
let cache: Cache | undefined;
const urlWithQuery = `${baseURL}?chunk=${id}`;
const backupUrlWithQuery = `${backupURL}?chunk=${id}`;
if (!fetched.json()) {
try {
cache = await caches.open("resources");
const cachedResponse = await cache.match(urlWithQuery);
if (cachedResponse) {
const json = await convertResponseToJSON<Scale, Type>(
cachedResponse,
);
if (json) {
console.log(`cache: ${path}?chunk=${id}`);
fetched.json.set(() => json);
}
}
} catch {}
}
if (!navigator.onLine) {
fetched.loading = false;
return;
}
let fetchedResponse: Response | undefined;
const fetchConfig: RequestInit = {
signal: AbortSignal.timeout(5000),
};
try {
fetchedResponse = await fetch(urlWithQuery, fetchConfig);
if (!fetchedResponse.ok) {
throw Error;
}
} catch {
try {
fetchedResponse = await fetch(backupUrlWithQuery, fetchConfig);
} catch {
fetched.loading = false;
return;
}
if (!fetchedResponse || !fetchedResponse.ok) {
fetched.loading = false;
return;
}
}
const clonedResponse = fetchedResponse.clone();
const json = await convertResponseToJSON<Scale, Type>(fetchedResponse);
if (json) {
console.log(`fetch: ${path}?chunk=${id}`);
if (!json) {
fetched.loading = false;
return;
}
const previousMap = fetched.json()?.dataset.map;
const newMap = json.dataset.map;
console.log(`fetch: ${path}?chunk=${id}`);
const previousLength = Object.keys(previousMap || []).length;
const newLength = Object.keys(newMap).length;
const previousMap = fetched.json()?.dataset;
const newMap = json.dataset.map;
if (!newLength) {
const previousLength = Object.keys(previousMap || []).length;
const newLength = Object.keys(newMap).length;
if (!newLength) {
fetched.loading = false;
return;
}
if (previousLength && previousLength === newLength) {
const previousLastValue = Object.values(previousMap || []).at(-1);
const newLastValue = Object.values(newMap).at(-1);
if (newLastValue === null && previousLastValue === null) {
fetched.at = new Date();
fetched.loading = false;
return;
}
} else if (typeof newLastValue === "number") {
if (previousLastValue === newLastValue) {
fetched.at = new Date();
fetched.loading = false;
return;
}
} else {
const previousLastOHLC = previousLastValue as OHLC;
const newLastOHLC = newLastValue as OHLC;
if (previousLength && previousLength <= newLength) {
const previousLastValue = Object.values(previousMap || []).at(-1);
const newLastValue = Object.values(newMap).at(-1);
if (typeof newLastValue === "number") {
if (previousLastValue === newLastValue) {
fetched.at = new Date();
fetched.loading = false;
return;
}
} else {
const previousLastOHLC = previousLastValue as OHLC;
const newLastOHLC = newLastValue as OHLC;
if (
previousLastOHLC.open === newLastOHLC.open &&
previousLastOHLC.high === newLastOHLC.high &&
previousLastOHLC.low === newLastOHLC.low &&
previousLastOHLC.close === newLastOHLC.close
) {
fetched.loading = false;
fetched.at = new Date();
return;
}
if (
previousLastOHLC.open === newLastOHLC.open &&
previousLastOHLC.high === newLastOHLC.high &&
previousLastOHLC.low === newLastOHLC.low &&
previousLastOHLC.close === newLastOHLC.close
) {
fetched.loading = false;
fetched.at = new Date();
return;
}
}
fetched.json.set(() => json);
if (cache) {
cache.put(urlWithQuery, clonedResponse);
}
}
} catch {
fetched.json.set(() => json);
async function saveToCache() {
try {
await cache?.put(urlWithQuery, clonedResponse);
} catch (_) {}
}
if (requestIdleCallbackPossible) {
requestIdleCallback(saveToCache);
} else {
setTimeout(saveToCache, 1);
}
fetched.at = new Date();
fetched.loading = false;
return;
}
};
fetched.at = new Date();
fetched.loading = false;
};
const resource: ResourceDataset<Scale, Type> = {
scale,
url: baseURL,
fetch: _fetch,
fetchedJSONs,
drop() {
dispose();
fetchedJSONs.forEach((fetched) => {
fetched.at = null;
fetched.json.set(null);
});
},
};
const resource: ResourceDataset<Scale, Type> = {
scale,
url: baseURL,
fetch: _fetch,
fetchedJSONs,
values: createLazyMemo(() => {
setActiveResources((resources) => resources.add(resource));
onCleanup(() =>
setActiveResources((resources) => {
resources.delete(resource);
return resources;
}),
);
const flat = fetchedJSONs.flatMap((fetched) => fetched.vec() || []);
return flat;
}),
drop() {
fetchedJSONs.forEach((fetched) => {
fetched.at = null;
fetched.json.set(null);
});
},
};
return resource;
return resource;
});
}
async function convertResponseToJSON<
@@ -244,3 +272,7 @@ async function convertResponseToJSON<
return null;
}
}
export function chunkIdToIndex(scale: ResourceScale, id: number) {
return scale === "date" ? id - 2009 : Math.floor(id / HEIGHT_CHUNK_SIZE);
}
+25 -48
View File
@@ -1,33 +1,14 @@
type Datasets = ReturnType<typeof import("./index").createDatasets>;
type DateDatasets = Datasets["date"];
type HeightDatasets = Datasets["height"];
type AnyDatasets = DateDatasets | HeightDatasets;
type ResourceScale = (typeof import("./index").scales)[index];
type DatasetValue<T> = T & Numbered & Valued;
interface Dataset<
Scale extends ResourceScale,
Value extends SingleValueData | CandlestickData = SingleValueData,
> {
scale: Scale;
values: Accessor<DatasetValue<Value>[]>;
}
type DatasetValue<T> = T & Valued;
interface ResourceDataset<
Scale extends ResourceScale,
Type extends OHLC | number = number,
FetchedDataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
Value extends SingleValueData | CandlestickData = Type extends number
? SingleValueData
: CandlestickData,
> extends Dataset<Scale, Value> {
> {
scale: Scale;
url: string;
fetch: (id: number) => void;
fetchedJSONs: FetchedResult<Scale, Type>[];
@@ -37,33 +18,20 @@ interface ResourceDataset<
interface FetchedResult<
Scale extends ResourceScale,
Type extends number | OHLC,
Dataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
Value extends DatasetValue<SingleValueData | CandlestickData> = DatasetValue<
Type extends number ? SingleValueData : CandlestickData
>,
> {
at: Date | null;
json: RWS<FetchedJSON<Scale, Type, Dataset> | null>;
json: RWS<FetchedJSON<Scale, Type> | null>;
vec: Accessor<Value[] | null>;
loading: boolean;
}
interface FetchedJSON<
Scale extends ResourceScale,
Type extends number | OHLC,
Dataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
> {
interface FetchedJSON<Scale extends ResourceScale, Type extends number | OHLC> {
source: FetchedSource;
chunk: FetchedChunk;
dataset: FetchedDataset<Scale, Type, Dataset>;
dataset: FetchedDataset<Scale, Type>;
}
type FetchedSource = string;
@@ -74,21 +42,24 @@ interface FetchedChunk {
next: string | null;
}
interface FetchedDataset<
type FetchedDataset<
Scale extends ResourceScale,
Type extends number | OHLC,
Dataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
> {
> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>;
interface Versioned {
version: number;
map: Dataset;
}
type FetchedDateDataset<T> = Record<string, T>;
type FetchedHeightDataset<T> = T[];
interface FetchedDateDataset<Type> extends Versioned {
map: Record<string, Type>;
}
interface FetchedHeightDataset<Type> extends Versioned {
map: Type[];
}
interface OHLC {
open: number;
@@ -96,3 +67,9 @@ interface OHLC {
low: number;
close: number;
}
type DatasetPath<Scale extends ResourceScale> = Scale extends "date"
? DatePath
: HeightPath;
type AnyDatasetPath = DatePath | HeightPath;
@@ -0,0 +1,65 @@
import { colors } from "/src/scripts/utils/colors";
import { defaultSeriesOptions } from "./options";
const DEFAULT_BASELINE_TOP_COLOR = colors.profit;
const DEFAULT_BASELINE_BOTTOM_COLOR = colors.loss;
export const DEFAULT_BASELINE_COLORS = [
DEFAULT_BASELINE_TOP_COLOR,
DEFAULT_BASELINE_BOTTOM_COLOR,
];
const transparent = `transparent`;
export const createBaseLineSeries = ({
chart,
dark,
color,
topColor,
bottomColor,
options,
}: {
chart: IChartApi;
dark: Accessor<boolean>;
color?: Color;
topColor?: Color;
bottomColor?: Color;
options?: DeepPartialBaselineOptions & {
base?: number;
};
}) => {
const topLineColor = topColor || color || DEFAULT_BASELINE_TOP_COLOR;
const bottomLineColor = bottomColor || color || DEFAULT_BASELINE_BOTTOM_COLOR;
function computeColors() {
return {
topLineColor: topLineColor(dark),
bottomLineColor: bottomLineColor(dark),
} as const;
}
const seriesOptions: DeepPartialBaselineOptions = {
priceScaleId: "right",
...defaultSeriesOptions,
// lineWidth: 1,
...options,
...(options?.base
? { baseValue: { type: "price", price: options?.base } }
: {}),
topFillColor1: transparent,
topFillColor2: transparent,
bottomFillColor1: transparent,
bottomFillColor2: transparent,
...computeColors(),
};
const series = chart.addBaselineSeries(seriesOptions);
createEffect(() => {
series.applyOptions(computeColors());
});
return series;
};
@@ -0,0 +1,47 @@
import { colors } from "/src/scripts/utils/colors";
export const createCandlesticksSeries = ({
chart,
dark,
options = {},
}: {
chart: IChartApi;
dark: Accessor<boolean>;
options?: PriceSeriesOptions;
}): [ISeriesApi<"Candlestick">, Color[]] => {
const { inverseColors } = options;
const _upColor = inverseColors ? colors.loss : colors.profit;
const _downColor = inverseColors ? colors.profit : colors.loss;
function computeColors() {
const upColor = _upColor(dark);
const downColor = _downColor(dark);
return {
upColor,
wickUpColor: upColor,
downColor,
wickDownColor: downColor,
} as const;
}
const candlestickSeries = chart.addCandlestickSeries({
baseLineVisible: false,
borderVisible: false,
priceLineVisible: false,
baseLineColor: "",
borderColor: "",
borderDownColor: "",
borderUpColor: "",
...options.seriesOptions,
...computeColors(),
});
createEffect(() => {
candlestickSeries.applyOptions(computeColors());
});
return [candlestickSeries, [_upColor, _downColor]];
};
@@ -1,11 +0,0 @@
import { chartState } from "./state";
export function cleanChart() {
console.log("chart: clean");
try {
chartState.chart?.remove();
} catch {}
chartState.chart = null;
}
@@ -1,68 +0,0 @@
import {
createChart as createClassicChart,
createChartEx as createCustomChart,
CrosshairMode,
} from "lightweight-charts";
import { colors } from "../../utils/colors";
import { priceToUSLocale } from "../../utils/locale";
import { cleanChart } from "./clean";
import { HorzScaleBehaviorHeight } from "./horzScaleBehavior";
import { chartState } from "./state";
export function createChart(scale: ResourceScale) {
cleanChart();
console.log(`chart: create (scale: ${scale})`);
const { white } = colors;
const options: DeepPartialChartOptions = {
autoSize: true,
layout: {
fontFamily: "Lexend",
background: { color: "transparent" },
fontSize: 14,
textColor: white,
},
grid: {
vertLines: { visible: false },
horzLines: { visible: false },
},
leftPriceScale: {
// borderColor: white,
},
rightPriceScale: {
// borderColor: white,
},
timeScale: {
minBarSpacing: scale === "date" ? 0.05 : 0.005,
shiftVisibleRangeOnNewBar: false,
allowShiftVisibleRangeOnWhitespaceReplacement: false,
},
crosshair: {
mode: CrosshairMode.Normal,
horzLine: {
color: white,
labelBackgroundColor: white,
},
vertLine: {
color: white,
labelBackgroundColor: white,
},
},
localization: {
priceFormatter: priceToUSLocale,
locale: "en-us",
},
};
if (scale === "date") {
chartState.chart = createClassicChart("chart", options);
} else {
const horzScaleBehavior = new HorzScaleBehaviorHeight();
// @ts-ignore
chartState.chart = createCustomChart("chart", horzScaleBehavior, options);
}
}
@@ -1,123 +0,0 @@
import { colors } from "/src/scripts/utils/colors";
import { priceToUSLocale } from "/src/scripts/utils/locale";
import { ONE_DAY_IN_MS } from "/src/scripts/utils/time";
import { chartState } from "./state";
import { GENESIS_DAY } from "./whitespace";
export const setMinMaxMarkers = ({
scale,
candlesticks,
range,
lowerOpacity,
}: {
scale: ResourceScale;
candlesticks: DatasetValue<CandlestickData | SingleValueData>[];
range: TimeRange;
lowerOpacity: boolean;
}) => {
const first = candlesticks.at(0);
if (!first) return;
const offset =
scale === "date"
? first.number - new Date(GENESIS_DAY).valueOf() / ONE_DAY_IN_MS
: 0;
const slicedDataList = range
? candlesticks.slice(
Math.ceil(range.from - offset < 0 ? 0 : range.from - offset),
Math.floor(range.to - offset) + 1,
)
: [];
const series = chartState.priceSeries;
if (!series) return;
if (slicedDataList.length) {
const markers: (SeriesMarker<Time> & Numbered)[] = [];
const seriesIsCandlestick = series.seriesType() === "Candlestick";
[
{
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 placement = Math[mathFunction](
...slicedDataList.map(
(data) =>
(placementAttribute in data
? data[placementAttribute]
: data.value) || 0,
),
);
const candle = slicedDataList.find(
(data) =>
(placementAttribute in data
? data[placementAttribute]
: data.value) === placement,
);
return (
candle &&
markers.push({
...markerOptions,
// date: candle.date,
number: candle.number,
time: candle.time,
color: lowerOpacity ? colors.darkWhite : colors.white,
size: 0,
text: priceToUSLocale(value),
})
);
},
);
series.setMarkers(sortWhitespaceDataArray(markers));
}
};
function sortWhitespaceDataArray<T extends WhitespaceData & Numbered>(
array: T[],
) {
return array.sort(({ number: a }, { number: b }) => a - b);
}
@@ -1,176 +0,0 @@
import { createRWS } from "/src/solid/rws";
import { colors } from "../../utils/colors";
import { getNumberOfDaysBetweenTwoDates } from "../../utils/date";
import { debounce } from "../../utils/debounce";
import { webSockets } from "../../ws";
import { createCandlesticksSeries } from "../series/creators/candlesticks";
import { createSeriesLegend } from "../series/creators/legend";
import { createLineSeries } from "../series/creators/line";
import { setMinMaxMarkers } from "./markers";
import { chartState } from "./state";
import { initTimeScale } from "./time";
export const PRICE_SCALE_MOMENTUM_ID = "momentum";
export const applyPriceSeries = <
Scale extends ResourceScale,
T extends SingleValueData,
>({
chart,
datasets,
preset,
dataset: _dataset,
options,
activeResources,
}: {
chart: IChartApi;
datasets: Datasets;
preset: Preset;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
dataset?: Dataset<Scale, T>;
options?: PriceSeriesOptions;
}) => {
const id = options?.id || "price";
const title = options?.title || "Price";
const dataset = createMemo(() => _dataset || datasets[preset.scale].price);
const url = "url" in dataset() ? (dataset() as any).url : undefined;
const priceScaleOptions: DeepPartial<PriceScaleOptions> = {
...(options?.halved
? {
scaleMargins: {
top: 0.05,
bottom: 0.55,
},
}
: {}),
...(options?.id || options?.title
? {}
: {
mode: 1,
// mode: PriceScaleMode.Logarithmic,
}),
...options?.priceScaleOptions,
};
const seriesType = createRWS(
checkIfUpClose(chart, chartState.range) || "Candlestick",
);
const debouncedCallback = debounce((range: TimeRange | null) => {
try {
seriesType.set((previous) => checkIfUpClose(chart, range) || previous);
} catch {}
}, 50);
chart?.timeScale().subscribeVisibleTimeRangeChange(debouncedCallback);
onCleanup(
() =>
chart === chartState.chart &&
chartState.chart
?.timeScale()
.unsubscribeVisibleTimeRangeChange(debouncedCallback),
);
const lowerOpacity = options?.lowerOpacity || options?.halved || false;
if (options?.halved) {
options.seriesOptions = {
...options.seriesOptions,
priceScaleId: "left",
};
}
const [ohlcSeries, ohlcColors] = createCandlesticksSeries(chart, {
...options,
lowerOpacity,
});
const ohlcLegend = createSeriesLegend({
id,
presetId: preset.id,
title,
color: () => ohlcColors,
series: ohlcSeries,
disabled: () => seriesType() !== "Candlestick",
url,
});
ohlcSeries.priceScale().applyOptions(priceScaleOptions);
// ---
const lineColor = lowerOpacity ? colors.darkWhite : colors.white;
const lineSeries = createLineSeries(chart, {
color: lineColor,
...options?.seriesOptions,
});
const lineLegend = createSeriesLegend({
id,
presetId: preset.id,
title,
color: () => lineColor,
series: lineSeries,
disabled: () => seriesType() !== "Line",
visible: ohlcLegend.visible,
url,
});
lineSeries.priceScale().applyOptions(priceScaleOptions);
// ---
// setMinMaxMarkers({
// scale: preset.scale,
// candlesticks:
// dataset?.values() || datasets[preset.scale].price.values() || ([] as any),
// range: chartState.range,
// lowerOpacity,
// });
initTimeScale({
activeResources,
});
createEffect(() => {
const d = dataset();
lineSeries.setData(d.values());
ohlcSeries.setData(d.values());
});
createEffect(() => {
if (preset.scale === "date") {
const latest = webSockets.liveKrakenCandle.latest();
if (latest) {
ohlcSeries.update(latest);
lineSeries.update(latest);
}
}
});
return { ohlcLegend, lineLegend };
};
function checkIfUpClose(chart: IChartApi, range?: TimeRange | null) {
if (!range) return undefined;
const from = new Date(range.from);
const to = new Date(range.to);
const width = chart.timeScale().width();
const difference = getNumberOfDaysBetweenTwoDates(from, to);
return width / difference >= 2.05
? "Candlestick"
: width / difference <= 1.95
? "Line"
: undefined;
}
@@ -1,40 +0,0 @@
import { createChart } from "./create";
import { chartState } from "./state";
import { setWhitespace } from "./whitespace";
export function renderChart({
datasets,
legendSetter,
preset,
activeResources,
}: {
datasets: Datasets;
legendSetter: Setter<PresetLegend>;
preset: Preset;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
}) {
const scale = preset.scale;
createChart(scale);
const chart = chartState.chart;
if (!chart) return;
try {
setWhitespace(chart, scale);
console.log(`preset: ${preset.id}`);
const legend = preset.applyPreset({
chart,
datasets,
preset,
activeResources,
});
legendSetter(legend);
} catch (error) {
console.error("chart: render: failed", error);
}
}
@@ -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,110 +0,0 @@
import { HEIGHT_CHUNK_SIZE } from "../../datasets";
import { debounce } from "../../utils/debounce";
import { writeURLParam } from "../../utils/urlParams";
import { setMinMaxMarkers } from "./markers";
import {
chartState,
LOCAL_STORAGE_RANGE_KEY,
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));
}, 1000);
export function initTimeScale({
activeResources,
}: {
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
}) {
setTimeScale(chartState.range);
const debouncedFetch = debounce((range: TimeRange | null) => {
if (!range) return;
let ids: number[] = [];
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);
} 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);
}
ids.forEach((id) => {
activeResources().forEach((resource) => resource.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));
}
export function getInitialRange(): 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;
}
const savedTimeRange = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_RANGE_KEY) || "null",
) as TimeRange | null;
if (savedTimeRange) {
return savedTimeRange;
}
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) {
console.log(range);
setTimeout(() => {
chartState.chart?.timeScale().setVisibleRange(range);
}, 1);
}
}
-9
View File
@@ -1,9 +0,0 @@
interface PriceSeriesOptions {
halved?: boolean;
title?: string;
id?: string;
lowerOpacity?: boolean;
inverseColors?: boolean;
seriesOptions?: DeepPartial<SeriesOptionsCommon>;
priceScaleOptions?: DeepPartial<PriceScaleOptions>;
}
@@ -1,50 +0,0 @@
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";
export const GENESIS_DAY = "2009-01-03";
// export const DAY_BEFORE_WHITEPAPER_DAY = "2008-10-30";
// export const WHITEPAPER_DAY = "2008-10-31";
const whitespaceStartDate = "1970-01-01";
const whitespaceEndDate = "2100-01-01";
const whitespaceDateDataset: (SingleValueData & Numbered)[] = new Array(
getNumberOfDaysBetweenTwoDates(
new Date(whitespaceStartDate),
new Date(whitespaceEndDate),
),
)
.fill(0)
.map((_, index) => {
const date = new Date(whitespaceStartDate);
date.setUTCDate(date.getUTCDay() + index);
return {
number: date.valueOf() / ONE_DAY_IN_MS,
time: dateToString(date),
value: NaN,
};
});
const whitespaceHeightDataset: (WhitespaceData & Numbered)[] = new Array(
840_000,
)
.fill(0)
.map(
(_, index) =>
({
time: index,
number: index,
}) as any,
);
export function setWhitespace(chart: IChartApi, scale: ResourceScale) {
const whitespaceSeries = createLineSeries(chart);
if (scale === "date") {
whitespaceSeries.setData(whitespaceDateDataset);
} else {
whitespaceSeries.setData(whitespaceHeightDataset);
}
}
+101
View File
@@ -0,0 +1,101 @@
import {
createChart as createClassicChart,
createChartEx as createCustomChart,
CrosshairMode,
} from "lightweight-charts";
import { colors } from "../utils/colors";
import { valueToString } from "../utils/locale";
import { HorzScaleBehaviorHeight } from "./horzScaleBehavior";
export function createChart({
scale,
element,
dark,
}: {
scale: ResourceScale;
element: HTMLElement;
dark: Accessor<boolean>;
}) {
console.log(`chart: create (scale: ${scale})`);
const options: DeepPartialChartOptions = {
autoSize: true,
layout: {
fontFamily: "Lexend",
background: { color: "transparent" },
fontSize: 14,
},
grid: {
vertLines: { visible: false },
horzLines: { visible: false },
},
timeScale: {
minBarSpacing: 0.05,
shiftVisibleRangeOnNewBar: false,
allowShiftVisibleRangeOnWhitespaceReplacement: false,
},
handleScale: {
axisDoubleClickReset: {
time: false,
},
},
crosshair: {
mode: CrosshairMode.Normal,
},
localization: {
priceFormatter: valueToString,
locale: "en-us",
},
};
let chart: IChartApi;
if (scale === "date") {
chart = createClassicChart(element, options);
} else {
const horzScaleBehavior = new HorzScaleBehaviorHeight();
// @ts-ignore
chart = createCustomChart(element, horzScaleBehavior, options);
}
chart.priceScale("right").applyOptions({
scaleMargins: {
top: 0.075,
bottom: 0.075,
},
minimumWidth: 78,
});
createEffect(() => {
const { white } = colors;
const textColor = white(dark);
const borderColor = dark() ? "#332F24" : "#F1E4E0";
chart.applyOptions({
layout: {
textColor,
},
rightPriceScale: {
borderColor,
},
timeScale: {
borderColor,
},
crosshair: {
horzLine: {
color: textColor,
labelBackgroundColor: textColor,
},
vertLine: {
color: textColor,
labelBackgroundColor: textColor,
},
},
});
});
return chart;
}
+209
View File
@@ -0,0 +1,209 @@
import { createRWS } from "/src/solid/rws";
import { chunkIdToIndex } from "../datasets/resource";
import { SeriesType } from "../presets/enums";
import { stringToId } from "../utils/id";
import { createBaseLineSeries, DEFAULT_BASELINE_COLORS } from "./baseLine";
import { createCandlesticksSeries } from "./candlesticks";
import { createHistogramSeries } from "./histogram";
import { createSeriesLegend } from "./legend";
import { createLineSeries } from "./line";
export function createSeriesGroup<Scale extends ResourceScale>({
scale,
datasets,
activeIds,
seriesConfig,
preset,
chartLegend,
chart,
index: seriesIndex,
disabled,
lastActiveIndex,
debouncedSetMinMaxMarkers,
dark,
}: {
scale: Scale;
datasets: Datasets;
activeIds: Accessor<number[]>;
seriesConfig: SeriesConfig;
preset: Preset;
chart: IChartApi;
index: number;
chartLegend: SeriesLegend[];
lastActiveIndex: Accessor<number | undefined>;
disabled?: Accessor<boolean>;
debouncedSetMinMaxMarkers: VoidFunction;
dark: Accessor<boolean>;
}) {
const {
datasetPath,
title,
colors,
color,
defaultVisible,
seriesType: type,
options,
priceScaleOptions,
} = seriesConfig;
const dataset = datasets.getOrImport(
scale,
datasetPath as DatasetPath<Scale>,
);
const seriesList: RWS<
ISeriesApi<"Baseline" | "Line" | "Histogram" | "Candlestick"> | undefined
>[] = new Array(dataset.fetchedJSONs.length);
const legend = createSeriesLegend({
scale,
id: stringToId(title),
presetId: preset.id,
title,
seriesList,
color: colors || color || DEFAULT_BASELINE_COLORS,
defaultVisible,
disabled,
dataset,
});
chartLegend.push(legend);
dataset.fetchedJSONs.forEach((json, index) => {
const series: (typeof seriesList)[number] = createRWS(undefined);
seriesList[index] = series;
createEffect(() => {
const values = json.vec();
if (!values) return;
if (seriesIndex > 0) {
let previous = chartLegend.at(seriesIndex - 1)?.seriesList[index];
if (!previous?.()) {
return;
}
}
untrack(() => {
let s = series();
if (!s) {
switch (type) {
case SeriesType.Based: {
s = createBaseLineSeries({
chart,
dark,
color,
topColor: seriesConfig.topColor,
bottomColor: seriesConfig.bottomColor,
options,
});
break;
}
case SeriesType.Candlestick: {
const candlestickSeries = createCandlesticksSeries({
chart,
options,
dark,
});
s = candlestickSeries[0];
if (!colors && !color) {
legend.color = candlestickSeries[1];
}
break;
}
case SeriesType.Histogram: {
s = createHistogramSeries({
chart,
options,
});
break;
}
default:
case SeriesType.Line: {
s = createLineSeries({
chart,
color,
dark,
options,
});
break;
}
}
if (priceScaleOptions) {
s.priceScale().applyOptions(priceScaleOptions);
}
series.set(s);
}
s.setData(values);
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 = activeIds();
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;
}
@@ -1,14 +1,22 @@
import { PRICE_SCALE_MOMENTUM_ID } from "../../chart/price";
import { defaultSeriesOptions } from "./options";
type HistogramOptions = DeepPartial<
HistogramStyleOptions & SeriesOptionsCommon
>;
export const createHistogramSeries = (
chart: IChartApi,
options?: HistogramOptions,
) => {
export const PRICE_SCALE_MOMENTUM_ID = "momentum";
export const createHistogramSeries = ({
chart,
// dark,
// color,
options,
}: {
chart: IChartApi;
// dark: Accessor<boolean>;
// color: Color;
options?: HistogramOptions;
}) => {
const seriesOptions: HistogramOptions = {
priceScaleId: "left",
...defaultSeriesOptions,
@@ -2,13 +2,14 @@
// https://github.com/tradingview/lightweight-charts/blob/master/tests/e2e/graphics/test-cases/horizontal-price-scale.js
import { type IHorzScaleBehavior } from "lightweight-charts";
import type { IHorzScaleBehavior } from "lightweight-charts";
export class HorzScaleBehaviorHeight implements IHorzScaleBehavior<number> {
options() {}
setOptions() {}
preprocessData() {}
updateFormatter() {}
createConverterToInternalObj() {
return (price) => price;
}
@@ -8,29 +8,27 @@ import {
} from "/src/scripts/utils/urlParams";
import { createRWS } from "/src/solid/rws";
import { chartState } from "../../chart/state";
import { setTimeScale } from "../../chart/time";
export function createSeriesLegend({
export function createSeriesLegend<Scale extends ResourceScale>({
id,
presetId,
title,
color,
series,
seriesList,
defaultVisible = true,
disabled: _disabled,
visible: _visible,
url,
dataset,
}: {
scale: Scale;
id: string;
presetId: string;
title: string;
color: Accessor<string | string[]>;
series: ISeriesApi<SeriesType>;
color: Color | Color[];
seriesList: Accessor<ISeriesApi<SeriesType> | undefined>[];
defaultVisible?: boolean;
disabled?: Accessor<boolean>;
visible?: RWS<boolean>;
url?: string;
dataset: ResourceDataset<Scale>;
}) {
const storageID = `${presetId}-${id}`;
@@ -44,15 +42,14 @@ export function createSeriesLegend({
const disabled = createMemo(_disabled || (() => false));
const drawn = createMemo(() => visible() && !disabled());
createEffect(() => {
if (disabled()) {
return;
}
const v = visible();
const d = disabled();
series.applyOptions({
visible: !d && v,
});
setTimeScale(chartState.range);
if (v !== defaultVisible) {
writeURLParam(id, v);
@@ -66,10 +63,11 @@ export function createSeriesLegend({
return {
id,
title,
series,
seriesList,
color,
visible,
disabled,
url,
drawn,
dataset,
};
}
+31
View File
@@ -0,0 +1,31 @@
import { defaultSeriesOptions } from "./options";
export const createLineSeries = ({
chart,
dark,
color,
options,
}: {
chart: IChartApi;
dark: Accessor<boolean>;
color: Color;
options?: DeepPartialLineOptions;
}) => {
function computeColors() {
return {
color: color(dark),
} as const;
}
const series = chart.addLineSeries({
...defaultSeriesOptions,
...options,
...computeColors(),
});
createEffect(() => {
series.applyOptions(computeColors());
});
return series;
};
@@ -0,0 +1,132 @@
import { colors } from "/src/scripts/utils/colors";
import { chunkIdToIndex } from "../datasets/resource";
import { dateFromTime } from "../utils/date";
import { valueToString } from "../utils/locale";
export function setMinMaxMarkers({
scale,
visibleRange,
legendList,
activeIds,
dark,
}: {
scale: ResourceScale;
visibleRange: TimeRange | undefined;
legendList: SeriesLegend[];
activeIds: Accessor<number[]>;
dark: Accessor<boolean>;
}) {
try {
if (!visibleRange) return;
const { from, to } = visibleRange;
const dateFrom = new Date(from as string);
const dateTo = new Date(to as string);
let max = undefined as [number, Time, number, ISeriesApi<any>] | undefined;
let min = undefined as [number, Time, number, ISeriesApi<any>] | undefined;
const ids = activeIds();
for (let i = 0; i < legendList.length; i++) {
const { seriesList, dataset } = legendList[i];
for (let j = 0; j < ids.length; j++) {
const id = ids[j];
const seriesIndex = chunkIdToIndex(scale, id);
const series = seriesList.at(seriesIndex)?.();
if (!series || !series?.options().visible) continue;
series.setMarkers([]);
const isCandlestick = series.seriesType() === "Candlestick";
const vec = dataset.fetchedJSONs.at(seriesIndex)?.vec();
if (!vec) return;
for (let k = 0; k < vec.length; k++) {
const data = vec[k];
let number;
if (scale === "date") {
const date = dateFromTime(data.time);
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(dark),
position: "belowBar" as const,
shape: "arrowUp" as const,
size: 0,
text: valueToString(min[2]),
};
}
if (max) {
maxMarker = {
weight: max[0],
time: max[1],
color: colors.white(dark),
position: "aboveBar" as const,
shape: "arrowDown" as const,
size: 0,
text: valueToString(max[2]),
};
}
if (min && max && min[3] === max[3] && minMarker && maxMarker) {
min[3].setMarkers(
[minMarker, maxMarker].sort((a, b) => a.weight - b.weight),
);
} else {
if (min && minMarker) {
min[3].setMarkers([minMarker]);
}
if (max && maxMarker) {
max[3].setMarkers([maxMarker]);
}
}
} catch (e) {}
}
@@ -0,0 +1,48 @@
import { dateFromTime, getNumberOfDaysBetweenTwoDates } from "../utils/date";
import { debounce } from "../utils/debounce";
export const debouncedUpdateVisiblePriceSeriesType = debounce(
updateVisiblePriceSeriesType,
50,
);
export function updateVisiblePriceSeriesType({
scale,
chart,
logicalRange,
timeRange,
priceSeriesType,
}: {
scale: ResourceScale;
chart: IChartApi;
logicalRange?: LogicalRange;
timeRange?: TimeRange;
priceSeriesType: RWS<PriceSeriesType>;
}) {
try {
const width = chart.timeScale().width();
let ratio: number;
if (logicalRange) {
ratio = (logicalRange.to - logicalRange.from) / width;
} else if (timeRange) {
if (scale === "date") {
ratio = getNumberOfDaysBetweenTwoDates(
dateFromTime(timeRange.from),
dateFromTime(timeRange.to),
);
} else {
ratio = ((timeRange.to as number) - (timeRange.from as number)) / width;
}
} else {
throw Error();
}
if (ratio <= 0.5) {
priceSeriesType.set("Candlestick");
} else {
priceSeriesType.set("Line");
}
} catch {}
}
@@ -1,28 +0,0 @@
import { defaultSeriesOptions } from "./options";
type AreaOptions = DeepPartial<AreaStyleOptions & SeriesOptionsCommon>;
export const createAreaSeries = (
chart: IChartApi,
options?: AreaOptions & {
color?: string;
},
) => {
const { color } = options || {};
// const fillColor = `${color}11`;
const fillColor = color;
const seriesOptions: AreaOptions = {
// priceScaleId: 'left',
...defaultSeriesOptions,
lineColor: color,
topColor: fillColor,
bottomColor: fillColor,
...options,
};
const series = chart.addAreaSeries(seriesOptions);
return series;
};
@@ -1,52 +0,0 @@
import { colors } from "/src/scripts/utils/colors";
import { defaultSeriesOptions } from "./options";
const DEFAULT_BASELINE_TOP_COLOR = colors.profit;
const DEFAULT_BASELINE_BOTTOM_COLOR = colors.loss;
export const DEFAULT_BASELINE_COLORS = [
DEFAULT_BASELINE_TOP_COLOR,
DEFAULT_BASELINE_BOTTOM_COLOR,
];
export const createBaseLineSeries = (
chart: IChartApi,
options: BaselineSeriesOptions,
) => {
const {
title,
color,
topColor,
topLineColor,
bottomColor,
bottomLineColor,
base,
lineColor,
} = options;
const allTopColor = topColor || color || DEFAULT_BASELINE_TOP_COLOR;
const topFillColor = `${allTopColor}`;
const allBottomColor = bottomColor || color || DEFAULT_BASELINE_BOTTOM_COLOR;
const bottomFillColor = `${allBottomColor}`;
const seriesOptions: DeepPartialBaselineOptions = {
priceScaleId: "right",
...defaultSeriesOptions,
lineWidth: 1,
...options,
...options.options,
...(base ? { baseValue: { type: "price", price: base } } : {}),
topLineColor: topLineColor || lineColor || allTopColor,
topFillColor1: topFillColor,
topFillColor2: topFillColor,
bottomLineColor: bottomLineColor || lineColor || allBottomColor,
bottomFillColor1: bottomFillColor,
bottomFillColor2: bottomFillColor,
title,
};
const series = chart.addBaselineSeries(seriesOptions);
return series;
};
@@ -1,42 +0,0 @@
import { colors } from "/src/scripts/utils/colors";
export const createCandlesticksSeries = (
chart: IChartApi,
options: PriceSeriesOptions,
): [ISeriesApi<"Candlestick">, string[]] => {
const { inverseColors, lowerOpacity } = options;
const upColor = lowerOpacity
? inverseColors
? colors.darkLoss
: colors.darkProfit
: inverseColors
? colors.loss
: colors.profit;
const downColor = lowerOpacity
? inverseColors
? colors.darkProfit
: colors.darkLoss
: inverseColors
? colors.profit
: colors.loss;
const candlestickSeries = chart.addCandlestickSeries({
baseLineVisible: false,
upColor,
wickUpColor: upColor,
downColor,
wickDownColor: downColor,
borderVisible: false,
priceLineVisible: false,
baseLineColor: "",
borderColor: "",
borderDownColor: "",
borderUpColor: "",
// lastValueVisible: false,
...options.seriesOptions,
});
return [candlestickSeries, [upColor, downColor]];
};
@@ -1,10 +0,0 @@
import { defaultSeriesOptions } from "./options";
export const createLineSeries = (
chart: IChartApi,
options?: DeepPartialLineOptions,
) =>
chart.addLineSeries({
...defaultSeriesOptions,
...options,
});
@@ -1,45 +0,0 @@
export const resetRightPriceScale = (
chart: IChartApi,
options?: FullPriceScaleOptions,
) => {
const finalOptions = {
...options,
scaleMargins: {
...(options?.halved
? {
top: 0.5,
bottom: 0.05,
}
: {
top: 0.1,
bottom: 0.1,
}),
...options?.scaleMargins,
},
};
chart.priceScale("right").applyOptions(finalOptions);
return finalOptions;
};
export const resetLeftPriceScale = (
chart: IChartApi,
options?: FullPriceScaleOptions,
) =>
chart.priceScale("left").applyOptions({
visible: false,
...options,
scaleMargins: {
...(options?.halved
? {
top: 0.475,
bottom: 0.025,
}
: {
top: 0.25,
bottom: 0.25,
}),
...options?.scaleMargins,
},
});
@@ -1,3 +0,0 @@
interface FullPriceScaleOptions extends DeepPartial<PriceScaleOptions> {
halved?: boolean;
}
+169
View File
@@ -0,0 +1,169 @@
import { HEIGHT_CHUNK_SIZE } from "../datasets";
import { debounce } from "../utils/debounce";
import { run } from "../utils/run";
import { tick } from "../utils/tick";
import { writeURLParam } from "../utils/urlParams";
const LOCAL_STORAGE_RANGE_KEY = "chart-range";
const URL_PARAMS_RANGE_FROM_KEY = "from";
const URL_PARAMS_RANGE_TO_KEY = "to";
export function setInitialTimeRange({
chart,
range,
}: {
chart: IChartApi;
range: TimeRange;
}) {
if (range) {
chart.timeScale().setVisibleRange(range);
// On small screen it doesn't it might not set it in time
const timeout = setTimeout(() => {
chart.timeScale().setVisibleRange(range);
}, 50);
onCleanup(() => {
clearTimeout(timeout);
});
}
}
export function getInitialTimeRange(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) {
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(getLocalStorageKey(scale)) || "null",
) as TimeRange | null;
if (savedTimeRange) {
return savedTimeRange;
}
switch (scale) {
case "date": {
const defaultTo = new Date();
const defaultFrom = new Date();
defaultFrom.setDate(defaultFrom.getUTCDate() - 6 * 30);
return {
from: defaultFrom.toJSON().split("T")[0],
to: defaultTo.toJSON().split("T")[0],
} satisfies TimeRange;
}
case "height": {
return {
from: 800_000,
to: 850_000,
} as any satisfies TimeRange;
}
}
}
export function initTimeScale({
scale,
activeIds,
exactRange,
chart,
}: {
scale: ResourceScale;
activeIds: RWS<number[]>;
exactRange: RWS<TimeRange>;
chart: IChartApi;
}) {
chart.timeScale().subscribeVisibleTimeRangeChange((range) => {
if (!range) return;
exactRange.set(range);
debouncedSetActiveIds({ exactRange: range, activeIds: activeIds });
debouncedSaveTimeRange({ scale, range });
});
}
function getLocalStorageKey(scale: ResourceScale) {
return `${LOCAL_STORAGE_RANGE_KEY}-${scale}`;
}
export function setActiveIds({
exactRange,
activeIds,
}: {
exactRange: TimeRange;
activeIds: RWS<number[]>;
}) {
let ids: number[] = [];
const today = new Date();
if (
typeof exactRange.from === "string" &&
typeof exactRange.to === "string"
) {
const from = new Date(exactRange.from).getUTCFullYear();
const to = new Date(exactRange.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(exactRange.from) / HEIGHT_CHUNK_SIZE);
const to = Math.floor(Number(exactRange.to) / HEIGHT_CHUNK_SIZE);
const length = to - from + 1;
ids = Array.from({ length }, (_, i) => (from + i) * HEIGHT_CHUNK_SIZE);
}
const old = activeIds();
if (
old.length !== ids.length ||
old.at(0) !== ids.at(0) ||
old.at(-1) !== ids.at(-1)
) {
console.log("range:", ids);
activeIds.set(ids);
}
}
const debouncedSetActiveIds = debounce(setActiveIds, 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, 250);
@@ -1,3 +1,12 @@
interface PriceSeriesOptions {
placement?: "top" | "bottom";
title?: string;
id?: string;
inverseColors?: boolean;
seriesOptions?: DeepPartial<SeriesOptionsCommon>;
priceScaleOptions?: DeepPartial<PriceScaleOptions>;
}
interface BaselineSeriesOptions {
color?: string;
topColor?: string;
@@ -0,0 +1,143 @@
import { dateToString, getNumberOfDaysBetweenTwoDates } from "../utils/date";
import { createLineSeries } from "./line";
export const GENESIS_DAY = "2009-01-03";
const whitespaceStartDate = new Date("1970-01-01");
const whitespaceStartDateYear = whitespaceStartDate.getUTCFullYear();
const whitespaceStartDateMonth = whitespaceStartDate.getUTCMonth();
const whitespaceStartDateDate = whitespaceStartDate.getUTCDate();
const whitespaceEndDate = new Date("2141-01-01");
const whitespaceDateDataset: (WhitespaceData | SingleValueData)[] = new Array(
getNumberOfDaysBetweenTwoDates(whitespaceStartDate, whitespaceEndDate),
);
// Hack to be able to scroll freely
// Setting them all to NaN is much slower
for (let i = 0; i < whitespaceDateDataset.length; i++) {
const date = new Date(
whitespaceStartDateYear,
whitespaceStartDateMonth,
whitespaceStartDateDate + i,
);
const time = dateToString(date);
if (i === whitespaceDateDataset.length - 1) {
whitespaceDateDataset[i] = {
time,
value: NaN,
};
} else {
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 = chart.addLineSeries();
if (scale === "date") {
whitespace.setData(whitespaceDateDataset);
} else {
whitespace.setData(whitespaceHeightDataset);
const time = whitespaceHeightDataset.length;
whitespace.update({
time: time as Time,
value: NaN,
});
}
return whitespace;
}
// ---
// import { HEIGHT_CHUNK_SIZE } from "../datasets";
// import { dateToString } from "../utils/date";
// export const GENESIS_DAY = "2009-01-03";
// function leapYear(year: number) {
// return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
// }
// const whitespaceStartDate = new Date("1970-01-01");
// export const whitespaceStartDateYear = whitespaceStartDate.getFullYear();
// const whitespaceStartDateMonth = whitespaceStartDate.getMonth();
// const whitespaceStartDateDate = whitespaceStartDate.getDate();
// const whitespaceEndDate = new Date("2141-01-01");
// const whitespaceEndDateYear = whitespaceEndDate.getFullYear();
// export const whitespaceDateDatasets: (WhitespaceData | SingleValueData)[][] =
// Array.from(
// { length: whitespaceEndDateYear - whitespaceStartDateYear },
// (_, i) => new Array(leapYear(whitespaceStartDateYear + i) ? 366 : 365),
// );
// for (let i = 0; i < whitespaceDateDatasets.length; i++) {
// const year = whitespaceStartDateYear + i;
// const whitespaceDateDataset = whitespaceDateDatasets[i];
// // Hack to be able to scroll freely
// // Setting them all to NaN is much slower
// for (let j = 0; j < whitespaceDateDataset.length; j++) {
// const date = new Date(
// year,
// whitespaceStartDateMonth,
// whitespaceStartDateDate + j,
// );
// const time = dateToString(date);
// if (j === whitespaceDateDataset.length - 1) {
// whitespaceDateDataset[j] = {
// time,
// value: NaN,
// };
// } else {
// whitespaceDateDataset[j] = {
// time,
// };
// }
// }
// }
// export const whitespaceHeightStart = -50_000;
// export const whitespaceHeightDatasets: (WhitespaceData | SingleValueData)[][] =
// Array.from(
// { length: (new Date().getUTCFullYear() - 2009 + 1) * 6 },
// () => new Array(HEIGHT_CHUNK_SIZE),
// );
// for (let i = 0; i < whitespaceHeightDatasets.length; i++) {
// const offset = HEIGHT_CHUNK_SIZE * i;
// const whitespaceHeightDataset = whitespaceHeightDatasets[i];
// for (let j = 0; j < whitespaceHeightDataset.length; j++) {
// const height = whitespaceHeightStart + offset + j;
// if (j === whitespaceHeightDataset.length - 1) {
// whitespaceHeightDataset[j] = {
// time: height as any,
// value: NaN,
// };
// } else {
// whitespaceHeightDataset[j] = {
// time: height as any,
// };
// }
// }
// }
+95 -124
View File
@@ -5,15 +5,8 @@ import {
import { liquidities } from "../../datasets/consts/liquidities";
import { colors } from "../../utils/colors";
import { createCohortPresetList } from "../templates/cohort";
import { applyMultipleSeries, SeriesType } from "../templates/multiple";
export function createPresets({
scale,
datasets,
}: {
scale: ResourceScale;
datasets: Datasets;
}): PartialPresetFolder {
export function createPresets(scale: ResourceScale): PartialPresetFolder {
return {
name: "Addresses",
tree: [
@@ -22,101 +15,70 @@ export function createPresets({
name: `Total Non Empty Addresses`,
title: `Total Non Empty Address`,
description: "",
unit: "Count",
icon: IconTablerWallet,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Total Non Empty Address`,
color: colors.bitcoin,
seriesType: SeriesType.Area,
dataset: params.datasets[scale].address_count,
},
],
});
},
bottom: [
{
title: `Total Non Empty Address`,
color: colors.bitcoin,
datasetPath: `/${scale}-to-address-count`,
},
],
},
{
scale,
name: `New Addresses`,
title: `New Addresses`,
description: "",
unit: "Count",
icon: IconTablerSparkles,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `New Addresses`,
color: colors.white,
dataset: params.datasets[scale].created_addresses,
},
],
});
},
bottom: [
{
title: `New Addresses`,
color: colors.bitcoin,
datasetPath: `/${scale}-to-new-addresses`,
},
],
},
{
scale,
name: `Total Addresses Created`,
title: `Total Addresses Created`,
description: "",
unit: "Count",
icon: IconTablerArchive,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Total Addresses Created`,
color: colors.bitcoin,
seriesType: SeriesType.Area,
dataset: params.datasets[scale].created_addresses,
},
],
});
},
bottom: [
{
title: `Total Addresses Created`,
color: colors.bitcoin,
datasetPath: `/${scale}-to-created-addresses`,
},
],
},
{
scale,
name: `Total Empty Addresses`,
title: `Total Empty Addresses`,
description: "",
unit: "Count",
icon: IconTablerTrash,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Total Empty Addresses`,
color: colors.darkWhite,
seriesType: SeriesType.Area,
dataset: params.datasets[scale].empty_addresses,
},
],
});
},
bottom: [
{
title: `Total Empty Addresses`,
color: colors.darkWhite,
datasetPath: `/${scale}-to-empty-addresses`,
},
],
},
{
name: "By Size",
tree: addressCohortsBySize.map(({ key, name }) =>
tree: addressCohortsBySize.map(({ key, name, size }) =>
createAddressPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
filenameAddon: size,
datasetId: key,
}),
),
},
@@ -125,11 +87,10 @@ export function createPresets({
name: "By Type",
tree: addressCohortsByType.map(({ key, name }) =>
createAddressPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
datasetId: key,
}),
),
},
@@ -137,82 +98,92 @@ export function createPresets({
} satisfies PartialPresetFolder;
}
function createAddressPresetFolder<Scale extends ResourceScale>({
datasets,
function createAddressPresetFolder({
scale,
color,
name,
datasetKey,
filenameAddon,
datasetId,
}: {
datasets: Datasets;
scale: Scale;
scale: ResourceScale;
name: string;
datasetKey: AddressCohortKey;
color: string;
filenameAddon?: string;
datasetId: AddressCohortId;
color: Color;
}): PartialPresetFolder {
return {
name,
name: filenameAddon ? `${name} - ${filenameAddon}` : name,
tree: [
createAddressCountPreset({ scale, name, datasetKey, color }),
createAddressCountPreset({ scale, name, datasetId, color }),
...createCohortPresetList({
title: name,
datasets,
scale,
name,
color,
datasetKey,
datasetId,
}),
createLiquidityFolder({
scale,
name,
datasetId,
color,
}),
{
name: `Split By Liquidity`,
tree: liquidities.map(
(liquidity): PartialPresetFolder => ({
name: liquidity.name,
tree: createCohortPresetList({
title: `${liquidity.name} ${name}`,
name: `${liquidity.name} ${name}`,
datasets,
scale,
color,
datasetKey: `${liquidity.key}_${datasetKey}`,
}),
}),
),
},
],
};
}
export function createAddressCountPreset<Scale extends ResourceScale>({
export function createLiquidityFolder({
scale,
color,
name,
datasetKey,
datasetId,
}: {
scale: Scale;
scale: ResourceScale;
name: string;
datasetKey: AddressCohortKey;
color: string;
datasetId: AddressCohortId | "";
color: Color;
}): 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,
datasetId: !datasetId ? liquidity.id : `${liquidity.id}-${datasetId}`,
}),
}),
),
};
}
export function createAddressCountPreset({
scale,
color,
name,
datasetId,
}: {
scale: ResourceScale;
name: string;
datasetId: AddressCohortId;
color: Color;
}): PartialPreset {
const addressCount: SeriesConfig = {
title: "Address Count",
color,
datasetPath: `/${scale}-to-${datasetId}-address-count`,
};
return {
scale,
name: `Address Count`,
title: `${name} Address Count`,
icon: IconTablerAddressBook,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Address Count",
color,
dataset: params.datasets[scale][`${datasetKey}_address_count`],
},
],
});
},
description: "",
unit: "Count",
icon: IconTablerAddressBook,
bottom: [addressCount],
};
}
+280 -199
View File
@@ -1,220 +1,301 @@
import { colors } from "../../utils/colors";
import { applyMultipleSeries, SeriesType } from "../templates/multiple";
export function createPresets() {
const scale: ResourceScale = "date";
import { createRecapPresets } from "../templates/recap";
export function createPresets(scale: ResourceScale) {
return {
name: "Blocks",
tree: [
{
scale,
icon: IconTablerWall,
name: "Height",
title: "Block Height",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Height",
color: colors.bitcoin,
dataset: params.datasets.date.last_height,
},
],
});
},
},
{
scale,
name: "Mined",
tree: [
{
scale,
icon: IconTablerCube,
name: "Daily Sum",
title: "Daily Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
...((scale === "date"
? [
{
scale,
icon: IconTablerWall,
name: "Height",
title: "Block Height",
description: "",
unit: "Height",
bottom: [
{
title: "Height",
color: colors.bitcoin,
datasetPath: `/date-to-last-height`,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1d_target,
options: {
lineStyle: 3,
// lineStyle: LineStyle.LargeDashed,
],
},
{
scale,
name: "Mined",
tree: [
{
scale,
icon: IconTablerCube,
name: "Daily Sum",
title: "Daily Sum Of Blocks Mined",
description: "",
unit: "Count",
bottom: [
{
title: "Target",
color: colors.white,
datasetPath: `/date-to-blocks-mined-1d-target`,
options: {
lineStyle: 3,
},
},
},
{
title: "1W Avg.",
color: colors.momentumYellow,
dataset: params.datasets.date.blocks_mined_1w_sma,
defaultVisible: false,
},
{
title: "1M Avg.",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1m_sma,
},
{
title: "Mined",
color: colors.darkBitcoin,
dataset: params.datasets.date.blocks_mined,
},
],
});
},
},
{
scale,
icon: IconTablerLetterW,
name: "Weekly Sum",
title: "Weekly Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1w_target,
options: {
lineStyle: 3,
// lineStyle: LineStyle.LargeDashed,
{
title: "1W Avg.",
color: colors.momentumYellow,
datasetPath: `/date-to-blocks-mined-1w-sma`,
defaultVisible: false,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1w_sum,
},
],
});
},
},
{
scale,
icon: IconTablerLetterM,
name: "Monthly Sum",
title: "Monthly Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1m_target,
options: {
// lineStyle: LineStyle.LargeDashed,
lineStyle: 3,
{
title: "1M Avg.",
color: colors.bitcoin,
datasetPath: `/date-to-blocks-mined-1m-sma`,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1m_sum,
},
],
});
},
},
{
scale,
icon: IconTablerLetterY,
name: "Yearly Sum",
title: "Yearly Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1y_target,
options: {
lineStyle: 3,
// lineStyle: LineStyle.LargeDashed,
{
title: "Mined",
color: colors.darkBitcoin,
datasetPath: `/date-to-blocks-mined`,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1y_sum,
},
],
});
},
},
{
scale,
icon: IconTablerWall,
name: "Total",
title: "Total Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
],
},
list: [
{
title: "Mined",
color: colors.bitcoin,
seriesType: SeriesType.Area,
dataset: params.datasets.date.total_blocks_mined,
},
],
});
{
scale,
icon: IconTablerLetterW,
name: "Weekly Sum",
title: "Weekly Sum Of Blocks Mined",
description: "",
unit: "Count",
bottom: [
{
title: "Target",
color: colors.white,
datasetPath: `/date-to-blocks-mined-1w-target`,
options: {
lineStyle: 3,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
datasetPath: `/date-to-blocks-mined-1w-sum`,
},
],
},
{
scale,
icon: IconTablerLetterM,
name: "Monthly Sum",
title: "Monthly Sum Of Blocks Mined",
description: "",
unit: "Count",
bottom: [
{
title: "Target",
color: colors.white,
datasetPath: `/date-to-blocks-mined-1m-target`,
options: {
lineStyle: 3,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
datasetPath: `/date-to-blocks-mined-1m-sum`,
},
],
},
{
scale,
icon: IconTablerLetterY,
name: "Yearly Sum",
title: "Yearly Sum Of Blocks Mined",
description: "",
unit: "Count",
bottom: [
{
title: "Target",
color: colors.white,
datasetPath: `/date-to-blocks-mined-1y-target`,
options: {
lineStyle: 3,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
datasetPath: `/date-to-blocks-mined-1y-sum`,
},
],
},
{
scale,
icon: IconTablerWall,
name: "Total",
title: "Total Blocks Mined",
description: "",
unit: "Count",
bottom: [
{
title: "Mined",
color: colors.bitcoin,
datasetPath: `/date-to-total-blocks-mined`,
},
],
},
],
},
},
],
},
{
scale,
name: "Size",
tree: createRecapPresets({
scale,
title: "Block Size",
color: colors.darkWhite,
unit: "Megabytes",
keySum: "/date-to-block-size-1d-sum",
keyAverage: "/date-to-block-size-1d-average",
keyMax: "/date-to-block-size-1d-max",
key90p: "/date-to-block-size-1d-90p",
key75p: "/date-to-block-size-1d-75p",
keyMedian: "/date-to-block-size-1d-median",
key25p: "/date-to-block-size-1d-25p",
key10p: "/date-to-block-size-1d-10p",
keyMin: "/date-to-block-size-1d-min",
}),
},
{
scale,
name: "Weight",
tree: createRecapPresets({
scale,
title: "Block Weight",
color: colors.darkWhite,
unit: "Weight",
keyAverage: "/date-to-block-weight-1d-average",
keyMax: "/date-to-block-weight-1d-max",
key90p: "/date-to-block-weight-1d-90p",
key75p: "/date-to-block-weight-1d-75p",
keyMedian: "/date-to-block-weight-1d-median",
key25p: "/date-to-block-weight-1d-25p",
key10p: "/date-to-block-weight-1d-10p",
keyMin: "/date-to-block-weight-1d-min",
}),
},
{
scale,
name: "VBytes",
tree: createRecapPresets({
scale,
title: "Block VBytes",
color: colors.darkWhite,
unit: "Virtual Bytes",
keyAverage: "/date-to-block-vbytes-1d-average",
keyMax: "/date-to-block-vbytes-1d-max",
key90p: "/date-to-block-vbytes-1d-90p",
key75p: "/date-to-block-vbytes-1d-75p",
keyMedian: "/date-to-block-vbytes-1d-median",
key25p: "/date-to-block-vbytes-1d-25p",
key10p: "/date-to-block-vbytes-1d-10p",
keyMin: "/date-to-block-vbytes-1d-min",
}),
},
{
scale,
name: "Interval",
tree: createRecapPresets({
scale,
title: "Block Interval",
color: colors.darkWhite,
unit: "Seconds",
keyAverage: "/date-to-block-interval-1d-average",
keyMax: "/date-to-block-interval-1d-max",
key90p: "/date-to-block-interval-1d-90p",
key75p: "/date-to-block-interval-1d-75p",
keyMedian: "/date-to-block-interval-1d-median",
key25p: "/date-to-block-interval-1d-25p",
key10p: "/date-to-block-interval-1d-10p",
keyMin: "/date-to-block-interval-1d-min",
}),
},
]
: [
{
scale,
icon: IconTablerMaximize,
name: "Size",
title: "Block Size",
description: "",
unit: "Megabytes",
bottom: [
{
title: "Size",
color: colors.darkWhite,
datasetPath: `/height-to-block-size`,
},
],
},
{
scale,
icon: IconTablerWeight,
name: "Weight",
title: "Block Weight",
description: "",
unit: "Weight",
bottom: [
{
title: "Weight",
color: colors.darkWhite,
datasetPath: `/height-to-block-weight`,
},
],
},
{
scale,
icon: IconTablerBinary,
name: "VBytes",
title: "Block VBytes",
description: "",
unit: "Virtual Bytes",
bottom: [
{
title: "VBytes",
color: colors.darkWhite,
datasetPath: `/height-to-block-vbytes`,
},
],
},
{
scale,
icon: IconTablerAlarm,
name: "Interval",
title: "Block Interval",
description: "",
unit: "Seconds",
bottom: [
{
title: "Interval",
color: colors.darkWhite,
datasetPath: `/height-to-block-interval`,
},
],
},
]) satisfies PartialPresetTree),
{
scale,
icon: IconTablerStack3,
name: "Cumulative Size",
title: "Cumulative Block Size",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Size (MB)",
color: colors.darkWhite,
seriesType: SeriesType.Area,
dataset: params.datasets.date.cumulative_block_size,
},
],
});
},
unit: "Megabytes",
bottom: [
{
title: "Size",
color: colors.darkWhite,
datasetPath: `/${scale}-to-cumulative-block-size`,
},
],
},
],
} satisfies PartialPresetFolder;
File diff suppressed because it is too large Load Diff
+831
View File
@@ -0,0 +1,831 @@
import { colors } from "../../utils/colors";
import { SeriesType } from "../enums";
import { createRatioFolder } from "../templates/ratio";
export function createPresets(scale: ResourceScale) {
return {
name: "Cointime Economics",
tree: [
{
name: "Prices",
tree: [
{
scale,
icon: IconTablerArrowsCross,
name: "All",
title: "All Cointime Prices",
description: "",
unit: "US Dollars",
top: [
{
title: "Vaulted Price",
color: colors.vaultedness,
datasetPath: `/${scale}-to-vaulted-price`,
},
{
title: "Active Price",
color: colors.liveliness,
datasetPath: `/${scale}-to-active-price`,
},
{
title: "True Market Mean",
color: colors.trueMarketMeanPrice,
datasetPath: `/${scale}-to-true-market-mean`,
},
{
title: "Realized Price",
color: colors.bitcoin,
datasetPath: `/${scale}-to-realized-price`,
},
{
title: "Cointime",
color: colors.cointimePrice,
datasetPath: `/${scale}-to-cointime-price`,
},
],
},
{
name: "Active",
tree: [
{
scale,
icon: IconTablerHeartBolt,
name: "Price",
title: "Active Price",
description: "",
unit: "US Dollars",
top: [
{
title: "Active Price",
color: colors.liveliness,
datasetPath: `/${scale}-to-active-price`,
},
],
},
createRatioFolder({
color: colors.liveliness,
ratioDatasetPath: `/${scale}-to-market-price-to-active-price-ratio`,
scale,
title: "Active Price",
valueDatasetPath: `/${scale}-to-active-price`,
}),
],
},
{
name: "Vaulted",
tree: [
{
scale,
icon: IconTablerBuildingBank,
name: "Price",
title: "Vaulted Price",
description: "",
unit: "US Dollars",
top: [
{
title: "Vaulted Price",
color: colors.vaultedness,
datasetPath: `/${scale}-to-vaulted-price`,
},
],
},
createRatioFolder({
color: colors.vaultedness,
ratioDatasetPath: `/${scale}-to-market-price-to-vaulted-price-ratio`,
scale,
title: "Vaulted Price",
valueDatasetPath: `/${scale}-to-vaulted-price`,
}),
],
},
{
name: "True Market Mean",
tree: [
{
scale,
icon: IconTablerStackMiddle,
name: "Price",
title: "True Market Mean",
description: "",
unit: "US Dollars",
top: [
{
title: "True Market Mean",
color: colors.trueMarketMeanPrice,
datasetPath: `/${scale}-to-true-market-mean`,
},
],
},
createRatioFolder({
color: colors.liveliness,
ratioDatasetPath: `/${scale}-to-market-price-to-true-market-mean-ratio`,
scale,
title: "True Market Mean",
valueDatasetPath: `/${scale}-to-true-market-mean`,
}),
],
},
{
name: "Cointime Price",
tree: [
{
scale,
icon: IconTablerStackMiddle,
name: "Price",
title: "Cointime Price",
description: "",
unit: "US Dollars",
top: [
{
title: "Cointime",
color: colors.cointimePrice,
datasetPath: `/${scale}-to-cointime-price`,
},
],
},
createRatioFolder({
color: colors.cointimePrice,
ratioDatasetPath: `/${scale}-to-market-price-to-cointime-price-ratio`,
scale,
title: "Cointime",
valueDatasetPath: `/${scale}-to-cointime-price`,
}),
],
},
],
},
{
name: "Capitalizations",
tree: [
{
scale,
icon: IconTablerArrowsCross,
name: "All",
title: "Cointime Capitalizations",
description: "",
unit: "US Dollars",
bottom: [
{
title: "Market Cap",
color: colors.white,
datasetPath: `/${scale}-to-market-cap`,
},
{
title: "Realized Cap",
color: colors.realizedCap,
datasetPath: `/${scale}-to-realized-cap`,
},
{
title: "Investor Cap",
color: colors.investorCap,
datasetPath: `/${scale}-to-investor-cap`,
},
{
title: "Thermo Cap",
color: colors.thermoCap,
datasetPath: `/${scale}-to-thermo-cap`,
},
],
},
{
scale,
icon: IconTablerPick,
name: "Thermo Cap",
title: "Thermo Cap",
description: "",
unit: "US Dollars",
bottom: [
{
title: "Thermo Cap",
color: colors.thermoCap,
datasetPath: `/${scale}-to-thermo-cap`,
},
],
},
{
scale,
icon: IconTablerTie,
name: "Investor Cap",
title: "Investor Cap",
description: "",
unit: "US Dollars",
bottom: [
{
title: "Investor Cap",
color: colors.investorCap,
datasetPath: `/${scale}-to-investor-cap`,
},
],
},
{
scale,
icon: IconTablerDivide,
name: "Thermo Cap To Investor Cap Ratio",
title: "Thermo Cap To Investor Cap Ratio",
description: "",
unit: "Percentage",
bottom: [
{
title: "Ratio",
color: colors.bitcoin,
datasetPath: `/${scale}-to-thermo-cap-to-investor-cap-ratio`,
},
],
},
],
},
{
name: "Coinblocks",
tree: [
{
scale,
icon: IconTablerArrowsCross,
name: "All",
title: "All Coinblocks",
description: "",
unit: "Coinblocks",
bottom: [
{
title: "Coinblocks Created",
color: colors.coinblocksCreated,
datasetPath: `/${scale}-to-coinblocks-created`,
},
{
title: "Coinblocks Destroyed",
color: colors.coinblocksDestroyed,
datasetPath: `/${scale}-to-coinblocks-destroyed`,
},
{
title: "Coinblocks Stored",
color: colors.coinblocksStored,
datasetPath: `/${scale}-to-coinblocks-stored`,
},
],
},
{
scale,
icon: IconTablerCube,
name: "Created",
title: "Coinblocks Created",
description: "",
unit: "Coinblocks",
bottom: [
{
title: "Coinblocks Created",
color: colors.coinblocksCreated,
datasetPath: `/${scale}-to-coinblocks-created`,
},
],
},
{
scale,
icon: IconTablerFileShredder,
name: "Destroyed",
title: "Coinblocks Destroyed",
description: "",
unit: "Coinblocks",
bottom: [
{
title: "Coinblocks Destroyed",
color: colors.coinblocksDestroyed,
datasetPath: `/${scale}-to-coinblocks-destroyed`,
},
],
},
{
scale,
icon: IconTablerBuildingWarehouse,
name: "Stored",
title: "Coinblocks Stored",
description: "",
unit: "Coinblocks",
bottom: [
{
title: "Coinblocks Stored",
color: colors.coinblocksStored,
datasetPath: `/${scale}-to-coinblocks-stored`,
},
],
},
],
},
{
name: "Cumulative Coinblocks",
tree: [
{
scale,
icon: IconTablerArrowsCross,
name: "All",
title: "All Cumulative Coinblocks",
description: "",
unit: "Coinblocks",
bottom: [
{
title: "Cumulative Coinblocks Created",
color: colors.coinblocksCreated,
datasetPath: `/${scale}-to-cumulative-coinblocks-created`,
},
{
title: "Cumulative Coinblocks Destroyed",
color: colors.coinblocksDestroyed,
datasetPath: `/${scale}-to-cumulative-coinblocks-destroyed`,
},
{
title: "Cumulative Coinblocks Stored",
color: colors.coinblocksStored,
datasetPath: `/${scale}-to-cumulative-coinblocks-stored`,
},
],
},
{
scale,
icon: IconTablerCube,
name: "Created",
title: "Cumulative Coinblocks Created",
description: "",
unit: "Coinblocks",
bottom: [
{
title: "Cumulative Coinblocks Created",
color: colors.coinblocksCreated,
datasetPath: `/${scale}-to-cumulative-coinblocks-created`,
},
],
},
{
scale,
icon: IconTablerFileShredder,
name: "Destroyed",
title: "Cumulative Coinblocks Destroyed",
description: "",
unit: "Coinblocks",
bottom: [
{
title: "Cumulative Coinblocks Destroyed",
color: colors.coinblocksDestroyed,
datasetPath: `/${scale}-to-cumulative-coinblocks-destroyed`,
},
],
},
{
scale,
icon: IconTablerBuildingWarehouse,
name: "Stored",
title: "Cumulative Coinblocks Stored",
description: "",
unit: "Coinblocks",
bottom: [
{
title: "Cumulative Coinblocks Stored",
color: colors.coinblocksStored,
datasetPath: `/${scale}-to-cumulative-coinblocks-stored`,
},
],
},
],
},
{
name: "Liveliness & Vaultedness",
tree: [
{
scale,
icon: IconTablerHeartBolt,
name: "Liveliness - Activity",
title: "Liveliness (Activity)",
description: "",
unit: "",
bottom: [
{
title: "Liveliness",
color: colors.liveliness,
datasetPath: `/${scale}-to-liveliness`,
},
],
},
{
scale,
icon: IconTablerBuildingBank,
name: "Vaultedness",
title: "Vaultedness",
description: "",
unit: "",
bottom: [
{
title: "Vaultedness",
color: colors.vaultedness,
datasetPath: `/${scale}-to-vaultedness`,
},
],
},
{
scale,
icon: IconTablerArrowsCross,
name: "Versus",
title: "Liveliness V. Vaultedness",
description: "",
unit: "",
bottom: [
{
title: "Liveliness",
color: colors.liveliness,
datasetPath: `/${scale}-to-liveliness`,
},
{
title: "Vaultedness",
color: colors.vaultedness,
datasetPath: `/${scale}-to-vaultedness`,
},
],
},
{
scale,
icon: IconTablerDivide,
name: "Activity To Vaultedness Ratio",
title: "Activity To Vaultedness Ratio",
description: "",
unit: "Percentage",
bottom: [
{
title: "Activity To Vaultedness Ratio",
color: colors.activityToVaultednessRatio,
datasetPath: `/${scale}-to-activity-to-vaultedness-ratio`,
},
],
},
{
scale,
icon: IconTablerHeartBolt,
name: "Concurrent Liveliness - Supply Adjusted Coindays Destroyed",
title: "Concurrent Liveliness - Supply Adjusted Coindays Destroyed",
description: "",
unit: "",
bottom: [
{
title: "Concurrent Liveliness 14d Median",
color: colors.liveliness,
datasetPath: `/${scale}-to-concurrent-liveliness-2w-median`,
},
{
title: "Concurrent Liveliness",
color: colors.darkLiveliness,
datasetPath: `/${scale}-to-concurrent-liveliness`,
},
],
},
{
scale,
icon: IconTablerStairs,
name: "Liveliness Incremental Change",
title: "Liveliness Incremental Change",
description: "",
unit: "",
bottom: [
{
title: "Liveliness Incremental Change",
color: colors.darkLiveliness,
seriesType: SeriesType.Based,
datasetPath: `/${scale}-to-liveliness-net-change`,
},
{
title: "Liveliness Incremental Change 14 Day Median",
color: colors.liveliness,
seriesType: SeriesType.Based,
datasetPath: `/${scale}-to-liveliness-net-change-2w-median`,
},
],
},
],
},
{
name: "Supply",
tree: [
{
scale,
icon: IconTablerBuildingBank,
name: "Vaulted",
title: "Vaulted Supply",
description: "",
unit: "Bitcoin",
bottom: [
{
title: "Vaulted Supply",
color: colors.vaultedness,
datasetPath: `/${scale}-to-vaulted-supply`,
},
],
},
{
scale,
icon: IconTablerHeartBolt,
name: "Active",
title: "Active Supply",
description: "",
unit: "Bitcoin",
bottom: [
{
title: "Active Supply",
color: colors.liveliness,
datasetPath: `/${scale}-to-active-supply`,
},
],
},
{
scale,
icon: IconTablerArrowsCross,
name: "Vaulted V. Active",
title: "Vaulted V. Active",
description: "",
unit: "Bitcoin",
bottom: [
{
title: "Circulating Supply",
color: colors.coinblocksCreated,
datasetPath: `/${scale}-to-supply`,
},
{
title: "Vaulted Supply",
color: colors.vaultedness,
datasetPath: `/${scale}-to-vaulted-supply`,
},
{
title: "Active Supply",
color: colors.liveliness,
datasetPath: `/${scale}-to-active-supply`,
},
],
},
// TODO: Fix, Bad data
// {
// id: 'asymptomatic-supply-regions',
// icon: IconTablerDirections,
// name: 'Asymptomatic Supply Regions',
// title: 'Asymptomatic Supply Regions',
// description: '',
// applyPreset(params) {
// return applyMultipleSeries({
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// id: 'min-vaulted',
// title: 'Min Vaulted Supply',
// color: colors.vaultedness,
// dataset: params.`/${scale}-to-dateToMinVaultedSupply,
// },
// {
// id: 'max-active',
// title: 'Max Active Supply',
// color: colors.liveliness,
// dataset: params.`/${scale}-to-dateToMaxActiveSupply,
// },
// ],
// })
// },
// },
{
scale,
icon: IconTablerBuildingBank,
name: "Vaulted Net Change",
title: "Vaulted Supply Net Change",
description: "",
unit: "Bitcoin",
bottom: [
{
title: "Vaulted Supply Net Change",
color: colors.vaultedness,
datasetPath: `/${scale}-to-vaulted-supply-net-change`,
},
],
},
{
scale,
icon: IconTablerHeartBolt,
name: "Active Net Change",
title: "Active Supply Net Change",
description: "",
unit: "Bitcoin",
bottom: [
{
title: "Active Supply Net Change",
color: colors.liveliness,
datasetPath: `/${scale}-to-active-supply-net-change`,
},
],
},
{
scale,
icon: IconTablerSwords,
name: "Active VS. Vaulted 90D Net Change",
title: "Active VS. Vaulted 90 Day Supply Net Change",
description: "",
unit: "Bitcoin",
bottom: [
{
title: "Active Supply Net Change",
color: colors.liveliness,
datasetPath: `/${scale}-to-active-supply-3m-net-change`,
seriesType: SeriesType.Based,
},
{
title: "Vaulted Supply Net Change",
color: colors.vaultedPrice,
seriesType: SeriesType.Based,
datasetPath: `/${scale}-to-vaulted-supply-3m-net-change`,
},
],
},
// TODO: Fix, Bad data
// {
// id: 'vaulted-supply-annualized-net-change',
// icon: IconTablerBuildingBank,
// name: 'Vaulted Annualized Net Change',
// title: 'Vaulted Supply Annualized Net Change',
// description: '',
// applyPreset(params) {
// return applyMultipleSeries({
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// id: 'vaulted-annualized-supply-net-change',
// title: 'Vaulted Supply Annualized Net Change',
// color: colors.vaultedness,
// dataset:
// `/${scale}-to-vaultedAnnualizedSupplyNetChange,
// },
// ],
// })
// },
// },
// TODO: Fix, Bad data
// {
// id: 'vaulting-rate',
// icon: IconTablerBuildingBank,
// name: 'Vaulting Rate',
// title: 'Vaulting Rate',
// description: '',
// applyPreset(params) {
// return applyMultipleSeries({
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// id: 'vaulting-rate',
// title: 'Vaulting Rate',
// color: colors.vaultedness,
// dataset: `/${scale}-to-vaultingRate,
// },
// {
// id: 'nominal-inflation-rate',
// title: 'Nominal Inflation Rate',
// color: colors.orange,
// dataset: params.`/${scale}-to-dateToYearlyInflationRate,
// },
// ],
// })
// },
// },
// TODO: Fix, Bad data
// {
// id: 'active-supply-net-change-decomposition',
// icon: IconTablerArrowsCross,
// name: 'Active Supply Net Change Decomposition (90D)',
// title: 'Active Supply Net 90 Day Change Decomposition',
// description: '',
// applyPreset(params) {
// return applyMultipleSeries({
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// id: 'issuance-change',
// title: 'Change From Issuance',
// color: colors.emerald,
// dataset:
// params.params.datasets[scale]
// [scale].activeSupplyChangeFromIssuance90dChange,
// },
// {
// id: 'transactions-change',
// title: 'Change From Transactions',
// color: colors.rose,
// dataset:
// params.params.datasets[scale]
// [scale].activeSupplyChangeFromTransactions90dChange,
// },
// // {
// // id: 'active',
// // title: 'Active Supply',
// // color: colors.liveliness,
// // dataset: `/${scale}-to-activeSupply,
// // },
// ],
// })
// },
// },
{
scale,
icon: IconTablerTrendingUp,
name: "In Profit",
title: "Cointime Supply In Profit",
description: "",
unit: "Bitcoin",
bottom: [
{
title: "Circulating Supply",
color: colors.coinblocksCreated,
datasetPath: `/${scale}-to-supply`,
},
{
title: "Vaulted Supply",
color: colors.vaultedness,
datasetPath: `/${scale}-to-vaulted-supply`,
},
{
title: "Supply in profit",
color: colors.bitcoin,
datasetPath: `/${scale}-to-supply-in-profit`,
},
],
},
{
scale,
icon: IconTablerTrendingDown,
name: "In Loss",
title: "Cointime Supply In Loss",
description: "",
unit: "Bitcoin",
bottom: [
{
title: "Circulating Supply",
color: colors.coinblocksCreated,
datasetPath: `/${scale}-to-supply`,
},
{
title: "Active Supply",
color: colors.liveliness,
datasetPath: `/${scale}-to-active-supply`,
},
{
title: "Supply in Loss",
color: colors.bitcoin,
datasetPath: `/${scale}-to-supply-in-loss`,
},
],
},
],
},
{
scale,
icon: IconTablerBuildingFactory,
name: "Cointime Yearly Inflation Rate",
title: "Cointime-Adjusted Yearly Inflation Rate",
description: "",
unit: "Percentage",
bottom: [
{
title: "Cointime Adjusted",
color: colors.coinblocksCreated,
datasetPath: `/${scale}-to-cointime-adjusted-yearly-inflation-rate`,
},
{
title: "Nominal",
color: colors.bitcoin,
datasetPath: `/${scale}-to-yearly-inflation-rate`,
},
],
},
{
scale,
icon: IconTablerWind,
name: "Cointime Velocity",
title: "Cointime-Adjusted Transactions Velocity",
description: "",
unit: "",
bottom: [
{
title: "Cointime Adjusted",
color: colors.coinblocksCreated,
datasetPath: `/${scale}-to-cointime-adjusted-velocity`,
},
{
title: "Nominal",
color: colors.bitcoin,
datasetPath: `/${scale}-to-transaction-velocity`,
},
],
},
],
} satisfies PartialPresetFolder;
}
+6
View File
@@ -0,0 +1,6 @@
export const enum SeriesType {
Line,
Based,
Histogram,
Candlestick,
}
+30 -55
View File
@@ -7,15 +7,8 @@ import {
} from "../../datasets/consts/age";
import { colors } from "../../utils/colors";
import { createCohortPresetFolder } from "../templates/cohort";
import { applyMultipleSeries } from "../templates/multiple";
export function createPresets({
scale,
datasets,
}: {
scale: ResourceScale;
datasets: Datasets;
}) {
export function createPresets(scale: ResourceScale) {
return {
name: "Hodlers",
tree: [
@@ -25,99 +18,81 @@ export function createPresets({
title: `Hodl Supply`,
description: "",
icon: IconTablerRipple,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `24h`,
color: colors.up_to_1d,
dataset:
params.datasets.date
.up_to_1d_supply_to_circulating_supply_ratio,
},
unit: "Percentage",
bottom: [
{
title: `24h`,
color: colors.up_to_1d,
datasetPath: `/${scale}-to-up-to-1d-supply-to-circulating-supply-ratio`,
},
...fromXToYCohorts.map(({ key, name, legend }) => ({
title: legend,
color: colors[key],
dataset:
params.datasets.date[
`${key}_supply_to_circulating_supply_ratio`
],
})),
...fromXToYCohorts.map(({ key, id, name, legend }) => ({
title: legend,
color: colors[key],
datasetPath:
`/${scale}-to-${id}-supply-to-circulating-supply-ratio` as const,
})),
{
title: `15y+`,
color: colors.from_15y,
dataset:
params.datasets.date
.from_15y_supply_to_circulating_supply_ratio,
},
],
});
},
{
title: `15y+`,
color: colors.from_15y,
datasetPath: `/${scale}-to-from-15y-supply-to-circulating-supply-ratio`,
},
],
},
...xthCohorts.map(({ key, name, legend }) =>
...xthCohorts.map(({ key, id, name, legend }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name: legend,
datasetKey: key,
datasetId: id,
title: name,
}),
),
{
name: "Up To X",
tree: upToCohorts.map(({ key, name }) =>
tree: upToCohorts.map(({ key, id, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
datasetId: id,
title: name,
}),
),
},
{
name: "From X To Y",
tree: fromXToYCohorts.map(({ key, name }) =>
tree: fromXToYCohorts.map(({ key, id, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
datasetId: id,
title: name,
}),
),
},
{
name: "From X",
tree: fromXCohorts.map(({ key, name }) =>
tree: fromXCohorts.map(({ key, id, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
datasetId: id,
title: name,
}),
),
},
{
name: "Years",
tree: yearCohorts.map(({ key, name }) =>
tree: yearCohorts.map(({ key, id, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
datasetId: id,
title: name,
}),
),
+54 -30
View File
@@ -1,12 +1,16 @@
import { phone } from "/src/env";
import { createRWS } from "/src/solid/rws";
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 createCoinblocksPresets } from "./cointime";
import { createPresets as createHodlersPresets } from "./hodlers";
import { createPresets as createMarketPresets } from "./market";
import { createPresets as createMinersPresets } from "./miners";
@@ -19,7 +23,7 @@ export const LOCAL_STORAGE_HISTORY_KEY = "history";
export const LOCAL_STORAGE_SELECTED_KEY = "preset";
export const LOCAL_STORAGE_VISITED_KEY = "visited";
export function createPresets(datasets: Datasets): Presets {
export function createPresets(): Presets {
const partialTree = [
{
name: "Dashboards (Coming soon)",
@@ -29,43 +33,56 @@ export function createPresets(datasets: Datasets): Presets {
name: "Charts",
tree: [
{
name: "By Date",
name: "By Block Date",
tree: [
createMarketPresets({ scale: "date", datasets }),
createBlocksPresets(),
createMarketPresets("date"),
createBlocksPresets("date"),
createMinersPresets("date"),
createTransactionsPresets("date"),
...createCohortPresetList({
datasets,
scale: "date",
color: colors.bitcoin,
datasetKey: "",
datasetId: "",
name: "",
title: "",
}),
createHodlersPresets({ scale: "date", datasets }),
createAddressesPresets({ scale: "date", datasets }),
createCoinblocksPresets({ scale: "date", datasets }),
createLiquidityFolder({
scale: "date",
color: colors.bitcoin,
datasetId: "",
name: "",
}),
createHodlersPresets("date"),
createAddressesPresets("date"),
createCoinblocksPresets("date"),
],
} satisfies PartialPresetFolder,
{
name: "By Height (Coming soon)",
tree: [
// createMarketPresets({ scale: "height", datasets }),
// createMinersPresets("height"),
// createTransactionsPresets("height"),
// ...createCohortPresetList({
// datasets,
// scale: "height",
// color: colors.bitcoin,
// name: "",
// datasetKey: "",
// title: "",
// }),
// createHodlersPresets({ scale: "height", datasets }),
// createAddressesPresets({ scale: "height", datasets }),
// createCoinblocksPresets({ scale: "height", datasets }),
],
name: "By Block Height - Desktop/Tablet Only",
tree: !phone
? [
createMarketPresets("height"),
createBlocksPresets("height"),
createMinersPresets("height"),
createTransactionsPresets("height"),
...createCohortPresetList({
scale: "height",
color: colors.bitcoin,
name: "",
datasetId: "",
title: "",
}),
createLiquidityFolder({
scale: "height",
color: colors.bitcoin,
datasetId: "",
name: "",
}),
createHodlersPresets("height"),
createAddressesPresets("height"),
createCoinblocksPresets("height"),
]
: [],
} satisfies PartialPresetFolder,
],
},
@@ -103,7 +120,7 @@ export function createPresets(datasets: Datasets): Presets {
const serializedHistory: SerializedPresetsHistory = history().map(
({ preset, date }) => ({
p: preset.id,
d: date.valueOf(),
d: date.getTime(),
}),
);
@@ -250,7 +267,14 @@ function checkIfDuplicateIds(ids: string[]) {
}
function findInitialPreset(presets: Preset[]): Preset {
const urlPreset = document.location.pathname.substring(1);
let urlPreset = document.location.pathname.substring(1);
if (phone && urlPreset.startsWith("height" satisfies ResourceScale)) {
urlPreset = urlPreset.replace(
"height" satisfies ResourceScale,
"date" satisfies ResourceScale,
);
}
return (
(urlPreset &&
@@ -1,11 +1,9 @@
import { averages } from "/src/scripts/datasets/date";
import { averages } from "/src/scripts/datasets/consts/averages";
import { colors } from "/src/scripts/utils/colors";
import { applyMultipleSeries } from "../../templates/multiple";
export function createPresets(datasets: Datasets): PartialPresetFolder {
const scale: ResourceScale = "date";
import { createRatioFolder } from "../../templates/ratio";
export function createPresets(scale: ResourceScale): PartialPresetFolder {
return {
name: "Averages",
tree: [
@@ -13,25 +11,21 @@ export function createPresets(datasets: Datasets): PartialPresetFolder {
scale,
icon: IconTablerMathAvg,
name: "All",
title: "All Averages",
applyPreset(params) {
return applyMultipleSeries({
...params,
list: averages.map((average) => ({
title: average.key.toUpperCase(),
color: colors[`_${average.key}`],
dataset: params.datasets.date[`price_${average.key}_sma`],
})),
});
},
title: "All Moving Averages",
description: "",
unit: "US Dollars",
top: averages.map((average) => ({
title: average.key.toUpperCase(),
color: colors[`_${average.key}`],
datasetPath: `/${scale}-to-price-${average.key}-sma`,
})),
},
...averages.map(({ name, key }) =>
createPresetFolder({
datasets,
scale,
color: colors[`_${key}`],
name,
title: `${name} Market Price Moving Average`,
key,
}),
),
@@ -41,38 +35,42 @@ export function createPresets(datasets: Datasets): PartialPresetFolder {
function createPresetFolder({
scale,
datasets,
color,
name,
title,
key,
}: {
datasets: Datasets;
scale: ResourceScale;
color: string;
color: Color;
name: string;
title: string;
key: AverageName;
}) {
return {
// id,
// name,
// tree: [
// {
scale,
name,
description: "",
icon: IconTablerMathAvg,
title: `${name} Moving Average`,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
tree: [
{
scale,
name: "Average",
title,
description: "",
unit: "US Dollars",
icon: IconTablerMathAvg,
top: [
{
title: `SMA`,
color,
dataset: datasets.date[`price_${key}_sma`],
datasetPath: `/${scale}-to-price-${key}-sma`,
},
],
});
},
} satisfies PartialPreset;
},
createRatioFolder({
scale,
color,
ratioDatasetPath: `/${scale}-to-market-price-to-price-${key}-sma-ratio`,
valueDatasetPath: `/${scale}-to-price-${key}-sma`,
title,
}),
],
} satisfies PartialPresetFolder;
}
+13 -48
View File
@@ -1,16 +1,9 @@
import { colors } from "../../utils/colors";
import { applyMultipleSeries } from "../templates/multiple";
import { createPresets as createAveragesPresets } from "./averages";
import { createPresets as createIndicatorsPresets } from "./indicators";
import { createPresets as createReturnsPresets } from "./returns";
export function createPresets({
scale,
datasets,
}: {
scale: ResourceScale;
datasets: Datasets;
}) {
export function createPresets(scale: ResourceScale) {
return {
name: "Market",
tree: [
@@ -19,57 +12,29 @@ export function createPresets({
icon: IconTablerCurrencyDollar,
name: "Price",
title: "Market Price",
applyPreset(params) {
return applyMultipleSeries({ ...params });
},
description: "",
},
{
scale,
icon: IconTablerPercentage,
name: "Performance",
title: "Market Performance",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceOptions: {
id: "performance",
title: "Performance",
priceScaleOptions: {
mode: 2,
},
},
});
},
description: "",
unit: "US Dollars",
},
{
scale,
icon: IconTablerInfinity,
name: "Capitalization",
title: "Market Capitalization",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Market Cap.",
dataset: params.datasets[scale].market_cap,
color: colors.bitcoin,
},
],
});
},
description: "",
unit: "US Dollars",
bottom: [
{
title: "Market Cap.",
datasetPath: `/${scale}-to-market-cap`,
color: colors.bitcoin,
},
],
},
createAveragesPresets(scale),
...(scale === "date"
? ([
createAveragesPresets(datasets),
createReturnsPresets(datasets),
createIndicatorsPresets(datasets),
createReturnsPresets(),
createIndicatorsPresets(),
] satisfies PartialPresetTree)
: []),
],
@@ -1,4 +1,4 @@
export function createPresets(datasets: Datasets) {
export function createPresets() {
return {
name: "Indicators",
tree: [],
+13 -24
View File
@@ -3,9 +3,9 @@ import {
totalReturns,
} from "/src/scripts/datasets/consts/returns";
import { applyMultipleSeries, SeriesType } from "../../templates/multiple";
import { SeriesType } from "../../enums";
export function createPresets(datasets: Datasets) {
export function createPresets() {
return {
name: "Returns",
tree: [
@@ -15,10 +15,9 @@ export function createPresets(datasets: Datasets) {
...totalReturns.map(({ name, key }) =>
createPreset({
scale: "date",
datasets,
name,
title: `${name} Total`,
key: `${key}_total`,
key: `${key}-total`,
}),
),
],
@@ -29,10 +28,9 @@ export function createPresets(datasets: Datasets) {
...compoundReturns.map(({ name, key }) =>
createPreset({
scale: "date",
datasets,
name,
title: `${name} Compound`,
key: `${key}_compound`,
key: `${key}-compound`,
}),
),
],
@@ -43,16 +41,14 @@ export function createPresets(datasets: Datasets) {
function createPreset({
scale,
datasets,
name,
title,
key,
}: {
scale: ResourceScale;
datasets: Datasets;
name: string;
title: string;
key: `${TotalReturnKey}_total` | `${CompoundReturnKey}_compound`;
key: `${TotalReturnKey}-total` | `${CompoundReturnKey}-compound`;
}): PartialPreset {
return {
scale,
@@ -60,20 +56,13 @@ function createPreset({
description: "",
icon: IconTablerReceiptTax,
title: `${title} Return`,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Return (%)`,
seriesType: SeriesType.Based,
dataset: datasets.date[`price_${key}_return`],
},
],
});
},
unit: "Percentage",
bottom: [
{
title: `Return`,
seriesType: SeriesType.Based,
datasetPath: `/date-to-price-${key}-return`,
},
],
};
}
File diff suppressed because it is too large Load Diff
-985
View File
@@ -1,985 +0,0 @@
import { percentiles } from "../../datasets/consts/percentiles";
import { colors } from "../../utils/colors";
import { applyMultipleSeries, SeriesType } from "./multiple";
export function createCohortPresetFolder<Scale extends ResourceScale>({
datasets,
scale,
color,
name,
datasetKey,
title,
}: {
datasets: Datasets;
scale: Scale;
name: string;
datasetKey: AnyPossibleCohortKey;
color: string;
title: string;
}) {
return {
name,
tree: createCohortPresetList({
title,
datasets,
name,
scale,
color,
datasetKey,
}),
} satisfies PartialPresetFolder;
}
export function createCohortPresetList<Scale extends ResourceScale>({
name,
datasets,
scale,
color,
datasetKey,
title,
}: {
name: string;
datasets: Datasets;
scale: Scale;
datasetKey: AnyPossibleCohortKey;
title: string;
color: string;
}) {
const datasetPrefix = datasetKey
? (`${datasetKey}_` as const)
: ("" as const);
return [
{
name: "UTXOs",
tree: [
{
scale,
name: `Count`,
title: `${title} Unspent Transaction Outputs Count`,
icon: () => IconTablerTicket,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Count",
color,
seriesType: SeriesType.Area,
dataset: params.datasets[scale][`${datasetPrefix}utxo_count`],
},
],
});
},
description: "",
},
],
},
{
name: "Realized",
tree: [
{
scale,
name: `Price`,
title: `${title} Realized Price`,
description: "",
icon: () => IconTablerTag,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
{
title: "Realized Price",
color,
dataset:
params.datasets[scale][`${datasetPrefix}realized_price`],
},
],
});
},
},
{
scale,
name: `Capitalization`,
title: `${title} Realized Capitalization`,
icon: () => IconTablerPigMoney,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `${name} Realized Cap.`,
color,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][`${datasetPrefix}realized_cap`],
},
...(datasetKey
? [
{
title: "Realized Cap.",
color: colors.bitcoin,
dataset: params.datasets[scale].realized_cap,
defaultVisible: false,
},
]
: []),
],
});
},
description: "",
},
{
scale,
name: `Capitalization 1M Net Change`,
title: `${title} Realized Capitalization 1 Month Net Change`,
icon: () => IconTablerStatusChange,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Net Change`,
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}realized_cap_1m_net_change`
],
},
],
});
},
description: "",
},
{
scale,
name: `Profit`,
title: `${title} Realized Profit`,
icon: () => IconTablerCash,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Realized Profit",
dataset:
params.datasets[scale][`${datasetPrefix}realized_profit`],
color: colors.profit,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: "Loss",
title: `${title} Realized Loss`,
icon: () => IconTablerCoffin,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Realized Loss",
dataset:
params.datasets[scale][`${datasetPrefix}realized_loss`],
color: colors.loss,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: `PNL`,
title: `${title} Realized Profit And Loss`,
icon: () => IconTablerArrowsVertical,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Profit",
color: colors.profit,
dataset:
params.datasets[scale][`${datasetPrefix}realized_profit`],
seriesType: SeriesType.Based,
},
{
title: "Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}negative_realized_loss`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL`,
title: `${title} Net Realized Profit And Loss`,
icon: () => IconTablerScale,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Net PNL",
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}net_realized_profit_and_loss`
],
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL Relative To Market Cap`,
title: `${title} Net Realized Profit And Loss Relative To Market Capitalization`,
icon: () => IconTablerDivide,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Net",
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}net_realized_profit_and_loss_to_market_cap_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: `Cumulative Profit`,
title: `${title} Cumulative Realized Profit`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Realized Profit",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_realized_profit`
],
},
],
});
},
description: "",
},
{
scale,
name: "Cumulative Loss",
title: `${title} Cumulative Realized Loss`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Realized Loss",
color: colors.loss,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_realized_loss`
],
},
],
});
},
description: "",
},
{
scale,
name: `Cumulative Net PNL`,
title: `${title} Cumulative Net Realized Profit And Loss`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Net Realized PNL",
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_net_realized_profit_and_loss`
],
},
],
});
},
description: "",
},
{
scale,
name: `Cumulative Net PNL 30 Day Change`,
title: `${title} Cumulative Net Realized Profit And Loss 30 Day Change`,
icon: () => IconTablerTimeDuration30,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Net Realized PNL 30d Change",
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_net_realized_profit_and_loss_1m_net_change`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
],
},
{
name: "Unrealized",
tree: [
{
scale,
name: `Profit`,
title: `${title} Unrealized Profit`,
icon: () => IconTablerMoodDollar,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Profit",
dataset:
params.datasets[scale][`${datasetPrefix}unrealized_profit`],
color: colors.profit,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: "Loss",
title: `${title} Unrealized Loss`,
icon: () => IconTablerMoodSadDizzy,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Loss",
dataset:
params.datasets[scale][`${datasetPrefix}unrealized_loss`],
color: colors.loss,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: `PNL`,
title: `${title} Unrealized Profit And Loss`,
icon: () => IconTablerArrowsVertical,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Profit",
color: colors.profit,
dataset:
params.datasets[scale][`${datasetPrefix}unrealized_profit`],
seriesType: SeriesType.Based,
},
{
title: "Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}negative_unrealized_loss`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL`,
title: `${title} Net Unrealized Profit And Loss`,
icon: () => IconTablerScale,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Net Unrealized PNL",
dataset:
params.datasets[scale][
`${datasetPrefix}net_unrealized_profit_and_loss`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL Relative To Market Cap`,
title: `${title} Net Unrealized Profit And Loss Relative To Total Market Capitalization`,
icon: () => IconTablerDivide,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Relative Net Unrealized PNL",
dataset:
params.datasets[scale][
`${datasetPrefix}net_unrealized_profit_and_loss_to_market_cap_ratio`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
],
},
{
name: "Supply",
tree: [
{
name: "Absolute",
tree: [
{
scale,
name: "All",
title: `${title} Profit And Loss`,
icon: () => IconTablerArrowsCross,
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "In Profit",
color: colors.profit,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit`
],
},
{
title: "In Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss`
],
},
{
title: "Total",
color: colors.white,
dataset: params.datasets[scale][`${datasetPrefix}supply`],
},
{
title: "Halved Total",
color: colors.gray,
dataset:
params.datasets[scale][`${datasetPrefix}halved_supply`],
options: {
lineStyle: 4,
},
},
],
});
},
},
{
scale,
name: `Total`,
title: `${title} Total supply`,
icon: () => IconTablerSum,
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color,
seriesType: SeriesType.Area,
dataset: params.datasets[scale][`${datasetPrefix}supply`],
},
],
});
},
},
{
scale,
name: "In Profit",
title: `${title} Supply In Profit`,
icon: () => IconTablerTrendingUp,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Loss",
title: `${title} Supply In Loss`,
icon: () => IconTablerTrendingDown,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.loss,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss`
],
},
],
});
},
description: "",
},
],
},
{
name: "Relative To Circulating",
tree: [
{
scale,
name: "All",
title: `${title} Profit And Loss Relative To Circulating Supply`,
icon: () => IconTablerArrowsCross,
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "In Profit",
color: colors.profit,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_circulating_supply_ratio`
],
},
{
title: "In Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_circulating_supply_ratio`
],
},
{
title: "100%",
color: colors.white,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_to_circulating_supply_ratio`
],
},
{
title: "50%",
color: colors.gray,
dataset:
params.datasets[scale][
`${datasetPrefix}halved_supply_to_circulating_supply_ratio`
],
options: {
lineStyle: 4,
},
},
],
});
},
},
{
scale,
name: `Total`,
title: `${title} Total supply Relative To Circulating Supply`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_to_circulating_supply_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Profit",
title: `${title} Supply In Profit Relative To Circulating Supply`,
icon: () => IconTablerTrendingUp,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_circulating_supply_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Loss",
title: `${title} Supply In Loss Relative To Circulating Supply`,
icon: () => IconTablerTrendingDown,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
seriesType: SeriesType.Area,
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_circulating_supply_ratio`
],
},
],
});
},
description: "",
},
],
},
{
name: "Relative To Own",
tree: [
{
scale,
name: "All",
title: `${title} Supply In Profit And Loss Relative To Own Supply`,
icon: () => IconTablerArrowsCross,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "In profit",
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_own_supply_ratio`
],
color: colors.profit,
},
{
title: "In loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_own_supply_ratio`
],
},
{
title: "100%",
color: colors.white,
dataset: params.datasets[scale][100],
options: {
lastValueVisible: false,
},
},
{
title: "50%",
color: colors.gray,
dataset: params.datasets[scale][50],
options: {
lineStyle: 4,
lastValueVisible: false,
},
},
],
});
},
description: "",
},
{
scale,
name: "In Profit",
title: `${title} Supply In Profit Relative To Own Supply`,
icon: () => IconTablerTrendingUp,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_own_supply_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Loss",
title: `${title} Supply In Loss Relative To Own Supply`,
icon: () => IconTablerTrendingDown,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
seriesType: SeriesType.Area,
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_own_supply_ratio`
],
},
],
});
},
description: "",
},
],
},
// createMomentumPresetFolder({
// datasets: datasets[scale],
// scale,
// id: `${scale}-${id}-supply-in-profit-and-loss-percentage-self`,
// title: `${title} Supply In Profit And Loss (% Self)`,
// datasetKey: `${datasetKey}SupplyPNL%Self`,
// }),
],
},
{
name: "Prices Paid",
tree: [
{
scale,
name: `Average`,
title: `${title} Average Price Paid - Realized Price`,
icon: () => IconTablerMathAvg,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
{
title: "Average",
color,
dataset:
params.datasets[scale][`${datasetPrefix}realized_price`],
},
],
});
},
description: "",
},
{
scale,
name: `Deciles`,
title: `${title} deciles`,
icon: () => IconTablerSquareHalf,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: percentiles
.filter(({ value }) => Number(value) % 10 === 0)
.map(({ name, key }) => ({
dataset: params.datasets[scale][`${datasetPrefix}${key}`],
color,
title: name,
})),
});
},
description: "",
},
...percentiles.map(
(percentile): PartialPreset => ({
scale,
name: percentile.name,
title: `${title} ${percentile.title}`,
icon: () => IconTablerSquareHalf,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
{
title: percentile.name,
color,
dataset:
params.datasets[scale][
`${datasetPrefix}${percentile.key}`
],
},
],
});
},
description: "",
}),
),
],
},
] satisfies PartialPresetTree;
}

Some files were not shown because too many files have changed in this diff Show More