general: snapshot

This commit is contained in:
k
2024-06-25 14:46:23 +02:00
parent 7604787fbb
commit 20a51f980b
27 changed files with 342 additions and 225 deletions
+17 -10
View File
@@ -2,23 +2,26 @@
## v. 0.1.2 | WIP
### Parser
-
![Image of the Satonomics Web App version 0.1.2](./assets/v0.1.2.jpg)
### App
- Performance
- Improved app's reactivity
- Added some chunk splitting for a faster initial load
- Global improvements that increased the Lighthouse's performance score from the low 30s to the high 70s
- Chart
- Fix legend hovering on mobile not resetting on touch end
- Fixed legend hovering on mobile not resetting on touch end
- Updated legend padding so that the scrollbar, if visible, is less in the way
- Added yearly time scale setters (from year 2009 to today)
- Misc
- Support mini window size, could be useful for embedded views
### Server
-
- Hopefully made scrollbars a little more subtle on WIndows and Linux, can't test
## v. 0.1.1 | 849240 - 2024/06/24
![Image of the Satonomics Web App version 0.1.1](./assets/v0.1.1.jpg)
### Parser
- Fixed overflow in `Price` struct which caused many Realized Caps and Realized Prices to have completely bogus data
@@ -26,7 +29,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
@@ -53,7 +56,7 @@
- Strip
- Temporarily removed the Home button on the strip bar on desktop as there is no landing page yet
- Settings
- Add version
- Added version
- PWA
- Fixed background update
- Changed update check frequency to 1 minute (~1kb to fetch every minute which is very reasonable)
@@ -64,3 +67,7 @@
### Price
- Deleted old price datasets and their backups
## v. 0.1.0 | 848642 - 2024/06/19
![Image of the Satonomics Web App version 0.1.0](./assets/v0.1.0.jpg)
+2
View File
@@ -1,5 +1,7 @@
# SATONOMICS
![Image of the Satonomics Web App](./assets/latest.jpg)
## Description
TLDR: Free, open source, verifiable and self-hostable Bitcoin on-chain data generator and visualizer
+9
View File
@@ -4,6 +4,7 @@ const texts = [
"satonomics",
"satonomics",
"satonomics",
"satonomics",
"stay humble, stack sats",
"21 million",
@@ -29,6 +30,14 @@ const texts = [
"be your own bank",
"resistance money",
"foss",
"permissionless",
"great reset",
"orange pill",
"borderless",
"anonymous",
"nyknyc",
"low time preference",
"absolute scarcity",
];
export const LOCAL_STORAGE_MARQUEE_KEY = "bg-marquee";
@@ -1,9 +1,7 @@
import { 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,6 +12,10 @@ export function Actions({
qrcode: RWS<string>;
fullscreen?: RWS<boolean>;
}) {
const ButtonShare = lazy(() =>
import("./buttonShare").then((d) => ({ default: d.ButtonShare })),
);
return (
<div class="flex space-x-1">
<Show when={fullscreen}>
@@ -37,18 +39,8 @@ export function Actions({
)}
</Show>
<Button
title="Share"
icon={() => IconTablerShare}
onClick={() => {
qrcode.set(() =>
generate(document.location.href).toDataURL({
on: [0xff, 0xff, 0xff, 0xff],
off: [0x00, 0x00, 0x00, 0x00],
}),
);
}}
/>
<ButtonShare qrcode={qrcode} />
<Button
title="Favorite"
colors={() =>
@@ -66,38 +58,3 @@ export function Actions({
</div>
);
}
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,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,20 @@
import { generate } from "lean-qr";
import { Button } from "./button";
export function ButtonShare({ qrcode }: { qrcode: RWS<string> }) {
return (
<Button
title="Share"
icon={() => IconTablerShare}
onClick={() => {
qrcode.set(() =>
generate(document.location.href).toDataURL({
on: [0xff, 0xff, 0xff, 0xff],
off: [0x00, 0x00, 0x00, 0x00],
}),
);
}}
/>
);
}
@@ -12,7 +12,7 @@ export function Legend({
let toggle = false;
return (
<div class="flex flex-1 items-center gap-1 overflow-y-auto">
<div class="-my-1.5 -ml-1.5 flex flex-1 items-center gap-1 overflow-y-auto p-1.5">
<For each={legendList()}>
{(legend) => {
const initialColors = {} as any;
@@ -5,29 +5,43 @@ import { ONE_DAY_IN_MS } from "/src/scripts/utils/time";
import { Box } from "../../box";
export function TimeScale() {
const today = new Date();
return (
<Box dark padded overflowY classes="short:hidden">
<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({})}>All Time</Button>
<Button onClick={() => setTimeScale({ days: 7 })}>1 Week</Button>
<Button onClick={() => setTimeScale({ days: 30 })}>1 Month</Button>
<Button onClick={() => setTimeScale({ days: 30 * 6 })}>6 Months</Button>
<Button
onClick={() =>
setTimeScale(
Math.ceil(
(new Date().valueOf() -
new Date(`${new Date().getUTCFullYear()}-01-01`).valueOf()) /
setTimeScale({
days: Math.ceil(
(today.valueOf() -
new Date(`${today.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>
<Button onClick={() => setTimeScale({ days: 365 })}>1 Year</Button>
<Button onClick={() => setTimeScale({ days: 2 * 365 })}>2 Years</Button>
<Button onClick={() => setTimeScale({ days: 4 * 365 })}>4 Years</Button>
<Button onClick={() => setTimeScale({ 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 onClick={() => setTimeScale({ year })}>{year}</Button>
)}
</For>
</Box>
);
}
@@ -43,25 +57,25 @@ function Button(props: ParentProps & { onClick: VoidFunction }) {
);
}
function setTimeScale(days?: number) {
const to = new Date();
function setTimeScale({ days, year }: { days?: number; year?: number }) {
let from = new Date();
let to = new Date();
if (days) {
const from = new Date();
if (year) {
from = new Date(`${year}-01-01`);
to = new Date(`${year}-12-31`);
} else if (days) {
from.setDate(from.getUTCDate() - days);
chartState.chart?.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,
});
from = new Date(GENESIS_DAY);
}
setRange({
from: (from.getTime() / 1000) as Time,
to: (to.getTime() / 1000) as Time,
});
}
function setRange(range: TimeRange) {
chartState.chart?.timeScale().setVisibleRange(range);
}
@@ -27,6 +27,10 @@ export function ChartFrame({
}) {
const legend = createRWS<PresetLegend>([]);
const Chart = lazy(() =>
import("./components/chart").then((d) => ({ default: d.Chart })),
);
return (
<div
class={classPropToString([
@@ -35,7 +39,7 @@ export function ChartFrame({
"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 classes="short:hidden">
@@ -56,7 +60,6 @@ export function ChartFrame({
<Chart
activeResources={activeResources}
datasets={datasets}
// fetchedDatasets={fetchedDatasets}
legendSetter={legend.set}
presets={presets}
/>
+3 -1
View File
@@ -12,7 +12,9 @@ export function FavoritesFrame({
return (
<div
class="flex-1 overflow-y-auto"
hidden={selectedFrame() !== "Favorites"}
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">
+6 -1
View File
@@ -11,7 +11,12 @@ export function HistoryFrame({
selectedFrame: Accessor<FrameName>;
}) {
return (
<div class="flex-1 overflow-y-auto" hidden={selectedFrame() !== "History"}>
<div
class="flex-1 overflow-y-auto"
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>
+7 -2
View File
@@ -12,9 +12,14 @@ export function SettingsFrame({
const value = marquee();
return (
<div class="flex-1 overflow-y-auto" hidden={selectedFrame() !== "Settings"}>
<div
class="flex-1 overflow-y-auto"
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" />
@@ -21,80 +21,82 @@ export function Tree({
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);
}}
/>
<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;
const childrenVisible = createMemo(() =>
openedFolders().has(thing.id),
);
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;
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);
if (selectedId === thing.id) {
return;
}
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>
// 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>
);
}
+2 -2
View File
@@ -62,12 +62,12 @@ export function Update() {
<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, please</span>
<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
Install
</button>
</div>
<button
+9 -4
View File
@@ -1,6 +1,6 @@
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";
@@ -18,7 +18,6 @@ import { Background, LOCAL_STORAGE_MARQUEE_KEY } from "./components/background";
import { ChartFrame } from "./components/frames/chart";
import { FavoritesFrame } from "./components/frames/favorites";
import { HistoryFrame } from "./components/frames/history";
import { SearchFrame } from "./components/frames/search";
import { SettingsFrame } from "./components/frames/settings";
import { TreeFrame } from "./components/frames/tree";
import { Qrcode } from "./components/qrcode";
@@ -156,6 +155,12 @@ export function App() {
const resizeInitialRange = createRWS<TimeRange | null>(null);
const SearchFrame = lazy(() =>
import("./components/frames/search").then((d) => ({
default: d.SearchFrame,
})),
);
return (
<>
<Background marquee={marquee} focused={tabFocused} />
@@ -194,7 +199,7 @@ export function App() {
<Show when={!windowSizeIsAtLeastMedium() || !fullscreen()}>
<div
class={classPropToString([
env.standalone && "border-t",
standalone && "border-t",
"md:short:hidden 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",
])}
>
@@ -242,7 +247,7 @@ export function App() {
<div
class={classPropToString([
env.standalone && "pb-6",
standalone && "pb-6",
"short:hidden flex justify-between gap-3 border-t border-white/10 bg-black/30 p-2 backdrop-blur-sm md:hidden",
])}
>
+7 -3
View File
@@ -1,3 +1,7 @@
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;
+40
View File
@@ -0,0 +1,40 @@
import { createResourceDataset } from "./resource";
export { averages } from "./consts/averages";
export function createScaleDatasets<Scale extends ResourceScale>({
scale,
setActiveResources,
groupedKeysToURLPath,
}: {
scale: Scale;
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
groupedKeysToURLPath: GroupedKeysToURLPath[Scale];
}) {
type Key = keyof typeof groupedKeysToURLPath;
type ResourceData = ReturnType<typeof createResourceDataset<Scale>>;
type ResourceDatasets = Record<Exclude<Key, "ohlc">, ResourceData>;
const datasets = groupedKeysToURLPath as any as ResourceDatasets;
for (const key in groupedKeysToURLPath) {
if ((key as Key) !== "ohlc") {
datasets[key as unknown as Exclude<Key, "ohlc">] = createResourceDataset({
scale,
path: groupedKeysToURLPath[key as Key] as any,
setActiveResources,
});
}
}
const price = createResourceDataset<Scale, OHLC>({
scale,
path: `/${scale}-to-ohlc`,
setActiveResources,
});
Object.assign(datasets, { price });
return datasets;
}
+12 -18
View File
@@ -14,33 +14,27 @@ export function createDateDatasets({
type ResourceDatasets = Record<Exclude<Key, "ohlc">, ResourceData>;
for (const _key in groupedKeysToURLPath) {
const key = _key as Key;
const datasets = groupedKeysToURLPath as any as ResourceDatasets;
if (key !== "ohlc") {
const path = groupedKeysToURLPath[key];
(groupedKeysToURLPath as any as ResourceDatasets)[key] =
createResourceDataset<"date">({
scale: "date",
path,
setActiveResources,
});
for (const key in groupedKeysToURLPath) {
if ((key as Key) !== "ohlc") {
datasets[key as Exclude<Key, "ohlc">] = createResourceDataset<"date">({
scale: "date",
path: groupedKeysToURLPath[key as Key],
setActiveResources,
});
}
}
const resourceDatasets = groupedKeysToURLPath as any as ResourceDatasets;
const price = createResourceDataset<"date", OHLC>({
scale: "date",
path: "/date-to-ohlc",
setActiveResources,
});
const datasets = {
price,
...resourceDatasets,
};
Object.assign(datasets, { price });
return datasets;
return datasets as ResourceDatasets & {
price: ResourceDataset<"date", OHLC>;
};
}
+12 -16
View File
@@ -12,31 +12,27 @@ export function createHeightDatasets({
type ResourceDatasets = Record<Exclude<Key, "ohlc">, ResourceData>;
for (const _key in groupedKeysToURLPath) {
const key = _key as Key;
const datasets = groupedKeysToURLPath as any as ResourceDatasets;
if (key !== "ohlc") {
const path = groupedKeysToURLPath[key];
(groupedKeysToURLPath as any as ResourceDatasets)[key] =
createResourceDataset<"height">({
scale: "height",
path,
setActiveResources,
});
for (const key in groupedKeysToURLPath) {
if ((key as Key) !== "ohlc") {
datasets[key as Exclude<Key, "ohlc">] = createResourceDataset<"height">({
scale: "height",
path: groupedKeysToURLPath[key as Key],
setActiveResources,
});
}
}
const resourceDatasets = groupedKeysToURLPath as any as ResourceDatasets;
const price = createResourceDataset<"height", OHLC>({
scale: "height",
path: "/height-to-ohlc",
setActiveResources,
});
return {
...resourceDatasets,
price,
Object.assign(datasets, { price });
return datasets as ResourceDatasets & {
price: ResourceDataset<"height", OHLC>;
};
}
+9 -9
View File
@@ -21,13 +21,6 @@ export function createResourceDataset<
path: string;
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
const baseURL = `${
// location.hostname === "localhost"
// ? "http://localhost:3110"
// : "https://api.satonomics.xyz"
"https://api.satonomics.xyz"
}${path}`;
type Dataset = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>;
@@ -36,6 +29,13 @@ export function createResourceDataset<
Type extends number ? SingleValueData : CandlestickData
>;
const baseURL = `${
// location.hostname === "localhost"
// ? "http://localhost:3110"
// : "https://api.satonomics.xyz"
"https://api.satonomics.xyz"
}${path}`;
const fetchedJSONs = new Array(
(new Date().getFullYear() - new Date("2009-01-01").getFullYear() + 2) *
(scale === "date" ? 1 : 6),
@@ -51,12 +51,12 @@ export function createResourceDataset<
vec: createMemo(() => {
const map = json()?.dataset.map || null;
const chunkId = json()?.chunk.id!;
if (!map) {
return null;
}
const chunkId = json()?.chunk.id!;
if (Array.isArray(map)) {
return map.map(
(value, index) =>
@@ -40,6 +40,9 @@ export function createChart(scale: ResourceScale) {
shiftVisibleRangeOnNewBar: false,
allowShiftVisibleRangeOnWhitespaceReplacement: false,
},
handleScale: {
axisDoubleClickReset: false,
},
crosshair: {
mode: CrosshairMode.Normal,
horzLine: {
@@ -34,9 +34,9 @@ export const applyPriceSeries = <
const id = options?.id || "price";
const title = options?.title || "Price";
const dataset = createMemo(() => _dataset || datasets[preset.scale].price);
const dataset = _dataset || datasets[preset.scale].price;
const url = "url" in dataset() ? (dataset() as any).url : undefined;
const url = "url" in dataset ? (dataset as any).url : undefined;
const priceScaleOptions: DeepPartial<PriceScaleOptions> = {
...(options?.halved
@@ -51,7 +51,6 @@ export const applyPriceSeries = <
? {}
: {
mode: 1,
// mode: PriceScaleMode.Logarithmic,
}),
...options?.priceScaleOptions,
};
@@ -139,9 +138,12 @@ export const applyPriceSeries = <
});
createEffect(() => {
const d = dataset();
lineSeries.setData(d.values());
ohlcSeries.setData(d.values());
const values = dataset.values();
if (values) {
lineSeries.setData(values);
ohlcSeries.setData(values);
}
});
createEffect(() => {
+5
View File
@@ -0,0 +1,5 @@
export function tick() {
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
+6
View File
@@ -21,6 +21,12 @@
}
}
html {
/* Foreground, Background */
scrollbar-color: #ffffff66 #00000066;
scrollbar-width: thin;
}
a {
@apply text-orange-300 hover:underline;
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB