git: reset

This commit is contained in:
k
2024-06-23 17:38:53 +02:00
commit a1a576d088
375 changed files with 40952 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
import { createRWS } from "/src/solid/rws";
const texts = [
"satonomics",
"satonomics",
"satonomics",
"stay humble, stack sats",
"21 million",
"cold storage",
"utxo",
"satoshi nakamoto",
"hodl",
`don't trust, verify`,
"zap",
"bitcoin",
"lightning",
"nostr",
"freedom tech",
"2008/10/31",
"2009/01/03",
"2010/05/22",
"hodl!",
"Hal Finney",
"Vote for better money",
"gradually then suddenly",
"timechain",
"self custody",
"be your own bank",
"resistance money",
"foss",
];
export const LOCAL_STORAGE_MARQUEE_KEY = "bg-marquee";
export function Background({
marquee: on,
focused,
}: {
marquee: Accessor<boolean>;
focused: Accessor<boolean>;
}) {
createEffect(() => {
if (on()) {
localStorage.removeItem(LOCAL_STORAGE_MARQUEE_KEY);
} else {
localStorage.setItem(LOCAL_STORAGE_MARQUEE_KEY, "false");
}
});
return (
<>
<div class="absolute h-full w-full overflow-hidden opacity-[0.0333] will-change-auto">
<div class="-m-[2rem] -space-y-1 overflow-hidden md:-m-[1rem]">
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
<Line on={on} focused={focused} />
</div>
</div>
<div class="absolute h-full w-full opacity-10 mix-blend-multiply">
<Noise />
</div>
<div class="absolute h-full w-full opacity-10 mix-blend-hard-light">
<Noise />
</div>
</>
);
}
function Line({
on,
focused,
}: {
on: Accessor<boolean>;
focused: Accessor<boolean>;
}) {
const shuffled = shuffle([...texts]);
shuffled.pop();
const joined = shuffled.join(". ");
return (
<div class="select-none whitespace-nowrap">
<TextWrapper on={on} focused={focused} joined={joined} />
</div>
);
}
function TextWrapper({
joined,
on,
focused,
}: {
on: Accessor<boolean>;
focused: Accessor<boolean>;
joined: string;
}) {
const seconds = joined.length * 2;
const wasOnceOn = createRWS(false);
createEffect(() => {
if (!wasOnceOn() && on()) {
wasOnceOn.set(true);
}
});
return (
<p
class="inline-block px-2 text-[5dvh] font-black uppercase leading-none"
style={{
...(wasOnceOn()
? {
animation: `marquee ${seconds}s linear infinite`,
"animation-play-state": focused() && on() ? "running" : "paused",
}
: {}),
}}
>
{joined} {wasOnceOn() ? joined : undefined}
</p>
);
}
function shuffle<T>([...arr]: T[]): T[] {
let m = arr.length;
while (m) {
const i = Math.floor(Math.random() * m--);
[arr[m], arr[i]] = [arr[i], arr[m]];
}
return arr;
}
function Noise() {
return (
<svg
class="size-full"
viewBox="0 0 200 200"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
>
<filter id="noiseFilter">
<feTurbulence
type="fractalNoise"
baseFrequency="3"
numOctaves="3"
stitchTiles="stitch"
/>
</filter>
<rect width="100%" height="100%" filter="url(#noiseFilter)" />
</svg>
);
}
+154
View File
@@ -0,0 +1,154 @@
import { createResizeObserver } from "@solid-primitives/resize-observer";
import { classPropToString } from "/src/solid/classes";
import { createRWS } from "/src/solid/rws";
export function Box({
flex = true,
absolute,
padded = true,
children,
dark,
overflowY,
}: {
flex?: boolean;
absolute?: "top" | "bottom";
padded?: boolean;
dark?: boolean;
overflowY?: boolean;
} & ParentProps) {
const maybeScrollable = createRWS<HTMLDivElement | undefined>(undefined);
const scrollable = createRWS(false);
const showLeftArrow = createRWS(false);
const showRightArrow = createRWS(false);
onMount(() => {
createResizeObserver(maybeScrollable, (_, el) => {
if (el !== maybeScrollable()) {
return;
}
scrollable.set(() => el.scrollWidth > el.clientWidth);
checkArrows();
});
});
function checkArrows() {
const offset = 20;
const target = maybeScrollable()!;
const left = target.scrollLeft;
const right = target.scrollWidth - target.scrollLeft - target.clientWidth;
showLeftArrow.set(() => left > offset);
showRightArrow.set(() => right > offset);
}
return (
<div
class={classPropToString([
"p-2",
absolute
? [
"absolute inset-x-0",
absolute === "top"
? "top-0"
: "pointer-events-none bottom-0 bg-gradient-to-b from-transparent to-black",
]
: "relative",
])}
>
<div
class={classPropToString([
"pointer-events-auto relative overflow-hidden rounded-xl border border-orange-200/10 shadow-md",
dark
? "bg-orange-100/5 backdrop-blur-sm"
: "bg-orange-200/10 backdrop-blur-md",
])}
>
<For
each={[
{
showArrow: showLeftArrow,
side: "left-0",
order: "",
buttonPadding: "pl-3 pr-2",
iconPadding: "pr-0.5",
scrollMultiplier: -1,
chevronIcon: IconTablerChevronLeft,
gradientDirection: "bg-gradient-to-r",
},
{
showArrow: showRightArrow,
side: "right-0",
order: "order-2",
buttonPadding: "pl-2 pr-3",
iconPadding: "pl-0.5",
scrollMultiplier: 1,
chevronIcon: IconTablerChevronRight,
gradientDirection: "bg-gradient-to-l",
},
]}
>
{(obj) => (
<Show when={scrollable() && obj.showArrow()}>
<div
class={[
obj.side,
"pointer-events-none absolute bottom-0 top-0 z-20 flex transition-opacity duration-200 ease-in-out",
].join(" ")}
>
<div
class={[
obj.order,
obj.buttonPadding,
"pointer-events-auto hidden h-full items-center bg-black/90 md:flex",
].join(" ")}
>
<button
onClick={() => {
maybeScrollable()?.scrollBy({
left: Math.floor(
maybeScrollable()!.clientWidth *
obj.scrollMultiplier *
0.8,
),
behavior: "smooth",
});
}}
class="rounded-full border border-orange-200/20 bg-black p-0.5 transition hover:scale-110 active:scale-100"
>
<Dynamic
component={obj.chevronIcon}
class={[`size-5 ${obj.iconPadding}`]}
/>
</button>
</div>
<div
class={[
obj.gradientDirection,
"h-full w-10 from-black/90 to-transparent",
].join(" ")}
/>
</div>
</Show>
)}
</For>
<div
ref={maybeScrollable.set}
onScroll={checkArrows}
class={classPropToString([
flex && "flex w-full space-x-2",
overflowY && "overflow-y-auto",
padded && "p-1.5",
])}
>
{children}
</div>
</div>
</div>
);
}
+13
View File
@@ -0,0 +1,13 @@
export function Button({
onClick,
children,
}: { onClick: VoidFunction } & ParentProps) {
return (
<button
class="group flex w-full flex-1 items-center justify-center rounded-lg px-2 py-1.5 hover:bg-orange-200/20 active:scale-95"
onClick={onClick}
>
{children}
</button>
);
}
@@ -0,0 +1,102 @@
import type { Generate } from "lean-qr";
import { chartState } from "/src/scripts/lightweightCharts/chart/state";
import { setTimeScale } from "/src/scripts/lightweightCharts/chart/time";
import { classPropToString } from "/src/solid/classes";
import { createRWS } from "/src/solid/rws";
export function Actions({
presets,
fullscreen,
qrcode,
}: {
presets: Presets;
qrcode: RWS<string>;
fullscreen?: RWS<boolean>;
}) {
const leanQRGenerate = createRWS<Generate | undefined>(undefined);
onMount(() => {
import("lean-qr").then((leanQR) => {
leanQRGenerate.set(() => leanQR.generate);
});
});
return (
<div class="flex space-x-1">
<Button
icon={() => IconTablerMaximize}
onClick={() => {
const range = chartState.range;
fullscreen?.set((b) => !b);
setTimeScale(range);
}}
classes="hidden md:block"
/>
<Button
icon={() => IconTablerShare}
disabled={() => !leanQRGenerate()}
onClick={() => {
let generate = leanQRGenerate();
if (generate) {
qrcode.set(() =>
generate(document.location.href).toDataURL({
on: [0xff, 0xff, 0xff, 0xff],
off: [0x00, 0x00, 0x00, 0x00],
}),
);
}
}}
classes="hidden md:block"
/>
<Button
colors={() =>
presets.selected().isFavorite()
? "text-amber-500 bg-amber-500/15 hover:bg-amber-500/30"
: ""
}
icon={() =>
presets.selected().isFavorite()
? IconTablerStarFilled
: IconTablerStar
}
onClick={() => presets.selected().isFavorite.set((b) => !b)}
/>
</div>
);
}
function Button({
icon,
colors,
onClick,
disabled,
classes,
}: {
icon: () => ValidComponent;
colors?: () => string;
onClick: VoidFunction;
disabled?: () => boolean;
classes?: string;
}) {
return (
<button
disabled={disabled?.()}
class={classPropToString([
colors?.() || (disabled?.() ? "" : "hover:bg-orange-200/15"),
!disabled?.() && "group",
classes,
"flex-none rounded-lg p-2 disabled:opacity-50",
])}
onClick={onClick}
>
<Dynamic
component={icon()}
class="size-[1.125rem] group-active:scale-90"
/>
</button>
);
}
@@ -0,0 +1,33 @@
import { cleanChart } from "/src/scripts/lightweightCharts/chart/clean";
import { renderChart } from "/src/scripts/lightweightCharts/chart/render";
export function Chart({
presets,
datasets,
legendSetter,
activeResources,
}: {
presets: Presets;
datasets: Datasets;
legendSetter: Setter<PresetLegend>;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
}) {
onMount(() => {
createEffect(() => {
const preset = presets.selected();
untrack(() =>
renderChart({
datasets,
preset,
legendSetter,
activeResources,
}),
);
});
onCleanup(cleanChart);
});
return <div id="chart" class="h-full w-full cursor-crosshair" />;
}
@@ -0,0 +1,134 @@
import { createRWS } from "/src/solid/rws";
const transparency = "66";
export function Legend({
legend: legendList,
}: {
legend: Accessor<PresetLegend>;
}) {
const hovering = createRWS<SeriesLegend | undefined>(undefined);
let toggle = false;
return (
<div class="flex flex-1 items-center gap-1 overflow-y-auto">
<For each={legendList()}>
{(legend) => {
const initialColors = {} as any;
const darkenColors = {} as any;
Object.entries(legend.series.options()).forEach(([k, v]) => {
if (k.toLowerCase().includes("color") && v) {
initialColors[k] = v;
darkenColors[k] = `${v}${transparency}`;
} else if (k === "lastValueVisible" && v) {
initialColors[k] = v;
darkenColors[k] = !v;
}
});
createEffect(() => {
if (hovering()) {
if (hovering()?.title !== legend.title) {
legend.series.applyOptions(darkenColors);
}
} else {
legend.series.applyOptions(initialColors);
}
});
let previousClickValueOf: number = 0;
return (
<Show when={!legend.disabled()}>
<button
onMouseEnter={() => {
hovering.set(legend);
}}
onMouseLeave={() => hovering.set(undefined)}
onClick={() => {
const currentClickValueOf = new Date().valueOf();
if (currentClickValueOf - previousClickValueOf > 300) {
legend.visible.set((visible) => !visible);
} else {
legendList().forEach((_legend) => {
if (_legend.title != legend.title) {
_legend.visible.set(toggle);
}
});
legend.visible.set(true);
toggle = !toggle;
}
previousClickValueOf = currentClickValueOf;
}}
class="flex flex-none items-center space-x-1.5 rounded-full py-1.5 pl-2 pr-2.5 hover:bg-orange-200/20 active:scale-[0.975]"
>
<span
class="flex size-4 flex-col overflow-hidden rounded-full"
style={{
opacity: legend.visible() ? 1 : 0.5,
}}
>
<For
each={
Array.isArray(legend.color())
? (legend.color() as string[])
: [legend.color() as string]
}
>
{(color) => (
<span
class="w-full flex-1"
style={{
"background-color": color,
}}
/>
)}
</For>
</span>
<span
class="text-white decoration-white decoration-wavy decoration-[1.5px]"
style={{
"text-decoration-line": !legend.visible()
? "line-through"
: undefined,
"--tw-text-opacity": legend.visible() ? 1 : 0.5,
}}
>
{legend.title}
</span>
<Show when={legend.url}>
{(url) => (
<a
class="-my-0.5 !-mr-1 inline-flex size-6 flex-col overflow-hidden rounded-full border border-orange-200/5 bg-orange-200 bg-opacity-5 p-1 pl-0.5 hover:bg-opacity-30"
style={{
opacity: legend.visible() ? 1 : 0.5,
}}
onClick={(event) => {
event.stopPropagation();
// event.preventDefault();
}}
href={url()}
target={
url()?.startsWith("/") || url()?.startsWith("http")
? "_blank"
: undefined
}
>
<IconTablerExternalLink />
</a>
)}
</Show>
</button>
</Show>
);
}}
</For>
</div>
);
}
@@ -0,0 +1,67 @@
import { chartState } from "/src/scripts/lightweightCharts/chart/state";
import { GENESIS_DAY } from "/src/scripts/lightweightCharts/chart/whitespace";
import { ONE_DAY_IN_MS } from "/src/scripts/utils/time";
import { Box } from "../../box";
export function TimeScale() {
return (
<Box dark padded overflowY>
<Button onClick={() => setTimeScale()}>All Time</Button>
<Button onClick={() => setTimeScale(7)}>1 Week</Button>
<Button onClick={() => setTimeScale(30)}>1 Month</Button>
<Button onClick={() => setTimeScale(30 * 6)}>6 Months</Button>
<Button
onClick={() =>
setTimeScale(
Math.ceil(
(new Date().valueOf() -
new Date(`${new Date().getUTCFullYear()}-01-01`).valueOf()) /
ONE_DAY_IN_MS,
),
)
}
>
Year To Date
</Button>
<Button onClick={() => setTimeScale(365)}>1 Year</Button>
<Button onClick={() => setTimeScale(2 * 365)}>2 Years</Button>
<Button onClick={() => setTimeScale(4 * 365)}>4 Years</Button>
<Button onClick={() => setTimeScale(8 * 365)}>8 Years</Button>
</Box>
);
}
function Button(props: ParentProps & { onClick: VoidFunction }) {
return (
<button
class="min-w-20 flex-shrink-0 flex-grow whitespace-nowrap rounded-lg px-2 py-1.5 hover:bg-white/20 active:scale-95"
onClick={props.onClick}
>
{props.children}
</button>
);
}
function setTimeScale(days?: number) {
const to = new Date();
if (days) {
const from = new Date();
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,
});
}
}
@@ -0,0 +1,12 @@
export function Title({ presets }: { presets: Presets }) {
return (
<div class="flex flex-1 items-center overflow-y-auto pb-1.5 text-orange-100/50">
<div class="flex-1 -space-y-1 whitespace-nowrap px-1 md:mt-0.5 md:-space-y-1.5">
<h3 class="text-xs">{`/ ${[...presets.selected().path.map(({ name }) => name), presets.selected().name].join(" / ")}`}</h3>
<h1 class="text-lg font-bold text-white md:text-xl">
{presets.selected().title}
</h1>
</div>
</div>
);
}
@@ -0,0 +1,72 @@
import { classPropToString } from "/src/solid/classes";
import { createRWS } from "/src/solid/rws";
import { Box } from "../box";
import { Actions } from "./components/actions";
import { Legend } from "./components/legend";
import { TimeScale } from "./components/timeScale";
import { Title } from "./components/title";
export function ChartFrame({
presets,
datasets,
activeResources,
hide,
qrcode,
standalone,
fullscreen,
}: {
presets: Presets;
hide?: Accessor<boolean>;
qrcode: RWS<string>;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
datasets: Datasets;
fullscreen?: RWS<boolean>;
standalone: boolean;
}) {
const legend = createRWS<PresetLegend>([]);
const Chart = lazy(() =>
import("./components/chart").then((d) => ({
default: d.Chart,
})),
);
return (
<div
class={classPropToString([
standalone &&
"rounded-2xl border border-orange-200/15 bg-gradient-to-b from-orange-100/5 to-black/10 to-80%",
"flex size-full min-h-0 flex-1 flex-col overflow-hidden",
])}
style={{
display: (hide ? !hide() : true) ? undefined : "none",
}}
>
<Box flex={false} dark>
<Title presets={presets} />
<div class="-mx-2 border-t border-orange-200/15" />
<div class="flex pt-1.5">
<Legend legend={legend} />
<div class="-my-1.5 border-l border-orange-200/15 pr-1.5" />
<Actions presets={presets} qrcode={qrcode} fullscreen={fullscreen} />
</div>
</Box>
<div class="-mt-2 min-h-0 flex-1">
<Chart
activeResources={activeResources}
datasets={datasets}
legendSetter={legend.set}
presets={presets}
/>
</div>
<TimeScale />
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
export function Counter({
count,
name,
setRef,
}: {
count: () => number;
name: string;
setRef?: Setter<HTMLDivElement | undefined>;
}) {
return (
<div
ref={setRef}
class="text-orange-100/75"
style={{
"border-style": count() ? "dashed" : "none",
}}
>
Counted{" "}
<span class="font-medium text-orange-400/75">
{count().toLocaleString("en-us")}
</span>{" "}
{name}
</div>
);
}
@@ -0,0 +1,48 @@
import { Header } from "./header";
import { Line } from "./line";
import { Number } from "./number";
export function FavoritesFrame({
presets,
selectedFrame,
}: {
presets: Presets;
selectedFrame: Accessor<FrameName>;
}) {
return (
<div
class="flex-1 overflow-y-auto"
hidden={selectedFrame() !== "Favorites"}
>
<div class="flex max-h-full min-h-0 flex-1 flex-col gap-4 p-4">
<Header title="Favorites">
<Number number={() => presets.favorites().length} /> presets marked as
favorites.
</Header>
<div class="-mx-4 border-t border-orange-200/10" />
<div
class="space-y-0.5 py-1"
style={{
display: !presets.favorites().length ? "none" : undefined,
}}
>
<For each={presets.favorites()}>
{(preset) => (
<Line
id={`favorite-${preset.id}`}
name={preset.title}
onClick={() => presets.select(preset)}
active={() => presets.selected() === preset}
header={`/ ${[...preset.path.map(({ name }) => name), preset.name].join(" / ")}`}
/>
)}
</For>
</div>
<div class="h-[25dvh] flex-none" />
</div>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export function Header({ title, children }: { title: string } & ParentProps) {
return (
<div>
<h3 class="text-lg font-bold md:text-xl">{title}</h3>
<p class="text-orange-100/75">{children}</p>
</div>
);
}
+80
View File
@@ -0,0 +1,80 @@
import { run } from "/src/scripts/utils/run";
import { Header } from "./header";
import { Line } from "./line";
export function HistoryFrame({
presets,
selectedFrame,
}: {
presets: Presets;
selectedFrame: Accessor<FrameName>;
}) {
return (
<div class="flex-1 overflow-y-auto" hidden={selectedFrame() !== "History"}>
<div class="flex max-h-full min-h-0 flex-1 flex-col p-4">
<Header title="History">List of previously visited presets.</Header>
<div
class="space-y-0.5 pt-4"
style={{
display: !presets.history().length ? "none" : undefined,
}}
>
<For each={presets.history()}>
{({ preset, date }, index) => (
<>
<Show
when={
index() === 0 ||
presets.history()[index()].date.toJSON().split("T")[0] !==
presets.history()[index() - 1].date.toJSON().split("T")[0]
}
>
<div class="sticky top-[-0.5rem] z-10 -mx-4 py-2">
<div class="border-y border-orange-200/10 bg-[rgb(25,15,15)] p-2">
<p class="ml-2">
<Switch fallback={date.toLocaleDateString()}>
<Match
when={
new Date().toJSON().split("T")[0] ===
date.toJSON().split("T")[0]
}
>
Today
</Match>
<Match
when={
run(() => {
const d = new Date();
d.setDate(d.getDate() - 1);
return d;
})
.toJSON()
.split("T")[0] === date.toJSON().split("T")[0]
}
>
Yesterday
</Match>
</Switch>
</p>
</div>
</div>
</Show>
<Line
id={`history-${preset.id}`}
name={preset.title}
onClick={() => presets.select(preset)}
active={() => presets.selected() === preset}
header={date.toLocaleTimeString()}
/>
</>
)}
</For>
</div>
<div class="h-[25dvh] flex-none" />
</div>
</div>
);
}
+90
View File
@@ -0,0 +1,90 @@
import { scrollIntoView } from "/src/scripts/utils/scroll";
import { classPropToString } from "/src/solid/classes";
import { createRWS } from "/src/solid/rws";
export function Line({
id,
name: _name,
icon,
active,
depth = 0,
onClick,
header,
tail,
classes: classes,
}: {
id: string;
name: string;
onClick: VoidFunction;
active?: Accessor<boolean>;
depth?: number;
header?: string;
icon?: () => JSXElement;
tail?: () => JSXElement;
classes?: () => string;
} & ParentProps) {
const ref = createRWS<HTMLButtonElement | undefined>(undefined);
const [name, ...nameRest] = _name.split(" - ");
return (
<button
id={id}
class={classPropToString([
active?.()
? "bg-orange-500/30 backdrop-blur-sm hover:bg-orange-500/50"
: "hover:bg-orange-500/15",
"relative -mx-2 flex w-[calc(100%+1rem)] items-center whitespace-nowrap rounded-lg px-2 hover:backdrop-blur-sm",
classes?.(),
])}
ref={ref.set}
onClick={() => {
onClick();
scrollIntoView(ref(), "nearest", "instant");
}}
title={name}
>
<For each={new Array(depth)}>
{() => (
<span class="ml-1 h-8 w-3 flex-none border-l border-orange-200/10" />
)}
</For>
<Show when={icon}>
{(icon) => (
<span
class="-my-0.5 mr-1"
// style={{
// "margin-left": `${depth}rem`,
// }}
>
{icon()()}
</span>
)}
</Show>
<span
class={classPropToString([
!icon && "px-1",
"inline-flex w-full flex-col -space-y-1 truncate py-1 text-left",
])}
>
<Show when={header}>
<span
class="truncate text-xs text-white text-opacity-50"
innerHTML={header}
/>
</Show>
<span class="space-x-1 truncate">
<span innerHTML={name} />
<Show when={nameRest.length}>
<span innerHTML={" - " + nameRest.join(" - ")} class="opacity-50" />
</Show>
</span>
</span>
<Show when={tail}>
{(absolute) => (
<span class="ml-0.5 flex items-center">{absolute()()}</span>
)}
</Show>
</button>
);
}
+7
View File
@@ -0,0 +1,7 @@
export function Number({ number }: { number: () => number }) {
return (
<span class="font-medium text-orange-400/75">
{number().toLocaleString("en-us")}
</span>
);
}
+318
View File
@@ -0,0 +1,318 @@
import uFuzzy from "@leeoniya/ufuzzy";
import { createVisibilityObserver } from "@solid-primitives/intersection-observer";
import { scrollIntoView } from "/src/scripts/utils/scroll";
import { createRWS } from "/src/solid/rws";
import { INPUT_PRESET_SEARCH_ID } from "../..";
import { Box } from "./box";
import { Button } from "./button";
import { Line } from "./line";
const PER_PAGE = 100;
export function SearchFrame({
presets,
selectedFrame,
}: {
presets: Presets;
selectedFrame: Accessor<FrameName>;
}) {
const counterRef = createRWS<HTMLDivElement | undefined>(undefined);
const search = createRWS("", {
equals: false,
});
const inputRef = createRWS<HTMLInputElement | undefined>(undefined);
const config: uFuzzy.Options = {
intraIns: Infinity,
intraChars: `[a-z\d' ]`,
};
const fuzzyMultiInsert = new uFuzzy({
intraIns: 1,
});
const fuzzyMultiInsertFuzzier = new uFuzzy(config);
const fuzzySingleError = new uFuzzy({
intraMode: 1,
...config,
});
const fuzzySingleErrorFuzzier = new uFuzzy({
intraMode: 1,
...config,
});
const haystack = presets.list.map(
(preset) =>
`${preset.title}\t/ ${[...preset.path.map(({ name }) => name), preset.name].join(" / ")}`,
);
const searchResult = createMemo(() => {
scrollIntoView(counterRef());
const needle = search();
if (!needle) return null;
const outOfOrder = 5;
const infoThresh = 5_000;
let result = fuzzyMultiInsert.search(
haystack,
needle,
undefined,
infoThresh,
);
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsert.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzySingleError.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzySingleErrorFuzzier.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsertFuzzier.search(
haystack,
needle,
undefined,
infoThresh,
);
}
if (!result?.[0]?.length || !result?.[1]) {
result = fuzzyMultiInsertFuzzier.search(
haystack,
needle,
outOfOrder,
infoThresh,
);
}
return result;
});
const resultCount = createMemo(() => searchResult()?.[0]?.length || 0);
return (
<div
class="relative flex size-full flex-1 flex-col"
style={{
display: selectedFrame() !== "Search" ? "none" : undefined,
}}
>
<div class="flex-1 space-y-1 overflow-y-auto p-4 pt-16">
<p class="py-2 text-orange-100/75">
<Show when={search()} fallback={"Write in the top bar to search."}>
Found{" "}
<span class="font-medium text-orange-400/75">
{resultCount().toLocaleString("en-us")}
</span>{" "}
presets.
</Show>
</p>
<Show when={search()}>
<div class="-mx-4 border-t border-orange-200/10" />
<div
class="py-1"
style={{
display: !resultCount() ? "none" : undefined,
}}
>
{(() => {
const r = searchResult();
if (r) {
return (
<ListSection
haystack={haystack}
presets={presets}
searchResult={() => r}
/>
);
} else {
return undefined;
}
})()}
</div>
</Show>
</div>
<Box absolute="top" padded={false}>
<div
class="relative flex w-full cursor-text items-center space-x-0.5 px-3 py-2 hover:bg-orange-200/5"
onClick={() => inputRef()?.focus()}
>
<IconTablerSearch />
<input
id={INPUT_PRESET_SEARCH_ID}
ref={inputRef.set}
class="w-full bg-transparent p-1 caret-orange-500 placeholder:text-orange-200/50 focus:outline-none"
placeholder="Search by name or path"
value={search()}
onInput={(event) => search.set(event.target.value)}
/>
<span class="-mx-1 flex size-5 flex-none items-center justify-center rounded-md border border-white text-xs font-bold">
<IconTablerSlash />
</span>
</div>
</Box>
<Box absolute="bottom">
<Button
onClick={() => {
search.set("");
inputRef()?.focus();
}}
>
Clear search
</Button>
</Box>
</div>
);
}
function ListSection({
searchResult,
pageIndex = 0,
haystack,
presets,
}: {
searchResult: Accessor<uFuzzy.SearchResult>;
pageIndex?: number;
haystack: string[];
presets: Presets;
}) {
const div = createRWS<HTMLDivElement | undefined>(undefined);
const useVisibilityObserver = createVisibilityObserver();
const visible = useVisibilityObserver(div);
const showNextPage = createMemo<boolean>(
(previous) => previous || visible(),
false,
);
const list = createMemo(() =>
computeList({
searchResult: searchResult(),
pageIndex,
haystack,
presets,
}),
);
return (
<div>
<For each={list()}>
{({ preset, path, title }) => (
<Line
id={`search-${preset.id}`}
name={title}
onClick={() => presets.select(preset)}
active={() => presets.selected() === preset}
header={path}
/>
)}
</For>
<Show when={list().length === PER_PAGE}>
<div ref={div.set}>
<Show when={showNextPage()}>
<ListSection
searchResult={searchResult}
haystack={haystack}
presets={presets}
pageIndex={pageIndex + 1}
/>
</Show>
</div>
</Show>
</div>
);
}
function computeList({
searchResult,
pageIndex,
haystack,
presets,
}: {
searchResult: uFuzzy.SearchResult;
pageIndex: number;
haystack: string[];
presets: Presets;
}) {
let list: {
preset: Preset;
path: string;
title: string;
}[] = [];
let [indexes, info, order] = searchResult || [null, null, null];
const minIndex = pageIndex * PER_PAGE;
if (indexes?.length) {
const maxIndex = Math.min(
(order || indexes).length - 1,
minIndex + PER_PAGE - 1,
);
list = Array(maxIndex - minIndex + 1);
if (info && order) {
for (let i = minIndex; i <= maxIndex; i++) {
let infoIdx = order[i];
const [title, path] = uFuzzy
.highlight(haystack[info.idx[infoIdx]], info.ranges[infoIdx])
.split("\t");
list[i % 100] = {
preset: presets.list[info.idx[infoIdx]],
path,
title,
};
}
} else {
for (let i = minIndex; i <= maxIndex; i++) {
let index = indexes[i];
const [title, path] = haystack[index].split("\t");
list[i % 100] = {
preset: presets.list[index],
path,
title,
};
}
}
}
return list;
}
@@ -0,0 +1,37 @@
import { Header } from "./header";
export function SettingsFrame({
marquee,
selectedFrame,
}: {
marquee: RWS<boolean>;
selectedFrame: Accessor<FrameName>;
}) {
const value = marquee();
return (
<div class="flex-1 overflow-y-auto" hidden={selectedFrame() !== "Settings"}>
<div class="space-y-4 p-4">
<Header title="Settings" />
<div class="-mx-4 border-t border-orange-200/10" />
<div class="space-y-2">
<p>Background</p>
<div>Opacity</div>
<div>
<label class="switch">
Scroll
<input
type="checkbox"
checked={value}
onChange={(event) => marquee.set(event.target.checked || false)}
/>
<span class="slider"></span>
</label>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,47 @@
import { Line } from "../../line";
export function File({
id,
name,
icon,
active,
depth,
onClick,
favorite,
visited,
}: {
id: string;
name: string;
icon: JSXElement;
active: Accessor<boolean>;
depth: number;
onClick: VoidFunction;
favorite: Accessor<boolean>;
visited: Accessor<boolean>;
}) {
const tail = createMemo(() =>
favorite() ? (
<span class="rounded-full bg-yellow-950 p-1">
<IconTablerStarFilled class="size-3 text-amber-500" />
</span>
) : !visited() ? (
<span class="mx-1.5 rounded-full bg-orange-500/50 p-1 text-transparent" />
) : undefined,
);
return (
<Line
id={id}
depth={depth}
active={active}
name={name}
icon={() => icon}
onClick={onClick}
tail={tail}
/>
);
}
function randomDegree(min = 0, max = 360) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
@@ -0,0 +1,39 @@
import { Line } from "../../line";
export function Folder({
id,
name,
depth,
open,
onClick,
children,
}: {
id: string;
name: string;
depth: number;
open: Accessor<boolean>;
onClick: VoidFunction;
children: number;
}) {
const icon = createMemo(() =>
open() ? <IconTablerFolderOpen /> : <IconTablerFolder />,
);
return (
<Line
id={id}
depth={depth}
name={name}
icon={icon}
onClick={onClick}
classes={() => (open() ? "text-orange-100/75" : "")}
tail={() => (
<Show when={!open()}>
<span class="rounded-full bg-white bg-opacity-[0.075] px-2 py-0.5 text-xs text-neutral-400">
{children}
</span>
</Show>
)}
></Line>
);
}
@@ -0,0 +1,117 @@
import { File } from "./file";
import { Folder } from "./folder";
export function Tree({
tree,
selected,
openedFolders,
depth = 0,
visible,
selectPreset,
path = [],
favorites,
}: {
tree: PresetTree;
selected: Accessor<Preset>;
selectPreset(preset: Preset): void;
openedFolders: RWS<Set<string>>;
depth?: number;
visible?: Accessor<boolean>;
path?: FilePath;
favorites: Accessor<Preset[]>;
}) {
return (
<div style={{ display: visible?.() === false ? "none" : undefined }}>
<For each={tree}>
{(thing) => {
const active = createMemo(() => thing.id === selected().id);
const favorite = createMemo(() =>
favorites().includes(thing as Preset),
);
const visited = (thing as Preset).visited;
if (!("tree" in thing)) {
return (
<File
id={thing.id}
name={thing.name}
active={active}
depth={depth}
icon={thing.icon || IconTablerFile}
favorite={favorite}
visited={visited}
onClick={() => {
const selectedId = selected().id;
if (selectedId === thing.id) {
return;
}
// Has been filled in createPresets
selectPreset(thing as Preset);
}}
/>
);
}
const childrenVisible = createMemo(() =>
openedFolders().has(thing.id),
);
const childCount = countChildren(thing);
return (
<div>
<Folder
id={thing.id}
name={thing.name}
depth={depth}
open={childrenVisible}
children={childCount}
onClick={() => {
openedFolders.set((s) => {
if (childrenVisible()) {
s.delete(thing.id);
} else {
s.add(thing.id);
}
return s;
});
}}
/>
<Tree
tree={thing.tree}
selected={selected}
depth={depth + 1}
openedFolders={openedFolders}
visible={childrenVisible}
path={[...path, { name: thing.name, id: thing.id }]}
selectPreset={selectPreset}
favorites={favorites}
/>
</div>
);
}}
</For>
</div>
);
}
function countChildren(folder: PresetFolder) {
let count = 0;
function _countChildren(tree: PartialPresetTree) {
tree.forEach((anyPreset) => {
if ("tree" in anyPreset) {
_countChildren(anyPreset.tree);
} else {
count += 1;
}
});
}
_countChildren(folder.tree);
return count;
}
@@ -0,0 +1,86 @@
import { scrollIntoView } from "/src/scripts/utils/scroll";
import { sleep, tick } from "/src/scripts/utils/sleep";
import { createRWS } from "/src/solid/rws";
import { Box } from "../box";
import { Button } from "../button";
import { Header } from "../header";
import { Number } from "../number";
import { Tree } from "./components/tree";
export function TreeFrame({
presets,
selectedFrame,
}: {
presets: Presets;
selectedFrame: Accessor<FrameName>;
}) {
const div = createRWS<HTMLDivElement | undefined>(undefined);
onMount(() => {
goToSelected(presets);
});
return (
<div
class="relative flex size-full flex-1 flex-col"
style={{
display: selectedFrame() !== "Tree" ? "none" : undefined,
}}
>
<div class="flex-1 overflow-y-auto">
<div class="flex max-h-full min-h-0 flex-1 flex-col gap-4 p-4">
<Header title="Folders">
<Number number={() => presets.list.length} /> presets organized in a
tree like structure.
</Header>
<div class="-mx-4 border-t border-orange-200/10" />
<Tree
tree={presets.tree}
openedFolders={presets.openedFolders}
selected={presets.selected}
selectPreset={presets.select}
favorites={presets.favorites}
/>
<div class="h-[50dvh] flex-none" />
</div>
</div>
<Box absolute="bottom">
<Button
onClick={() => {
presets.openedFolders.set((s) => {
s.clear();
return s;
});
sleep(10);
scrollIntoView(div());
}}
>
Close all folders
</Button>
<Button onClick={() => goToSelected(presets)}>Go to selected</Button>
</Box>
</div>
);
}
async function goToSelected(presets: Presets) {
batch(() =>
presets.selected().path.forEach(({ id }) => {
presets.openedFolders.set((s) => {
s.add(id);
return s;
});
}),
);
await tick();
scrollIntoView(document.getElementById(presets.selected().id), "center");
}
+18
View File
@@ -0,0 +1,18 @@
export function Qrcode({ qrcode }: { qrcode: RWS<string> }) {
return (
<Show when={qrcode()}>
<div
class="absolute inset-0 z-50 flex h-full w-full items-center justify-center bg-black"
onClick={() => {
qrcode.set("");
}}
>
<img
class="aspect-square max-h-full grow object-contain"
src={qrcode()}
style={{ "image-rendering": "pixelated" }}
/>
</div>
</Show>
);
}
@@ -0,0 +1,9 @@
import { Clickable } from "./clickable";
export function Anchor(args: {
title: string;
href: string;
icon?: () => ValidComponent;
}) {
return <Clickable {...args} />;
}
@@ -0,0 +1,26 @@
import { Anchor } from "./anchor";
export function AnchorAPI() {
return (
<Anchor
title="API"
icon={() => () => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.13468 2.41153C3.88395 3.0478 3.37143 3.79772 3.37143 4.4186C3.37143 5.03949 3.88395 5.78941 5.13468 6.42568C6.3444 7.04109 8.06359 7.44186 10 7.44186C11.9364 7.44186 13.6556 7.04109 14.8653 6.42568C16.1161 5.78941 16.6286 5.03949 16.6286 4.4186C16.6286 3.79772 16.1161 3.0478 14.8653 2.41153C13.6556 1.79612 11.9364 1.39535 10 1.39535C8.06359 1.39535 6.3444 1.79612 5.13468 2.41153ZM16.6286 6.93694C16.2841 7.21648 15.8934 7.46274 15.4786 7.67372C14.0411 8.40502 12.1032 8.83721 10 8.83721C7.89684 8.83721 5.95889 8.40502 4.52136 7.67372C4.10664 7.46274 3.71588 7.21648 3.37143 6.93694V10C3.37143 10.6209 3.88395 11.3708 5.13468 12.0071C6.3444 12.6225 8.06359 13.0233 10 13.0233C11.9364 13.0233 13.6556 12.6225 14.8653 12.0071C16.1161 11.3708 16.6286 10.6209 16.6286 10V6.93694ZM18 4.4186C18 2.98447 16.8752 1.87393 15.4786 1.16349C14.0411 0.432186 12.1032 0 10 0C7.89684 0 5.95889 0.432186 4.52136 1.16349C3.12484 1.87393 2 2.98447 2 4.4186V15.5814C2 17.0155 3.12484 18.1261 4.52136 18.8365C5.95889 19.5678 7.89684 20 10 20C12.1032 20 14.0411 19.5678 15.4786 18.8365C16.8752 18.1261 18 17.0155 18 15.5814V4.4186ZM16.6286 12.5183C16.2841 12.7979 15.8934 13.0441 15.4786 13.2551C14.0411 13.9864 12.1032 14.4186 10 14.4186C7.89684 14.4186 5.95889 13.9864 4.52136 13.2551C4.10664 13.0441 3.71588 12.7979 3.37143 12.5183V15.5814C3.37143 16.2023 3.88395 16.9522 5.13468 17.5885C6.3444 18.2039 8.06359 18.6047 10 18.6047C11.9364 18.6047 13.6556 18.2039 14.8653 17.5885C16.1161 16.9522 16.6286 16.2023 16.6286 15.5814V12.5183ZM6.34285 10C6.34285 10.5138 5.93351 10.9302 5.42857 10.9302C4.92362 10.9302 4.51428 10.5138 4.51428 10C4.51428 9.48625 4.92362 9.06977 5.42857 9.06977C5.93351 9.06977 6.34285 9.48625 6.34285 10ZM9.0857 11.8605C9.59065 11.8605 9.99999 11.444 9.99999 10.9302C9.99999 10.4165 9.59065 10 9.0857 10C8.58076 10 8.17142 10.4165 8.17142 10.9302C8.17142 11.444 8.58076 11.8605 9.0857 11.8605ZM6.34285 15.5814C6.34285 16.0951 5.93351 16.5116 5.42857 16.5116C4.92362 16.5116 4.51428 16.0951 4.51428 15.5814C4.51428 15.0676 4.92362 14.6512 5.42857 14.6512C5.93351 14.6512 6.34285 15.0676 6.34285 15.5814ZM9.0857 17.4419C9.59065 17.4419 9.99999 17.0254 9.99999 16.5116C9.99999 15.9979 9.59065 15.5814 9.0857 15.5814C8.58076 15.5814 8.17142 15.9979 8.17142 16.5116C8.17142 17.0254 8.58076 17.4419 9.0857 17.4419Z"
fill="currentColor"
></path>
</svg>
)}
href="https://api.satonomics.xyz"
/>
);
}
@@ -0,0 +1,11 @@
import { Anchor } from "./anchor";
export function AnchorGit() {
return (
<Anchor
title="Git"
icon={() => IconTablerGitMerge}
href="https://codeberg.org/satonomics/satonomics"
/>
);
}
@@ -0,0 +1,32 @@
import { Anchor } from "./anchor";
export function AnchorHome() {
return (
<Anchor
title="Home"
icon={() => () => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.61843 17.395H10.3816C12.1046 17.395 13.288 17.3933 14.198 17.3045C15.0844 17.218 15.5498 17.0602 15.8839 16.8482C16.3124 16.5763 16.6761 16.2149 16.9497 15.7891C17.1631 15.4571 17.3218 14.9946 17.4089 14.1138C17.4983 13.2096 17.5 12.0337 17.5 10.3216C17.5 8.25521 17.4763 7.61464 17.2665 7.07287C17.1488 6.76889 16.9887 6.48284 16.7909 6.22312C16.4384 5.76023 15.9032 5.40234 14.1365 4.31233L13.9563 4.20109C12.9121 3.55687 12.2055 3.12231 11.6218 2.82577C11.0608 2.54075 10.7049 2.43259 10.3882 2.39747C10.1302 2.36886 9.86981 2.36886 9.61184 2.39747C9.29509 2.43259 8.9392 2.54075 8.37818 2.82577C7.79446 3.12231 7.08787 3.55687 6.04374 4.20109L5.86345 4.31233C4.09679 5.40234 3.56162 5.76023 3.20909 6.22312C3.01129 6.48284 2.85119 6.76889 2.73348 7.07287C2.52369 7.61464 2.5 8.25521 2.5 10.3216C2.5 12.0337 2.50169 13.2096 2.59108 14.1138C2.67816 14.9946 2.83688 15.4571 3.05029 15.7891C3.32393 16.2149 3.68762 16.5763 4.11606 16.8482C4.45021 17.0602 4.91563 17.218 5.80203 17.3045C6.71202 17.3933 7.89539 17.395 9.61843 17.395ZM1.33354 6.5376C1 7.39893 1 8.37315 1 10.3216C1 13.6861 1 15.3684 1.78613 16.5914C2.17705 17.1996 2.6966 17.7159 3.30866 18.1043C4.53951 18.8855 6.23249 18.8855 9.61843 18.8855H10.3816C13.7675 18.8855 15.4605 18.8855 16.6913 18.1043C17.3034 17.7159 17.823 17.1996 18.2139 16.5914C19 15.3684 19 13.6861 19 10.3216C19 8.37315 19 7.39893 18.6665 6.5376C18.4983 6.10334 18.2696 5.6947 17.987 5.32367C17.4265 4.58775 16.5936 4.07385 14.9278 3.04605L14.7475 2.93482C12.7009 1.67205 11.6775 1.04067 10.5545 0.916147C10.186 0.875282 9.81402 0.875282 9.44549 0.916147C8.32248 1.04067 7.29915 1.67205 5.25249 2.93482L5.0722 3.04605C3.40637 4.07385 2.57345 4.58775 2.01299 5.32367C1.73042 5.6947 1.5017 6.10334 1.33354 6.5376Z"
fill="currentColor"
></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.25 14.0001C6.25 13.5858 6.58579 13.2501 7 13.2501H13C13.4142 13.2501 13.75 13.5858 13.75 14.0001C13.75 14.4143 13.4142 14.7501 13 14.7501H7C6.58579 14.7501 6.25 14.4143 6.25 14.0001Z"
fill="currentColor"
></path>
</svg>
)}
href="https://satonomics.xyz"
/>
);
}
@@ -0,0 +1,36 @@
export function AnchorLogo() {
return (
<a
class="inline-flex justify-center rounded-lg bg-gradient-to-br from-orange-500 to-orange-800 p-4"
href="https://app.satonomics.xyz"
title="Reload"
>
<svg
class="-m-1.5 size-7"
width="100%"
height="100%"
viewBox="0 0 24 24"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
fill="currentColor"
>
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,5.12904)">
<rect x="4.25" y="3.751" width="14.023" height="1.52" />
</g>
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,0.129039)">
<rect x="4.25" y="3.751" width="14.023" height="1.52" />
</g>
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,-4.87096)">
<rect x="4.25" y="3.751" width="14.023" height="1.52" />
</g>
<g transform="matrix(0.285256,0,0,2.63158,8.78759,-9.87096)">
<rect x="4.25" y="3.751" width="14.023" height="1.52" />
</g>
<g transform="matrix(0.285256,0,0,2.63158,8.78759,10.129)">
<rect x="4.25" y="3.751" width="14.023" height="1.52" />
</g>
</svg>
</a>
);
}
@@ -0,0 +1,24 @@
import { Anchor } from "./anchor";
export function AnchorNostr() {
return (
<Anchor
title="Nostr"
icon={() => (props: { class?: string }) => (
<svg
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
class={props.class}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M13.7502 1.5C13.3359 1.5 13.0002 1.83579 13.0002 2.25C13.0002 2.44257 13.0717 2.61663 13.191 2.74981C13.2963 2.86751 13.4367 2.95067 13.5937 2.98382C13.6435 2.99433 13.6958 3 13.7502 3C14.1644 3 14.5002 2.66421 14.5002 2.25C14.5002 1.83579 14.1644 1.5 13.7502 1.5ZM11.5002 2.25C11.5002 1.00736 12.5075 0 13.7502 0C14.7298 0 15.5632 0.626106 15.8721 1.5H17.2502C17.6644 1.5 18.0002 1.83579 18.0002 2.25C18.0002 2.66421 17.6644 3 17.2502 3H15.8721C15.7646 3.30433 15.5934 3.5786 15.3746 3.80685C15.8823 4.15684 16.2746 4.56859 16.5559 5.03129C17.0977 5.92273 17.1689 6.90747 16.9843 7.79359C16.803 8.66356 16.4559 9.21659 16.076 9.55603C16.0407 9.58759 16.0055 9.61689 15.9707 9.64408C15.9901 9.86441 15.987 10.0911 15.961 10.3234C15.8963 11.095 15.7248 11.9737 14.9635 12.7682C14.447 13.3072 13.7758 13.6448 13.2782 13.8437C13.0474 13.936 12.8402 14.0039 12.6813 14.0508C12.5887 14.2176 12.4689 14.451 12.3551 14.7243C12.1564 15.2017 11.999 15.7473 12.0002 16.2484C12.0011 16.664 12.1111 17.3476 12.2365 17.9768C12.2743 18.1665 12.3122 18.3447 12.3464 18.5001H13.2502C13.6644 18.5001 14.0002 18.8359 14.0002 19.2501C14.0002 19.6643 13.6644 20.0001 13.2502 20.0001H9.50017C9.15558 20.0001 8.85533 19.7653 8.77228 19.4308L9.50017 19.2501C8.77228 19.4308 8.77228 19.4308 8.77228 19.4308L8.77052 19.4237L8.76601 19.4053L8.74947 19.337C8.73534 19.2781 8.71532 19.1935 8.69136 19.0891C8.64351 18.8806 8.5796 18.5917 8.51548 18.2701C8.39148 17.6482 8.25146 16.8318 8.25017 16.2518C8.24854 15.5227 8.455 14.8065 8.6849 14.2341C8.16639 14.1203 7.6883 13.9364 7.28499 13.7003C6.8511 13.4462 6.41419 13.0729 6.17829 12.5938H2.75029C2.4353 12.5938 2.15363 12.3969 2.04556 12.1011C1.93749 11.8052 2.0258 11.4733 2.26663 11.2703L3.18599 10.4953C2.80831 10.4564 2.51374 10.1372 2.51374 9.74926C2.51374 8.4257 3.50881 7.51335 4.62109 6.95581C5.76228 6.38377 7.24656 6.05918 8.72722 5.97699C10.2102 5.89468 11.7615 6.05149 13.0442 6.50251C13.9451 6.81927 14.793 7.31253 15.3456 8.03745C15.4073 7.89899 15.4675 7.71949 15.5158 7.48766C15.6401 6.89125 15.5786 6.31137 15.2741 5.81039C14.9722 5.31375 14.3741 4.8005 13.2451 4.44292C12.7831 4.33682 12.3768 4.0893 12.0732 3.75019C11.7174 3.35262 11.5002 2.82579 11.5002 2.25ZM10.8948 14.3362C10.6852 14.3506 10.4759 14.3595 10.2686 14.3634C10.0228 14.8666 9.74868 15.5834 9.75016 16.2484C9.75109 16.664 9.86108 17.3476 9.98652 17.9768C10.0243 18.1665 10.0622 18.3447 10.0964 18.5001H10.8124C10.7969 18.4258 10.7812 18.349 10.7655 18.2701C10.6415 17.6482 10.5015 16.8318 10.5002 16.2518C10.4986 15.5522 10.6874 14.8745 10.8948 14.3362Z"
></path>
</svg>
)}
href="https://primal.net/p/npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44"
/>
);
}
@@ -0,0 +1,14 @@
import { Clickable } from "./clickable";
export function Button(
args: {
title: string;
selected?: Accessor<boolean>;
onClick?: VoidFunction;
icon?: () => ValidComponent;
hideOnDesktop?: boolean;
hideOnMobile?: boolean;
} & ParentProps,
) {
return <Clickable {...args} />;
}
@@ -0,0 +1,27 @@
import { Button } from "./button";
export function ButtonChart({
selected,
setSelected,
}: {
selected: Accessor<FrameName>;
setSelected: Setter<FrameName>;
}) {
const frameName: FrameName = "Chart";
return (
<Button
title={frameName}
selected={() => selected() === frameName}
onClick={() => {
setSelected(frameName);
}}
icon={() =>
selected() === frameName
? IconTablerChartAreaFilled
: IconTablerChartLine
}
hideOnDesktop
/>
);
}
@@ -0,0 +1,24 @@
import { Button } from "./button";
export function ButtonFavorites({
selected,
setSelected,
}: {
selected: Accessor<FrameName>;
setSelected: Setter<FrameName>;
}) {
const frameName: FrameName = "Favorites";
return (
<Button
title={frameName}
selected={() => selected() === frameName}
onClick={() => {
setSelected(frameName);
}}
icon={() =>
selected() === frameName ? IconTablerStarFilled : IconTablerStar
}
/>
);
}
@@ -0,0 +1,22 @@
import { Button } from "./button";
export function ButtonHistory({
selected,
setSelected,
}: {
selected: Accessor<FrameName>;
setSelected: Setter<FrameName>;
}) {
const frameName: FrameName = "History";
return (
<Button
title={frameName}
selected={() => selected() === frameName}
onClick={() => {
setSelected(frameName);
}}
icon={() => IconTablerHistory}
/>
);
}
@@ -0,0 +1,10 @@
import { Button } from "./button";
export function ButtonRefresh() {
return (
<Button title="Refresh" onClick={() => document.location.reload()}>
<IconTablerRefreshAlert class="absolute size-5 animate-ping text-orange-400" />
<IconTablerRefreshAlert class="relative size-5 text-orange-300" />
</Button>
);
}
@@ -0,0 +1,24 @@
import { Button } from "./button";
export function ButtonSearch({
selected,
setSelected,
}: {
selected: Accessor<FrameName>;
setSelected: Setter<FrameName>;
}) {
const frameName: FrameName = "Search";
return (
<Button
title={frameName}
selected={() => selected() === frameName}
onClick={() => {
setSelected(frameName);
}}
icon={() =>
selected() === frameName ? IconTablerZoomFilled : IconTablerSearch
}
/>
);
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,58 @@
import { Button } from "./button";
export function ButtonTree({
selected,
setSelected,
}: {
selected: Accessor<FrameName>;
setSelected: Setter<FrameName>;
}) {
const frameName: FrameName = "Tree";
return (
<Button
title={frameName}
selected={() => selected() === frameName}
onClick={() => {
setSelected(frameName);
}}
icon={() =>
selected() === frameName
? (props: { class?: string }) => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class={props.class}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.22892 18C4.9566 18 3.93098 18 3.1244 17.8935C2.28697 17.7828 1.58187 17.5461 1.02187 16.9958C0.461866 16.4455 0.221 15.7526 0.108411 14.9296C-3.31178e-05 14.137 -1.7645e-05 13.1291 1.54211e-06 11.8788L1.04302e-06 6.29541C-2.62958e-05 5.47395 -4.90919e-05 4.78896 0.0743487 4.24516C0.152876 3.67118 0.325617 3.15297 0.74937 2.73655C1.17312 2.32012 1.70045 2.15037 2.28453 2.0732C2.8379 2.00009 3.53494 2.00011 4.37086 2.00013L5.92614 2.00007C6.57086 1.99946 7.08108 1.99899 7.55104 2.18869C8.021 2.37838 8.38357 2.73117 8.84172 3.17694L9.06221 3.39116C9.20357 3.52844 9.28285 3.60481 9.34651 3.65795C9.37487 3.68162 9.39161 3.69332 9.40051 3.699C9.40473 3.70169 9.40712 3.70298 9.40801 3.70345L9.40914 3.70399L9.41033 3.70438C9.41129 3.70466 9.41392 3.7054 9.41885 3.7064C9.42924 3.70851 9.44952 3.71175 9.48662 3.7145C9.5699 3.72068 9.68092 3.72113 9.87966 3.72113L13.0938 3.72111C13.6755 3.72097 14.072 3.72087 14.4167 3.78961C15.7901 4.06347 16.8634 5.11825 17.1421 6.46785C17.2021 6.75842 17.2105 7.08647 17.2116 7.53472C17.4034 7.54922 17.5834 7.56801 17.7514 7.59237C18.5137 7.70289 19.1943 7.94917 19.6331 8.57761C20.0718 9.20605 20.0607 9.91868 19.8913 10.6574C19.7278 11.3702 19.3805 12.2552 18.9553 13.3384L18.6619 14.0857C18.3405 14.9047 18.0787 15.5717 17.8049 16.0905C17.5191 16.6321 17.1912 17.0712 16.7057 17.3985C16.2202 17.7258 15.6854 17.8685 15.0682 17.9356C14.4771 18 13.7497 18 12.8565 18L6.22892 18ZM5.81464 3.37155C6.62543 3.37155 6.83809 3.3835 7.02081 3.45726C7.20352 3.53101 7.36333 3.6694 7.94003 4.22946L8.08126 4.36662L8.1337 4.41774C8.34688 4.62596 8.57694 4.85067 8.8789 4.97256C9.18087 5.09444 9.50523 5.09353 9.8058 5.09268L9.87966 5.09254H13.0114C13.7067 5.09254 13.9504 5.096 14.1392 5.13363C14.9632 5.29795 15.6072 5.93081 15.7744 6.74058C15.805 6.88885 15.8134 7.07165 15.8155 7.48813C15.5174 7.48575 15.2019 7.48576 14.8692 7.48577H7.25506C6.70131 7.48575 6.23171 7.48573 5.84482 7.52626C5.43306 7.56939 5.05328 7.66313 4.69832 7.88868C4.34336 8.11422 4.10052 8.41609 3.89152 8.76739C3.69515 9.09748 3.50247 9.51829 3.27524 10.0146L2.3991 11.9279C2.00422 12.7902 1.66601 13.5287 1.435 14.1586C1.39636 13.5526 1.39555 12.7992 1.39555 11.8286V6.34295C1.39555 5.46158 1.39703 4.86953 1.45745 4.4279C1.51517 4.006 1.61493 3.82543 1.73617 3.70628C1.85741 3.58714 2.04116 3.48911 2.47049 3.43238C2.91989 3.37301 3.52235 3.37155 4.41924 3.37155H5.81464Z"
fill="currentColor"
></path>
</svg>
)
: (props: { class?: string }) => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class={props.class}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.59655 2.20712C7.10136 1.9989 6.56115 1.99943 5.9023 2.00007L4.40479 2.00015C3.57853 2.00013 2.88271 2.0001 2.32874 2.07318C1.74135 2.15066 1.20072 2.32242 0.764844 2.75008C0.328798 3.1779 0.153514 3.70882 0.0744639 4.28569C-4.74114e-05 4.82945 -2.52828e-05 5.51233 9.81743e-07 6.32281V11.8675C-1.65965e-05 13.1029 -3.08677e-05 14.1058 0.108284 14.8963C0.221156 15.72 0.464085 16.4241 1.03541 16.9846C1.60656 17.545 2.32369 17.7831 3.16265 17.8938C3.96804 18 4.99002 18 6.2493 18H13.7507C15.01 18 16.032 18 16.8374 17.8938C17.6763 17.7831 18.3934 17.545 18.9646 16.9846C19.5359 16.4241 19.7788 15.72 19.8917 14.8963C20 14.1058 20 13.1029 20 11.8676V9.94525C20 8.70992 20 7.70702 19.8917 6.91657C19.7788 6.09287 19.5359 5.38878 18.9646 4.82823C18.3934 4.26785 17.6763 4.02972 16.8374 3.91905C16.0319 3.81281 15.0099 3.81283 13.7506 3.81285L9.91202 3.81285C9.70527 3.81285 9.59336 3.81232 9.51046 3.80596C9.47861 3.80352 9.461 3.80081 9.45249 3.79919C9.44546 3.79427 9.43137 3.78367 9.40771 3.76281C9.34589 3.70835 9.26838 3.62926 9.12578 3.48235L8.91813 3.26831C8.46421 2.79975 8.09187 2.4154 7.59655 2.20712ZM2.53158 3.55817C2.97217 3.50005 3.5649 3.49846 4.45741 3.49846H5.77707C6.19724 3.49846 6.45952 3.50169 6.63994 3.51453C6.81907 3.52729 6.91262 3.54925 6.99675 3.58462C7.08084 3.61998 7.16148 3.67125 7.29433 3.78964C7.42818 3.90891 7.6114 4.09298 7.90119 4.39152L8.02253 4.51653L8.07907 4.57502C8.29018 4.79381 8.5293 5.04163 8.85233 5.17747C9.17524 5.31324 9.52282 5.31222 9.82983 5.31132L9.91202 5.31115H13.6951C15.023 5.31115 15.9424 5.31274 16.6345 5.40404C17.3048 5.49246 17.6468 5.6525 17.8873 5.88854C18.1277 6.12441 18.2906 6.45944 18.3807 7.11653C18.4737 7.79534 18.4753 8.69706 18.4753 10.0001V11.8128C18.4753 13.1158 18.4737 14.0175 18.3807 14.6963C18.2906 15.3534 18.1277 15.6884 17.8873 15.9243C17.6468 16.1603 17.3048 16.3204 16.6345 16.4088C15.9424 16.5001 15.023 16.5017 13.6951 16.5017H6.30494C4.97698 16.5017 4.05764 16.5001 3.36549 16.4088C2.69519 16.3204 2.35324 16.1603 2.11266 15.9243C1.87226 15.6884 1.70936 15.3534 1.61932 14.6963C1.5263 14.0175 1.52468 13.1158 1.52468 11.8128V6.37469C1.52468 5.49891 1.5263 4.91765 1.5855 4.48566C1.64172 4.07541 1.73696 3.91355 1.8421 3.81039C1.94741 3.70706 2.11288 3.6134 2.53158 3.55817Z"
fill="currentColor"
></path>
</svg>
)
}
/>
);
}
@@ -0,0 +1,36 @@
import { classPropToString } from "/src/solid/classes";
export function Clickable({
selected,
onClick,
href,
icon,
children,
title,
}: {
title: string;
selected?: Accessor<boolean>;
onClick?: VoidFunction;
href?: string;
icon?: () => ValidComponent;
} & ParentProps) {
return (
<Dynamic
component={onClick ? "button" : href ? "a" : "span"}
class={classPropToString([
selected?.() ? "bg-orange-200/10" : "opacity-50 hover:bg-orange-200/10",
"select-none rounded-lg p-3.5 hover:text-orange-400 hover:opacity-100 active:scale-90",
])}
title={title}
onClick={onClick}
href={href}
target={
href?.startsWith("/") || href?.startsWith("http") ? "_blank" : undefined
}
>
<Show when={icon} fallback={children}>
{(icon) => <Dynamic component={icon()()} class="size-5" />}
</Show>
</Dynamic>
);
}
+65
View File
@@ -0,0 +1,65 @@
import { AnchorAPI } from "./components/anchorAPI";
import { AnchorGit } from "./components/anchorGit";
import { AnchorHome } from "./components/anchorHome";
import { AnchorLogo } from "./components/anchorLogo";
import { AnchorNostr } from "./components/anchorNostr";
import { ButtonChart } from "./components/buttonChart";
import { ButtonFavorites } from "./components/buttonFavorites";
import { ButtonHistory } from "./components/buttonHistory";
import { ButtonRefresh } from "./components/buttonRefresh";
import { ButtonSearch } from "./components/buttonSearch";
import { ButtonSettings } from "./components/buttonSettings";
import { ButtonTree } from "./components/buttonTree";
export function StripDesktop({
selected,
setSelected,
needsRefresh,
}: {
selected: Accessor<FrameName>;
setSelected: Setter<FrameName>;
needsRefresh: Accessor<boolean>;
}) {
return (
<>
<AnchorLogo />
<ButtonTree selected={selected} setSelected={setSelected} />
<ButtonFavorites selected={selected} setSelected={setSelected} />
<ButtonSearch selected={selected} setSelected={setSelected} />
<ButtonHistory selected={selected} setSelected={setSelected} />
<ButtonSettings selected={selected} setSelected={setSelected} />
<div class="size-full" />
<Show when={needsRefresh()}>
<ButtonRefresh />
</Show>
<AnchorAPI />
<AnchorGit />
<AnchorNostr />
{/* <AnchorHome /> */}
</>
);
}
export function StripMobile({
selected,
setSelected,
}: {
selected: Accessor<FrameName>;
setSelected: Setter<FrameName>;
}) {
return (
<>
<ButtonChart selected={selected} setSelected={setSelected} />
<ButtonTree selected={selected} setSelected={setSelected} />
<ButtonFavorites selected={selected} setSelected={setSelected} />
<ButtonSearch selected={selected} setSelected={setSelected} />
<ButtonHistory selected={selected} setSelected={setSelected} />
<ButtonSettings selected={selected} setSelected={setSelected} />
</>
);
}
+309
View File
@@ -0,0 +1,309 @@
import { createRWS } from "/src/solid/rws";
import { env } from "../env";
import { createDatasets } from "../scripts/datasets";
import { chartState } from "../scripts/lightweightCharts/chart/state";
import { setTimeScale } from "../scripts/lightweightCharts/chart/time";
import { createPresets } from "../scripts/presets";
import { priceToUSLocale } from "../scripts/utils/locale";
import { sleep } from "../scripts/utils/sleep";
import {
readBooleanFromStorage,
saveToStorage,
} from "../scripts/utils/storage";
import { readBooleanURLParam, writeURLParam } from "../scripts/utils/urlParams";
import { webSockets } from "../scripts/ws";
import { classPropToString } from "../solid/classes";
import { Background, LOCAL_STORAGE_MARQUEE_KEY } from "./components/background";
import { ChartFrame } from "./components/frames/chart";
import { TreeFrame } from "./components/frames/tree";
import { StripDesktop, StripMobile } from "./components/strip";
import { registerServiceWorker } from "./scripts/register";
const LOCAL_STORAGE_BAR_KEY = "bar-width";
const LOCAL_STORAGE_FULLSCREEN = "fullscrenn";
export const INPUT_PRESET_SEARCH_ID = "input-search-preset";
export function App() {
const needRefresh = registerServiceWorker().needRefresh[0];
const tabFocused = createRWS(true);
const qrcode = createRWS("");
const fullscreen = createRWS(
readBooleanURLParam(LOCAL_STORAGE_FULLSCREEN) ||
readBooleanFromStorage(LOCAL_STORAGE_FULLSCREEN) ||
false,
);
const activeResources = createRWS<Set<ResourceDataset<any, any>>>(new Set(), {
equals: false,
});
const datasets = createDatasets({
setActiveResources: activeResources.set,
});
const windowWidth = createRWS(window.innerWidth);
const windowResizeCallback = () => {
windowWidth.set(window.innerWidth);
};
window.addEventListener("resize", windowResizeCallback);
onCleanup(() => window.removeEventListener("resize", windowResizeCallback));
const windowSizeIsAtLeastMedium = createMemo(() => windowWidth() >= 720);
const barWidth = createRWS(
Number(localStorage.getItem(LOCAL_STORAGE_BAR_KEY)),
);
createEffect(() => {
localStorage.setItem(LOCAL_STORAGE_BAR_KEY, String(barWidth()));
});
createEffect(() => {
if (fullscreen()) {
writeURLParam(LOCAL_STORAGE_FULLSCREEN, "true");
saveToStorage(LOCAL_STORAGE_FULLSCREEN, fullscreen());
} else {
writeURLParam(LOCAL_STORAGE_FULLSCREEN, undefined);
saveToStorage(LOCAL_STORAGE_FULLSCREEN, undefined);
}
});
const _selectedFrame = createRWS<FrameName>("Chart");
const selectedFrame = createMemo(() =>
windowSizeIsAtLeastMedium() && _selectedFrame() === "Chart"
? "Tree"
: _selectedFrame(),
);
const presets = createPresets(datasets);
const marquee = createRWS(!localStorage.getItem(LOCAL_STORAGE_MARQUEE_KEY));
const resizingBarStart = createRWS<number | undefined>(undefined);
createEffect(
() => {
if (!windowSizeIsAtLeastMedium() && presets.selected()) {
_selectedFrame.set("Chart");
}
},
{
deffer: true,
},
);
onMount(() => {
webSockets.openAll();
createEffect(() => {
const latest = webSockets.liveKrakenCandle.latest();
if (latest) {
const close = latest.close;
console.log("close:", close);
document.title = `${priceToUSLocale(latest.close, false)} | Satonomics`;
}
});
});
const FavoritesFrame = lazy(() =>
import("./components/frames/favorites").then((d) => ({
default: d.FavoritesFrame,
})),
);
const HistoryFrame = lazy(() =>
import("./components/frames/history").then((d) => ({
default: d.HistoryFrame,
})),
);
const SearchFrame = lazy(() =>
import("./components/frames/search").then((d) => ({
default: d.SearchFrame,
})),
);
const SettingsFrame = lazy(() =>
import("./components/frames/settings").then((d) => ({
default: d.SettingsFrame,
})),
);
const Qrcode = lazy(() =>
import("./components/qrcode").then((d) => ({
default: d.Qrcode,
})),
);
const documentVisibilityChange = () =>
tabFocused.set(document.visibilityState === "visible");
document.addEventListener("visibilitychange", documentVisibilityChange);
onCleanup(() =>
document.removeEventListener("visibilitychange", documentVisibilityChange),
);
const documentOnKeyDown = async (event: KeyboardEvent) => {
switch (event.key) {
case "Escape": {
event.stopPropagation();
event.preventDefault();
_selectedFrame.set("Chart");
break;
}
case "/": {
event.stopPropagation();
event.preventDefault();
_selectedFrame.set("Search");
await sleep(50);
document.getElementById(INPUT_PRESET_SEARCH_ID)?.focus();
break;
}
}
};
document.addEventListener("keydown", documentOnKeyDown);
onCleanup(() => document.removeEventListener("keydown", documentOnKeyDown));
const resizeInitialRange = createRWS<TimeRange | null>(null);
return (
<>
<Background marquee={marquee} focused={tabFocused} />
<div
class="relative h-dvh selection:bg-orange-800"
style={{
"user-select": resizingBarStart() !== undefined ? "none" : undefined,
}}
onMouseMove={(event) => {
const start = resizingBarStart();
if (start !== undefined) {
barWidth.set(event.x - start + 384);
setTimeScale(resizeInitialRange());
}
}}
onMouseUp={() => resizingBarStart.set(undefined)}
onMouseLeave={() => resizingBarStart.set(undefined)}
onTouchEnd={() => resizingBarStart.set(undefined)}
onTouchCancel={() => resizingBarStart.set(undefined)}
>
<Qrcode qrcode={qrcode} />
<div class="flex size-full flex-col md:flex-row md:p-3">
<Show when={!windowSizeIsAtLeastMedium() || !fullscreen()}>
<div
class={classPropToString([
env.standalone && "border-t",
"flex h-full flex-col overflow-hidden border-white/10 bg-gradient-to-b from-orange-500/10 to-orange-950/10 md:flex-row md:rounded-2xl md:border",
])}
>
<div class="hidden flex-col gap-2 border-r border-white/10 bg-black/30 p-3 backdrop-blur-sm md:flex">
<StripDesktop
selected={selectedFrame}
setSelected={_selectedFrame.set}
needsRefresh={needRefresh}
/>
</div>
<div
class="flex h-full min-h-0 md:min-w-[384px]"
style={{
...(windowSizeIsAtLeastMedium()
? {
width: `min(${barWidth()}px, 75dvw)`,
}
: {}),
}}
>
<Show when={!windowSizeIsAtLeastMedium()}>
<ChartFrame
presets={presets}
hide={() => selectedFrame() !== "Chart"}
qrcode={qrcode}
standalone={false}
datasets={datasets}
activeResources={activeResources}
/>
</Show>
<TreeFrame presets={presets} selectedFrame={selectedFrame} />
<FavoritesFrame
presets={presets}
selectedFrame={selectedFrame}
/>
<SearchFrame presets={presets} selectedFrame={selectedFrame} />
<HistoryFrame presets={presets} selectedFrame={selectedFrame} />
<SettingsFrame
marquee={marquee}
selectedFrame={selectedFrame}
/>
</div>
<div
class={classPropToString([
env.standalone && "pb-6",
"flex justify-between gap-3 border-t border-white/10 bg-black/30 p-2 backdrop-blur-sm md:hidden",
])}
>
<StripMobile
selected={selectedFrame}
setSelected={_selectedFrame.set}
/>
</div>
</div>
</Show>
<Show when={!fullscreen()}>
<div
class="mx-[3px] my-8 hidden w-[6px] cursor-col-resize items-center justify-center rounded-full bg-orange-100 opacity-0 hover:opacity-50 md:block"
onMouseDown={(event) => {
resizeInitialRange.set(chartState.range);
resizingBarStart() === undefined &&
// TODO: set size of bar instead
resizingBarStart.set(event.clientX);
}}
onTouchStart={(event) => {
resizeInitialRange.set(chartState.range);
resizingBarStart() === undefined &&
resizingBarStart.set(event.touches[0].clientX);
}}
onDblClick={() => {
resizeInitialRange.set(chartState.range);
barWidth.set(0);
setTimeScale(resizeInitialRange());
}}
/>
</Show>
<Show when={windowSizeIsAtLeastMedium()}>
<div class="flex min-w-0 flex-1">
<ChartFrame
standalone={true}
presets={presets}
qrcode={qrcode}
fullscreen={fullscreen}
activeResources={activeResources}
datasets={datasets}
/>
</div>
</Show>
</div>
</div>
</>
);
}
+67
View File
@@ -0,0 +1,67 @@
import { useRegisterSW } from "virtual:pwa-register/solid";
import { FIVE_MINUTES_IN_MS } from "/src/scripts/utils/time";
export function registerServiceWorker() {
return useRegisterSW({
onRegisteredSW(swUrl, registered) {
console.log("sw: registered", registered);
if (registered) {
const callback = async () => {
if (!(!registered.installing && navigator)) return;
if ("connection" in navigator && !navigator.onLine) return;
const resp = await fetch(swUrl, {
cache: "no-store",
headers: {
cache: "no-store",
"cache-control": "no-cache",
},
});
if (resp?.status === 200) {
await registered.update();
}
};
callback();
setInterval(callback, FIVE_MINUTES_IN_MS);
}
},
onRegisterError(error) {
console.log("sw: registration error", error);
},
onNeedRefresh() {
console.log("sw: needs refresh");
},
});
}
// From update.tsx
// onMount(async () => {
// if ('serviceWorker' in navigator) {
// try {
// const registration = await navigator.serviceWorker.register('/sw.js')
// registration.addEventListener('updatefound', () => {
// const worker = registration.installing
// worker?.addEventListener('statechange', () => {
// if (
// worker.state === 'activated' &&
// navigator.serviceWorker.controller
// ) {
// ;(Object.entries(props.resources) as Entries<ResourcesHTTP>)
// .map(([_, value]) => value.fetch)
// .forEach((fetch) => fetch())
// setTimeout(() => updateAvailable.set(true), FIVE_SECOND_IN_MS)
// }
// })
// })
// } catch {}
// }
// })
+7
View File
@@ -0,0 +1,7 @@
type FrameName =
| "Chart"
| "Tree"
| "Favorites"
| "Search"
| "History"
| "Settings";
+3
View File
@@ -0,0 +1,3 @@
export const env = {
standalone: "standalone" in window.navigator && !!window.navigator.standalone,
};
+18
View File
@@ -0,0 +1,18 @@
/* @refresh reload */
import { render } from "solid-js/web";
import "./styles.css";
const root = document.getElementById("root");
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
);
}
render(() => {
const App = lazy(() => import("./app").then((d) => ({ default: d.App })));
return <App />;
}, root!);
@@ -0,0 +1,30 @@
export const addressCohortsBySize = [
{
key: "plankton",
name: "Plankton",
},
{
key: "shrimp",
name: "Shrimp",
},
{ key: "crab", name: "Crab" },
{ key: "fish", name: "Fish" },
{ key: "shark", name: "Shark" },
{ key: "whale", name: "Whale" },
{ key: "humpback", name: "Humpback" },
{ key: "megalodon", name: "Megalodon" },
] as const;
export const addressCohortsByType = [
{ key: "p2pk", name: "P2PK" },
{ key: "p2pkh", name: "P2PKH" },
{ key: "p2sh", name: "P2SH" },
{ key: "p2wpkh", name: "P2WPKH" },
{ key: "p2wsh", name: "P2WSH" },
{ key: "p2tr", name: "P2TR" },
] as const;
export const addressCohorts = [
...addressCohortsBySize,
...addressCohortsByType,
] as const;
+147
View File
@@ -0,0 +1,147 @@
export const xthCohorts = [
{
key: "sth",
name: "Short Term Holders",
legend: "STH",
},
{
key: "lth",
name: "Long Term Holders",
legend: "LTH",
},
] as const;
export const upToCohorts = [
{ key: "up_to_1d", name: "Up To 1 Day", legend: "1D" },
{ key: "up_to_1w", name: "Up To 1 Week", legend: "1W" },
{ key: "up_to_1m", name: "Up To 1 Month", legend: "1M" },
{ key: "up_to_2m", name: "Up To 2 Months", legend: "2M" },
{ key: "up_to_3m", name: "Up To 3 Months", legend: "3M" },
{ key: "up_to_4m", name: "Up To 4 Months", legend: "4M" },
{ key: "up_to_5m", name: "Up To 5 Months", legend: "5M" },
{ key: "up_to_6m", name: "Up To 6 Months", legend: "6M" },
{ key: "up_to_1y", name: "Up To 1 Year", legend: "1Y" },
{ key: "up_to_2y", name: "Up To 2 Years", legend: "2Y" },
{ key: "up_to_3y", name: "Up To 3 Years", legend: "3Y" },
{ key: "up_to_5y", name: "Up To 5 Years", legend: "5Y" },
{ key: "up_to_7y", name: "Up To 7 Years", legend: "7Y" },
{ key: "up_to_10y", name: "Up To 10 Years", legend: "10Y" },
{ key: "up_to_15y", name: "Up To 15 Years", legend: "15Y" },
] as const;
export const fromXToYCohorts = [
{
key: "from_1d_to_1w",
name: "From 1 Day To 1 Week",
legend: "1D - 1W",
},
{
key: "from_1w_to_1m",
name: "From 1 Week To 1 Month",
legend: "1W - 1M",
},
{
key: "from_1m_to_3m",
name: "From 1 Month To 3 Months",
legend: "1M - 3M",
},
{
key: "from_3m_to_6m",
name: "From 3 Months To 6 Months",
legend: "3M - 6M",
},
{
key: "from_6m_to_1y",
name: "From 6 Months To 1 Year",
legend: "6M - 1Y",
},
{
key: "from_1y_to_2y",
name: "From 1 Year To 2 Years",
legend: "1Y - 2Y",
},
{
key: "from_2y_to_3y",
name: "From 2 Years To 3 Years",
legend: "2Y - 3Y",
},
{
key: "from_3y_to_5y",
name: "From 3 Years To 5 Years",
legend: "3Y - 5Y",
},
{
key: "from_5y_to_7y",
name: "From 5 Years To 7 Years",
legend: "5Y - 7Y",
},
{
key: "from_7y_to_10y",
name: "From 7 Years To 10 Years",
legend: "7Y - 10Y",
},
{
key: "from_10y_to_15y",
name: "From 10 Years To 15 Years",
legend: "10Y - 15Y",
},
] as const;
export const fromXCohorts = [
{
key: "from_1y",
name: "From 1 Year",
legend: "1Y+",
},
{
key: "from_2y",
name: "From 2 Years",
legend: "2Y+",
},
{
key: "from_4y",
name: "From 4 Years",
legend: "4Y+",
},
{
key: "from_10y",
name: "From 10 Years",
legend: "10Y+",
},
{
key: "from_15y",
name: "From 15 Years",
legend: "15Y+",
},
] as const;
export const yearCohorts = [
{ key: "year_2009", name: "2009" },
{ key: "year_2010", name: "2010" },
{ key: "year_2011", name: "2011" },
{ key: "year_2012", name: "2012" },
{ key: "year_2013", name: "2013" },
{ key: "year_2014", name: "2014" },
{ key: "year_2015", name: "2015" },
{ key: "year_2016", name: "2016" },
{ key: "year_2017", name: "2017" },
{ key: "year_2018", name: "2018" },
{ key: "year_2019", name: "2019" },
{ key: "year_2020", name: "2020" },
{ key: "year_2021", name: "2021" },
{ key: "year_2022", name: "2022" },
{ key: "year_2023", name: "2023" },
{ key: "year_2024", name: "2024" },
] as const;
export const ageCohorts = [
{
key: "",
name: "",
},
...xthCohorts,
...upToCohorts,
...fromXToYCohorts,
...fromXCohorts,
...yearCohorts,
] as const;
@@ -0,0 +1,15 @@
export const averages = [
{ name: "1 Week", key: "1w", days: 7 },
{ name: "8 Days", key: "8d", days: 8 },
{ name: "13 Days", key: "13d", days: 13 },
{ name: "21 Days", key: "21d", days: 21 },
{ name: "1 Month", key: "1m", days: 30 },
{ name: "34 Days", key: "34d", days: 34 },
{ name: "55 Days", key: "55d", days: 55 },
{ name: "89 Days", key: "89d", days: 89 },
{ name: "144 Days", key: "144d", days: 144 },
{ name: "1 Year", key: "1y", days: 365 },
{ name: "2 Years", key: "2y", days: 2 * 365 },
{ name: "200 Weeks", key: "200w", days: 200 * 7 },
{ name: "4 Years", key: "4y", days: 4 * 365 },
] as const;
@@ -0,0 +1,11 @@
export const liquidities = [
{
key: "illiquid",
name: "Illiquid",
},
{ key: "liquid", name: "Liquid" },
{
key: "highly_liquid",
name: "Highly Liquid",
},
] as const;
@@ -0,0 +1,116 @@
export const percentiles = [
{
key: "median_price_paid",
name: "Median",
title: "Median Paid",
value: 50,
},
{
key: "95p_price_paid",
name: `95%`,
title: `95th Percentile Paid`,
value: 95,
},
{
key: "90p_price_paid",
name: `90%`,
title: `90th Percentile Paid`,
value: 90,
},
{
key: "85p_price_paid",
name: `85%`,
title: `85th Percentile Paid`,
value: 85,
},
{
key: "80p_price_paid",
name: `80%`,
title: `80th Percentile Paid`,
value: 80,
},
{
key: "75p_price_paid",
name: `75%`,
title: `75th Percentile Paid`,
value: 75,
},
{
key: "70p_price_paid",
name: `70%`,
title: `70th Percentile Paid`,
value: 70,
},
{
key: "65p_price_paid",
name: `65%`,
title: `65th Percentile Paid`,
value: 65,
},
{
key: "60p_price_paid",
name: `60%`,
title: `60th Percentile Paid`,
value: 60,
},
{
key: "55p_price_paid",
name: `55%`,
title: `55th Percentile Paid`,
value: 55,
},
{
key: "45p_price_paid",
name: `45%`,
title: `45th Percentile Paid`,
value: 45,
},
{
key: "40p_price_paid",
name: `40%`,
title: `40th Percentile Paid`,
value: 40,
},
{
key: "35p_price_paid",
name: `35%`,
title: `35th Percentile Paid`,
value: 35,
},
{
key: "30p_price_paid",
name: `30%`,
title: `30th Percentile Paid`,
value: 30,
},
{
key: "25p_price_paid",
name: `25%`,
title: `25th Percentile Paid`,
value: 25,
},
{
key: "20p_price_paid",
name: `20%`,
title: `20th Percentile Paid`,
value: 20,
},
{
key: "15p_price_paid",
name: `15%`,
title: `15th Percentile Paid`,
value: 15,
},
{
key: "10p_price_paid",
name: `10%`,
title: `10th Percentile Paid`,
value: 10,
},
{
key: "05p_price_paid",
name: `5%`,
title: `5th Percentile Paid`,
value: 5,
},
] as const;
@@ -0,0 +1,14 @@
export const totalReturns = [
{ name: "1 Day", key: "1d" },
{ name: "1 Month", key: "1m" },
{ name: "6 Months", key: "6m" },
{ name: "1 Year", key: "1y" },
{ name: "2 Years", key: "2y" },
{ name: "3 Years", key: "3y" },
{ name: "4 Years", key: "4y" },
{ name: "6 Years", key: "6y" },
{ name: "8 Years", key: "8y" },
{ name: "10 Years", key: "10y" },
] as const;
export const compoundReturns = [{ name: "4 Years", key: "4y" }] as const;
+19
View File
@@ -0,0 +1,19 @@
type AgeCohortKey = (typeof import("./age").ageCohorts)[number]["key"];
type AddressCohortKey =
(typeof import("./address").addressCohorts)[number]["key"];
type LiquidityKey = (typeof import("./liquidities").liquidities)[number]["key"];
type AddressCohortKeySplitByLiquidity = `${LiquidityKey}_${AddressCohortKey}`;
type AnyCohortKey = AgeCohortKey | AddressCohortKey;
type AnyPossibleCohortKey = AnyCohortKey | AddressCohortKeySplitByLiquidity;
type AverageName = (typeof import("./averages").averages)[number]["key"];
type TotalReturnKey = (typeof import("./returns").totalReturns)[number]["key"];
type CompoundReturnKey =
(typeof import("./returns").compoundReturns)[number]["key"];
+41
View File
@@ -0,0 +1,41 @@
import groupedKeysToPath from "/src/../../datasets/grouped_keys_to_url_path.json";
import { createResourceDataset } from "./resource";
export { averages } from "./consts/averages";
export function createDateDatasets({
setActiveResources,
}: {
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
type Key = keyof typeof groupedKeysToPath.date;
type ResourceData = ReturnType<typeof createResourceDataset<"date">>;
const resourceDatasets = {} as Record<Exclude<Key, "ohlc">, ResourceData>;
Object.entries(groupedKeysToPath.date).forEach(([_key, path]) => {
const key = _key as Key;
if (key !== "ohlc") {
resourceDatasets[key] = createResourceDataset<"date">({
scale: "date",
path,
setActiveResources,
});
}
});
const price = createResourceDataset<"date", OHLC>({
scale: "date",
path: "/date-to-ohlc",
setActiveResources,
});
const datasets = {
price,
...resourceDatasets,
};
return datasets;
}
+36
View File
@@ -0,0 +1,36 @@
import groupedKeysToPath from "/src/../../datasets/grouped_keys_to_url_path.json";
import { createResourceDataset } from "./resource";
export function createHeightDatasets({
setActiveResources,
}: {
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
type Key = keyof typeof groupedKeysToPath.height;
type ResourceData = ReturnType<typeof createResourceDataset<"height">>;
const resourceDatasets = {} as Record<Exclude<Key, "ohlc">, ResourceData>;
Object.keys(groupedKeysToPath.height).forEach(([_key, path]) => {
const key = _key as Key;
if (key !== "ohlc") {
resourceDatasets[key] = createResourceDataset<"height">({
scale: "height",
path,
setActiveResources,
});
}
});
const price = createResourceDataset<"height", OHLC>({
scale: "height",
path: "/height-to-ohlc",
setActiveResources,
});
return {
...resourceDatasets,
price,
};
}
+17
View File
@@ -0,0 +1,17 @@
import { createDateDatasets } from "./date";
import { createHeightDatasets } from "./height";
export const scales = ["date" as const, "height" as const];
export const HEIGHT_CHUNK_SIZE = 10_000;
export function createDatasets({
setActiveResources,
}: {
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
return {
date: createDateDatasets({ setActiveResources }),
height: createHeightDatasets({ setActiveResources }),
} satisfies Record<ResourceScale, any>;
}
+246
View File
@@ -0,0 +1,246 @@
import { createLazyMemo } from "@solid-primitives/memo";
import {
ONE_DAY_IN_MS,
ONE_HOUR_IN_MS,
ONE_MINUTE_IN_MS,
} from "/src/scripts/utils/time";
import { createRWS } from "/src/solid/rws";
import { HEIGHT_CHUNK_SIZE } from ".";
export function createResourceDataset<
Scale extends ResourceScale,
Type extends OHLC | number = number,
>({
scale,
path,
setActiveResources,
}: {
scale: Scale;
path: string;
setActiveResources: Setter<Set<ResourceDataset<any, any>>>;
}) {
const baseURL = `${
location.hostname === "localhost"
? "http://localhost:3110"
: "https://api.satonomics.xyz"
}${path}`;
type Dataset = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>;
type Value = DatasetValue<
Type extends number ? SingleValueData : CandlestickData
>;
const fetchedJSONs = new Array(
(new Date().getFullYear() - new Date("2009-01-01").getFullYear()) *
(scale === "date" ? 2 : 8),
)
.fill(null)
.map((): FetchedResult<Scale, Type> => {
const json = createRWS<FetchedJSON<Scale, Type, Dataset> | null>(null);
return {
at: null,
json,
loading: false,
vec: createMemo(() => {
const map = json()?.dataset.map || null;
const chunkId = json()?.chunk.id!;
if (!map) {
return null;
}
if (Array.isArray(map)) {
return map.map(
(value, index) =>
({
number: chunkId + index,
time: (chunkId + index) as Time,
...(typeof value !== "number" && value !== null
? { ...(value as OHLC), value: value.close }
: { value: value === null ? NaN : (value as number) }),
}) as any as Value,
);
} else {
return Object.entries(map).map(
([date, value]) =>
({
number: new Date(date).valueOf() / ONE_DAY_IN_MS,
time: date,
...(typeof value !== "number" && value !== null
? { ...(value as OHLC), value: value.close }
: { value: value === null ? NaN : (value as number) }),
}) as any as Value,
);
}
}),
};
}) as FetchedResult<Scale, Type>[];
const _fetch = async (id: number) => {
const index =
scale === "date" ? id - 2009 : Math.floor(id / HEIGHT_CHUNK_SIZE);
if (
index < 0 ||
(scale === "date" && id > new Date().getUTCFullYear()) ||
(scale === "height" &&
id > 165 * 365 * (new Date().getUTCFullYear() - 2009))
) {
return;
}
const fetched = fetchedJSONs.at(index);
if (!fetched || fetched.loading) {
return;
} else if (fetched.at) {
const diff = new Date().valueOf() - fetched.at.valueOf();
if (
diff < ONE_MINUTE_IN_MS ||
(index < fetchedJSONs.findLastIndex((json) => json.at) &&
diff < ONE_HOUR_IN_MS)
) {
return;
}
}
fetched.loading = true;
let cache: Cache | undefined;
const urlWithQuery = `${baseURL}?chunk=${id}`;
if (!fetched.json()) {
try {
cache = await caches.open("resources");
const cachedResponse = await cache.match(urlWithQuery);
if (cachedResponse) {
const json = await convertResponseToJSON<Scale, Type>(cachedResponse);
if (json) {
console.log(`cache: ${path}?chunk=${id}`);
fetched.json.set(() => json);
}
}
} catch {}
}
try {
const fetchedResponse = await fetch(urlWithQuery);
if (!fetchedResponse.ok) {
fetched.loading = false;
return;
}
const clonedResponse = fetchedResponse.clone();
const json = await convertResponseToJSON<Scale, Type>(fetchedResponse);
if (json) {
console.log(`fetch: ${path}?chunk=${id}`);
const previousMap = fetched.json()?.dataset.map;
const newMap = json.dataset.map;
const previousLength = Object.keys(previousMap || []).length;
const newLength = Object.keys(newMap).length;
if (!newLength) {
fetched.loading = false;
return;
}
if (previousLength && previousLength <= newLength) {
const previousLastValue = Object.values(previousMap || []).at(-1);
const newLastValue = Object.values(newMap).at(-1);
if (typeof newLastValue === "number") {
if (previousLastValue === newLastValue) {
fetched.at = new Date();
fetched.loading = false;
return;
}
} else {
const previousLastOHLC = previousLastValue as OHLC;
const newLastOHLC = newLastValue as OHLC;
if (
previousLastOHLC.open === newLastOHLC.open &&
previousLastOHLC.high === newLastOHLC.high &&
previousLastOHLC.low === newLastOHLC.low &&
previousLastOHLC.close === newLastOHLC.close
) {
fetched.loading = false;
fetched.at = new Date();
return;
}
}
}
fetched.json.set(() => json);
if (cache) {
cache.put(urlWithQuery, clonedResponse);
}
}
} catch {
fetched.loading = false;
return;
}
fetched.at = new Date();
fetched.loading = false;
};
const resource: ResourceDataset<Scale, Type> = {
scale,
url: baseURL,
fetch: _fetch,
fetchedJSONs,
values: createLazyMemo(() => {
setActiveResources((resources) => resources.add(resource));
onCleanup(() =>
setActiveResources((resources) => {
resources.delete(resource);
return resources;
}),
);
const flat = fetchedJSONs.flatMap((fetched) => fetched.vec() || []);
return flat;
}),
drop() {
fetchedJSONs.forEach((fetched) => {
fetched.at = null;
fetched.json.set(null);
});
},
};
return resource;
}
async function convertResponseToJSON<
Scale extends ResourceScale,
Type extends number | OHLC,
>(response: Response) {
try {
return (await response.json()) as FetchedJSON<Scale, Type>;
} catch (_) {
return null;
}
}
+98
View File
@@ -0,0 +1,98 @@
type Datasets = ReturnType<typeof import("./index").createDatasets>;
type DateDatasets = Datasets["date"];
type HeightDatasets = Datasets["height"];
type AnyDatasets = DateDatasets | HeightDatasets;
type ResourceScale = (typeof import("./index").scales)[index];
type DatasetValue<T> = T & Numbered & Valued;
interface Dataset<
Scale extends ResourceScale,
Value extends SingleValueData | CandlestickData = SingleValueData,
> {
scale: Scale;
values: Accessor<DatasetValue<Value>[]>;
}
interface ResourceDataset<
Scale extends ResourceScale,
Type extends OHLC | number = number,
FetchedDataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
Value extends SingleValueData | CandlestickData = Type extends number
? SingleValueData
: CandlestickData,
> extends Dataset<Scale, Value> {
url: string;
fetch: (id: number) => void;
fetchedJSONs: FetchedResult<Scale, Type>[];
drop: VoidFunction;
}
interface FetchedResult<
Scale extends ResourceScale,
Type extends number | OHLC,
Dataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
Value extends DatasetValue<SingleValueData | CandlestickData> = DatasetValue<
Type extends number ? SingleValueData : CandlestickData
>,
> {
at: Date | null;
json: RWS<FetchedJSON<Scale, Type, Dataset> | null>;
vec: Accessor<Value[] | null>;
loading: boolean;
}
interface FetchedJSON<
Scale extends ResourceScale,
Type extends number | OHLC,
Dataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
> {
source: FetchedSource;
chunk: FetchedChunk;
dataset: FetchedDataset<Scale, Type, Dataset>;
}
type FetchedSource = string;
interface FetchedChunk {
id: number;
previous: string | null;
next: string | null;
}
interface FetchedDataset<
Scale extends ResourceScale,
Type extends number | OHLC,
Dataset extends
| FetchedDateDataset<Type>
| FetchedHeightDataset<Type> = Scale extends "date"
? FetchedDateDataset<Type>
: FetchedHeightDataset<Type>,
> {
version: number;
map: Dataset;
}
type FetchedDateDataset<T> = Record<string, T>;
type FetchedHeightDataset<T> = T[];
interface OHLC {
open: number;
high: number;
low: number;
close: number;
}
@@ -0,0 +1,11 @@
import { chartState } from "./state";
export function cleanChart() {
console.log("chart: clean");
try {
chartState.chart?.remove();
} catch {}
chartState.chart = null;
}
@@ -0,0 +1,68 @@
import {
createChart as createClassicChart,
createChartEx as createCustomChart,
CrosshairMode,
} from "lightweight-charts";
import { colors } from "../../utils/colors";
import { priceToUSLocale } from "../../utils/locale";
import { cleanChart } from "./clean";
import { HorzScaleBehaviorHeight } from "./horzScaleBehavior";
import { chartState } from "./state";
export function createChart(scale: ResourceScale) {
cleanChart();
console.log(`chart: create (scale: ${scale})`);
const { white } = colors;
const options: DeepPartialChartOptions = {
autoSize: true,
layout: {
fontFamily: "Lexend",
background: { color: "transparent" },
fontSize: 14,
textColor: white,
},
grid: {
vertLines: { visible: false },
horzLines: { visible: false },
},
leftPriceScale: {
// borderColor: white,
},
rightPriceScale: {
// borderColor: white,
},
timeScale: {
minBarSpacing: scale === "date" ? 0.05 : 0.005,
shiftVisibleRangeOnNewBar: false,
allowShiftVisibleRangeOnWhitespaceReplacement: false,
},
crosshair: {
mode: CrosshairMode.Normal,
horzLine: {
color: white,
labelBackgroundColor: white,
},
vertLine: {
color: white,
labelBackgroundColor: white,
},
},
localization: {
priceFormatter: priceToUSLocale,
locale: "en-us",
},
};
if (scale === "date") {
chartState.chart = createClassicChart("chart", options);
} else {
const horzScaleBehavior = new HorzScaleBehaviorHeight();
// @ts-ignore
chartState.chart = createCustomChart("chart", horzScaleBehavior, options);
}
}
@@ -0,0 +1,89 @@
// @ts-nocheck
// https://github.com/tradingview/lightweight-charts/blob/master/tests/e2e/graphics/test-cases/horizontal-price-scale.js
import { type IHorzScaleBehavior } from "lightweight-charts";
export class HorzScaleBehaviorHeight implements IHorzScaleBehavior<number> {
options() {}
setOptions() {}
preprocessData() {}
updateFormatter() {}
createConverterToInternalObj() {
return (price) => price;
}
key(item) {
return item;
}
cacheKey(item) {
return item;
}
convertHorzItemToInternal(item) {
return item;
}
formatHorzItem(item) {
return item;
}
formatTickmark(tickMark) {
return tickMark.time.toLocaleString("en-us");
}
maxTickMarkWeight(tickMarks) {
return tickMarks.reduce(markWithGreaterWeight, tickMarks[0]).weight;
}
fillWeightsForPoints(sortedTimePoints, startIndex) {
for (let index = startIndex; index < sortedTimePoints.length; ++index) {
sortedTimePoints[index].timeWeight = computeWeight(
sortedTimePoints[index].time,
);
}
}
}
function markWithGreaterWeight(a, b) {
return a.weight > b.weight ? a : b;
}
function computeWeight(value: number) {
// if (value === Math.ceil(value / 1000000) * 1000000) {
// return 12;
// }
if (value === Math.ceil(value / 100000) * 100000) {
return 11;
}
if (value === Math.ceil(value / 10000) * 10000) {
return 10;
}
if (value === Math.ceil(value / 1000) * 1000) {
return 9;
}
if (value === Math.ceil(value / 100) * 100) {
return 8;
}
if (value === Math.ceil(value / 50) * 50) {
return 7;
}
if (value === Math.ceil(value / 25) * 25) {
return 6;
}
if (value === Math.ceil(value / 10) * 10) {
return 5;
}
if (value === Math.ceil(value / 5) * 5) {
return 4;
}
if (value === Math.ceil(value)) {
return 3;
}
if (value * 2 === Math.ceil(value * 2)) {
return 1;
}
return 0;
}
@@ -0,0 +1,123 @@
import { colors } from "/src/scripts/utils/colors";
import { priceToUSLocale } from "/src/scripts/utils/locale";
import { ONE_DAY_IN_MS } from "/src/scripts/utils/time";
import { chartState } from "./state";
import { GENESIS_DAY } from "./whitespace";
export const setMinMaxMarkers = ({
scale,
candlesticks,
range,
lowerOpacity,
}: {
scale: ResourceScale;
candlesticks: DatasetValue<CandlestickData | SingleValueData>[];
range: TimeRange;
lowerOpacity: boolean;
}) => {
const first = candlesticks.at(0);
if (!first) return;
const offset =
scale === "date"
? first.number - new Date(GENESIS_DAY).valueOf() / ONE_DAY_IN_MS
: 0;
const slicedDataList = range
? candlesticks.slice(
Math.ceil(range.from - offset < 0 ? 0 : range.from - offset),
Math.floor(range.to - offset) + 1,
)
: [];
const series = chartState.priceSeries;
if (!series) return;
if (slicedDataList.length) {
const markers: (SeriesMarker<Time> & Numbered)[] = [];
const seriesIsCandlestick = series.seriesType() === "Candlestick";
[
{
mathFunction: "min" as const,
placementAttribute: seriesIsCandlestick
? ("low" as const)
: ("close" as const),
// valueAttribute: 'low' as const,
markerOptions: {
position: "belowBar" as const,
shape: "arrowUp" as const,
},
},
{
mathFunction: "max" as const,
placementAttribute: seriesIsCandlestick
? ("high" as const)
: ("close" as const),
// valueAttribute: 'high' as const,
markerOptions: {
position: "aboveBar" as const,
shape: "arrowDown" as const,
},
},
].map(
({
mathFunction,
placementAttribute,
// valueAttribute,
markerOptions,
}) => {
const value = Math[mathFunction](
// ...slicedDataList.map((data) => data[valueAttribute] || 0),
...slicedDataList.map(
(data) =>
(placementAttribute in data
? data[placementAttribute]
: data.value) || 0,
),
);
const placement = Math[mathFunction](
...slicedDataList.map(
(data) =>
(placementAttribute in data
? data[placementAttribute]
: data.value) || 0,
),
);
const candle = slicedDataList.find(
(data) =>
(placementAttribute in data
? data[placementAttribute]
: data.value) === placement,
);
return (
candle &&
markers.push({
...markerOptions,
// date: candle.date,
number: candle.number,
time: candle.time,
color: lowerOpacity ? colors.darkWhite : colors.white,
size: 0,
text: priceToUSLocale(value),
})
);
},
);
series.setMarkers(sortWhitespaceDataArray(markers));
}
};
function sortWhitespaceDataArray<T extends WhitespaceData & Numbered>(
array: T[],
) {
return array.sort(({ number: a }, { number: b }) => a - b);
}
@@ -0,0 +1,176 @@
import { createRWS } from "/src/solid/rws";
import { colors } from "../../utils/colors";
import { getNumberOfDaysBetweenTwoDates } from "../../utils/date";
import { debounce } from "../../utils/debounce";
import { webSockets } from "../../ws";
import { createCandlesticksSeries } from "../series/creators/candlesticks";
import { createSeriesLegend } from "../series/creators/legend";
import { createLineSeries } from "../series/creators/line";
import { setMinMaxMarkers } from "./markers";
import { chartState } from "./state";
import { initTimeScale } from "./time";
export const PRICE_SCALE_MOMENTUM_ID = "momentum";
export const applyPriceSeries = <
Scale extends ResourceScale,
T extends SingleValueData,
>({
chart,
datasets,
preset,
dataset: _dataset,
options,
activeResources,
}: {
chart: IChartApi;
datasets: Datasets;
preset: Preset;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
dataset?: Dataset<Scale, T>;
options?: PriceSeriesOptions;
}) => {
const id = options?.id || "price";
const title = options?.title || "Price";
const dataset = createMemo(() => _dataset || datasets[preset.scale].price);
const url = "url" in dataset() ? (dataset() as any).url : undefined;
const priceScaleOptions: DeepPartial<PriceScaleOptions> = {
...(options?.halved
? {
scaleMargins: {
top: 0.05,
bottom: 0.55,
},
}
: {}),
...(options?.id || options?.title
? {}
: {
mode: 1,
// mode: PriceScaleMode.Logarithmic,
}),
...options?.priceScaleOptions,
};
const seriesType = createRWS(
checkIfUpClose(chart, chartState.range) || "Candlestick",
);
const debouncedCallback = debounce((range: TimeRange | null) => {
try {
seriesType.set((previous) => checkIfUpClose(chart, range) || previous);
} catch {}
}, 50);
chart?.timeScale().subscribeVisibleTimeRangeChange(debouncedCallback);
onCleanup(
() =>
chart === chartState.chart &&
chartState.chart
?.timeScale()
.unsubscribeVisibleTimeRangeChange(debouncedCallback),
);
const lowerOpacity = options?.lowerOpacity || options?.halved || false;
if (options?.halved) {
options.seriesOptions = {
...options.seriesOptions,
priceScaleId: "left",
};
}
const [ohlcSeries, ohlcColors] = createCandlesticksSeries(chart, {
...options,
lowerOpacity,
});
const ohlcLegend = createSeriesLegend({
id,
presetId: preset.id,
title,
color: () => ohlcColors,
series: ohlcSeries,
disabled: () => seriesType() !== "Candlestick",
url,
});
ohlcSeries.priceScale().applyOptions(priceScaleOptions);
// ---
const lineColor = lowerOpacity ? colors.darkWhite : colors.white;
const lineSeries = createLineSeries(chart, {
color: lineColor,
...options?.seriesOptions,
});
const lineLegend = createSeriesLegend({
id,
presetId: preset.id,
title,
color: () => lineColor,
series: lineSeries,
disabled: () => seriesType() !== "Line",
visible: ohlcLegend.visible,
url,
});
lineSeries.priceScale().applyOptions(priceScaleOptions);
// ---
// setMinMaxMarkers({
// scale: preset.scale,
// candlesticks:
// dataset?.values() || datasets[preset.scale].price.values() || ([] as any),
// range: chartState.range,
// lowerOpacity,
// });
initTimeScale({
activeResources,
});
createEffect(() => {
const d = dataset();
lineSeries.setData(d.values());
ohlcSeries.setData(d.values());
});
createEffect(() => {
if (preset.scale === "date") {
const latest = webSockets.liveKrakenCandle.latest();
if (latest) {
ohlcSeries.update(latest);
lineSeries.update(latest);
}
}
});
return { ohlcLegend, lineLegend };
};
function checkIfUpClose(chart: IChartApi, range?: TimeRange | null) {
if (!range) return undefined;
const from = new Date(range.from);
const to = new Date(range.to);
const width = chart.timeScale().width();
const difference = getNumberOfDaysBetweenTwoDates(from, to);
return width / difference >= 2.05
? "Candlestick"
: width / difference <= 1.95
? "Line"
: undefined;
}
@@ -0,0 +1,40 @@
import { createChart } from "./create";
import { chartState } from "./state";
import { setWhitespace } from "./whitespace";
export function renderChart({
datasets,
legendSetter,
preset,
activeResources,
}: {
datasets: Datasets;
legendSetter: Setter<PresetLegend>;
preset: Preset;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
}) {
const scale = preset.scale;
createChart(scale);
const chart = chartState.chart;
if (!chart) return;
try {
setWhitespace(chart, scale);
console.log(`preset: ${preset.id}`);
const legend = preset.applyPreset({
chart,
datasets,
preset,
activeResources,
});
legendSetter(legend);
} catch (error) {
console.error("chart: render: failed", error);
}
}
@@ -0,0 +1,10 @@
import { getInitialRange } from "./time";
export const LOCAL_STORAGE_RANGE_KEY = "chart-range";
export const URL_PARAMS_RANGE_FROM_KEY = "from";
export const URL_PARAMS_RANGE_TO_KEY = "to";
export const chartState = {
chart: null as IChartApi | null,
range: getInitialRange(),
};
@@ -0,0 +1,110 @@
import { HEIGHT_CHUNK_SIZE } from "../../datasets";
import { debounce } from "../../utils/debounce";
import { writeURLParam } from "../../utils/urlParams";
import { setMinMaxMarkers } from "./markers";
import {
chartState,
LOCAL_STORAGE_RANGE_KEY,
URL_PARAMS_RANGE_FROM_KEY,
URL_PARAMS_RANGE_TO_KEY,
} from "./state";
const debouncedUpdateURLParams = debounce((range: TimeRange | null) => {
if (!range) return;
writeURLParam(URL_PARAMS_RANGE_FROM_KEY, String(range.from));
writeURLParam(URL_PARAMS_RANGE_TO_KEY, String(range.to));
localStorage.setItem(LOCAL_STORAGE_RANGE_KEY, JSON.stringify(range));
}, 1000);
export function initTimeScale({
activeResources,
}: {
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
}) {
setTimeScale(chartState.range);
const debouncedFetch = debounce((range: TimeRange | null) => {
if (!range) return;
let ids: number[] = [];
if (typeof range.from === "string" && typeof range.to === "string") {
const from = new Date(range.from).getUTCFullYear();
const to = new Date(range.to).getUTCFullYear();
ids = Array.from({ length: to - from + 1 }, (_, i) => i + from);
} else {
const from = Math.floor(Number(range.from) / HEIGHT_CHUNK_SIZE);
const to = Math.floor(Number(range.to) / HEIGHT_CHUNK_SIZE);
const length = to - from + 1;
ids = Array.from({ length }, (_, i) => (from + i) * HEIGHT_CHUNK_SIZE);
}
ids.forEach((id) => {
activeResources().forEach((resource) => resource.fetch(id));
});
}, 100);
debouncedFetch(chartState.range);
let timeout = setTimeout(() => {
chartState.chart?.timeScale().subscribeVisibleTimeRangeChange((range) => {
debouncedFetch(range);
debouncedUpdateURLParams(range);
range = range || chartState.range;
chartState.range = range;
});
}, 50);
onCleanup(() => clearTimeout(timeout));
}
export function getInitialRange(): TimeRange {
const urlParams = new URLSearchParams(window.location.search);
const urlFrom = urlParams.get(URL_PARAMS_RANGE_FROM_KEY);
const urlTo = urlParams.get(URL_PARAMS_RANGE_TO_KEY);
if (urlFrom && urlTo) {
return {
from: urlFrom,
to: urlTo,
} satisfies TimeRange;
}
const savedTimeRange = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_RANGE_KEY) || "null",
) as TimeRange | null;
if (savedTimeRange) {
return savedTimeRange;
}
const defaultTo = new Date();
const defaultFrom = new Date();
defaultFrom.setDate(defaultFrom.getUTCDate() - 6 * 30);
const defaultTimeRange = {
from: defaultFrom.toJSON().split("T")[0],
to: defaultTo.toJSON().split("T")[0],
} satisfies TimeRange;
return defaultTimeRange;
}
export function setTimeScale(range: TimeRange | null) {
if (range) {
console.log(range);
setTimeout(() => {
chartState.chart?.timeScale().setVisibleRange(range);
}, 1);
}
}
+9
View File
@@ -0,0 +1,9 @@
interface PriceSeriesOptions {
halved?: boolean;
title?: string;
id?: string;
lowerOpacity?: boolean;
inverseColors?: boolean;
seriesOptions?: DeepPartial<SeriesOptionsCommon>;
priceScaleOptions?: DeepPartial<PriceScaleOptions>;
}
@@ -0,0 +1,50 @@
import { dateToString, getNumberOfDaysBetweenTwoDates } from "../../utils/date";
import { ONE_DAY_IN_MS } from "../../utils/time";
import { createLineSeries } from "../series/creators/line";
export const DAY_BEFORE_GENESIS_DAY = "2009-01-02";
export const GENESIS_DAY = "2009-01-03";
// export const DAY_BEFORE_WHITEPAPER_DAY = "2008-10-30";
// export const WHITEPAPER_DAY = "2008-10-31";
const whitespaceStartDate = "1970-01-01";
const whitespaceEndDate = "2100-01-01";
const whitespaceDateDataset: (SingleValueData & Numbered)[] = new Array(
getNumberOfDaysBetweenTwoDates(
new Date(whitespaceStartDate),
new Date(whitespaceEndDate),
),
)
.fill(0)
.map((_, index) => {
const date = new Date(whitespaceStartDate);
date.setUTCDate(date.getUTCDay() + index);
return {
number: date.valueOf() / ONE_DAY_IN_MS,
time: dateToString(date),
value: NaN,
};
});
const whitespaceHeightDataset: (WhitespaceData & Numbered)[] = new Array(
840_000,
)
.fill(0)
.map(
(_, index) =>
({
time: index,
number: index,
}) as any,
);
export function setWhitespace(chart: IChartApi, scale: ResourceScale) {
const whitespaceSeries = createLineSeries(chart);
if (scale === "date") {
whitespaceSeries.setData(whitespaceDateDataset);
} else {
whitespaceSeries.setData(whitespaceHeightDataset);
}
}
@@ -0,0 +1,28 @@
import { defaultSeriesOptions } from "./options";
type AreaOptions = DeepPartial<AreaStyleOptions & SeriesOptionsCommon>;
export const createAreaSeries = (
chart: IChartApi,
options?: AreaOptions & {
color?: string;
},
) => {
const { color } = options || {};
// const fillColor = `${color}11`;
const fillColor = color;
const seriesOptions: AreaOptions = {
// priceScaleId: 'left',
...defaultSeriesOptions,
lineColor: color,
topColor: fillColor,
bottomColor: fillColor,
...options,
};
const series = chart.addAreaSeries(seriesOptions);
return series;
};
@@ -0,0 +1,52 @@
import { colors } from "/src/scripts/utils/colors";
import { defaultSeriesOptions } from "./options";
const DEFAULT_BASELINE_TOP_COLOR = colors.profit;
const DEFAULT_BASELINE_BOTTOM_COLOR = colors.loss;
export const DEFAULT_BASELINE_COLORS = [
DEFAULT_BASELINE_TOP_COLOR,
DEFAULT_BASELINE_BOTTOM_COLOR,
];
export const createBaseLineSeries = (
chart: IChartApi,
options: BaselineSeriesOptions,
) => {
const {
title,
color,
topColor,
topLineColor,
bottomColor,
bottomLineColor,
base,
lineColor,
} = options;
const allTopColor = topColor || color || DEFAULT_BASELINE_TOP_COLOR;
const topFillColor = `${allTopColor}`;
const allBottomColor = bottomColor || color || DEFAULT_BASELINE_BOTTOM_COLOR;
const bottomFillColor = `${allBottomColor}`;
const seriesOptions: DeepPartialBaselineOptions = {
priceScaleId: "right",
...defaultSeriesOptions,
lineWidth: 1,
...options,
...options.options,
...(base ? { baseValue: { type: "price", price: base } } : {}),
topLineColor: topLineColor || lineColor || allTopColor,
topFillColor1: topFillColor,
topFillColor2: topFillColor,
bottomLineColor: bottomLineColor || lineColor || allBottomColor,
bottomFillColor1: bottomFillColor,
bottomFillColor2: bottomFillColor,
title,
};
const series = chart.addBaselineSeries(seriesOptions);
return series;
};
@@ -0,0 +1,42 @@
import { colors } from "/src/scripts/utils/colors";
export const createCandlesticksSeries = (
chart: IChartApi,
options: PriceSeriesOptions,
): [ISeriesApi<"Candlestick">, string[]] => {
const { inverseColors, lowerOpacity } = options;
const upColor = lowerOpacity
? inverseColors
? colors.darkLoss
: colors.darkProfit
: inverseColors
? colors.loss
: colors.profit;
const downColor = lowerOpacity
? inverseColors
? colors.darkProfit
: colors.darkLoss
: inverseColors
? colors.profit
: colors.loss;
const candlestickSeries = chart.addCandlestickSeries({
baseLineVisible: false,
upColor,
wickUpColor: upColor,
downColor,
wickDownColor: downColor,
borderVisible: false,
priceLineVisible: false,
baseLineColor: "",
borderColor: "",
borderDownColor: "",
borderUpColor: "",
// lastValueVisible: false,
...options.seriesOptions,
});
return [candlestickSeries, [upColor, downColor]];
};
@@ -0,0 +1,30 @@
import { PRICE_SCALE_MOMENTUM_ID } from "../../chart/price";
import { defaultSeriesOptions } from "./options";
type HistogramOptions = DeepPartial<
HistogramStyleOptions & SeriesOptionsCommon
>;
export const createHistogramSeries = (
chart: IChartApi,
options?: HistogramOptions,
) => {
const seriesOptions: HistogramOptions = {
priceScaleId: "left",
...defaultSeriesOptions,
...options,
};
const series = chart.addHistogramSeries(seriesOptions);
try {
chart.priceScale(PRICE_SCALE_MOMENTUM_ID).applyOptions({
scaleMargins: {
top: 0.9,
bottom: 0,
},
});
} catch {}
return series;
};
@@ -0,0 +1,75 @@
import {
readBooleanFromStorage,
saveToStorage,
} from "/src/scripts/utils/storage";
import {
readBooleanURLParam,
writeURLParam,
} from "/src/scripts/utils/urlParams";
import { createRWS } from "/src/solid/rws";
import { chartState } from "../../chart/state";
import { setTimeScale } from "../../chart/time";
export function createSeriesLegend({
id,
presetId,
title,
color,
series,
defaultVisible = true,
disabled: _disabled,
visible: _visible,
url,
}: {
id: string;
presetId: string;
title: string;
color: Accessor<string | string[]>;
series: ISeriesApi<SeriesType>;
defaultVisible?: boolean;
disabled?: Accessor<boolean>;
visible?: RWS<boolean>;
url?: string;
}) {
const storageID = `${presetId}-${id}`;
const visible =
_visible ||
createRWS(
readBooleanURLParam(id) ??
readBooleanFromStorage(storageID) ??
defaultVisible,
);
const disabled = createMemo(_disabled || (() => false));
createEffect(() => {
const v = visible();
const d = disabled();
series.applyOptions({
visible: !d && v,
});
setTimeScale(chartState.range);
if (v !== defaultVisible) {
writeURLParam(id, v);
saveToStorage(storageID, v);
} else {
writeURLParam(id, undefined);
saveToStorage(storageID, undefined);
}
});
return {
id,
title,
series,
color,
visible,
disabled,
url,
};
}
@@ -0,0 +1,10 @@
import { defaultSeriesOptions } from "./options";
export const createLineSeries = (
chart: IChartApi,
options?: DeepPartialLineOptions,
) =>
chart.addLineSeries({
...defaultSeriesOptions,
...options,
});
@@ -0,0 +1,7 @@
export const defaultSeriesOptions: DeepPartial<SeriesOptionsCommon> = {
// @ts-ignore
lineWidth: 1.5,
priceLineVisible: false,
baseLineVisible: false,
baseLineColor: "",
};
@@ -0,0 +1,13 @@
interface BaselineSeriesOptions {
color?: string;
topColor?: string;
topLineColor?: string;
bottomColor?: string;
bottomLineColor?: string;
lineColor?: string;
base?: number;
options?: DeepPartialBaselineOptions;
title?: string;
}
type SeriesLegend = ReturnType<typeof import("./legend").createSeriesLegend>;
@@ -0,0 +1,45 @@
export const resetRightPriceScale = (
chart: IChartApi,
options?: FullPriceScaleOptions,
) => {
const finalOptions = {
...options,
scaleMargins: {
...(options?.halved
? {
top: 0.5,
bottom: 0.05,
}
: {
top: 0.1,
bottom: 0.1,
}),
...options?.scaleMargins,
},
};
chart.priceScale("right").applyOptions(finalOptions);
return finalOptions;
};
export const resetLeftPriceScale = (
chart: IChartApi,
options?: FullPriceScaleOptions,
) =>
chart.priceScale("left").applyOptions({
visible: false,
...options,
scaleMargins: {
...(options?.halved
? {
top: 0.475,
bottom: 0.025,
}
: {
top: 0.25,
bottom: 0.25,
}),
...options?.scaleMargins,
},
});
@@ -0,0 +1,3 @@
interface FullPriceScaleOptions extends DeepPartial<PriceScaleOptions> {
halved?: boolean;
}
+218
View File
@@ -0,0 +1,218 @@
import {
addressCohortsBySize,
addressCohortsByType,
} from "../../datasets/consts/address";
import { liquidities } from "../../datasets/consts/liquidities";
import { colors } from "../../utils/colors";
import { createCohortPresetList } from "../templates/cohort";
import { applyMultipleSeries, SeriesType } from "../templates/multiple";
export function createPresets({
scale,
datasets,
}: {
scale: ResourceScale;
datasets: Datasets;
}): PartialPresetFolder {
return {
name: "Addresses",
tree: [
{
scale,
name: `Total Non Empty Addresses`,
title: `Total Non Empty Address`,
description: "",
icon: IconTablerWallet,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Total Non Empty Address`,
color: colors.bitcoin,
seriesType: SeriesType.Area,
dataset: params.datasets[scale].address_count,
},
],
});
},
},
{
scale,
name: `New Addresses`,
title: `New Addresses`,
description: "",
icon: IconTablerSparkles,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `New Addresses`,
color: colors.white,
dataset: params.datasets[scale].created_addresses,
},
],
});
},
},
{
scale,
name: `Total Addresses Created`,
title: `Total Addresses Created`,
description: "",
icon: IconTablerArchive,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Total Addresses Created`,
color: colors.bitcoin,
seriesType: SeriesType.Area,
dataset: params.datasets[scale].created_addresses,
},
],
});
},
},
{
scale,
name: `Total Empty Addresses`,
title: `Total Empty Addresses`,
description: "",
icon: IconTablerTrash,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Total Empty Addresses`,
color: colors.darkWhite,
seriesType: SeriesType.Area,
dataset: params.datasets[scale].empty_addresses,
},
],
});
},
},
{
name: "By Size",
tree: addressCohortsBySize.map(({ key, name }) =>
createAddressPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
}),
),
},
{
scale,
name: "By Type",
tree: addressCohortsByType.map(({ key, name }) =>
createAddressPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
}),
),
},
],
} satisfies PartialPresetFolder;
}
function createAddressPresetFolder<Scale extends ResourceScale>({
datasets,
scale,
color,
name,
datasetKey,
}: {
datasets: Datasets;
scale: Scale;
name: string;
datasetKey: AddressCohortKey;
color: string;
}): PartialPresetFolder {
return {
name,
tree: [
createAddressCountPreset({ scale, name, datasetKey, color }),
...createCohortPresetList({
title: name,
datasets,
scale,
name,
color,
datasetKey,
}),
{
name: `Split By Liquidity`,
tree: liquidities.map(
(liquidity): PartialPresetFolder => ({
name: liquidity.name,
tree: createCohortPresetList({
title: `${liquidity.name} ${name}`,
name: `${liquidity.name} ${name}`,
datasets,
scale,
color,
datasetKey: `${liquidity.key}_${datasetKey}`,
}),
}),
),
},
],
};
}
export function createAddressCountPreset<Scale extends ResourceScale>({
scale,
color,
name,
datasetKey,
}: {
scale: Scale;
name: string;
datasetKey: AddressCohortKey;
color: string;
}): PartialPreset {
return {
scale,
name: `Address Count`,
title: `${name} Address Count`,
icon: IconTablerAddressBook,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Address Count",
color,
dataset: params.datasets[scale][`${datasetKey}_address_count`],
},
],
});
},
description: "",
};
}
+221
View File
@@ -0,0 +1,221 @@
import { colors } from "../../utils/colors";
import { applyMultipleSeries, SeriesType } from "../templates/multiple";
export function createPresets() {
const scale: ResourceScale = "date";
return {
name: "Blocks",
tree: [
{
scale,
icon: IconTablerWall,
name: "Height",
title: "Block Height",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Height",
color: colors.bitcoin,
dataset: params.datasets.date.last_height,
},
],
});
},
},
{
scale,
name: "Mined",
tree: [
{
scale,
icon: IconTablerCube,
name: "Daily Sum",
title: "Daily Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1d_target,
options: {
lineStyle: 3,
// lineStyle: LineStyle.LargeDashed,
},
},
{
title: "1W Avg.",
color: colors.momentumYellow,
dataset: params.datasets.date.blocks_mined_1w_sma,
defaultVisible: false,
},
{
title: "1M Avg.",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1m_sma,
},
{
title: "Mined",
color: colors.darkBitcoin,
dataset: params.datasets.date.blocks_mined,
},
],
});
},
},
{
scale,
icon: IconTablerLetterW,
name: "Weekly Sum",
title: "Weekly Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1w_target,
options: {
lineStyle: 3,
// lineStyle: LineStyle.LargeDashed,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1w_sum,
},
],
});
},
},
{
scale,
icon: IconTablerLetterM,
name: "Monthly Sum",
title: "Monthly Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1m_target,
options: {
// lineStyle: LineStyle.LargeDashed,
lineStyle: 3,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1m_sum,
},
],
});
},
},
{
scale,
icon: IconTablerLetterY,
name: "Yearly Sum",
title: "Yearly Sum Of Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Target",
color: colors.white,
dataset: params.datasets.date.blocks_mined_1y_target,
options: {
lineStyle: 3,
// lineStyle: LineStyle.LargeDashed,
},
},
{
title: "Sum Mined",
color: colors.bitcoin,
dataset: params.datasets.date.blocks_mined_1y_sum,
},
],
});
},
},
{
scale,
icon: IconTablerWall,
name: "Total",
title: "Total Blocks Mined",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Mined",
color: colors.bitcoin,
seriesType: SeriesType.Area,
dataset: params.datasets.date.total_blocks_mined,
},
],
});
},
},
],
},
{
scale,
icon: IconTablerStack3,
name: "Cumulative Size",
title: "Cumulative Block Size",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Size (MB)",
color: colors.darkWhite,
seriesType: SeriesType.Area,
dataset: params.datasets.date.cumulative_block_size,
},
],
});
},
},
],
} satisfies PartialPresetFolder;
}
File diff suppressed because it is too large Load Diff
+127
View File
@@ -0,0 +1,127 @@
import {
fromXCohorts,
fromXToYCohorts,
upToCohorts,
xthCohorts,
yearCohorts,
} from "../../datasets/consts/age";
import { colors } from "../../utils/colors";
import { createCohortPresetFolder } from "../templates/cohort";
import { applyMultipleSeries } from "../templates/multiple";
export function createPresets({
scale,
datasets,
}: {
scale: ResourceScale;
datasets: Datasets;
}) {
return {
name: "Hodlers",
tree: [
{
scale,
name: `Hodl Supply`,
title: `Hodl Supply`,
description: "",
icon: IconTablerRipple,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `24h`,
color: colors.up_to_1d,
dataset:
params.datasets.date
.up_to_1d_supply_to_circulating_supply_ratio,
},
...fromXToYCohorts.map(({ key, name, legend }) => ({
title: legend,
color: colors[key],
dataset:
params.datasets.date[
`${key}_supply_to_circulating_supply_ratio`
],
})),
{
title: `15y+`,
color: colors.from_15y,
dataset:
params.datasets.date
.from_15y_supply_to_circulating_supply_ratio,
},
],
});
},
},
...xthCohorts.map(({ key, name, legend }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name: legend,
datasetKey: key,
title: name,
}),
),
{
name: "Up To X",
tree: upToCohorts.map(({ key, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
title: name,
}),
),
},
{
name: "From X To Y",
tree: fromXToYCohorts.map(({ key, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
title: name,
}),
),
},
{
name: "From X",
tree: fromXCohorts.map(({ key, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
title: name,
}),
),
},
{
name: "Years",
tree: yearCohorts.map(({ key, name }) =>
createCohortPresetFolder({
datasets,
scale,
color: colors[key],
name,
datasetKey: key,
title: name,
}),
),
},
],
} satisfies PartialPresetFolder;
}
+296
View File
@@ -0,0 +1,296 @@
import { createRWS } from "/src/solid/rws";
import { colors } from "../utils/colors";
import { replaceHistory } from "../utils/history";
import { stringToId } from "../utils/id";
import { resetURLParams } from "../utils/urlParams";
import { createPresets as createAddressesPresets } from "./addresses";
import { createPresets as createBlocksPresets } from "./blocks";
import { createPresets as createCoinblocksPresets } from "./coinblocks";
import { createPresets as createHodlersPresets } from "./hodlers";
import { createPresets as createMarketPresets } from "./market";
import { createPresets as createMinersPresets } from "./miners";
import { createCohortPresetList } from "./templates/cohort";
import { createPresets as createTransactionsPresets } from "./transactions";
export const LOCAL_STORAGE_FAVORITES_KEY = "favorites";
export const LOCAL_STORAGE_FOLDERS_KEY = "folders";
export const LOCAL_STORAGE_HISTORY_KEY = "history";
export const LOCAL_STORAGE_SELECTED_KEY = "preset";
export const LOCAL_STORAGE_VISITED_KEY = "visited";
export function createPresets(datasets: Datasets): Presets {
const partialTree = [
{
name: "Dashboards (Coming soon)",
tree: [],
},
{
name: "Charts",
tree: [
{
name: "By Date",
tree: [
createMarketPresets({ scale: "date", datasets }),
createBlocksPresets(),
createMinersPresets("date"),
createTransactionsPresets("date"),
...createCohortPresetList({
datasets,
scale: "date",
color: colors.bitcoin,
datasetKey: "",
name: "",
title: "",
}),
createHodlersPresets({ scale: "date", datasets }),
createAddressesPresets({ scale: "date", datasets }),
createCoinblocksPresets({ scale: "date", datasets }),
],
} satisfies PartialPresetFolder,
{
name: "By Height (Coming soon)",
tree: [
// createMarketPresets({ scale: "height", datasets }),
// createMinersPresets("height"),
// createTransactionsPresets("height"),
// ...createCohortPresetList({
// datasets,
// scale: "height",
// color: colors.bitcoin,
// name: "",
// datasetKey: "",
// title: "",
// }),
// createHodlersPresets({ scale: "height", datasets }),
// createAddressesPresets({ scale: "height", datasets }),
// createCoinblocksPresets({ scale: "height", datasets }),
],
} satisfies PartialPresetFolder,
],
},
];
const { list, ids, tree } = flatten(partialTree);
checkIfDuplicateIds(ids);
setIsFavorites(list);
setVisited(list);
const favorites = createMemo(() =>
list.filter((preset) => preset.isFavorite()),
);
createEffect(() => {
localStorage.setItem(
LOCAL_STORAGE_FAVORITES_KEY,
JSON.stringify(favorites().map((p) => p.id)),
);
});
const visited = createMemo(() => list.filter((preset) => preset.visited()));
createEffect(() => {
localStorage.setItem(
LOCAL_STORAGE_VISITED_KEY,
JSON.stringify(visited().map((p) => p.id)),
);
});
createEffect(() => {
const serializedHistory: SerializedPresetsHistory = history().map(
({ preset, date }) => ({
p: preset.id,
d: date.valueOf(),
}),
);
localStorage.setItem(
LOCAL_STORAGE_HISTORY_KEY,
JSON.stringify(serializedHistory),
);
});
const history: PresetsHistorySignal = createRWS(getHistory(list), {
equals: false,
});
const selected = createRWS(findInitialPreset(list), {
equals: false,
});
createEffect((previousPreset: Preset) => {
if (previousPreset && previousPreset !== selected()) {
resetURLParams();
}
return selected();
}, selected());
createEffect(() => selected().visited.set(true));
const select = (preset: Preset) => {
if (selected().id === preset.id) {
return;
}
history.set((l) => {
l.unshift({
date: new Date(),
preset,
});
return l;
});
_select(preset, selected.set);
};
const openedFolders = createRWS(
new Set(
JSON.parse(
localStorage.getItem(LOCAL_STORAGE_FOLDERS_KEY) || "[]",
) as string[],
),
{
equals: false,
},
);
createEffect(() => {
localStorage.setItem(
LOCAL_STORAGE_FOLDERS_KEY,
JSON.stringify(Array.from(openedFolders())),
);
});
return {
tree,
list,
selected,
favorites,
history,
select,
openedFolders,
};
}
function _select(preset: Preset, set: Setter<Preset>) {
const key = LOCAL_STORAGE_SELECTED_KEY;
const value = preset.id;
localStorage.setItem(key, value);
replaceHistory({ pathname: `/${value}` });
set(preset);
}
function flatten(partialTree: PartialPresetTree) {
const result: { list: Preset[]; ids: string[] } = { list: [], ids: [] };
const _flatten = (partialTree: PartialPresetTree, path?: FilePath) => {
partialTree.forEach((anyPreset) => {
if ("tree" in anyPreset) {
const id = stringToId(
`${(path || [])?.map(({ name }) => name).join(" ")} ${anyPreset.name} folder`,
);
const presetFolder: PresetFolder = {
...anyPreset,
tree: anyPreset.tree as PresetTree,
id,
};
Object.assign(anyPreset, presetFolder);
result.ids.push(presetFolder.id);
return _flatten(presetFolder.tree, [
...(path || []),
{
name: presetFolder.name,
id: presetFolder.id,
},
]);
} else {
const preset = {
...anyPreset,
path: path || [],
isFavorite: createRWS(false),
visited: createRWS(false),
id: `${anyPreset.scale}-to-${stringToId(anyPreset.title)}`,
} satisfies Preset;
result.list.push(Object.assign(anyPreset, preset));
result.ids.push(preset.id);
}
});
};
_flatten(partialTree);
return { ...result, tree: partialTree as PresetTree };
}
function checkIfDuplicateIds(ids: string[]) {
if (ids.length !== new Set(ids).size) {
const m = new Map<string, number>();
ids.forEach((id) => {
m.set(id, (m.get(id) || 0) + 1);
});
console.log(
[...m.entries()].filter(([_, value]) => value > 1).map(([key, _]) => key),
);
throw Error("ID duplicate");
}
}
function findInitialPreset(presets: Preset[]): Preset {
const urlPreset = document.location.pathname.substring(1);
return (
(urlPreset &&
(presets.find((preset) => preset.id === urlPreset) ||
presets.find(
(preset) =>
preset.id === localStorage.getItem(LOCAL_STORAGE_SELECTED_KEY),
))) ||
presets[0]
);
}
function setIsFavorites(list: Preset[]) {
(
JSON.parse(
localStorage.getItem(LOCAL_STORAGE_FAVORITES_KEY) || "[]",
) as string[]
).forEach((id) => {
list.find((preset) => preset.id === id)?.isFavorite.set(true);
});
}
function setVisited(list: Preset[]) {
(
JSON.parse(
localStorage.getItem(LOCAL_STORAGE_VISITED_KEY) || "[]",
) as string[]
).forEach((id) => {
list.find((preset) => preset.id === id)?.visited.set(true);
});
}
function getHistory(list: Preset[]): PresetsHistory {
return (
JSON.parse(
localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY) || "[]",
) as SerializedPresetsHistory
).flatMap(({ p, d }) => {
const preset = list.find((preset) => preset.id === p);
return preset ? [{ preset, date: new Date(d) }] : [];
});
}
@@ -0,0 +1,78 @@
import { averages } from "/src/scripts/datasets/date";
import { colors } from "/src/scripts/utils/colors";
import { applyMultipleSeries } from "../../templates/multiple";
export function createPresets(datasets: Datasets): PartialPresetFolder {
const scale: ResourceScale = "date";
return {
name: "Averages",
tree: [
{
scale,
icon: IconTablerMathAvg,
name: "All",
title: "All Averages",
applyPreset(params) {
return applyMultipleSeries({
...params,
list: averages.map((average) => ({
title: average.key.toUpperCase(),
color: colors[`_${average.key}`],
dataset: params.datasets.date[`price_${average.key}_sma`],
})),
});
},
description: "",
},
...averages.map(({ name, key }) =>
createPresetFolder({
datasets,
scale,
color: colors[`_${key}`],
name,
key,
}),
),
],
};
}
function createPresetFolder({
scale,
datasets,
color,
name,
key,
}: {
datasets: Datasets;
scale: ResourceScale;
color: string;
name: string;
key: AverageName;
}) {
return {
// id,
// name,
// tree: [
// {
scale,
name,
description: "",
icon: IconTablerMathAvg,
title: `${name} Moving Average`,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
{
title: `SMA`,
color,
dataset: datasets.date[`price_${key}_sma`],
},
],
});
},
} satisfies PartialPreset;
}
+77
View File
@@ -0,0 +1,77 @@
import { colors } from "../../utils/colors";
import { applyMultipleSeries } from "../templates/multiple";
import { createPresets as createAveragesPresets } from "./averages";
import { createPresets as createIndicatorsPresets } from "./indicators";
import { createPresets as createReturnsPresets } from "./returns";
export function createPresets({
scale,
datasets,
}: {
scale: ResourceScale;
datasets: Datasets;
}) {
return {
name: "Market",
tree: [
{
scale,
icon: IconTablerCurrencyDollar,
name: "Price",
title: "Market Price",
applyPreset(params) {
return applyMultipleSeries({ ...params });
},
description: "",
},
{
scale,
icon: IconTablerPercentage,
name: "Performance",
title: "Market Performance",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceOptions: {
id: "performance",
title: "Performance",
priceScaleOptions: {
mode: 2,
},
},
});
},
description: "",
},
{
scale,
icon: IconTablerInfinity,
name: "Capitalization",
title: "Market Capitalization",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Market Cap.",
dataset: params.datasets[scale].market_cap,
color: colors.bitcoin,
},
],
});
},
description: "",
},
...(scale === "date"
? ([
createAveragesPresets(datasets),
createReturnsPresets(datasets),
createIndicatorsPresets(datasets),
] satisfies PartialPresetTree)
: []),
],
} satisfies PartialPresetFolder;
}
@@ -0,0 +1,6 @@
export function createPresets(datasets: Datasets) {
return {
name: "Indicators",
tree: [],
} satisfies PartialPresetFolder;
}
@@ -0,0 +1,79 @@
import {
compoundReturns,
totalReturns,
} from "/src/scripts/datasets/consts/returns";
import { applyMultipleSeries, SeriesType } from "../../templates/multiple";
export function createPresets(datasets: Datasets) {
return {
name: "Returns",
tree: [
{
name: "Total",
tree: [
...totalReturns.map(({ name, key }) =>
createPreset({
scale: "date",
datasets,
name,
title: `${name} Total`,
key: `${key}_total`,
}),
),
],
},
{
name: "Compound",
tree: [
...compoundReturns.map(({ name, key }) =>
createPreset({
scale: "date",
datasets,
name,
title: `${name} Compound`,
key: `${key}_compound`,
}),
),
],
},
],
} satisfies PartialPresetFolder;
}
function createPreset({
scale,
datasets,
name,
title,
key,
}: {
scale: ResourceScale;
datasets: Datasets;
name: string;
title: string;
key: `${TotalReturnKey}_total` | `${CompoundReturnKey}_compound`;
}): PartialPreset {
return {
scale,
name,
description: "",
icon: IconTablerReceiptTax,
title: `${title} Return`,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Return (%)`,
seriesType: SeriesType.Based,
dataset: datasets.date[`price_${key}_return`],
},
],
});
},
};
}
+902
View File
@@ -0,0 +1,902 @@
import { colors } from "../../utils/colors";
import { applyMultipleSeries, SeriesType } from "../templates/multiple";
export function createPresets(scale: ResourceScale) {
return {
name: "Miners",
tree: [
{
name: "Coinbases",
tree: [
...(scale === "date"
? ([
{
name: "Last",
tree: [
{
scale,
icon: IconTablerCoinBitcoin,
name: "In Bitcoin",
title: "Last Coinbase (In Bitcoin)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.bitcoin,
dataset: params.datasets[scale].last_coinbase,
},
],
});
},
},
{
scale,
icon: IconTablerCoin,
name: "In Dollars",
title: "Last Coinbase (In Dollars)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.dollars,
dataset:
params.datasets[scale].last_coinbase_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Daily Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Daily Sum Of Bitcoin Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].coinbase,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Daily Sum Of Dollar Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale].coinbase_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Yearly Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Yearly Sum Of Bitcoin Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].coinbase_1y_sum,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Yearly Sum Of Dollar Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale]
.coinbase_in_dollars_1y_sum,
},
],
});
},
},
],
},
{
scale,
name: "Cumulative",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Cumulative Bitcoin Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Bitcoin)",
color: colors.bitcoin,
dataset:
params.datasets[scale].cumulative_coinbase,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Cumulative Dollar Coinbases",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Coinbases (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale]
.cumulative_coinbase_in_dollars,
},
],
});
},
},
],
},
] satisfies PartialPresetTree)
: []),
],
},
{
name: "Subsidies",
tree: [
...(scale === "date"
? ([
{
name: "Last",
tree: [
{
scale,
icon: IconTablerCoinBitcoin,
name: "In Bitcoin",
title: "Last Subsidy (In Bitcoin)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.bitcoin,
dataset: params.datasets[scale].last_subsidy,
},
],
});
},
},
{
scale,
icon: IconTablerCoin,
name: "In Dollars",
title: "Last Subsidy (In Dollars)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.dollars,
dataset:
params.datasets[scale].last_subsidy_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Daily Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Daily Sum Of Bitcoin Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].subsidy,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Daily Sum Of Dollar Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale].subsidy_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Yearly Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Yearly Sum Of Bitcoin Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].subsidy_1y_sum,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Yearly Sum Of Dollar Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale]
.subsidy_in_dollars_1y_sum,
},
],
});
},
},
],
},
{
scale,
name: "Cumulative",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Cumulative Bitcoin Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Bitcoin)",
color: colors.bitcoin,
dataset:
params.datasets[scale].cumulative_subsidy,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Cumulative Dollar Subsidies",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidies (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale]
.cumulative_subsidy_in_dollars,
},
],
});
},
},
],
},
] satisfies PartialPresetTree)
: []),
],
},
{
name: "Fees",
tree: [
...(scale === "date"
? ([
{
name: "Last",
tree: [
{
scale,
icon: IconTablerCoinBitcoin,
name: "In Bitcoin",
title: "Last Fees (In Bitcoin)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.bitcoin,
dataset: params.datasets[scale].last_fees,
},
],
});
},
},
{
scale,
icon: IconTablerCoin,
name: "In Dollars",
title: "Last Fees (In Dollars)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Last",
color: colors.dollars,
dataset:
params.datasets[scale].last_fees_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Daily Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Daily Sum Of Bitcoin Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].fees,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Daily Sum Of Dollar Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Dollars)",
color: colors.dollars,
dataset: params.datasets[scale].fees_in_dollars,
},
],
});
},
},
],
},
{
scale,
name: "Yearly Sum",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Yearly Sum Of Bitcoin Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].fees_1y_sum,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Yearly Sum Of Dollar Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale].fees_in_dollars_1y_sum,
},
],
});
},
},
],
},
{
scale,
name: "Cumulative",
tree: [
{
scale,
icon: IconTablerMoneybag,
name: "In Bitcoin",
title: "Cumulative Bitcoin Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Bitcoin)",
color: colors.bitcoin,
dataset: params.datasets[scale].cumulative_fees,
},
],
});
},
},
{
scale,
icon: IconTablerCash,
name: "In Dollars",
title: "Cumulative Dollar Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Fees (Dollars)",
color: colors.dollars,
dataset:
params.datasets[scale]
.cumulative_fees_in_dollars,
},
],
});
},
},
],
},
] satisfies PartialPresetTree)
: []),
],
},
{
scale,
icon: IconTablerSwords,
name: "Subsidy V. Fees",
title: "Subsidy V. Fees",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Subsidy (%)",
color: colors.bitcoin,
dataset: params.datasets[scale].subsidy_to_coinbase_ratio,
},
{
title: "Fees (%)",
color: colors.darkBitcoin,
dataset: params.datasets[scale].fees_to_coinbase_ratio,
},
],
});
},
},
...(scale === "date"
? ([
{
scale,
icon: IconTablerCalculator,
name: "Puell Multiple",
title: "Puell Multiple",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "Multiple",
color: colors.bitcoin,
dataset: params.datasets.date.puell_multiple,
},
],
});
},
},
{
scale,
icon: IconTablerPick,
name: "Hash Rate",
title: "Hash Rate (EH/s)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "1M SMA",
color: colors.momentumYellow,
dataset: params.datasets.date.hash_rate_1m_sma,
},
{
title: "1W SMA",
color: colors.bitcoin,
dataset: params.datasets.date.hash_rate_1w_sma,
},
{
title: "Rate",
color: colors.darkBitcoin,
dataset: params.datasets.date.hash_rate,
},
],
});
},
},
{
scale,
icon: IconTablerRibbonHealth,
name: "Hash Ribbon",
title: "Hash Ribbon (EH/s)",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "1M SMA",
color: colors.profit,
dataset: params.datasets.date.hash_rate_1m_sma,
},
{
title: "2M SMA",
color: colors.loss,
dataset: params.datasets.date.hash_rate_2m_sma,
},
],
});
},
},
{
scale,
icon: IconTablerTag,
name: "Hash Price",
title: "Hash Price",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Price ($/PH/s)",
color: colors.dollars,
dataset: params.datasets.date.hash_price,
},
],
});
},
},
] satisfies PartialPreset[])
: []),
{
scale,
icon: IconTablerWeight,
name: "Difficulty",
title: "Difficulty",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "Difficulty",
color: colors.bitcoin,
dataset: params.datasets[scale].difficulty,
},
],
});
},
},
...(scale === "date"
? ([
{
scale,
icon: IconTablerAdjustments,
name: "Difficulty Adjustment",
title: "Difficulty Adjustment",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Adjustment (%)",
// color: colors.bitcoin,
seriesType: SeriesType.Based,
dataset: params.datasets[scale].difficulty_adjustment,
},
],
});
},
},
] satisfies PartialPreset[])
: []),
{
scale,
icon: IconTablerBuildingFactory,
name: "Annualized Issuance",
title: "Annualized Issuance",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "Issuance",
color: colors.bitcoin,
dataset: params.datasets[scale].annualized_issuance,
},
],
});
},
},
{
scale,
icon: IconTablerBuildingFactory2,
name: "Yearly Inflation Rate",
title: "Yearly Inflation Rate",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "Rate (%)",
color: colors.bitcoin,
dataset: params.datasets[scale].yearly_inflation_rate,
},
],
});
},
},
// For scale === "height"
// block_size,
// block_weight,
// block_vbytes,
// block_interval,
],
} satisfies PartialPresetFolder;
}
+985
View File
@@ -0,0 +1,985 @@
import { percentiles } from "../../datasets/consts/percentiles";
import { colors } from "../../utils/colors";
import { applyMultipleSeries, SeriesType } from "./multiple";
export function createCohortPresetFolder<Scale extends ResourceScale>({
datasets,
scale,
color,
name,
datasetKey,
title,
}: {
datasets: Datasets;
scale: Scale;
name: string;
datasetKey: AnyPossibleCohortKey;
color: string;
title: string;
}) {
return {
name,
tree: createCohortPresetList({
title,
datasets,
name,
scale,
color,
datasetKey,
}),
} satisfies PartialPresetFolder;
}
export function createCohortPresetList<Scale extends ResourceScale>({
name,
datasets,
scale,
color,
datasetKey,
title,
}: {
name: string;
datasets: Datasets;
scale: Scale;
datasetKey: AnyPossibleCohortKey;
title: string;
color: string;
}) {
const datasetPrefix = datasetKey
? (`${datasetKey}_` as const)
: ("" as const);
return [
{
name: "UTXOs",
tree: [
{
scale,
name: `Count`,
title: `${title} Unspent Transaction Outputs Count`,
icon: () => IconTablerTicket,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Count",
color,
seriesType: SeriesType.Area,
dataset: params.datasets[scale][`${datasetPrefix}utxo_count`],
},
],
});
},
description: "",
},
],
},
{
name: "Realized",
tree: [
{
scale,
name: `Price`,
title: `${title} Realized Price`,
description: "",
icon: () => IconTablerTag,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
{
title: "Realized Price",
color,
dataset:
params.datasets[scale][`${datasetPrefix}realized_price`],
},
],
});
},
},
{
scale,
name: `Capitalization`,
title: `${title} Realized Capitalization`,
icon: () => IconTablerPigMoney,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `${name} Realized Cap.`,
color,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][`${datasetPrefix}realized_cap`],
},
...(datasetKey
? [
{
title: "Realized Cap.",
color: colors.bitcoin,
dataset: params.datasets[scale].realized_cap,
defaultVisible: false,
},
]
: []),
],
});
},
description: "",
},
{
scale,
name: `Capitalization 1M Net Change`,
title: `${title} Realized Capitalization 1 Month Net Change`,
icon: () => IconTablerStatusChange,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: `Net Change`,
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}realized_cap_1m_net_change`
],
},
],
});
},
description: "",
},
{
scale,
name: `Profit`,
title: `${title} Realized Profit`,
icon: () => IconTablerCash,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Realized Profit",
dataset:
params.datasets[scale][`${datasetPrefix}realized_profit`],
color: colors.profit,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: "Loss",
title: `${title} Realized Loss`,
icon: () => IconTablerCoffin,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Realized Loss",
dataset:
params.datasets[scale][`${datasetPrefix}realized_loss`],
color: colors.loss,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: `PNL`,
title: `${title} Realized Profit And Loss`,
icon: () => IconTablerArrowsVertical,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Profit",
color: colors.profit,
dataset:
params.datasets[scale][`${datasetPrefix}realized_profit`],
seriesType: SeriesType.Based,
},
{
title: "Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}negative_realized_loss`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL`,
title: `${title} Net Realized Profit And Loss`,
icon: () => IconTablerScale,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Net PNL",
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}net_realized_profit_and_loss`
],
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL Relative To Market Cap`,
title: `${title} Net Realized Profit And Loss Relative To Market Capitalization`,
icon: () => IconTablerDivide,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Net",
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}net_realized_profit_and_loss_to_market_cap_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: `Cumulative Profit`,
title: `${title} Cumulative Realized Profit`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Realized Profit",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_realized_profit`
],
},
],
});
},
description: "",
},
{
scale,
name: "Cumulative Loss",
title: `${title} Cumulative Realized Loss`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Realized Loss",
color: colors.loss,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_realized_loss`
],
},
],
});
},
description: "",
},
{
scale,
name: `Cumulative Net PNL`,
title: `${title} Cumulative Net Realized Profit And Loss`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Net Realized PNL",
seriesType: SeriesType.Based,
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_net_realized_profit_and_loss`
],
},
],
});
},
description: "",
},
{
scale,
name: `Cumulative Net PNL 30 Day Change`,
title: `${title} Cumulative Net Realized Profit And Loss 30 Day Change`,
icon: () => IconTablerTimeDuration30,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Cumulative Net Realized PNL 30d Change",
dataset:
params.datasets[scale][
`${datasetPrefix}cumulative_net_realized_profit_and_loss_1m_net_change`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
],
},
{
name: "Unrealized",
tree: [
{
scale,
name: `Profit`,
title: `${title} Unrealized Profit`,
icon: () => IconTablerMoodDollar,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Profit",
dataset:
params.datasets[scale][`${datasetPrefix}unrealized_profit`],
color: colors.profit,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: "Loss",
title: `${title} Unrealized Loss`,
icon: () => IconTablerMoodSadDizzy,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Loss",
dataset:
params.datasets[scale][`${datasetPrefix}unrealized_loss`],
color: colors.loss,
seriesType: SeriesType.Area,
},
],
});
},
description: "",
},
{
scale,
name: `PNL`,
title: `${title} Unrealized Profit And Loss`,
icon: () => IconTablerArrowsVertical,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Profit",
color: colors.profit,
dataset:
params.datasets[scale][`${datasetPrefix}unrealized_profit`],
seriesType: SeriesType.Based,
},
{
title: "Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}negative_unrealized_loss`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL`,
title: `${title} Net Unrealized Profit And Loss`,
icon: () => IconTablerScale,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Net Unrealized PNL",
dataset:
params.datasets[scale][
`${datasetPrefix}net_unrealized_profit_and_loss`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
{
scale,
name: `Net PNL Relative To Market Cap`,
title: `${title} Net Unrealized Profit And Loss Relative To Total Market Capitalization`,
icon: () => IconTablerDivide,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Relative Net Unrealized PNL",
dataset:
params.datasets[scale][
`${datasetPrefix}net_unrealized_profit_and_loss_to_market_cap_ratio`
],
seriesType: SeriesType.Based,
},
],
});
},
description: "",
},
],
},
{
name: "Supply",
tree: [
{
name: "Absolute",
tree: [
{
scale,
name: "All",
title: `${title} Profit And Loss`,
icon: () => IconTablerArrowsCross,
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "In Profit",
color: colors.profit,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit`
],
},
{
title: "In Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss`
],
},
{
title: "Total",
color: colors.white,
dataset: params.datasets[scale][`${datasetPrefix}supply`],
},
{
title: "Halved Total",
color: colors.gray,
dataset:
params.datasets[scale][`${datasetPrefix}halved_supply`],
options: {
lineStyle: 4,
},
},
],
});
},
},
{
scale,
name: `Total`,
title: `${title} Total supply`,
icon: () => IconTablerSum,
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color,
seriesType: SeriesType.Area,
dataset: params.datasets[scale][`${datasetPrefix}supply`],
},
],
});
},
},
{
scale,
name: "In Profit",
title: `${title} Supply In Profit`,
icon: () => IconTablerTrendingUp,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Loss",
title: `${title} Supply In Loss`,
icon: () => IconTablerTrendingDown,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.loss,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss`
],
},
],
});
},
description: "",
},
],
},
{
name: "Relative To Circulating",
tree: [
{
scale,
name: "All",
title: `${title} Profit And Loss Relative To Circulating Supply`,
icon: () => IconTablerArrowsCross,
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "In Profit",
color: colors.profit,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_circulating_supply_ratio`
],
},
{
title: "In Loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_circulating_supply_ratio`
],
},
{
title: "100%",
color: colors.white,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_to_circulating_supply_ratio`
],
},
{
title: "50%",
color: colors.gray,
dataset:
params.datasets[scale][
`${datasetPrefix}halved_supply_to_circulating_supply_ratio`
],
options: {
lineStyle: 4,
},
},
],
});
},
},
{
scale,
name: `Total`,
title: `${title} Total supply Relative To Circulating Supply`,
icon: () => IconTablerSum,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_to_circulating_supply_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Profit",
title: `${title} Supply In Profit Relative To Circulating Supply`,
icon: () => IconTablerTrendingUp,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_circulating_supply_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Loss",
title: `${title} Supply In Loss Relative To Circulating Supply`,
icon: () => IconTablerTrendingDown,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
seriesType: SeriesType.Area,
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_circulating_supply_ratio`
],
},
],
});
},
description: "",
},
],
},
{
name: "Relative To Own",
tree: [
{
scale,
name: "All",
title: `${title} Supply In Profit And Loss Relative To Own Supply`,
icon: () => IconTablerArrowsCross,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "In profit",
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_own_supply_ratio`
],
color: colors.profit,
},
{
title: "In loss",
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_own_supply_ratio`
],
},
{
title: "100%",
color: colors.white,
dataset: params.datasets[scale][100],
options: {
lastValueVisible: false,
},
},
{
title: "50%",
color: colors.gray,
dataset: params.datasets[scale][50],
options: {
lineStyle: 4,
lastValueVisible: false,
},
},
],
});
},
description: "",
},
{
scale,
name: "In Profit",
title: `${title} Supply In Profit Relative To Own Supply`,
icon: () => IconTablerTrendingUp,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
color: colors.profit,
seriesType: SeriesType.Area,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_profit_to_own_supply_ratio`
],
},
],
});
},
description: "",
},
{
scale,
name: "In Loss",
title: `${title} Supply In Loss Relative To Own Supply`,
icon: () => IconTablerTrendingDown,
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Supply",
seriesType: SeriesType.Area,
color: colors.loss,
dataset:
params.datasets[scale][
`${datasetPrefix}supply_in_loss_to_own_supply_ratio`
],
},
],
});
},
description: "",
},
],
},
// createMomentumPresetFolder({
// datasets: datasets[scale],
// scale,
// id: `${scale}-${id}-supply-in-profit-and-loss-percentage-self`,
// title: `${title} Supply In Profit And Loss (% Self)`,
// datasetKey: `${datasetKey}SupplyPNL%Self`,
// }),
],
},
{
name: "Prices Paid",
tree: [
{
scale,
name: `Average`,
title: `${title} Average Price Paid - Realized Price`,
icon: () => IconTablerMathAvg,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
{
title: "Average",
color,
dataset:
params.datasets[scale][`${datasetPrefix}realized_price`],
},
],
});
},
description: "",
},
{
scale,
name: `Deciles`,
title: `${title} deciles`,
icon: () => IconTablerSquareHalf,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: percentiles
.filter(({ value }) => Number(value) % 10 === 0)
.map(({ name, key }) => ({
dataset: params.datasets[scale][`${datasetPrefix}${key}`],
color,
title: name,
})),
});
},
description: "",
},
...percentiles.map(
(percentile): PartialPreset => ({
scale,
name: percentile.name,
title: `${title} ${percentile.title}`,
icon: () => IconTablerSquareHalf,
applyPreset(params) {
return applyMultipleSeries({
...params,
list: [
{
title: percentile.name,
color,
dataset:
params.datasets[scale][
`${datasetPrefix}${percentile.key}`
],
},
],
});
},
description: "",
}),
),
],
},
] satisfies PartialPresetTree;
}
@@ -0,0 +1,121 @@
// import { PriceScaleMode } from "lightweight-charts";
// import {
// applyMultipleSeries,
// colors,
// PRICE_SCALE_MOMENTUM_ID,
// SeriesType,
// } from "/src/scripts";
// // type HeightMomentumKey =
// // | `${AnyPossibleCohortKey}SupplyPNL%Self`
// // | `${AnyPossibleCohortKey}RealizedPriceRatio`
// // | "activePriceRatio"
// // | "vaultedPriceRatio"
// // | "trueMarketMeanRatio";
// // type DateMomentumKey = HeightMomentumKey | `price${AverageName}MARatio`;
// export function createMomentumPresetFolder<
// Scale extends ResourceScale,
// Key extends string,
// >({
// datasets,
// scale,
// id,
// title,
// datasetKey,
// }: {
// datasets: Record<`${Key}${MomentumKey}`, Dataset<ResourceScale>>;
// scale: Scale;
// id: string;
// title: string;
// datasetKey: Key;
// }): PartialPresetFolder {
// return {
// id: `${scale}-${id}-momentum`,
// name: "Momentum",
// tree: [
// {
// id: `${scale}-${id}-momentum-value`,
// name: "Value",
// title: `${title} Momentum`,
// icon: () => IconTablerRollercoaster,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// list: [
// {
// title: "Momentum",
// colors: colors.momentum,
// seriesType: SeriesType.Histogram,
// dataset: datasets[`${datasetKey}Momentum`],
// options: {
// priceScaleId: PRICE_SCALE_MOMENTUM_ID,
// lastValueVisible: false,
// },
// },
// ],
// });
// },
// description: "",
// },
// {
// id: `${scale}-${id}-momentum-buy-low-sell-high`,
// name: "BLSH - Buy Low Sell High",
// tree: [
// {
// id: `${scale}-${id}-buy-low-sell-high-bitcoin-returns`,
// name: "Bitcoin Returns",
// title: `${title} Momentum Based Buy Low Sell High Bitcoin Returns`,
// icon: () => IconTablerReceiptBitcoin,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// mode: PriceScaleMode.Percentage,
// },
// list: [
// {
// title: "Bitcoin Returns",
// dataset:
// datasets[`${datasetKey}MomentumBLSHBitcoinReturns`],
// color: colors.bitcoin,
// },
// ],
// });
// },
// description: "",
// },
// {
// id: `${scale}-${id}-momentum-buy-low-sell-high-dollar-returns`,
// name: "Dollar Returns",
// title: `${title} Momentum Based Buy Low Sell High Dollar Returns`,
// icon: () => IconTablerReceiptDollar,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// mode: PriceScaleMode.Percentage,
// },
// list: [
// {
// title: "Dollar Returns",
// dataset: datasets[`${datasetKey}MomentumBLSHDollarReturns`],
// color: colors.dollars,
// },
// ],
// });
// },
// description: "",
// },
// ],
// },
// ],
// };
// }
@@ -0,0 +1,183 @@
import { applyPriceSeries } from "../../lightweightCharts/chart/price";
import { chartState } from "../../lightweightCharts/chart/state";
import { setTimeScale } from "../../lightweightCharts/chart/time";
import { createAreaSeries } from "../../lightweightCharts/series/creators/area";
import {
createBaseLineSeries,
DEFAULT_BASELINE_COLORS,
} from "../../lightweightCharts/series/creators/baseLine";
import { createHistogramSeries } from "../../lightweightCharts/series/creators/histogram";
import { createSeriesLegend } from "../../lightweightCharts/series/creators/legend";
import { createLineSeries } from "../../lightweightCharts/series/creators/line";
import { resetRightPriceScale } from "../../lightweightCharts/series/options/priceScale";
import { stringToId } from "../../utils/id";
export enum SeriesType {
Normal,
Based,
Area,
Histogram,
}
export function applyMultipleSeries<
Scale extends ResourceScale,
DS extends Dataset<Scale> & Partial<ResourceDataset<Scale>>,
>({
chart,
list = [],
preset,
priceScaleOptions,
datasets,
priceDataset,
priceOptions,
activeResources,
}: {
chart: IChartApi;
preset: Preset;
priceDataset?: DS;
priceOptions?: PriceSeriesOptions;
priceScaleOptions?: FullPriceScaleOptions;
list?: (
| {
dataset: DS;
color?: string;
colors?: undefined;
seriesType: SeriesType.Based;
title: string;
options?: BaselineSeriesOptions;
defaultVisible?: boolean;
}
| {
dataset: DS;
color?: string;
colors?: string[];
seriesType: SeriesType.Histogram;
title: string;
options?: DeepPartialHistogramOptions;
defaultVisible?: boolean;
}
| {
dataset: DS;
color: string;
colors?: undefined;
seriesType?: SeriesType.Normal | SeriesType.Area;
title: string;
options?: DeepPartialLineOptions;
defaultVisible?: boolean;
}
)[];
datasets: Datasets;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
}): PresetLegend {
const { halved } = priceScaleOptions || {};
const price = applyPriceSeries({
chart,
datasets,
preset,
dataset: priceDataset,
activeResources,
options: {
...priceOptions,
halved,
},
});
const legendList: PresetLegend = [price.lineLegend, price.ohlcLegend];
const isAnyArea = list.find(
(config) => config.seriesType === SeriesType.Area,
);
const rightPriceScaleOptions = resetRightPriceScale(chart, {
...priceScaleOptions,
...(isAnyArea
? {
scaleMargins: {
bottom: 0,
},
}
: {}),
});
[...list]
.reverse()
.forEach(
({
dataset,
color,
colors,
seriesType: type,
title,
options,
defaultVisible,
}) => {
let series: ISeriesApi<"Baseline" | "Line" | "Area" | "Histogram">;
if (type === SeriesType.Based) {
series = createBaseLineSeries(chart, {
color,
...options,
});
} else if (type === SeriesType.Area) {
series = createAreaSeries(chart, {
color,
autoscaleInfoProvider: (getInfo: () => AutoscaleInfo | null) => {
const info = getInfo();
if (info) {
info.priceRange.minValue = 0;
}
return info;
},
...options,
});
} else if (type === SeriesType.Histogram) {
series = createHistogramSeries(chart, {
color,
...options,
});
} else {
series = createLineSeries(chart, {
color,
...options,
});
}
legendList.splice(
0,
0,
createSeriesLegend({
id: stringToId(title),
presetId: preset.id,
title,
series,
color: () => colors || color || DEFAULT_BASELINE_COLORS,
defaultVisible,
url: dataset.url,
}),
);
createEffect(() => {
series.setData(dataset?.values() || []);
setTimeScale(chartState.range);
});
},
);
createEffect(() => {
const options = {
scaleMargins: {
top:
price.lineLegend.visible() || price.ohlcLegend.visible()
? rightPriceScaleOptions.scaleMargins.top
: rightPriceScaleOptions.scaleMargins.bottom,
bottom: rightPriceScaleOptions.scaleMargins.bottom,
},
};
chart.priceScale("right").applyOptions(options);
});
return legendList;
}
+289
View File
@@ -0,0 +1,289 @@
// import {
// applyMultipleSeries,
// colors,
// createMomentumPresetFolder,
// SeriesType,
// } from "/src/scripts";
// // type HeightRatioKey =
// // | `${AnyPossibleCohortKey}RealizedPrice`
// // | "activePrice"
// // | "vaultedPrice"
// // | "trueMarketMean";
// // // type DateRatioKey = HeightRatioKey;
// // type DateRatioKey = HeightRatioKey | `price${AverageName}MA`;
// export function createRatioPresetFolder<
// Scale extends ResourceScale,
// Key extends string,
// >({
// datasets,
// scale,
// id,
// title,
// datasetKey,
// color,
// }: {
// datasets: Record<`${Key}${RatioKey}`, Dataset<ResourceScale>>;
// scale: Scale;
// id: string;
// title: string;
// color: string;
// datasetKey: Key;
// }): PartialPresetFolder {
// return {
// id: `${scale}-${id}-ratio`,
// name: "Ratio",
// tree: [
// {
// id: `${scale}-${id}-ratio-value`,
// name: `Value`,
// title: `Bitcoin Price to ${title} Ratio`,
// icon: () => IconTablerDivide,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// title: "Ratio",
// seriesType: SeriesType.Based,
// dataset: datasets[`${datasetKey}Ratio`],
// options: {
// base: 1,
// },
// },
// ],
// });
// },
// description: "",
// },
// {
// id: `${scale}-${id}-ratio-1y-average`,
// name: "Averages",
// tree: [
// {
// id: `${scale}-${id}-ratio-averages`,
// name: `7 Day VS. 1 Year`,
// title: `Bitcoin Price to ${title} Ratio Moving Averages`,
// icon: () => IconTablerSwords,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// title: "Ratio",
// seriesType: SeriesType.Based,
// color: colors.gray,
// dataset: datasets[`${datasetKey}Ratio`],
// options: {
// base: 1,
// },
// },
// {
// title: "7 Day Moving Average",
// color: colors.closes7DMA,
// dataset: datasets[`${datasetKey}Ratio7DayMovingAverage`],
// },
// {
// title: "1 Year Moving Average",
// color: colors.closes1YMA,
// dataset: datasets[`${datasetKey}Ratio1YearMovingAverage`],
// },
// ],
// });
// },
// description: "",
// },
// createMomentumPresetFolder({
// datasets,
// scale,
// id: `${scale}-${id}-ratio-averages`,
// title: `${title} Ratio Moving Averages`,
// datasetKey: `${datasetKey}Ratio`,
// }),
// ],
// },
// {
// id: `${scale}-${id}-ratio-extremes`,
// name: "Extremes",
// tree: [
// {
// id: `${scale}-${id}-extreme-top-ratios`,
// name: "Top Ratios",
// description: "",
// icon: () => IconTablerJetpack,
// title: `${title} Extreme Top Ratios`,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// id: "ratio",
// title: "Ratio",
// color: colors.white,
// seriesType: SeriesType.Based,
// dataset: datasets[`${datasetKey}Ratio`],
// options: {
// base: 1,
// options: {
// baseLineColor: color,
// baseLineVisible: true,
// },
// },
// },
// {
// id: "99.9-percentile",
// title: "99.9th Percentile",
// dataset: datasets[`${datasetKey}Ratio99.9Percentile`],
// color: colors.extremeMax,
// },
// {
// id: "99.5-percentile",
// title: "99.5th Percentile",
// color: colors.extremeMiddle,
// dataset: datasets[`${datasetKey}Ratio99.5Percentile`],
// },
// {
// id: "99-percentile",
// title: "99th Percentile",
// color: colors.extremeMin,
// dataset: datasets[`${datasetKey}Ratio99Percentile`],
// },
// ],
// });
// },
// },
// {
// id: `${scale}-${id}-extreme-bottom-ratios`,
// name: "Bottom Ratios",
// description: "",
// icon: () => IconTablerScubaMask,
// title: `${title} Extreme Bottom Ratios`,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// priceScaleOptions: {
// halved: true,
// },
// list: [
// {
// id: "ratio",
// title: "Ratio",
// color: colors.white,
// seriesType: SeriesType.Based,
// dataset: datasets[`${datasetKey}Ratio`],
// options: {
// base: 1,
// options: {
// baseLineColor: color,
// baseLineVisible: true,
// },
// },
// },
// {
// id: "1-percentile",
// title: "1st Percentile",
// color: colors.extremeMin,
// dataset: datasets[`${datasetKey}Ratio1Percentile`],
// },
// {
// id: "0.5-percentile",
// title: "0.5th Percentile",
// color: colors.extremeMiddle,
// dataset: datasets[`${datasetKey}Ratio0.5Percentile`],
// },
// {
// id: "0.1-percentile",
// title: "0.1th Percentile",
// color: colors.extremeMax,
// dataset: datasets[`${datasetKey}Ratio0.1Percentile`],
// },
// ],
// });
// },
// },
// {
// id: `${scale}-${id}-extreme-top-prices`,
// name: "Top Prices",
// description: "",
// icon: () => IconTablerRocket,
// title: `${title} Extreme Top Prices`,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// list: [
// {
// id: "99.9-percentile",
// title: "99.9th Percentile",
// color: colors.extremeMax,
// dataset: datasets[`${datasetKey}Ratio99.9Price`],
// },
// {
// id: "99.5-percentile",
// title: "99.5th Percentile",
// color: colors.extremeMiddle,
// dataset: datasets[`${datasetKey}Ratio99.5Price`],
// },
// {
// id: "99-percentile",
// title: "99th Percentile",
// color: colors.extremeMin,
// dataset: datasets[`${datasetKey}Ratio99Price`],
// },
// ],
// });
// },
// },
// {
// id: `${scale}-${id}-extreme-bottom-prices`,
// name: "Bottom Prices",
// description: "",
// icon: () => IconTablerSubmarine,
// title: `${title} Extreme Bottom Prices`,
// applyPreset(params) {
// return applyMultipleSeries({
// scale,
// ...params,
// list: [
// {
// id: "1-percentile",
// title: "1st Percentile",
// color: colors.extremeMin,
// dataset: datasets[`${datasetKey}Ratio1Price`],
// },
// {
// id: "0.5-percentile",
// title: "0.5th Percentile",
// color: colors.extremeMiddle,
// dataset: datasets[`${datasetKey}Ratio0.5Price`],
// },
// {
// id: "0.1-percentile",
// title: "0.1th Percentile",
// color: colors.extremeMax,
// dataset: datasets[`${datasetKey}Ratio0.1Price`],
// },
// ],
// });
// },
// },
// ],
// },
// ],
// };
// }
@@ -0,0 +1,225 @@
import { colors } from "../../utils/colors";
import { applyMultipleSeries } from "../templates/multiple";
export function createPresets(scale: ResourceScale) {
return {
name: "Transactions",
tree: [
{
scale,
icon: IconTablerHandThreeFingers,
name: "Count",
title: "Transaction Count",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "1M SMA",
color: colors.momentumYellow,
dataset: params.datasets[scale].transaction_count_1m_sma,
},
{
title: "1W SMA",
color: colors.bitcoin,
dataset: params.datasets[scale].transaction_count_1w_sma,
},
{
title: "Raw",
color: colors.darkBitcoin,
dataset: params.datasets[scale].transaction_count,
},
],
});
},
},
{
name: "Volume",
tree: [
{
scale,
icon: IconTablerCoinBitcoin,
name: "In Bitcoin",
title: "Transaction Volume",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "1M SMA",
color: colors.momentumYellow,
dataset: params.datasets[scale].transaction_volume_1m_sma,
},
{
title: "1W SMA",
color: colors.bitcoin,
dataset: params.datasets[scale].transaction_volume_1w_sma,
},
{
title: "Raw",
color: colors.darkBitcoin,
dataset: params.datasets[scale].transaction_volume,
},
],
});
},
},
{
scale,
icon: IconTablerCoin,
name: "In Dollars",
title: "Transaction Volume In Dollars",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
mode: 1,
},
list: [
{
title: "1M SMA",
color: colors.lightDollars,
dataset:
params.datasets[scale]
.transaction_volume_in_dollars_1m_sma,
},
{
title: "1W SMA",
color: colors.dollars,
dataset:
params.datasets[scale]
.transaction_volume_in_dollars_1w_sma,
},
{
title: "Raw",
color: colors.darkDollars,
dataset:
params.datasets[scale].transaction_volume_in_dollars,
},
],
});
},
},
],
},
{
name: "Annualized Volume",
tree: [
{
scale,
icon: IconTablerCoinBitcoin,
name: "In Bitcoin",
title: "Annualized Transaction Volume",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Volume",
color: colors.bitcoin,
dataset:
params.datasets[scale].annualized_transaction_volume,
},
],
});
},
},
{
scale,
icon: IconTablerCoin,
name: "In Dollars",
title: "Annualized Transaction Volume In Dollars",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Volume",
color: colors.dollars,
dataset:
params.datasets[scale]
.annualized_transaction_volume_in_dollars,
},
],
});
},
},
],
},
{
scale,
icon: IconTablerWind,
name: "Velocity",
title: "Transactions Velocity",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "Transactions Velocity",
color: colors.bitcoin,
dataset: params.datasets[scale].transaction_velocity,
},
],
});
},
},
{
scale,
icon: IconTablerAlarm,
name: "Per Second",
title: "Transactions Per Second",
description: "",
applyPreset(params) {
return applyMultipleSeries({
...params,
priceScaleOptions: {
halved: true,
},
list: [
{
title: "1M SMA",
color: colors.lightBitcoin,
dataset: params.datasets[scale].transactions_per_second_1m_sma,
},
{
title: "1W SMA",
color: colors.bitcoin,
dataset: params.datasets[scale].transactions_per_second_1w_sma,
},
{
title: "Raw",
color: colors.darkBitcoin,
dataset: params.datasets[scale].transactions_per_second,
},
],
});
},
},
],
} satisfies PartialPresetFolder;
}
+62
View File
@@ -0,0 +1,62 @@
interface PartialPreset {
scale: ResourceScale;
icon?: () => JSXElement;
name: string;
title: string;
applyPreset: ApplyPreset;
description: string;
}
interface Preset extends PartialPreset {
id: string;
path: FilePath;
isFavorite: RWS<boolean>;
visited: RWS<boolean>;
}
type FilePath = {
id: string;
name: string;
}[];
type ApplyPreset = (params: {
chart: IChartApi;
datasets: Datasets;
preset: Preset;
activeResources: Accessor<Set<ResourceDataset<any, any>>>;
}) => ApplyPresetReturn;
type ApplyPresetReturn = PresetLegend;
interface PartialPresetFolder {
name: string;
tree: PartialPresetTree;
}
interface PresetFolder extends PartialPresetFolder {
id: string;
tree: PresetTree;
}
type PartialPresetTree = (PartialPreset | PartialPresetFolder)[];
type PresetTree = (Preset | PresetFolder)[];
// type PresetList = Preset[];
// type FavoritePresets = Accessor<Preset[]>;
type PresetsHistory = { date: Date; preset: Preset }[];
type PresetsHistorySignal = RWS<PresetsHistory>;
type SerializedPresetsHistory = { p: string; d: number }[];
interface Presets {
tree: (Preset | PresetFolder)[];
list: Preset[];
favorites: Accessor<Preset[]>;
history: PresetsHistorySignal;
selected: RWS<Preset>;
openedFolders: RWS<Set<string>>;
select(preset: Preset): void;
}
type PresetLegend = SeriesLegend[];
+16
View File
@@ -0,0 +1,16 @@
export function sortedInsert(array: number[], value: number) {
let low = 0;
let high = array.length;
while (low < high) {
const mid = (low + high) >>> 1;
if (array[mid] < value) {
low = mid + 1;
} else {
high = mid;
}
}
array.splice(low, 0, value);
}
+246
View File
@@ -0,0 +1,246 @@
import {
amber as amberTailwind,
blue as blueTailwind,
cyan as cyanTailwind,
emerald as emeraldTailwind,
fuchsia as fuchsiaTailwind,
neutral as grayTailwind,
green as greenTailwind,
indigo as indigoTailwind,
lime as limeTailwind,
orange as orangeTailwind,
pink as pinkTailwind,
purple as purpleTailwind,
red as redTailwind,
rose as roseTailwind,
sky as skyTailwind,
teal as tealTailwind,
violet as violetTailwind,
yellow as yellowTailwind,
} from "tailwindcss/colors";
// ---
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// DO NOT USE TRANSPARENCY HERE
// ---
const lightRed = redTailwind[300];
const red = redTailwind[500];
const darkRed = redTailwind[900];
const orange = orangeTailwind[500];
const darkOrange = orangeTailwind[900];
const amber = amberTailwind[500];
const darkAmber = amberTailwind[900];
const yellow = yellowTailwind[500];
const darkYellow = yellowTailwind[500];
const lime = limeTailwind[500];
const darkLime = limeTailwind[900];
const green = greenTailwind[500];
const darkGreen = greenTailwind[900];
const lightEmerald = emeraldTailwind[300];
const emerald = emeraldTailwind[500];
const darkEmerald = emeraldTailwind[900];
const teal = tealTailwind[500];
const darkTeal = tealTailwind[900];
const cyan = cyanTailwind[500];
const darkCyan = cyanTailwind[900];
const sky = skyTailwind[500];
const darkSky = skyTailwind[900];
const blue = blueTailwind[500];
const darkBlue = blueTailwind[900];
const indigo = indigoTailwind[500];
const darkIndigo = indigoTailwind[900];
const violet = violetTailwind[500];
const darkViolet = violetTailwind[900];
const purple = purpleTailwind[500];
const darkPurple = purpleTailwind[900];
const fuchsia = fuchsiaTailwind[500];
const darkFuchsia = fuchsiaTailwind[900];
const pink = pinkTailwind[500];
const darkPink = pinkTailwind[900];
const rose = roseTailwind[500];
const darkRose = roseTailwind[900];
const darkWhite = grayTailwind[400];
const gray = grayTailwind[600];
const black = "#000000";
const white = "#ffffff";
export const convertCandleToCandleColor = (
candle: { close: number; open: number },
inverse?: boolean,
) =>
(candle.close || 1) > (candle.open || 0)
? !inverse
? green
: red
: !inverse
? red
: green;
export const convertCandleToVolumeColor = (
candle: { close: number; open: number },
inverse?: boolean,
) =>
(candle.close || 1) > (candle.open || 0)
? !inverse
? darkGreen
: darkRed
: !inverse
? darkRed
: darkGreen;
export const colors = {
white,
darkWhite,
gray,
lightBitcoin: yellow,
bitcoin: orange,
darkBitcoin: darkOrange,
lightDollars: lime,
dollars: emerald,
darkDollars: darkEmerald,
_1d: lightRed,
_1w: red,
_8d: orange,
_13d: amber,
_21d: yellow,
_1m: lime,
_34d: green,
_55d: emerald,
_89d: teal,
_144d: cyan,
_6m: sky,
_1y: blue,
_2y: indigo,
_200w: violet,
_4y: purple,
_10y: fuchsia,
p2pk: lime,
p2pkh: violet,
p2sh: emerald,
p2wpkh: cyan,
p2wsh: pink,
p2tr: blue,
crab: red,
fish: lime,
humpback: violet,
plankton: emerald,
shark: cyan,
shrimp: pink,
whale: blue,
megalodon: purple,
realizedPrice: orange,
oneMonthHolders: cyan,
threeMonthsHolders: lime,
sth: yellow,
sixMonthsHolder: red,
oneYearHolders: pink,
twoYearsHolders: purple,
lth: fuchsia,
balancedPrice: yellow,
cointimePrice: yellow,
trueMarketMeanPrice: blue,
vaultedPrice: green,
cvdd: lime,
terminalPrice: red,
loss: red,
darkLoss: darkRed,
profit: green,
darkProfit: darkGreen,
thermoCap: green,
investorCap: rose,
realizedCap: orange,
ethereum: indigo,
usdt: emerald,
usdc: blue,
ust: red,
busd: yellow,
usdd: emerald,
frax: gray,
dai: amber,
tusd: indigo,
pyusd: blue,
darkLiveliness: darkRose,
liveliness: rose,
vaultedness: green,
activityToVaultednessRatio: violet,
up_to_1d: lightRed,
up_to_1w: red,
up_to_1m: orange,
up_to_2m: orange,
up_to_3m: orange,
up_to_4m: orange,
up_to_5m: orange,
up_to_6m: orange,
up_to_1y: orange,
up_to_2y: orange,
up_to_3y: orange,
up_to_4y: orange,
up_to_5y: orange,
up_to_7y: orange,
up_to_10y: orange,
up_to_15y: orange,
from_10y_to_15y: purple,
from_7y_to_10y: violet,
from_5y_to_7y: indigo,
from_3y_to_5y: sky,
from_2y_to_3y: teal,
from_1y_to_2y: green,
from_6m_to_1y: lime,
from_3m_to_6m: yellow,
from_1m_to_3m: amber,
from_1w_to_1m: orange,
from_1d_to_1w: red,
from_1y: green,
from_2y: teal,
from_4y: indigo,
from_10y: violet,
from_15y: fuchsia,
coinblocksCreated: purple,
coinblocksDestroyed: red,
coinblocksStored: green,
momentum: [green, yellow, red],
momentumGreen: green,
momentumYellow: yellow,
momentumRed: red,
extremeMax: red,
extremeMiddle: orange,
extremeMin: yellow,
year_2009: yellow,
year_2010: yellow,
year_2011: yellow,
year_2012: yellow,
year_2013: yellow,
year_2014: yellow,
year_2015: yellow,
year_2016: yellow,
year_2017: yellow,
year_2018: yellow,
year_2019: yellow,
year_2020: yellow,
year_2021: yellow,
year_2022: yellow,
year_2023: yellow,
year_2024: yellow,
};
+10
View File
@@ -0,0 +1,10 @@
// import { ONE_DAY_IN_MS } from "./time";
import { ONE_DAY_IN_MS } from "./time";
export const dateToString = (date: Date) => date.toJSON().split("T")[0];
// export const FIVE_MONTHS_IN_DAYS = 30 * 5;
export const getNumberOfDaysBetweenTwoDates = (oldest: Date, youngest: Date) =>
Math.round(Math.abs((youngest.valueOf() - oldest.valueOf()) / ONE_DAY_IN_MS));
+19
View File
@@ -0,0 +1,19 @@
export const debounce = <F extends (...args: any[]) => any>(
callback: F,
wait = 250,
) => {
let timeoutId: number | undefined;
let latestArgs: Parameters<F>;
return (...args: Parameters<F>) => {
latestArgs = args;
if (!timeoutId) {
timeoutId = window.setTimeout(async () => {
await callback(...latestArgs);
timeoutId = undefined;
}, wait);
}
};
};
+12
View File
@@ -0,0 +1,12 @@
export function replaceHistory({
urlParams,
pathname,
}: {
urlParams?: URLSearchParams;
pathname?: string;
}) {
urlParams ||= new URLSearchParams(window.location.search);
pathname ||= window.location.pathname;
window.history.replaceState(null, "", `${pathname}?${urlParams.toString()}`);
}
+3
View File
@@ -0,0 +1,3 @@
export function stringToId(s: string) {
return s.replace(/\W/g, " ").trim().replace(/ +/g, "-").toLowerCase();
}
+31
View File
@@ -0,0 +1,31 @@
export const priceToUSLocale = (price: number, compact = true) => {
const absolutePrice = Math.abs(price);
const lessThan100 = absolutePrice < 100;
const lessThan1000 = absolutePrice < 1_000;
const biggerThanMillion = absolutePrice >= 1_000_000;
return numberToUSLocale(
price,
lessThan1000 ? (lessThan100 ? 2 : 1) : biggerThanMillion ? 3 : 0,
biggerThanMillion && compact
? {
notation: "compact",
compactDisplay: "short",
}
: undefined,
);
};
export const percentageToUSLocale = (percentage: number) =>
numberToUSLocale(percentage, 1);
const numberToUSLocale = (
value: number,
digits: number,
options?: Intl.NumberFormatOptions | undefined,
) =>
value.toLocaleString("en-us", {
...options,
minimumFractionDigits: digits,
maximumFractionDigits: digits,
});
+22
View File
@@ -0,0 +1,22 @@
import { computeSum } from "./sum";
export const computeAverage = (values: number[]) =>
computeSum(values) / values.length;
export const computeMovingAverage = <
T extends SingleValueData = SingleValueData,
>(
dataset: T[],
interval: number,
) => {
if (!dataset.length) return [];
return dataset.map((data, index) => ({
...data,
value: computeAverage(
dataset
.slice(Math.max(index - interval + 1, 0), index + 1)
.map((data) => data.value || 1),
),
}));
};
+5
View File
@@ -0,0 +1,5 @@
export function random<T>(array: T[]) {
if (array && array.length) {
return array[Math.floor(Math.random() * array.length)];
}
}

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