global: snapshot

This commit is contained in:
k
2024-10-26 16:41:38 +02:00
parent 7114c3bdf9
commit f5754780a8
30 changed files with 888 additions and 541 deletions

View File

@@ -262,7 +262,7 @@ export function init({
),
);
const field = utils.dom.createField({
const field = utils.dom.createHorizontalChoiceField({
choices: chartModes,
selected: chartMode(),
id,
@@ -326,8 +326,8 @@ export function init({
ratio =
utils.getNumberOfDaysBetweenTwoDates(
utils.dateFromTime(from),
utils.dateFromTime(to),
utils.date.fromTime(from),
utils.date.fromTime(to),
) / width;
} else {
const to = /** @type {number} */ (visibleTimeRange.to);
@@ -963,7 +963,7 @@ export function init({
let number;
if (scale === "date") {
const date = utils.dateFromTime(data.time);
const date = utils.date.fromTime(data.time);
number = date.getTime();

View File

@@ -30,13 +30,55 @@ function initPackages() {
/**
* @template T
* @param {T} initialValue
* @param {SignalOptions<T>} [options]
* @param {SignalOptions<T> & {save?: {id?: string; param?: string; serialize: (v: NonNullable<T>) => string; deserialize: (v: string) => NonNullable<T>}}} [options]
* @returns {Signal<T>}
*/
createSignal(initialValue, options) {
const [get, set] = this.createSolidSignal(initialValue, options);
// @ts-ignore
get.set = set;
if (options?.save) {
const save = options.save;
let serialized = null;
if (save.param) {
serialized = utils.url.readParam(save.param);
}
if (serialized === null && save.id) {
serialized = utils.storage.read(save.id);
}
if (serialized) {
set(save.deserialize(serialized));
}
let firstEffect = true;
this.createEffect(() => {
const value = get();
if (!save) return;
if (!firstEffect && save.id) {
if (value !== undefined && value !== null) {
localStorage.setItem(save.id, save.serialize(value));
} else {
localStorage.removeItem(save.id);
}
}
if (save.param) {
if (value !== undefined && value !== null) {
utils.url.writeParam(save.param, save.serialize(value));
} else {
utils.url.removeParam(save.param);
}
}
firstEffect = false;
});
}
// @ts-ignore
return get;
},
@@ -434,7 +476,7 @@ function initPackages() {
whitespaceStartDateDate + i,
);
const time = utils.dateToString(date);
const time = utils.date.toString(date);
if (i === whitespaceDateDataset.length - 1) {
whitespaceDateDataset[i] = {
@@ -859,7 +901,7 @@ const utils = {
* @param {string} args.selected
* @param {{createEffect: CreateEffect}} args.signals
*/
createField({ title, id, choices, selected, signals }) {
createHorizontalChoiceField({ title, id, choices, selected, signals }) {
const field = window.document.createElement("div");
field.classList.add("field");
@@ -941,12 +983,12 @@ const utils = {
},
/**
* @param {string} key
* @param {string | boolean | undefined} value
* @param {string | boolean | null | undefined} value
*/
writeParam(key, value) {
const urlParams = new URLSearchParams(window.location.search);
if (value !== undefined) {
if (value !== null && value !== undefined) {
urlParams.set(key, String(value));
} else {
urlParams.delete(key);
@@ -966,9 +1008,7 @@ const utils = {
* @returns {boolean | null}
*/
readBoolParam(key) {
const urlParams = new URLSearchParams(window.location.search);
const parameter = urlParams.get(key);
const parameter = this.readParam(key);
if (parameter) {
return utils.isSerializedBooleanTrue(parameter);
@@ -976,6 +1016,29 @@ const utils = {
return null;
},
/**
*
* @param {string} key
* @returns {number | null}
*/
readNumberParam(key) {
const parameter = this.readParam(key);
if (parameter) {
return Number(parameter);
}
return null;
},
/**
*
* @param {string} key
* @returns {string | null}
*/
readParam(key) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(key);
},
pathnameToSelectedId() {
return window.document.location.pathname.substring(1);
},
@@ -1044,11 +1107,21 @@ const utils = {
},
},
storage: {
/**
* @param {string} key
*/
readNumber(key) {
const saved = this.read(key);
if (saved) {
return Number(saved);
}
return null;
},
/**
* @param {string} key
*/
readBool(key) {
const saved = localStorage.getItem(key);
const saved = this.read(key);
if (saved) {
return utils.isSerializedBooleanTrue(saved);
}
@@ -1056,7 +1129,13 @@ const utils = {
},
/**
* @param {string} key
* @param {string | boolean | undefined} value
*/
read(key) {
return localStorage.getItem(key);
},
/**
* @param {string} key
* @param {string | boolean | null | undefined} value
*/
write(key, value) {
value !== undefined && value !== null
@@ -1070,6 +1149,22 @@ const utils = {
this.write(key, undefined);
},
},
serde: {
number: {
/**
* @param {number} v
*/
serialize(v) {
return String(v);
},
/**
* @param {string} v
*/
deserialize(v) {
return Number(v);
},
},
},
formatters: {
dollars: new Intl.NumberFormat("en-US", {
style: "currency",
@@ -1083,6 +1178,37 @@ const utils = {
maximumFractionDigits: 2,
}),
},
date: {
todayUTC() {
const today = new Date();
return new Date(
Date.UTC(
today.getUTCFullYear(),
today.getUTCMonth(),
today.getUTCDate(),
0,
0,
0,
),
);
},
/**
* @param {Date} date
* @returns {string}
*/
toString(date) {
return date.toJSON().split("T")[0];
},
/**
* @param {Time} time
*/
fromTime(time) {
return typeof time === "string"
? new Date(time)
: // @ts-ignore
new Date(time.year, time.month, time.day);
},
},
/**
*
* @template {(...args: any[]) => any} F
@@ -1118,22 +1244,6 @@ const utils = {
setTimeout(callback, timeout);
}
},
/**
* @param {Date} date
* @returns {string}
*/
dateToString(date) {
return date.toJSON().split("T")[0];
},
/**
* @param {Time} time
*/
dateFromTime(time) {
return typeof time === "string"
? new Date(time)
: // @ts-ignore
new Date(time.year, time.month, time.day);
},
/**
* @param {Date} oldest
* @param {Date} youngest
@@ -1186,9 +1296,9 @@ const env = initEnv();
function createConstants() {
const ONE_SECOND_IN_MS = 1_000;
const FIVE_SECOND_IN_MS = 5 * ONE_SECOND_IN_MS;
const TEN_SECOND_IN_MS = 2 * FIVE_SECOND_IN_MS;
const ONE_MINUTE_IN_MS = 6 * TEN_SECOND_IN_MS;
const FIVE_SECONDS_IN_MS = 5 * ONE_SECOND_IN_MS;
const TEN_SECONDS_IN_MS = 2 * FIVE_SECONDS_IN_MS;
const ONE_MINUTE_IN_MS = 6 * TEN_SECONDS_IN_MS;
const FIVE_MINUTES_IN_MS = 5 * ONE_MINUTE_IN_MS;
const TEN_MINUTES_IN_MS = 2 * FIVE_MINUTES_IN_MS;
const ONE_HOUR_IN_MS = 6 * TEN_MINUTES_IN_MS;
@@ -1200,8 +1310,8 @@ function createConstants() {
return {
ONE_SECOND_IN_MS,
FIVE_SECOND_IN_MS,
TEN_SECOND_IN_MS,
FIVE_SECONDS_IN_MS,
TEN_SECONDS_IN_MS,
ONE_MINUTE_IN_MS,
FIVE_MINUTES_IN_MS,
TEN_MINUTES_IN_MS,
@@ -1596,22 +1706,11 @@ function createColors(dark) {
probability0_1p: red,
probability0_5p: orange,
probability1p: 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,
epoch_1: red,
epoch_2: orange,
epoch_3: yellow,
epoch_4: green,
epoch_5: blue,
};
}
/**
@@ -2059,7 +2158,7 @@ function initWebSockets(signals) {
const date = new Date(Number(timestamp) * 1000);
const dateStr = utils.dateToString(date);
const dateStr = utils.date.toString(date);
/** @type {DatasetCandlestickData} */
const candle = {
@@ -2121,7 +2220,7 @@ packages.signals().then((signals) =>
});
}
fetchLastHeight();
setInterval(fetchLastHeight, consts.TEN_SECOND_IN_MS, {});
setInterval(fetchLastHeight, consts.TEN_SECONDS_IN_MS, {});
return lastHeight;
}

View File

@@ -217,23 +217,12 @@ function createPartialOptions(colors) {
},
]);
const year = /** @type {const} */ ([
{ id: "year-2009", key: "year_2009", name: "2009" },
{ id: "year-2010", key: "year_2010", name: "2010" },
{ id: "year-2011", key: "year_2011", name: "2011" },
{ id: "year-2012", key: "year_2012", name: "2012" },
{ id: "year-2013", key: "year_2013", name: "2013" },
{ id: "year-2014", key: "year_2014", name: "2014" },
{ id: "year-2015", key: "year_2015", name: "2015" },
{ id: "year-2016", key: "year_2016", name: "2016" },
{ id: "year-2017", key: "year_2017", name: "2017" },
{ id: "year-2018", key: "year_2018", name: "2018" },
{ id: "year-2019", key: "year_2019", name: "2019" },
{ id: "year-2020", key: "year_2020", name: "2020" },
{ id: "year-2021", key: "year_2021", name: "2021" },
{ id: "year-2022", key: "year_2022", name: "2022" },
{ id: "year-2023", key: "year_2023", name: "2023" },
{ id: "year-2024", key: "year_2024", name: "2024" },
const epochs = /** @type {const} */ ([
{ id: "epoch-1", key: "epoch_1", name: "1" },
{ id: "epoch-2", key: "epoch_2", name: "2" },
{ id: "epoch-3", key: "epoch_3", name: "3" },
{ id: "epoch-4", key: "epoch_4", name: "4" },
{ id: "epoch-5", key: "epoch_5", name: "5" },
]);
const age = /** @type {const} */ ([
@@ -246,7 +235,7 @@ function createPartialOptions(colors) {
...upTo,
...fromXToY,
...fromX,
...year,
...epochs,
]);
const size = /** @type {const} */ ([
@@ -475,7 +464,7 @@ function createPartialOptions(colors) {
upTo,
fromX,
fromXToY,
year,
epochs,
age,
type,
size,
@@ -3713,8 +3702,8 @@ function createPartialOptions(colors) {
),
},
{
name: "Years",
tree: groups.year.map(({ key, id, name }) =>
name: "Epochs",
tree: groups.epochs.map(({ key, id, name }) =>
createCohortOptionsGroup({
scale,
color: colors[key],
@@ -4824,10 +4813,10 @@ function createPartialOptions(colors) {
name: "Simulations",
tree: [
{
icon: "🧪",
icon: "💰",
kind: "simulation",
title: "Dollar Cost Average Simulation",
name: "Dollar Cost Average",
title: "Simulation: Save In Bitcoin",
name: "Save In Bitcoin",
},
],
},

View File

@@ -32,4 +32,378 @@ export function init({
webSockets,
}) {
const simulationElement = elements.simulation;
const parametersElement = window.document.createElement("div");
simulationElement.append(parametersElement);
const resultsElement = window.document.createElement("div");
simulationElement.append(resultsElement);
const storagePrefix = "save-in-bitcoin";
const settings = {
initial: signals.createSignal(/** @type {number | null} */ (1000), {
save: {
...utils.serde.number,
id: `${storagePrefix}-initial-amount`,
param: "initial-amount",
},
}),
later: signals.createSignal(/** @type {number | null} */ (0), {
save: {
...utils.serde.number,
id: `${storagePrefix}-later-amount`,
param: "later-amount",
},
}),
recurrent: {
amount: signals.createSignal(/** @type {number | null} */ (100), {
save: {
...utils.serde.number,
id: `${storagePrefix}-recurrent-amount`,
param: "recurrent-amount",
},
}),
},
};
const initialGroup = createParameterGroup({
title: "Initial",
description:
"The initial amount of dollars you're willing to eventually save in Bitcoin.",
});
parametersElement.append(initialGroup);
initialGroup.append(
createInputField({
name: "Directly converted",
input: createInputDollar({
id: "simulation-dollars-initial",
title: "Initial amount of dollars converted",
signal: settings.initial,
}),
}),
);
initialGroup.append(
createInputField({
name: "Converted over time",
input: createInputDollar({
id: "simulation-dollars-later",
title: "Dollars to spread over time",
signal: settings.later,
}),
}),
);
parametersElement.append(createHrElement());
const recurrentGroup = createParameterGroup({
title: "Recurrent",
description:
"The recurrent amount of dollars you're willing to eventually save in Bitcoin.",
});
parametersElement.append(recurrentGroup);
recurrentGroup.append(
createInputField({
name: "Amount",
input: createInputDollar({
id: "simulation-dollars-recurrent",
title: "Recurrent dollar amount",
signal: settings.recurrent.amount,
}),
}),
);
const frequencyUL = appendUl({ parent: recurrentGroup });
[{ name: "Daily" }, { name: "Weekly" }, { name: "Monthly" }].forEach(
({ name }) => {
const li = appendLi({ name, parent: frequencyUL });
},
);
const frequencyChoiceUL = appendUl({ parent: recurrentGroup });
[
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
].forEach((name) => {
const li = appendLi({ name, parent: frequencyChoiceUL });
});
parametersElement.append(createHrElement());
const today = signals.createSignal(utils.date.todayUTC());
setInterval(() => {
today.set(utils.date.todayUTC());
}, consts.FIVE_SECONDS_IN_MS);
const intervalGroup = createParameterGroup({
title: "Interval",
description: "wkfpweokf",
});
parametersElement.append(intervalGroup);
/**
* @param {Object} args
* @param {HTMLElement} args.parent
*/
function appendDiv({ parent }) {
const div = window.document.createElement("div");
parent.append(div);
return div;
}
function createInputDateField() {
const div = appendDiv({ parent: intervalGroup });
appendInputDate({
id: "",
title: "",
value: "2021-04-15",
parent: div,
signals,
today,
});
appendButton({
onClick: () => {},
text: "Reset",
title: "",
parent: div,
});
return div;
}
createInputDateField();
createInputDateField();
parametersElement.append(createHrElement());
const feesGroup = createParameterGroup({
title: "Fees",
description:
"The amount of fees (in %) from where you'll be exchanging your currency",
});
parametersElement.append(feesGroup);
createInputNumber({
id: "",
title: "",
value: 0.25,
parent: feesGroup,
min: 0,
max: 10,
});
parametersElement.append(createHrElement());
const strategyGroup = createParameterGroup({
title: "Strategy",
description: "The strategy used to convert your fiat into Bitcoin",
});
parametersElement.append(strategyGroup);
const ulStrategies = appendUl({ parent: strategyGroup });
["All in", "Weighted Local", "Weighted Cycle"].forEach((strategy) => {
appendLi({
name: strategy,
parent: ulStrategies,
});
});
//
// On the side
// Value in Bitcoin
// Value in Dollars + total converted
//
// Value min estimated value in 4 years
//
}
/**
* @param {Object} args
* @param {HTMLInputElement} args.input
* @param {string} args.name
*/
function createInputField({ name, input }) {
const div = window.document.createElement("div");
const label = window.document.createElement("label");
div.append(label);
// @ts-ignore
label.for = input.id;
label.innerHTML = name;
div.append(input);
return div;
}
/**
* @param {Object} args
* @param {string} args.title
* @param {string} args.description
*/
function createParameterGroup({ title, description }) {
const div = window.document.createElement("div");
const wrapper = window.document.createElement("div");
div.append(wrapper);
const titleElement = window.document.createElement("h4");
titleElement.innerHTML = title;
wrapper.append(titleElement);
const descriptionElement = window.document.createElement("small");
descriptionElement.innerHTML = description;
wrapper.append(descriptionElement);
return div;
}
function createHrElement() {
return window.document.createElement("hr");
}
/**
*@param {Object} args
*@param {string} args.id
*@param {string} args.title
*@param {number} args.value
*@param {HTMLElement} args.parent
*@param {number} args.min
*@param {number} args.max
*/
function createInputNumber({ id, title, value, parent, min, max }) {
const input = window.document.createElement("input");
input.id = id;
input.title = title;
input.type = "number";
input.value = String(value);
input.min = String(min);
input.max = String(max);
parent.append(input);
return input;
}
/**
* @param {Object} args
* @param {string} args.id
* @param {string} args.title
* @param {Signal<number | null>} args.signal
*/
function createInputDollar({ id, title, signal }) {
const input = window.document.createElement("input");
input.id = id;
input.type = "number";
input.placeholder = "US Dollars";
input.min = "0";
input.title = title;
const value = signal();
input.value = value !== null ? String(value) : "";
input.addEventListener("input", () => {
const value = input.value;
signal.set(value ? Number(value) : null);
});
return input;
}
/**
* @param {Object} args
* @param {string} args.id
* @param {string} args.title
* @param {Signal<number | null>} args.signal
*/
function createInputRangePercentage({ id, title, signal }) {
const input = window.document.createElement("input");
input.id = id;
input.title = title;
input.type = "range";
input.min = "0";
input.max = "100";
const value = signal();
input.value = value !== null ? String(value) : "";
input.addEventListener("input", () => {
const value = input.value;
signal.set(value ? Number(value) : null);
});
return input;
}
/**
* @param {Object} args
* @param {HTMLElement} args.parent
*/
function appendUl({ parent }) {
const ul = window.document.createElement("ul");
parent.append(ul);
return ul;
}
/**
* @param {Object} args
* @param {string} args.name
* @param {HTMLUListElement} args.parent
*/
function appendLi({ name, parent }) {
const li = window.document.createElement("li");
li.innerHTML = name;
parent.append(li);
return li;
}
/**
*@param {Object} args
*@param {string} args.id
*@param {string} args.title
*@param {string} args.value
*@param {HTMLElement} args.parent
*@param {Accessor<Date>} args.today
*@param {Signals} args.signals
*/
function appendInputDate({ id, title, value, parent, today, signals }) {
const input = window.document.createElement("input");
input.id = id;
input.title = title;
input.type = "date";
input.value = value;
input.min = "2011-01-01";
signals.createEffect(() => {
input.max = today().toJSON().split("T")[0];
});
parent.append(input);
return input;
}
/**
*@param {Object} args
*@param {string} args.title
*@param {string} args.text
*@param {HTMLElement} args.parent
*@param {VoidFunction} args.onClick
*/
function appendButton({ title, text, onClick, parent }) {
const button = window.document.createElement("button");
button.title = title;
button.innerHTML = text;
button.addEventListener("click", onClick);
parent.append(button);
return button;
}

View File

@@ -127,8 +127,8 @@ interface PartialChartOption extends PartialOption {
interface PartialSimulationOption extends PartialOption {
kind: "simulation";
title: "Dollar Cost Average Simulation";
name: "Dollar Cost Average";
title: string;
name: string;
}
interface PartialPdfOption extends PartialOption {