global: snapshot
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 408 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="icon" type="image/png" sizes="196x196" href="/assets/pwa/2024-11-20_09-41-25/favicon-196.png">
|
||||
<link rel="apple-touch-icon" href="/assets/pwa/2024-11-20_09-41-25/apple-icon-180.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2048-2732.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2732-2048.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1668-2388.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2388-1668.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1536-2048.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2048-1536.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1488-2266.jpg" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2266-1488.jpg" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1640-2360.jpg" media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2360-1640.jpg" media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1668-2224.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2224-1668.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1620-2160.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2160-1620.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1290-2796.jpg" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2796-1290.jpg" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1179-2556.jpg" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2556-1179.jpg" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1284-2778.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2778-1284.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1170-2532.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2532-1170.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1125-2436.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2436-1125.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1242-2688.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2688-1242.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-828-1792.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1792-828.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1242-2208.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2208-1242.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-750-1334.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1334-750.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-640-1136.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1136-640.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -6,7 +6,7 @@ OUTPUT="/assets/pwa/${DATE}"
|
||||
mkdir ".${OUTPUT}"
|
||||
cp "./assets/pwa/index.html" ".${OUTPUT}/"
|
||||
|
||||
pwa-asset-generator "../assets/logo-icon.svg" ".${OUTPUT}" \
|
||||
pwa-asset-generator "../assets/dove-orange.svg" ".${OUTPUT}" \
|
||||
--index ".${OUTPUT}/index.html" \
|
||||
--manifest "./manifest.webmanifest" \
|
||||
--favicon \
|
||||
@@ -15,19 +15,19 @@ pwa-asset-generator "../assets/logo-icon.svg" ".${OUTPUT}" \
|
||||
--quality "100" \
|
||||
--opaque "false"
|
||||
|
||||
pwa-asset-generator "../assets/logo-dove-light.svg" ".${OUTPUT}" \
|
||||
pwa-asset-generator "../assets/dove-white.svg" ".${OUTPUT}" \
|
||||
--index ".${OUTPUT}/index.html" \
|
||||
--manifest "./manifest.webmanifest" \
|
||||
--icon-only \
|
||||
--background "#f26610" \
|
||||
--padding "5%" \
|
||||
--padding "10%" \
|
||||
--path-override "${OUTPUT}" \
|
||||
--quality "100"
|
||||
|
||||
pwa-asset-generator "../assets/logo-dove-light.svg" ".${OUTPUT}" \
|
||||
pwa-asset-generator "../assets/logo-stamp-orange.svg" ".${OUTPUT}" \
|
||||
--index ".${OUTPUT}/index.html" \
|
||||
--splash-only \
|
||||
--background "#f26610" \
|
||||
--padding "min(35vh, 35vw)" \
|
||||
--padding "min(30vh, 30vw)" \
|
||||
--path-override "${OUTPUT}" \
|
||||
--quality "100"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<title>kibō</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A better, FOSS, Bitcoin-only, self-hostable Glassnode"
|
||||
content="An open source Bitcoin Core data extractor and visualizer"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
@@ -318,8 +318,7 @@
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
gap: 0.25rem;
|
||||
display: flow-root;
|
||||
align-items: baseline;
|
||||
text-decoration-style: dotted;
|
||||
text-decoration-thickness: 1px;
|
||||
@@ -368,6 +367,7 @@
|
||||
line-height: var(--line-height-sm);
|
||||
opacity: 0.5;
|
||||
color: inherit;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,6 +458,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
field,
|
||||
h1 {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@@ -519,7 +524,7 @@
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
/* color: var(--off-color); */
|
||||
color: var(--off-color);
|
||||
position: relative;
|
||||
pointer-events: all;
|
||||
|
||||
@@ -540,22 +545,6 @@
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
> a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> span.value {
|
||||
opacity: 0.5;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
|
||||
&:has(+ *) {
|
||||
margin-right: 0.375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -620,16 +609,6 @@
|
||||
pointer-events: auto;
|
||||
padding: 0.5rem 1.5rem;
|
||||
gap: 1rem;
|
||||
|
||||
> label {
|
||||
text-align: center;
|
||||
min-width: 3.5rem;
|
||||
color: var(--off-color);
|
||||
|
||||
&:has(input:checked) {
|
||||
color: var(--color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,8 +667,8 @@
|
||||
font-weight: var(--font-weight-base);
|
||||
|
||||
h4 + & {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,11 +686,12 @@
|
||||
|
||||
summary {
|
||||
list-style: none;
|
||||
display: inline-flex;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&::marker,
|
||||
&::-webkit-details-marker {
|
||||
@@ -771,6 +751,8 @@
|
||||
height: 0.375rem;
|
||||
border-radius: 9999px;
|
||||
align-self: center;
|
||||
display: inline-block;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -925,12 +907,6 @@
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
|
||||
#chart-list > & {
|
||||
bottom: 1.75rem;
|
||||
width: 80px;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
.shadow-left {
|
||||
position: absolute;
|
||||
@@ -960,6 +936,80 @@
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chart-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
z-index: 20;
|
||||
|
||||
> .chart-wrapper {
|
||||
position: relative;
|
||||
min-height: 0px;
|
||||
width: 100%;
|
||||
cursor: crosshair;
|
||||
height: 100%;
|
||||
|
||||
&:has(+ .chart-wrapper:not([hidden])) {
|
||||
height: calc(100% - 62px);
|
||||
}
|
||||
|
||||
> fieldset {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: var(--main-padding);
|
||||
padding-right: var(--main-padding);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
gap: 0.5rem;
|
||||
|
||||
> div.field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
gap: 1rem;
|
||||
|
||||
> legend,
|
||||
> div {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
> hr {
|
||||
min-width: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
padding: 0.5rem;
|
||||
margin: -0.5rem;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .chart-div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> .shadow-bottom {
|
||||
bottom: 1.75rem;
|
||||
width: 80px;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- ------- -->
|
||||
@@ -1010,181 +1060,362 @@
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="196x196"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/favicon-196.png"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/favicon-196.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-icon-180.png"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-icon-180.png"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2048-2732.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2048-2732.jpg"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2732-2048.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2732-2048.jpg"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1668-2388.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1668-2388.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2388-1668.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2388-1668.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1536-2048.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1536-2048.jpg"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2048-1536.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2048-1536.jpg"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1488-2266.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1488-2266.jpg"
|
||||
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2266-1488.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2266-1488.jpg"
|
||||
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1640-2360.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1640-2360.jpg"
|
||||
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2360-1640.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2360-1640.jpg"
|
||||
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1668-2224.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1668-2224.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2224-1668.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2224-1668.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1620-2160.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1620-2160.jpg"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2160-1620.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2160-1620.jpg"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1290-2796.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1290-2796.jpg"
|
||||
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2796-1290.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2796-1290.jpg"
|
||||
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1179-2556.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1179-2556.jpg"
|
||||
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2556-1179.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2556-1179.jpg"
|
||||
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1284-2778.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1284-2778.jpg"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2778-1284.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2778-1284.jpg"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1170-2532.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1170-2532.jpg"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2532-1170.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2532-1170.jpg"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1125-2436.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1125-2436.jpg"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2436-1125.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2436-1125.jpg"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1242-2688.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1242-2688.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2688-1242.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2688-1242.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-828-1792.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-828-1792.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1792-828.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1792-828.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1242-2208.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1242-2208.jpg"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-2208-1242.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2208-1242.jpg"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-750-1334.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-750-1334.jpg"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1334-750.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1334-750.jpg"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-640-1136.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-640-1136.jpg"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-09-17_09-06-03/apple-splash-1136-640.jpg"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1136-640.jpg"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="196x196"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/favicon-196.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-icon-180.png"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2048-2732.jpg"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2732-2048.jpg"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1668-2388.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2388-1668.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1536-2048.jpg"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2048-1536.jpg"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1488-2266.jpg"
|
||||
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2266-1488.jpg"
|
||||
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1640-2360.jpg"
|
||||
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2360-1640.jpg"
|
||||
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1668-2224.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2224-1668.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1620-2160.jpg"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2160-1620.jpg"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1290-2796.jpg"
|
||||
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2796-1290.jpg"
|
||||
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1179-2556.jpg"
|
||||
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2556-1179.jpg"
|
||||
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1284-2778.jpg"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2778-1284.jpg"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1170-2532.jpg"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2532-1170.jpg"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1125-2436.jpg"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2436-1125.jpg"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1242-2688.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2688-1242.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-828-1792.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1792-828.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1242-2208.jpg"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-2208-1242.jpg"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-750-1334.jpg"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1334-750.jpg"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-640-1136.jpg"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/pwa/2024-11-20_09-41-25/apple-splash-1136-640.jpg"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
</head>
|
||||
@@ -1300,12 +1531,14 @@
|
||||
</header>
|
||||
<div id="charts" hidden>
|
||||
<fieldset id="legend"></fieldset>
|
||||
<div id="chart-list">
|
||||
<div id="charts-chart-list" class="chart-list">
|
||||
<div class="shadow-top"></div>
|
||||
<div class="shadow-bottom"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="simulation" hidden></div>
|
||||
<div id="live-price" hidden></div>
|
||||
<div id="moscow-time" hidden></div>
|
||||
</section>
|
||||
</aside>
|
||||
<div id="share-div" hidden>
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
"name": "kibō",
|
||||
"short_name": "kibō",
|
||||
"description": "A better, FOSS, Bitcoin-only, self-hostable Glassnode",
|
||||
"categories": ["bitcoin", "on-chain", "data"],
|
||||
"categories": [
|
||||
"bitcoin",
|
||||
"on-chain",
|
||||
"data"
|
||||
],
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
@@ -12,28 +16,28 @@
|
||||
"lang": "en",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/pwa/2024-09-17_09-06-03/manifest-icon-192.maskable.png",
|
||||
"src": "/assets/pwa/2024-11-20_09-41-25/manifest-icon-192.maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/pwa/2024-09-17_09-06-03/manifest-icon-192.maskable.png",
|
||||
"src": "/assets/pwa/2024-11-20_09-41-25/manifest-icon-192.maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/pwa/2024-09-17_09-06-03/manifest-icon-512.maskable.png",
|
||||
"src": "/assets/pwa/2024-11-20_09-41-25/manifest-icon-512.maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/pwa/2024-09-17_09-06-03/manifest-icon-512.maskable.png",
|
||||
"src": "/assets/pwa/2024-11-20_09-41-25/manifest-icon-512.maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -224,14 +224,6 @@ export function init({
|
||||
}
|
||||
createFetchChunksOfVisibleDatasetsEffect();
|
||||
|
||||
function resetChartListElement() {
|
||||
while (
|
||||
elements.chartList.lastElementChild?.classList.contains("chart-wrapper")
|
||||
) {
|
||||
elements.chartList.lastElementChild?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} parent
|
||||
* @param {number} chartIndex
|
||||
@@ -767,7 +759,7 @@ export function init({
|
||||
/** @type {AnyDatasetPath} */
|
||||
const datasetPath = `${s}-to-price`;
|
||||
|
||||
const dataset = datasets.getOrImport(s, datasetPath);
|
||||
const dataset = datasets.getOrCreate(s, datasetPath);
|
||||
|
||||
// Don't trigger reactivity by design
|
||||
activeDatasets().add(dataset);
|
||||
@@ -806,7 +798,7 @@ export function init({
|
||||
});
|
||||
|
||||
function createLiveCandleUpdateEffect() {
|
||||
signals.createEffect(webSockets.krakenCandle.latest, (latest) => {
|
||||
signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
|
||||
if (!latest) return;
|
||||
|
||||
const index = utils.chunkIdToIndex(s, latest.year);
|
||||
@@ -825,95 +817,6 @@ export function init({
|
||||
return priceSeries;
|
||||
}
|
||||
|
||||
function resetLegendElement() {
|
||||
elements.legend.innerHTML = "";
|
||||
}
|
||||
|
||||
function initTimeScaleElement() {
|
||||
const GENESIS_DAY = "2009-01-03";
|
||||
|
||||
/**
|
||||
* @param {HTMLButtonElement} button
|
||||
* @param {ChartOption} option
|
||||
*/
|
||||
function setTimeScale(button, option) {
|
||||
const chart = charts.at(-1);
|
||||
if (!chart) return;
|
||||
const timeScale = chart.timeScale();
|
||||
|
||||
const year = button.dataset.year;
|
||||
let days = button.dataset.days;
|
||||
let toHeight = button.dataset.to;
|
||||
|
||||
switch (option.scale) {
|
||||
case "date": {
|
||||
let from = new Date();
|
||||
let to = new Date();
|
||||
to.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
if (!days && typeof button.dataset.yearToDate === "string") {
|
||||
days = String(
|
||||
Math.ceil(
|
||||
(to.getTime() -
|
||||
new Date(`${to.getUTCFullYear()}-01-01`).getTime()) /
|
||||
consts.ONE_DAY_IN_MS,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (year) {
|
||||
from = new Date(`${year}-01-01`);
|
||||
to = new Date(`${year}-12-31`);
|
||||
} else if (days) {
|
||||
from.setDate(from.getUTCDate() - Number(days));
|
||||
} else {
|
||||
from = new Date(GENESIS_DAY);
|
||||
}
|
||||
|
||||
timeScale.setVisibleRange({
|
||||
from: /** @type {Time} */ (from.getTime() / 1000),
|
||||
to: /** @type {Time} */ (to.getTime() / 1000),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "height": {
|
||||
timeScale.setVisibleRange({
|
||||
from: /** @type {Time} */ (0),
|
||||
to: /** @type {Time} */ (Number(toHeight?.slice(0, -1)) * 1_000),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} timeScaleButtons
|
||||
*/
|
||||
function initGoToButtons(timeScaleButtons) {
|
||||
Array.from(timeScaleButtons.children).forEach((button) => {
|
||||
if (button.tagName !== "BUTTON") throw "Expect a button";
|
||||
button.addEventListener("click", () => {
|
||||
const option = options.selected();
|
||||
if (option.kind === "chart") {
|
||||
setTimeScale(/** @type {HTMLButtonElement} */ (button), option);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// initGoToButtons(elements.timeScaleDateButtons);
|
||||
// initGoToButtons(elements.timeScaleHeightButtons);
|
||||
|
||||
// function createScaleButtonsToggleEffect() {
|
||||
// const isDate = signals.createMemo(() => scale() === "date");
|
||||
// signals.createEffect(isDate, (isDate) => {
|
||||
// elements.timeScaleDateButtons.hidden = !isDate;
|
||||
// elements.timeScaleHeightButtons.hidden = isDate;
|
||||
// });
|
||||
// }
|
||||
// createScaleButtonsToggleEffect();
|
||||
}
|
||||
initTimeScaleElement();
|
||||
|
||||
/**
|
||||
* @param {ChartOption} option
|
||||
*/
|
||||
@@ -933,15 +836,12 @@ export function init({
|
||||
(list) => (list ? [list] : []),
|
||||
);
|
||||
|
||||
resetLegendElement();
|
||||
resetChartListElement();
|
||||
|
||||
/** @type {Series[]} */
|
||||
const allSeries = [];
|
||||
|
||||
charts = chartsBlueprints.map((seriesBlueprints, chartIndex) => {
|
||||
const { chartDiv, unitName, chartMode } = createChartDiv(
|
||||
elements.chartList,
|
||||
elements.chartsChartList,
|
||||
chartIndex,
|
||||
);
|
||||
|
||||
@@ -1148,7 +1048,7 @@ export function init({
|
||||
}
|
||||
|
||||
[...seriesBlueprints].reverse().forEach((seriesBlueprint, index) => {
|
||||
const dataset = datasets.getOrImport(
|
||||
const dataset = datasets.getOrCreate(
|
||||
scale,
|
||||
seriesBlueprint.datasetPath,
|
||||
);
|
||||
@@ -1266,11 +1166,31 @@ export function init({
|
||||
});
|
||||
}
|
||||
|
||||
function resetLegendElement() {
|
||||
elements.legend.innerHTML = "";
|
||||
}
|
||||
|
||||
function resetChartListElement() {
|
||||
while (
|
||||
elements.chartsChartList.lastElementChild?.classList.contains(
|
||||
"chart-wrapper",
|
||||
)
|
||||
) {
|
||||
elements.chartsChartList.lastElementChild?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
charts.forEach((chart) => chart.remove());
|
||||
charts = [];
|
||||
resetLegendElement();
|
||||
resetChartListElement();
|
||||
}
|
||||
|
||||
function createApplyChartOptionEffect() {
|
||||
signals.createEffect(selected, (option) => {
|
||||
signals.createRoot(() => {
|
||||
applyChartOption(option);
|
||||
});
|
||||
reset();
|
||||
applyChartOption(option);
|
||||
});
|
||||
}
|
||||
createApplyChartOptionEffect();
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @import {Options} from './options';
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {Consts} args.consts
|
||||
* @param {LightweightCharts} args.lightweightCharts
|
||||
* @param {Signals} args.signals
|
||||
* @param {Utilities} args.utils
|
||||
* @param {Options} args.options
|
||||
* @param {Datasets} args.datasets
|
||||
* @param {WebSockets} args.webSockets
|
||||
* @param {Elements} args.elements
|
||||
* @param {Ids} args.ids
|
||||
* @param {Accessor<boolean>} args.dark
|
||||
*/
|
||||
export function init({
|
||||
colors,
|
||||
consts,
|
||||
dark,
|
||||
datasets,
|
||||
elements,
|
||||
ids,
|
||||
lightweightCharts,
|
||||
options,
|
||||
signals,
|
||||
utils,
|
||||
webSockets,
|
||||
}) {
|
||||
const livePriceElement = elements.livePrice;
|
||||
|
||||
const price = window.document.createElement("h1");
|
||||
livePriceElement.append(price);
|
||||
|
||||
signals.createEffect(webSockets.kraken1dCandle.latest, (candle) => {
|
||||
if (!candle) return;
|
||||
price.innerHTML = utils.formatters.dollars.format(candle.close);
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* @import { Option, ResourceDataset, TimeScale, TimeRange, Unit, Marker, Weighted, DatasetPath, OHLC, FetchedJSON, DatasetValue, FetchedResult, AnyDatasetPath, SeriesBlueprint, BaselineSpecificSeriesBlueprint, CandlestickSpecificSeriesBlueprint, LineSpecificSeriesBlueprint, SpecificSeriesBlueprintWithChart, Signal, Color, DatasetCandlestickData, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, AnyPath, SimulationOption } from "./types/self"
|
||||
* @import {createChart as CreateClassicChart, createChartEx as CreateCustomChart, LineStyleOptions} from "../packages/lightweight-charts/v4.2.0/types";
|
||||
* @import * as _ from "../packages/ufuzzy/v1.0.14/types"
|
||||
* @import { DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, SingleValueData, ISeriesApi, Time, LogicalRange, SeriesMarker, CandlestickData, SeriesType, BaselineStyleOptions, SeriesOptionsCommon } from "../packages/lightweight-charts/v4.2.0/types"
|
||||
* @import { DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, SingleValueData, ISeriesApi, Time, LineData, LogicalRange, SeriesMarker, CandlestickData, SeriesType, BaselineStyleOptions, SeriesOptionsCommon } from "../packages/lightweight-charts/v4.2.0/types"
|
||||
* @import { DatePath, HeightPath, LastPath } from "./types/paths";
|
||||
* @import { SignalOptions } from "../packages/solid-signals/2024-11-02/types/core/core"
|
||||
* @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "../packages/solid-signals/2024-11-02/types/core/owner"
|
||||
@@ -19,7 +19,22 @@ function initPackages() {
|
||||
createSolidSignal: /** @type {CreateSignal} */ (
|
||||
_signals.createSignal
|
||||
),
|
||||
createEffect: /** @type {CreateEffect} */ (_signals.createEffect),
|
||||
createSolidEffect: /** @type {CreateEffect} */ (
|
||||
_signals.createEffect
|
||||
),
|
||||
createEffect: /** @type {CreateEffect} */ (compute, effect) => {
|
||||
let dispose = /** @type {VoidFunction | null} */ (null);
|
||||
// @ts-ignore
|
||||
_signals.createEffect(compute, (v) => {
|
||||
dispose?.();
|
||||
signals.createRoot((_dispose) => {
|
||||
dispose = _dispose;
|
||||
effect(v);
|
||||
});
|
||||
signals.onCleanup(() => dispose?.());
|
||||
});
|
||||
signals.onCleanup(() => dispose?.());
|
||||
},
|
||||
createMemo: /** @type {CreateMemo} */ (_signals.createMemo),
|
||||
createRoot: /** @type {CreateRoot} */ (_signals.createRoot),
|
||||
getOwner: /** @type {GetOwner} */ (_signals.getOwner),
|
||||
@@ -220,8 +235,15 @@ function initPackages() {
|
||||
* @param {HTMLElement} args.element
|
||||
* @param {Signals} args.signals
|
||||
* @param {Colors} args.colors
|
||||
* @param {DeepPartial<ChartOptions>} [args.options]
|
||||
*/
|
||||
function createChart({ scale, element, signals, colors }) {
|
||||
function createChart({
|
||||
scale,
|
||||
element,
|
||||
signals,
|
||||
colors,
|
||||
options: _options = {},
|
||||
}) {
|
||||
/** @satisfies {DeepPartial<ChartOptions>} */
|
||||
const options = {
|
||||
autoSize: true,
|
||||
@@ -257,6 +279,7 @@ function initPackages() {
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
..._options,
|
||||
};
|
||||
|
||||
/** @type {IChartApi} */
|
||||
@@ -597,7 +620,6 @@ function initPackages() {
|
||||
let packagePromise = null;
|
||||
|
||||
return function () {
|
||||
let p = null;
|
||||
if (!packagePromise) {
|
||||
// @ts-ignore
|
||||
packagePromise = imports[key]();
|
||||
@@ -640,6 +662,20 @@ const utils = {
|
||||
yield() {
|
||||
return this.sleep(0);
|
||||
},
|
||||
array: {
|
||||
/**
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
*/
|
||||
range(start, end) {
|
||||
const range = [];
|
||||
while (start <= end) {
|
||||
range.push(start);
|
||||
start += 1;
|
||||
}
|
||||
return range;
|
||||
},
|
||||
},
|
||||
dom: {
|
||||
/**
|
||||
* @param {string} id
|
||||
@@ -743,13 +779,16 @@ const utils = {
|
||||
/**
|
||||
* @param {Object} arg
|
||||
* @param {string} arg.text
|
||||
* @param {string} arg.title
|
||||
* @param {VoidFunction} arg.onClick
|
||||
*/
|
||||
createButtonElement({ text, onClick }) {
|
||||
createButtonElement({ text, onClick, title }) {
|
||||
const button = window.document.createElement("button");
|
||||
|
||||
button.innerHTML = text;
|
||||
|
||||
button.title = title;
|
||||
|
||||
button.addEventListener("click", onClick);
|
||||
|
||||
return button;
|
||||
@@ -773,6 +812,8 @@ const utils = {
|
||||
}) {
|
||||
const label = window.document.createElement("label");
|
||||
|
||||
inputId = inputId.toLowerCase();
|
||||
|
||||
const input = window.document.createElement("input");
|
||||
input.type = "radio";
|
||||
input.name = inputName;
|
||||
@@ -898,6 +939,130 @@ const utils = {
|
||||
|
||||
return field;
|
||||
},
|
||||
createUlElement() {
|
||||
return window.document.createElement("ul");
|
||||
},
|
||||
createLiElement() {
|
||||
return window.document.createElement("li");
|
||||
},
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {string} args.id
|
||||
* @param {string} args.title
|
||||
* @param {Signal<number | null>} args.signal
|
||||
* @param {number} args.min
|
||||
* @param {number} args.max
|
||||
* @param {number} args.step
|
||||
* @param {{createEffect: typeof CreateEffect}} args.signals
|
||||
*/
|
||||
createInputNumberElement({ id, title, signal, min, max, step, signals }) {
|
||||
const input = window.document.createElement("input");
|
||||
input.id = id;
|
||||
input.title = title;
|
||||
input.type = "number";
|
||||
input.min = String(min);
|
||||
input.max = String(max);
|
||||
input.step = String(step);
|
||||
|
||||
let stateValue = /** @type {string | null} */ (null);
|
||||
|
||||
signals.createEffect(
|
||||
() => {
|
||||
const value = signal();
|
||||
return value ? String(value) : "";
|
||||
},
|
||||
(value) => {
|
||||
if (stateValue !== value) {
|
||||
input.value = value;
|
||||
stateValue = value;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const valueSer = input.value;
|
||||
const value = Number(valueSer);
|
||||
if (value >= min && value <= max) {
|
||||
stateValue = valueSer;
|
||||
signal.set(value);
|
||||
}
|
||||
});
|
||||
|
||||
return input;
|
||||
},
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {string} args.id
|
||||
* @param {string} args.title
|
||||
* @param {Signal<Date | null>} args.signal
|
||||
* @param {{createEffect: typeof CreateEffect}} args.signals
|
||||
*/
|
||||
createInputDate({ id, title, signal, signals }) {
|
||||
const input = window.document.createElement("input");
|
||||
input.id = id;
|
||||
input.title = title;
|
||||
input.type = "date";
|
||||
const min = "2011-01-01";
|
||||
const minDate = new Date(min);
|
||||
const maxDate = new Date();
|
||||
const max = utils.date.toString(maxDate);
|
||||
input.min = min;
|
||||
input.max = max;
|
||||
|
||||
let stateValue = /** @type {string | null} */ (null);
|
||||
|
||||
signals.createEffect(
|
||||
() => {
|
||||
const date = signal();
|
||||
return date ? utils.date.toString(date) : "";
|
||||
},
|
||||
(value) => {
|
||||
if (stateValue !== value) {
|
||||
input.value = value;
|
||||
stateValue = value;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
input.addEventListener("change", () => {
|
||||
const value = input.value;
|
||||
const date = new Date(value);
|
||||
if (date >= minDate && date <= maxDate) {
|
||||
stateValue = value;
|
||||
signal.set(value ? date : null);
|
||||
}
|
||||
});
|
||||
|
||||
return input;
|
||||
},
|
||||
/**
|
||||
* @param {Object} param0
|
||||
* @param {string} param0.title
|
||||
* @param {string} param0.description
|
||||
*/
|
||||
createHeader({ title, description }) {
|
||||
const headerElement = window.document.createElement("header");
|
||||
|
||||
const div = window.document.createElement("div");
|
||||
headerElement.append(div);
|
||||
|
||||
const h1 = window.document.createElement("h1");
|
||||
div.append(h1);
|
||||
|
||||
const titleElement = window.document.createElement("span");
|
||||
titleElement.append(title);
|
||||
h1.append(titleElement);
|
||||
|
||||
const descriptionElement = window.document.createElement("small");
|
||||
descriptionElement.append(description);
|
||||
h1.append(descriptionElement);
|
||||
|
||||
return {
|
||||
headerElement,
|
||||
titleElement,
|
||||
descriptionElement,
|
||||
};
|
||||
},
|
||||
},
|
||||
url: {
|
||||
chartParamsWhitelist: ["from", "to"],
|
||||
@@ -1124,6 +1289,20 @@ const utils = {
|
||||
return Number(v);
|
||||
},
|
||||
},
|
||||
date: {
|
||||
/**
|
||||
* @param {Date} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return utils.date.toString(v);
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return new Date(v);
|
||||
},
|
||||
},
|
||||
},
|
||||
formatters: {
|
||||
dollars: new Intl.NumberFormat("en-US", {
|
||||
@@ -1168,6 +1347,25 @@ const utils = {
|
||||
: // @ts-ignore
|
||||
new Date(time.year, time.month, time.day);
|
||||
},
|
||||
/**
|
||||
* @param {Date} start
|
||||
*/
|
||||
getRangeUpToToday(start) {
|
||||
return this.getRange(start, new Date());
|
||||
},
|
||||
/**
|
||||
* @param {Date} start
|
||||
* @param {Date} end
|
||||
*/
|
||||
getRange(start, end) {
|
||||
const dates = /** @type {Date[]} */ ([]);
|
||||
let currentDate = new Date(start);
|
||||
while (currentDate <= end) {
|
||||
dates.push(new Date(currentDate));
|
||||
currentDate.setUTCDate(currentDate.getUTCDate() + 1);
|
||||
}
|
||||
return dates;
|
||||
},
|
||||
},
|
||||
/**
|
||||
*
|
||||
@@ -1328,14 +1526,14 @@ const elements = {
|
||||
selectedTitle: utils.dom.getElementById("selected-title"),
|
||||
selectedDescription: utils.dom.getElementById("selected-description"),
|
||||
selectors: utils.dom.getElementById("frame-selectors"),
|
||||
chartList: utils.dom.getElementById("chart-list"),
|
||||
chartsChartList: utils.dom.getElementById("charts-chart-list"),
|
||||
legend: utils.dom.getElementById("legend"),
|
||||
style: getComputedStyle(window.document.documentElement),
|
||||
// timeScaleDateButtons: utils.dom.getElementById("timescale-date-buttons"),
|
||||
// timeScaleHeightButtons: utils.dom.getElementById("timescale-height-buttons"),
|
||||
selectedHeader: utils.dom.getElementById("selected-header"),
|
||||
charts: utils.dom.getElementById("charts"),
|
||||
simulation: utils.dom.getElementById("simulation"),
|
||||
livePrice: utils.dom.getElementById("live-price"),
|
||||
moscowTime: utils.dom.getElementById("moscow-time"),
|
||||
};
|
||||
/** @typedef {typeof elements} Elements */
|
||||
|
||||
@@ -1951,6 +2149,21 @@ function createDatasets(signals) {
|
||||
scale,
|
||||
url: baseURL,
|
||||
fetch: _fetch,
|
||||
fetchRange(start, end) {
|
||||
const promises = /** @type {Promise<void>[]} */ ([]);
|
||||
switch (scale) {
|
||||
case "date": {
|
||||
utils.array.range(start, end).forEach((year) => {
|
||||
promises.push(this.fetch(year));
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw "Unsupported";
|
||||
}
|
||||
}
|
||||
return Promise.all(promises);
|
||||
},
|
||||
fetchedJSONs,
|
||||
// drop() {
|
||||
// dispose();
|
||||
@@ -1985,7 +2198,7 @@ function createDatasets(signals) {
|
||||
* @param {DatasetPath<S>} path
|
||||
* @returns {ResourceDataset<S>}
|
||||
*/
|
||||
function getOrImport(scale, path) {
|
||||
function getOrCreate(scale, path) {
|
||||
if (scale === "date") {
|
||||
const found = date.get(/** @type {DatePath} */ (path));
|
||||
if (found) return /** @type {ResourceDataset<S>} */ (found);
|
||||
@@ -2018,7 +2231,7 @@ function createDatasets(signals) {
|
||||
}
|
||||
|
||||
return {
|
||||
getOrImport,
|
||||
getOrCreate,
|
||||
};
|
||||
}
|
||||
/** @typedef {ReturnType<typeof createDatasets>} Datasets */
|
||||
@@ -2088,18 +2301,18 @@ function initWebSockets(signals) {
|
||||
|
||||
/**
|
||||
* @param {(candle: DatasetCandlestickData) => void} callback
|
||||
* @returns
|
||||
* @param {number} interval
|
||||
*/
|
||||
function krakenCandleWebSocketCreator(callback) {
|
||||
const ws = new WebSocket("wss://ws.kraken.com");
|
||||
function krakenCandleWebSocketCreator(callback, interval) {
|
||||
const ws = new WebSocket("wss://ws.kraken.com/v2");
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
event: "subscribe",
|
||||
pair: ["XBT/USD"],
|
||||
subscription: {
|
||||
name: "ohlc",
|
||||
method: "subscribe",
|
||||
params: {
|
||||
channel: "ohlc",
|
||||
symbol: ["BTC/USD"],
|
||||
interval: 1440,
|
||||
},
|
||||
}),
|
||||
@@ -2109,11 +2322,11 @@ function initWebSockets(signals) {
|
||||
ws.addEventListener("message", (message) => {
|
||||
const result = JSON.parse(message.data);
|
||||
|
||||
if (!Array.isArray(result)) return;
|
||||
if (result.channel !== "ohlc") return;
|
||||
|
||||
const [timestamp, _, open, high, low, close, __, volume] = result[1];
|
||||
const { timestamp, open, high, low, close } = result.data.at(-1);
|
||||
|
||||
const date = new Date(Number(timestamp) * 1000);
|
||||
const date = new Date(timestamp);
|
||||
|
||||
const dateStr = utils.date.toString(date);
|
||||
|
||||
@@ -2134,12 +2347,18 @@ function initWebSockets(signals) {
|
||||
return ws;
|
||||
}
|
||||
|
||||
const krakenCandle = createWebsocket(krakenCandleWebSocketCreator);
|
||||
const kraken1dCandle = createWebsocket((callback) =>
|
||||
krakenCandleWebSocketCreator(callback, 1440),
|
||||
);
|
||||
const kraken5mnCandle = createWebsocket((callback) =>
|
||||
krakenCandleWebSocketCreator(callback, 5),
|
||||
);
|
||||
|
||||
krakenCandle.open();
|
||||
kraken1dCandle.open();
|
||||
kraken5mnCandle.open();
|
||||
|
||||
function createDocumentTitleEffect() {
|
||||
signals.createEffect(krakenCandle.latest, (latest) => {
|
||||
signals.createEffect(kraken5mnCandle.latest, (latest) => {
|
||||
if (latest) {
|
||||
const close = latest.close;
|
||||
console.log("close:", close);
|
||||
@@ -2153,7 +2372,8 @@ function initWebSockets(signals) {
|
||||
createDocumentTitleEffect();
|
||||
|
||||
return {
|
||||
krakenCandle,
|
||||
kraken1dCandle,
|
||||
// kraken5mnCandle,
|
||||
};
|
||||
}
|
||||
/** @typedef {ReturnType<typeof initWebSockets>} WebSockets */
|
||||
@@ -2252,6 +2472,8 @@ packages.signals().then((signals) =>
|
||||
);
|
||||
let firstChartOption = true;
|
||||
let firstSimulationOption = true;
|
||||
let firstLivePriceOption = true;
|
||||
let firstMoscowTimeOption = true;
|
||||
|
||||
signals.createEffect(options.selected, (option) => {
|
||||
if (previousElement) {
|
||||
@@ -2262,7 +2484,13 @@ packages.signals().then((signals) =>
|
||||
utils.url.replaceHistory({ pathname: option.id });
|
||||
}
|
||||
|
||||
const hideTop = option.kind === "home" || option.kind === "pdf";
|
||||
const hideTop =
|
||||
option.kind === "home" ||
|
||||
option.kind === "pdf" ||
|
||||
option.kind === "live-price" ||
|
||||
option.kind === "converter" ||
|
||||
option.kind === "moscow-time";
|
||||
|
||||
elements.selectedHeader.hidden = hideTop;
|
||||
|
||||
elements.selectedTitle.innerHTML = option.title;
|
||||
@@ -2277,6 +2505,8 @@ packages.signals().then((signals) =>
|
||||
// break;
|
||||
// }
|
||||
case "chart": {
|
||||
console.log("chart", option);
|
||||
|
||||
element = elements.charts;
|
||||
|
||||
lastChartOption.set(option);
|
||||
@@ -2333,7 +2563,7 @@ packages.signals().then((signals) =>
|
||||
ids,
|
||||
lightweightCharts,
|
||||
options,
|
||||
selected: /** @type {any} */ (lastChartOption),
|
||||
selected: option,
|
||||
signals,
|
||||
utils,
|
||||
webSockets,
|
||||
@@ -2347,7 +2577,77 @@ packages.signals().then((signals) =>
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
case "live-price": {
|
||||
console.log("live-price");
|
||||
|
||||
element = elements.livePrice;
|
||||
|
||||
if (firstLivePriceOption) {
|
||||
const lightweightCharts = packages.lightweightCharts();
|
||||
const script = import("./live-price.js");
|
||||
|
||||
utils.dom.importStyleAndThen("/styles/live-price.css", () =>
|
||||
script.then(({ init }) =>
|
||||
lightweightCharts.then((lightweightCharts) =>
|
||||
signals.runWithOwner(owner, () =>
|
||||
init({
|
||||
colors,
|
||||
consts,
|
||||
dark,
|
||||
datasets,
|
||||
elements,
|
||||
ids,
|
||||
lightweightCharts,
|
||||
options,
|
||||
signals,
|
||||
utils,
|
||||
webSockets,
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
firstLivePriceOption = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case "moscow-time": {
|
||||
console.log("moscow-time");
|
||||
|
||||
element = elements.moscowTime;
|
||||
|
||||
if (firstLivePriceOption) {
|
||||
const lightweightCharts = packages.lightweightCharts();
|
||||
const script = import("./moscow-time.js");
|
||||
|
||||
utils.dom.importStyleAndThen("/styles/moscow-time.css", () =>
|
||||
script.then(({ init }) =>
|
||||
signals.runWithOwner(owner, () =>
|
||||
init({
|
||||
colors,
|
||||
consts,
|
||||
dark,
|
||||
datasets,
|
||||
elements,
|
||||
ids,
|
||||
options,
|
||||
signals,
|
||||
utils,
|
||||
webSockets,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
firstLivePriceOption = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case "converter":
|
||||
case "home":
|
||||
case "pdf":
|
||||
case "url": {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @import {Options} from './options';
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {Consts} args.consts
|
||||
* @param {Signals} args.signals
|
||||
* @param {Utilities} args.utils
|
||||
* @param {Options} args.options
|
||||
* @param {Datasets} args.datasets
|
||||
* @param {WebSockets} args.webSockets
|
||||
* @param {Elements} args.elements
|
||||
* @param {Ids} args.ids
|
||||
* @param {Accessor<boolean>} args.dark
|
||||
*/
|
||||
export function init({
|
||||
colors,
|
||||
consts,
|
||||
dark,
|
||||
datasets,
|
||||
elements,
|
||||
ids,
|
||||
options,
|
||||
signals,
|
||||
utils,
|
||||
webSockets,
|
||||
}) {
|
||||
const moscowTimeElement = elements.moscowTime;
|
||||
|
||||
const satsPerDollar = signals.createMemo(
|
||||
() =>
|
||||
100_000_000 /
|
||||
// webSockets.kraken5mnCandle.latest()?.close ||
|
||||
(webSockets.kraken1dCandle.latest()?.close || 0),
|
||||
);
|
||||
|
||||
const p = window.document.createElement("h1");
|
||||
moscowTimeElement.append(p);
|
||||
|
||||
signals.createEffect(satsPerDollar, (satsPerDollar) => {
|
||||
p.innerHTML = utils.formatters.dollars.format(satsPerDollar);
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
* @param {Colors} args.colors
|
||||
* @param {Consts} args.consts
|
||||
* @param {LightweightCharts} args.lightweightCharts
|
||||
* @param {Accessor<ChartOption>} args.selected
|
||||
* @param {SimulationOption} args.selected
|
||||
* @param {Signals} args.signals
|
||||
* @param {Utilities} args.utils
|
||||
* @param {Options} args.options
|
||||
@@ -38,22 +38,27 @@ export function init({
|
||||
const resultsElement = window.document.createElement("div");
|
||||
simulationElement.append(resultsElement);
|
||||
|
||||
const getDefaultIntervalStart = () => new Date("2021-04-15");
|
||||
const getDefaultIntervalEnd = () => new Date();
|
||||
|
||||
const storagePrefix = "save-in-bitcoin";
|
||||
const settings = {
|
||||
initial: signals.createSignal(/** @type {number | null} */ (1000), {
|
||||
save: {
|
||||
...utils.serde.number,
|
||||
id: `${storagePrefix}-initial-amount`,
|
||||
param: "initial-amount",
|
||||
},
|
||||
}),
|
||||
later: signals.createSignal(/** @type {number | null} */ (0), {
|
||||
save: {
|
||||
...utils.serde.number,
|
||||
id: `${storagePrefix}-later-amount`,
|
||||
param: "later-amount",
|
||||
},
|
||||
}),
|
||||
initial: {
|
||||
firstDay: signals.createSignal(/** @type {number | null} */ (1000), {
|
||||
save: {
|
||||
...utils.serde.number,
|
||||
id: `${storagePrefix}-initial-amount`,
|
||||
param: "initial-amount",
|
||||
},
|
||||
}),
|
||||
overTime: signals.createSignal(/** @type {number | null} */ (0), {
|
||||
save: {
|
||||
...utils.serde.number,
|
||||
id: `${storagePrefix}-later-amount`,
|
||||
param: "later-amount",
|
||||
},
|
||||
}),
|
||||
},
|
||||
recurrent: {
|
||||
amount: signals.createSignal(/** @type {number | null} */ (100), {
|
||||
save: {
|
||||
@@ -63,8 +68,45 @@ export function init({
|
||||
},
|
||||
}),
|
||||
},
|
||||
interval: {
|
||||
start: signals.createSignal(
|
||||
/** @type {Date | null} */ (getDefaultIntervalStart()),
|
||||
{
|
||||
save: {
|
||||
...utils.serde.date,
|
||||
id: `${storagePrefix}-interval-start`,
|
||||
param: "interval-start",
|
||||
},
|
||||
},
|
||||
),
|
||||
end: signals.createSignal(
|
||||
/** @type {Date | null} */ (getDefaultIntervalEnd()),
|
||||
{
|
||||
save: {
|
||||
...utils.serde.date,
|
||||
id: `${storagePrefix}-interval-end`,
|
||||
param: "interval-end",
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
fees: {
|
||||
percentage: signals.createSignal(/** @type {number | null} */ (0.25), {
|
||||
save: {
|
||||
...utils.serde.number,
|
||||
id: `${storagePrefix}-percentage`,
|
||||
param: "percentage",
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const { headerElement } = utils.dom.createHeader({
|
||||
title: selected.title,
|
||||
description: selected.serializedPath,
|
||||
});
|
||||
parametersElement.append(headerElement);
|
||||
|
||||
const initialGroup = createParameterGroup({
|
||||
title: "Initial",
|
||||
description:
|
||||
@@ -78,7 +120,7 @@ export function init({
|
||||
input: createInputDollar({
|
||||
id: "simulation-dollars-initial",
|
||||
title: "Initial amount of dollars converted",
|
||||
signal: settings.initial,
|
||||
signal: settings.initial.firstDay,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -89,12 +131,17 @@ export function init({
|
||||
input: createInputDollar({
|
||||
id: "simulation-dollars-later",
|
||||
title: "Dollars to spread over time",
|
||||
signal: settings.later,
|
||||
signal: settings.initial.overTime,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
parametersElement.append(createHrElement());
|
||||
const topUpGroup = createParameterGroup({
|
||||
title: "Top Up",
|
||||
description:
|
||||
"The topUp amount of dollars you're willing to eventually save in Bitcoin.",
|
||||
});
|
||||
parametersElement.append(topUpGroup);
|
||||
|
||||
const recurrentGroup = createParameterGroup({
|
||||
title: "Recurrent",
|
||||
@@ -105,7 +152,7 @@ export function init({
|
||||
|
||||
recurrentGroup.append(
|
||||
createInputField({
|
||||
name: "Amount",
|
||||
name: "Maximum Amount",
|
||||
input: createInputDollar({
|
||||
id: "simulation-dollars-recurrent",
|
||||
title: "Recurrent dollar amount",
|
||||
@@ -114,34 +161,92 @@ export function init({
|
||||
}),
|
||||
);
|
||||
|
||||
const frequencyUL = appendUl({ parent: recurrentGroup });
|
||||
|
||||
[{ name: "Daily" }, { name: "Weekly" }, { name: "Monthly" }].forEach(
|
||||
({ name }) => {
|
||||
const li = appendLi({ name, parent: frequencyUL });
|
||||
},
|
||||
);
|
||||
|
||||
const frequencyChoiceUL = appendUl({ parent: recurrentGroup });
|
||||
const frequencyUL = utils.dom.createUlElement();
|
||||
recurrentGroup.append(frequencyUL);
|
||||
|
||||
[
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
].forEach((name) => {
|
||||
const li = appendLi({ name, parent: frequencyChoiceUL });
|
||||
{ name: "Daily" },
|
||||
{
|
||||
name: "Weekly",
|
||||
sub: [
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Monthly",
|
||||
sub: [
|
||||
"The 1st",
|
||||
"The 2nd",
|
||||
"The 3rd",
|
||||
"The 4th",
|
||||
"The 5th",
|
||||
"The 6th",
|
||||
"The 7th",
|
||||
"The 8th",
|
||||
"The 9th",
|
||||
"The 10th",
|
||||
"The 11th",
|
||||
"The 12th",
|
||||
"The 13th",
|
||||
"The 14th",
|
||||
"The 15th",
|
||||
"The 16th",
|
||||
"The 17th",
|
||||
"The 18th",
|
||||
"The 19th",
|
||||
"The 20th",
|
||||
"The 21st",
|
||||
"The 22nd",
|
||||
"The 23rd",
|
||||
"The 24th",
|
||||
"The 25th",
|
||||
"The 26th",
|
||||
"The 27th",
|
||||
"The 28th",
|
||||
],
|
||||
},
|
||||
].forEach(({ name, sub }, index) => {
|
||||
const li = utils.dom.createLiElement();
|
||||
const { label, input } = utils.dom.createLabeledInput({
|
||||
inputId: `frequency-${name}`,
|
||||
inputName: "frequency",
|
||||
inputValue: name.toLowerCase(),
|
||||
labelTitle: name,
|
||||
inputChecked: !index,
|
||||
onClick: () => {},
|
||||
});
|
||||
label.append(name);
|
||||
li.append(label);
|
||||
if (sub) {
|
||||
const parentName = name;
|
||||
const ul = utils.dom.createUlElement();
|
||||
li.append(ul);
|
||||
sub.forEach((name) => {
|
||||
const li = utils.dom.createLiElement();
|
||||
const { label, input } = utils.dom.createLabeledInput({
|
||||
inputId: `frequency-${parentName}-${name}`,
|
||||
inputName: `frequency-${parentName}`,
|
||||
inputValue: name.toLowerCase(),
|
||||
labelTitle: name,
|
||||
inputChecked: !index,
|
||||
onClick: () => {},
|
||||
});
|
||||
label.append(name);
|
||||
li.append(label);
|
||||
ul.append(li);
|
||||
});
|
||||
}
|
||||
frequencyUL.append(li);
|
||||
});
|
||||
|
||||
parametersElement.append(createHrElement());
|
||||
|
||||
const today = signals.createSignal(utils.date.todayUTC());
|
||||
setInterval(() => {
|
||||
today.set(utils.date.todayUTC());
|
||||
}, consts.FIVE_SECONDS_IN_MS);
|
||||
const frequencyChoiceUL = utils.dom.createUlElement();
|
||||
recurrentGroup.append(frequencyChoiceUL);
|
||||
|
||||
const intervalGroup = createParameterGroup({
|
||||
title: "Interval",
|
||||
@@ -149,42 +254,22 @@ export function init({
|
||||
});
|
||||
parametersElement.append(intervalGroup);
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {HTMLElement} args.parent
|
||||
*/
|
||||
function appendDiv({ parent }) {
|
||||
const div = window.document.createElement("div");
|
||||
parent.append(div);
|
||||
return div;
|
||||
}
|
||||
console.log("weofpwklfpwkofwepokf");
|
||||
|
||||
function createInputDateField() {
|
||||
const div = appendDiv({ parent: intervalGroup });
|
||||
|
||||
appendInputDate({
|
||||
id: "",
|
||||
title: "",
|
||||
value: "2021-04-15",
|
||||
parent: div,
|
||||
signals,
|
||||
today,
|
||||
});
|
||||
|
||||
appendButton({
|
||||
onClick: () => {},
|
||||
text: "Reset",
|
||||
title: "",
|
||||
parent: div,
|
||||
});
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
createInputDateField();
|
||||
createInputDateField();
|
||||
|
||||
parametersElement.append(createHrElement());
|
||||
createInputDateField({
|
||||
signal: settings.interval.start,
|
||||
getDefault: getDefaultIntervalStart,
|
||||
parent: intervalGroup,
|
||||
signals,
|
||||
utils,
|
||||
});
|
||||
createInputDateField({
|
||||
signal: settings.interval.end,
|
||||
getDefault: getDefaultIntervalEnd,
|
||||
parent: intervalGroup,
|
||||
signals,
|
||||
utils,
|
||||
});
|
||||
|
||||
const feesGroup = createParameterGroup({
|
||||
title: "Fees",
|
||||
@@ -193,39 +278,275 @@ export function init({
|
||||
});
|
||||
parametersElement.append(feesGroup);
|
||||
|
||||
createInputNumber({
|
||||
const input = utils.dom.createInputNumberElement({
|
||||
id: "",
|
||||
title: "",
|
||||
value: 0.25,
|
||||
parent: feesGroup,
|
||||
signal: settings.fees.percentage,
|
||||
min: 0,
|
||||
max: 10,
|
||||
max: 50,
|
||||
step: 0.01,
|
||||
signals,
|
||||
});
|
||||
feesGroup.append(input);
|
||||
|
||||
// parametersElement.append(utils.dom.createHrElement());
|
||||
|
||||
// const strategyGroup = createParameterGroup({
|
||||
// title: "Strategy",
|
||||
// description: "The strategy used to convert your fiat into Bitcoin",
|
||||
// });
|
||||
// parametersElement.append(strategyGroup);
|
||||
|
||||
// const ulStrategies = utils.dom.createUlElement();
|
||||
// strategyGroup.append(ulStrategies);
|
||||
|
||||
// ["All in", "Weighted Local", "Weighted Cycle"].forEach((strategy) => {
|
||||
// const li = utils.dom.createLiElement();
|
||||
// li.append(strategy);
|
||||
// ulStrategies.append(li);
|
||||
// });
|
||||
|
||||
const parent = window.document.createElement("div");
|
||||
parent.classList.add("chart-list");
|
||||
resultsElement.append(parent);
|
||||
|
||||
signals.createEffect(settings.interval.start, (start) => {
|
||||
console.log("start", start);
|
||||
});
|
||||
|
||||
parametersElement.append(createHrElement());
|
||||
|
||||
const strategyGroup = createParameterGroup({
|
||||
title: "Strategy",
|
||||
description: "The strategy used to convert your fiat into Bitcoin",
|
||||
signals.createEffect(settings.interval.end, (end) => {
|
||||
console.log("end", end);
|
||||
});
|
||||
parametersElement.append(strategyGroup);
|
||||
|
||||
const ulStrategies = appendUl({ parent: strategyGroup });
|
||||
const owner = signals.getOwner();
|
||||
|
||||
["All in", "Weighted Local", "Weighted Cycle"].forEach((strategy) => {
|
||||
appendLi({
|
||||
name: strategy,
|
||||
parent: ulStrategies,
|
||||
const closes = datasets.getOrCreate("date", "date-to-close");
|
||||
closes.fetchRange(2009, new Date().getUTCFullYear()).then(() => {
|
||||
signals.runWithOwner(owner, () => {
|
||||
signals.createEffect(
|
||||
() => ({
|
||||
initialAmount: settings.initial.firstDay() || 0,
|
||||
recurrentAmount: settings.recurrent.amount() || 0,
|
||||
dollarsLeft: settings.initial.overTime() || 0,
|
||||
start: settings.interval.start(),
|
||||
end: settings.interval.end(),
|
||||
fees: settings.fees.percentage(),
|
||||
}),
|
||||
// ({ initialAmount, recurrentAmount, dollarsLeft, start, end }) => {
|
||||
// console.log({
|
||||
// start,
|
||||
// end,
|
||||
// });
|
||||
// },
|
||||
({ initialAmount, recurrentAmount, dollarsLeft, start, end, fees }) => {
|
||||
console.log({ start, end });
|
||||
parent.innerHTML = "";
|
||||
|
||||
if (!start || !end || start > end) return;
|
||||
|
||||
const range = utils.date.getRange(start, end);
|
||||
|
||||
let investedAmount = 0;
|
||||
|
||||
/** @type {LineData<Time>[]} */
|
||||
const investedData = [];
|
||||
/** @type {LineData<Time>[]} */
|
||||
const returnData = [];
|
||||
/** @type {LineData<Time>[]} */
|
||||
const bitcoinData = [];
|
||||
/** @type {LineData<Time>[]} */
|
||||
const resultData = [];
|
||||
/** @type {LineData<Time>[]} */
|
||||
const dollarsData = [];
|
||||
/** @type {LineData<Time>[]} */
|
||||
const totalData = [];
|
||||
/** @type {LineData<Time>[]} */
|
||||
const investmentData = [];
|
||||
/** @type {LineData<Time>[]} */
|
||||
const bitcoinAddedData = [];
|
||||
|
||||
let bitcoin = 0;
|
||||
|
||||
let feesPaid = 0;
|
||||
|
||||
range.forEach((date, index) => {
|
||||
const year = date.getUTCFullYear();
|
||||
const time = utils.date.toString(date);
|
||||
|
||||
const close = closes.fetchedJSONs
|
||||
.at(utils.chunkIdToIndex("date", year))
|
||||
?.json()?.dataset.map[utils.date.toString(date)];
|
||||
|
||||
if (!close) return;
|
||||
|
||||
let investmentPreFees =
|
||||
(!index ? initialAmount : 0) + recurrentAmount;
|
||||
|
||||
if (dollarsLeft > 0) {
|
||||
if (dollarsLeft >= recurrentAmount) {
|
||||
investmentPreFees += recurrentAmount;
|
||||
dollarsLeft -= recurrentAmount;
|
||||
} else {
|
||||
investmentPreFees += dollarsLeft;
|
||||
dollarsLeft = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let investment = investmentPreFees * (1 - (fees || 0) / 100);
|
||||
feesPaid += investmentPreFees - investment;
|
||||
|
||||
const bitcoinAdded = investment / close;
|
||||
bitcoin += bitcoinAdded;
|
||||
|
||||
investedAmount += investment;
|
||||
|
||||
const _return = close * bitcoin;
|
||||
|
||||
bitcoinData.push({
|
||||
time,
|
||||
value: bitcoin,
|
||||
});
|
||||
|
||||
investedData.push({
|
||||
time,
|
||||
value: investedAmount,
|
||||
});
|
||||
|
||||
returnData.push({
|
||||
time,
|
||||
value: _return,
|
||||
});
|
||||
|
||||
resultData.push({
|
||||
time,
|
||||
value: (_return / investedAmount - 1) * 100,
|
||||
});
|
||||
|
||||
dollarsData.push({
|
||||
time,
|
||||
value: dollarsLeft,
|
||||
});
|
||||
|
||||
totalData.push({
|
||||
time,
|
||||
value: dollarsLeft + _return,
|
||||
});
|
||||
|
||||
investmentData.push({
|
||||
time,
|
||||
value: investment,
|
||||
});
|
||||
|
||||
bitcoinAddedData.push({
|
||||
time,
|
||||
value: bitcoinAdded,
|
||||
});
|
||||
});
|
||||
|
||||
(() => {
|
||||
const chartWrapper = window.document.createElement("div");
|
||||
chartWrapper.classList.add("chart-wrapper");
|
||||
parent.append(chartWrapper);
|
||||
|
||||
const chartDiv = window.document.createElement("div");
|
||||
chartDiv.classList.add("chart-div");
|
||||
chartWrapper.append(chartDiv);
|
||||
|
||||
const chart = lightweightCharts.createChart({
|
||||
scale: "date",
|
||||
element: chartDiv,
|
||||
signals,
|
||||
colors,
|
||||
options: {
|
||||
handleScale: false,
|
||||
handleScroll: false,
|
||||
},
|
||||
});
|
||||
|
||||
const line = chart.addLineSeries();
|
||||
|
||||
line.setData(investedData);
|
||||
|
||||
const line2 = chart.addLineSeries();
|
||||
|
||||
line2.setData(returnData);
|
||||
|
||||
const line3 = chart.addLineSeries();
|
||||
|
||||
line3.setData(dollarsData);
|
||||
|
||||
const line4 = chart.addLineSeries();
|
||||
|
||||
line4.setData(totalData);
|
||||
|
||||
const line5 = chart.addLineSeries();
|
||||
|
||||
line5.setData(investmentData);
|
||||
|
||||
chart.timeScale().fitContent();
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const chartWrapper = window.document.createElement("div");
|
||||
chartWrapper.classList.add("chart-wrapper");
|
||||
parent.append(chartWrapper);
|
||||
|
||||
const chartDiv = window.document.createElement("div");
|
||||
chartDiv.classList.add("chart-div");
|
||||
chartWrapper.append(chartDiv);
|
||||
|
||||
const chart = lightweightCharts.createChart({
|
||||
scale: "date",
|
||||
element: chartDiv,
|
||||
signals,
|
||||
colors,
|
||||
options: {
|
||||
handleScale: false,
|
||||
handleScroll: false,
|
||||
},
|
||||
});
|
||||
|
||||
const line = chart.addLineSeries();
|
||||
|
||||
line.setData(bitcoinData);
|
||||
|
||||
const line2 = chart.addLineSeries();
|
||||
|
||||
line2.setData(bitcoinAddedData);
|
||||
|
||||
chart.timeScale().fitContent();
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const chartWrapper = window.document.createElement("div");
|
||||
chartWrapper.classList.add("chart-wrapper");
|
||||
parent.append(chartWrapper);
|
||||
|
||||
const chartDiv = window.document.createElement("div");
|
||||
chartDiv.classList.add("chart-div");
|
||||
chartWrapper.append(chartDiv);
|
||||
|
||||
const chart = lightweightCharts.createChart({
|
||||
scale: "date",
|
||||
element: chartDiv,
|
||||
signals,
|
||||
colors,
|
||||
options: {
|
||||
handleScale: false,
|
||||
handleScroll: false,
|
||||
},
|
||||
});
|
||||
|
||||
const line = chart.addLineSeries();
|
||||
|
||||
line.setData(resultData);
|
||||
|
||||
chart.timeScale().fitContent();
|
||||
})();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
// On the side
|
||||
// Value in Bitcoin
|
||||
// Value in Dollars + total converted
|
||||
//
|
||||
// Value min estimated value in 4 years
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,31 +590,6 @@ function createParameterGroup({ title, description }) {
|
||||
return div;
|
||||
}
|
||||
|
||||
function createHrElement() {
|
||||
return window.document.createElement("hr");
|
||||
}
|
||||
|
||||
/**
|
||||
*@param {Object} args
|
||||
*@param {string} args.id
|
||||
*@param {string} args.title
|
||||
*@param {number} args.value
|
||||
*@param {HTMLElement} args.parent
|
||||
*@param {number} args.min
|
||||
*@param {number} args.max
|
||||
*/
|
||||
function createInputNumber({ id, title, value, parent, min, max }) {
|
||||
const input = window.document.createElement("input");
|
||||
input.id = id;
|
||||
input.title = title;
|
||||
input.type = "number";
|
||||
input.value = String(value);
|
||||
input.min = String(min);
|
||||
input.max = String(max);
|
||||
parent.append(input);
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {string} args.id
|
||||
@@ -320,90 +616,36 @@ function createInputDollar({ id, title, signal }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {string} args.id
|
||||
* @param {string} args.title
|
||||
* @param {Signal<number | null>} args.signal
|
||||
*
|
||||
* @param {Object} arg
|
||||
* @param {Signal<Date | null>} arg.signal
|
||||
* @param {() => Date | null} arg.getDefault
|
||||
* @param {HTMLElement} arg.parent
|
||||
* @param {Utilities} arg.utils
|
||||
* @param {Signals} arg.signals
|
||||
*/
|
||||
function createInputRangePercentage({ id, title, signal }) {
|
||||
const input = window.document.createElement("input");
|
||||
input.id = id;
|
||||
input.title = title;
|
||||
input.type = "range";
|
||||
input.min = "0";
|
||||
input.max = "100";
|
||||
function createInputDateField({ signal, getDefault, parent, signals, utils }) {
|
||||
const div = window.document.createElement("div");
|
||||
parent.append(div);
|
||||
|
||||
const value = signal();
|
||||
input.value = value !== null ? String(value) : "";
|
||||
div.append(
|
||||
utils.dom.createInputDate({
|
||||
id: "",
|
||||
title: "",
|
||||
signal,
|
||||
signals,
|
||||
}),
|
||||
);
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const value = input.value;
|
||||
signal.set(value ? Number(value) : null);
|
||||
const button = utils.dom.createButtonElement({
|
||||
onClick: () => {
|
||||
signal.set(getDefault());
|
||||
},
|
||||
text: "Reset",
|
||||
title: "Reset field",
|
||||
});
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {HTMLElement} args.parent
|
||||
*/
|
||||
function appendUl({ parent }) {
|
||||
const ul = window.document.createElement("ul");
|
||||
parent.append(ul);
|
||||
return ul;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {string} args.name
|
||||
* @param {HTMLUListElement} args.parent
|
||||
*/
|
||||
function appendLi({ name, parent }) {
|
||||
const li = window.document.createElement("li");
|
||||
li.innerHTML = name;
|
||||
parent.append(li);
|
||||
return li;
|
||||
}
|
||||
|
||||
/**
|
||||
*@param {Object} args
|
||||
*@param {string} args.id
|
||||
*@param {string} args.title
|
||||
*@param {string} args.value
|
||||
*@param {HTMLElement} args.parent
|
||||
*@param {Accessor<Date>} args.today
|
||||
*@param {Signals} args.signals
|
||||
*/
|
||||
function appendInputDate({ id, title, value, parent, today, signals }) {
|
||||
const input = window.document.createElement("input");
|
||||
input.id = id;
|
||||
input.title = title;
|
||||
input.type = "date";
|
||||
input.value = value;
|
||||
|
||||
input.min = "2011-01-01";
|
||||
|
||||
signals.createEffect(today, (today) => {
|
||||
input.max = today.toJSON().split("T")[0];
|
||||
});
|
||||
|
||||
parent.append(input);
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
*@param {Object} args
|
||||
*@param {string} args.title
|
||||
*@param {string} args.text
|
||||
*@param {HTMLElement} args.parent
|
||||
*@param {VoidFunction} args.onClick
|
||||
*/
|
||||
function appendButton({ title, text, onClick, parent }) {
|
||||
const button = window.document.createElement("button");
|
||||
button.title = title;
|
||||
button.innerHTML = text;
|
||||
button.addEventListener("click", onClick);
|
||||
parent.append(button);
|
||||
return button;
|
||||
div.append(button);
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,6 @@ type Unit =
|
||||
| "Weight";
|
||||
|
||||
interface PartialOption {
|
||||
// icon: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -111,6 +110,18 @@ interface PartialHomeOption extends PartialOption {
|
||||
name: "Home";
|
||||
}
|
||||
|
||||
interface PartialLivePriceOption extends PartialOption {
|
||||
kind: "live-price";
|
||||
}
|
||||
|
||||
interface PartialMoscowTimeOption extends PartialOption {
|
||||
kind: "moscow-time";
|
||||
}
|
||||
|
||||
interface PartialConverterOption extends PartialOption {
|
||||
kind: "converter";
|
||||
}
|
||||
|
||||
interface PartialChartOption extends PartialOption {
|
||||
scale: TimeScale;
|
||||
title: string;
|
||||
@@ -147,6 +158,9 @@ interface PartialOptionsGroup {
|
||||
|
||||
type AnyPartialOption =
|
||||
| PartialHomeOption
|
||||
| PartialLivePriceOption
|
||||
| PartialMoscowTimeOption
|
||||
| PartialConverterOption
|
||||
| PartialChartOption
|
||||
| PartialSimulationOption
|
||||
| PartialPdfOption
|
||||
@@ -168,6 +182,9 @@ type OptionPath = {
|
||||
|
||||
type HomeOption = PartialHomeOption & ProcessedOptionAddons;
|
||||
type SimulationOption = PartialSimulationOption & ProcessedOptionAddons;
|
||||
type LivePriceOption = PartialLivePriceOption & ProcessedOptionAddons;
|
||||
type MoscowTimeOption = PartialMoscowTimeOption & ProcessedOptionAddons;
|
||||
type ConverterOption = PartialConverterOption & ProcessedOptionAddons;
|
||||
|
||||
interface PdfOption extends PartialPdfOption, ProcessedOptionAddons {
|
||||
kind: "pdf";
|
||||
@@ -183,6 +200,9 @@ interface ChartOption extends PartialChartOption, ProcessedOptionAddons {
|
||||
|
||||
type Option =
|
||||
| HomeOption
|
||||
| LivePriceOption
|
||||
| MoscowTimeOption
|
||||
| ConverterOption
|
||||
| PdfOption
|
||||
| UrlOption
|
||||
| ChartOption
|
||||
@@ -208,7 +228,8 @@ interface ResourceDataset<
|
||||
> {
|
||||
scale: Scale;
|
||||
url: string;
|
||||
fetch: (id: number) => void;
|
||||
fetch: (id: number) => Promise<void>;
|
||||
fetchRange: (start: number, end: number) => Promise<void[]>;
|
||||
fetchedJSONs: FetchedResult<Scale, Type>[];
|
||||
// drop: VoidFunction;
|
||||
}
|
||||
|
||||
@@ -57,75 +57,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
> #chart-list {
|
||||
margin-top: 1rem;
|
||||
position: relative;
|
||||
.chart-list {
|
||||
margin-left: var(--negative-main-padding);
|
||||
margin-right: calc(var(--negative-main-padding) - 0.5rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
z-index: 20;
|
||||
|
||||
> .chart-wrapper {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
min-height: 0px;
|
||||
width: 100%;
|
||||
cursor: crosshair;
|
||||
|
||||
&:has(+ .chart-wrapper:not([hidden])) {
|
||||
height: calc(100% - 62px);
|
||||
}
|
||||
|
||||
> fieldset {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: var(--main-padding);
|
||||
padding-right: var(--main-padding);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
gap: 0.5rem;
|
||||
color: var(--off-color);
|
||||
|
||||
> div.field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
gap: 1rem;
|
||||
|
||||
> legend,
|
||||
> div {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
> hr {
|
||||
min-width: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
padding: 0.5rem;
|
||||
margin: -0.5rem;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .chart-div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#live-price {
|
||||
> h1 {
|
||||
font-size: 2.5rem;
|
||||
line-height: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,17 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
> div:first-child {
|
||||
max-width: 20rem;
|
||||
border-right: 1px;x
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
/* min-height: 0; */
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 32rem;
|
||||
gap: 1rem;
|
||||
gap: 3rem;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
@@ -26,4 +29,29 @@
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-list {
|
||||
max-height: 500px;
|
||||
/* margin-left: var(--negative-main-padding);*/
|
||||
margin-right: calc(var(--negative-main-padding) - 0.5rem);
|
||||
}
|
||||
|
||||
li {
|
||||
label:has(input:not(:checked)) + ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0.75rem;
|
||||
margin-left:0.25rem;
|
||||
border-left: 1px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
|
||||
li {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||