mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-16 05:28:12 -07:00
heatmaps: part 22
This commit is contained in:
@@ -25,12 +25,16 @@ import { createNetworkSection } from "./network.js";
|
|||||||
import { createMiningSection } from "./mining.js";
|
import { createMiningSection } from "./mining.js";
|
||||||
import { createCointimeSection } from "./cointime.js";
|
import { createCointimeSection } from "./cointime.js";
|
||||||
import { createInvestingSection } from "./investing.js";
|
import { createInvestingSection } from "./investing.js";
|
||||||
import { demoHeatmapOption } from "../../src/heatmap/demo.js";
|
|
||||||
import {
|
import {
|
||||||
oracleOutputsHeatmapOption,
|
oracleOutputsHeatmapOption,
|
||||||
oraclePaymentsHeatmapOption,
|
oraclePaymentsHeatmapOption,
|
||||||
} from "../../src/heatmap/oracle.js";
|
} from "../../src/heatmap/oracle.js";
|
||||||
import { urpdSupplyHeatmapOption } from "../../src/heatmap/urpd.js";
|
import {
|
||||||
|
urpdAgeBandHeatmapFolders,
|
||||||
|
urpdAllHeatmapOptions,
|
||||||
|
urpdLthHeatmapOptions,
|
||||||
|
urpdSthHeatmapOptions,
|
||||||
|
} from "../../src/heatmap/urpd.js";
|
||||||
|
|
||||||
// Re-export types for external consumers
|
// Re-export types for external consumers
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
@@ -305,14 +309,18 @@ export function createPartialOptions() {
|
|||||||
{
|
{
|
||||||
name: "Heatmaps",
|
name: "Heatmaps",
|
||||||
tree: [
|
tree: [
|
||||||
demoHeatmapOption,
|
|
||||||
{
|
{
|
||||||
name: "oracle histograms",
|
name: "Output Values",
|
||||||
tree: [oracleOutputsHeatmapOption, oraclePaymentsHeatmapOption],
|
tree: [oracleOutputsHeatmapOption, oraclePaymentsHeatmapOption],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "URPD",
|
name: "Price Distributions",
|
||||||
tree: [urpdSupplyHeatmapOption],
|
tree: [
|
||||||
|
...urpdAllHeatmapOptions,
|
||||||
|
{ name: "STH", tree: urpdSthHeatmapOptions },
|
||||||
|
{ name: "LTH", tree: urpdLthHeatmapOptions },
|
||||||
|
{ name: "Age Bands", tree: urpdAgeBandHeatmapFolders },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ export function createDateControls(option, onChange) {
|
|||||||
const persistedFrom = createHeatmapPersistedValue(
|
const persistedFrom = createHeatmapPersistedValue(
|
||||||
option,
|
option,
|
||||||
"from",
|
"from",
|
||||||
"hm_from",
|
"from",
|
||||||
rangeChoiceLabel(defaultFromChoice),
|
rangeChoiceLabel(defaultFromChoice),
|
||||||
);
|
);
|
||||||
const persistedTo = createHeatmapPersistedValue(
|
const persistedTo = createHeatmapPersistedValue(
|
||||||
option,
|
option,
|
||||||
"to",
|
"to",
|
||||||
"hm_to",
|
"to",
|
||||||
rangeChoiceLabel(defaultToChoice),
|
rangeChoiceLabel(defaultToChoice),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -18,24 +18,24 @@ export function createYControls(option, onChange) {
|
|||||||
choices,
|
choices,
|
||||||
String(option.defaults?.yMin ?? ""),
|
String(option.defaults?.yMin ?? ""),
|
||||||
fallbackMinChoice,
|
fallbackMinChoice,
|
||||||
axisChoiceKey,
|
axisChoiceValueKey,
|
||||||
);
|
);
|
||||||
const defaultMaxChoice = findChoiceByKey(
|
const defaultMaxChoice = findChoiceByKey(
|
||||||
choices,
|
choices,
|
||||||
String(option.defaults?.yMax ?? ""),
|
String(option.defaults?.yMax ?? ""),
|
||||||
fallbackMaxChoice,
|
fallbackMaxChoice,
|
||||||
axisChoiceKey,
|
axisChoiceValueKey,
|
||||||
);
|
);
|
||||||
const persistedMin = createHeatmapPersistedValue(
|
const persistedMin = createHeatmapPersistedValue(
|
||||||
option,
|
option,
|
||||||
"y-min",
|
"y-min",
|
||||||
"hm_y_min",
|
"min",
|
||||||
axisChoiceKey(defaultMinChoice),
|
axisChoiceKey(defaultMinChoice),
|
||||||
);
|
);
|
||||||
const persistedMax = createHeatmapPersistedValue(
|
const persistedMax = createHeatmapPersistedValue(
|
||||||
option,
|
option,
|
||||||
"y-max",
|
"y-max",
|
||||||
"hm_y_max",
|
"max",
|
||||||
axisChoiceKey(defaultMaxChoice),
|
axisChoiceKey(defaultMaxChoice),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -105,10 +105,15 @@ export function createYControls(option, onChange) {
|
|||||||
|
|
||||||
/** @param {HeatmapAxisChoice} choice */
|
/** @param {HeatmapAxisChoice} choice */
|
||||||
function axisChoiceKey(choice) {
|
function axisChoiceKey(choice) {
|
||||||
return String(choice.value);
|
return choice.key ?? choice.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {HeatmapAxisChoice} choice */
|
/** @param {HeatmapAxisChoice} choice */
|
||||||
function axisChoiceLabel(choice) {
|
function axisChoiceLabel(choice) {
|
||||||
return choice.label;
|
return choice.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {HeatmapAxisChoice} choice */
|
||||||
|
function axisChoiceValueKey(choice) {
|
||||||
|
return String(choice.value);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import { createAverageGrid } from "./grid.js";
|
|
||||||
import { INFERNO_LUT, intensityColor } from "./lut.js";
|
|
||||||
import { GENESIS_DATE, todayISODate } from "./time.js";
|
|
||||||
import { defaultTooltip } from "./tooltip/index.js";
|
|
||||||
|
|
||||||
const ROWS = 160;
|
|
||||||
const DAY_MS = 86_400_000;
|
|
||||||
const GENESIS_TIME = Date.parse(`${GENESIS_DATE}T00:00:00Z`);
|
|
||||||
|
|
||||||
/** @satisfies {PartialHeatmapOption} */
|
|
||||||
export const demoHeatmapOption = {
|
|
||||||
kind: "heatmap",
|
|
||||||
name: "Demo",
|
|
||||||
title: "Heatmap Demo",
|
|
||||||
points: {
|
|
||||||
fetch: fetchDemoPoints,
|
|
||||||
},
|
|
||||||
grid: createAverageGrid({ yMin: 0, yMax: 1, nativeRows: ROWS }),
|
|
||||||
color: intensityColor(INFERNO_LUT),
|
|
||||||
tooltip: defaultTooltip(),
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} date
|
|
||||||
* @param {AbortSignal} signal
|
|
||||||
* @returns {Promise<HeatmapPoints>}
|
|
||||||
*/
|
|
||||||
async function fetchDemoPoints(date, signal) {
|
|
||||||
throwIfAborted(signal);
|
|
||||||
|
|
||||||
const values = new Float32Array(ROWS);
|
|
||||||
const endTime = Date.parse(`${todayISODate()}T00:00:00Z`);
|
|
||||||
const time = Date.parse(`${date}T00:00:00Z`);
|
|
||||||
const x = Math.min(
|
|
||||||
1,
|
|
||||||
Math.max(
|
|
||||||
0,
|
|
||||||
(time - GENESIS_TIME) / Math.max(DAY_MS, endTime - GENESIS_TIME),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let row = 0; row < ROWS; row++) {
|
|
||||||
const y = row / (ROWS - 1);
|
|
||||||
const ridge = Math.exp(-((y - (0.75 - x * 0.45)) ** 2) / 0.01);
|
|
||||||
const blob = Math.exp(
|
|
||||||
-(((x - 0.72) ** 2) / 0.018 + ((y - 0.28) ** 2) / 0.028),
|
|
||||||
);
|
|
||||||
const floor = x * 0.18 + (1 - y) * 0.12;
|
|
||||||
values[row] = Math.min(1, Math.max(0, ridge * 0.65 + blob * 0.45 + floor));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
kind: "implicit",
|
|
||||||
yStart: 0,
|
|
||||||
yStep: 1 / (ROWS - 1),
|
|
||||||
values,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {AbortSignal} signal */
|
|
||||||
function throwIfAborted(signal) {
|
|
||||||
if (signal.aborted) {
|
|
||||||
throw new DOMException("The operation was aborted.", "AbortError");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,7 +35,9 @@ export function createAverageGrid({
|
|||||||
const sums = new Float64Array(cols * rows);
|
const sums = new Float64Array(cols * rows);
|
||||||
const counts = new Uint32Array(cols * rows);
|
const counts = new Uint32Array(cols * rows);
|
||||||
const maxByCol = new Float64Array(cols);
|
const maxByCol = new Float64Array(cols);
|
||||||
|
const magnitudeMaxByCol = new Float64Array(cols);
|
||||||
let maxValue = 0;
|
let maxValue = 0;
|
||||||
|
let magnitudeMaxValue = 0;
|
||||||
const ySpan = yMax - yMin;
|
const ySpan = yMax - yMin;
|
||||||
|
|
||||||
/** @param {number} dateIndex */
|
/** @param {number} dateIndex */
|
||||||
@@ -77,14 +79,22 @@ export function createAverageGrid({
|
|||||||
/** @param {number} col */
|
/** @param {number} col */
|
||||||
function updateColumnMax(col) {
|
function updateColumnMax(col) {
|
||||||
let max = 0;
|
let max = 0;
|
||||||
|
let magnitudeMax = 0;
|
||||||
for (let row = 0; row < rows; row++) {
|
for (let row = 0; row < rows; row++) {
|
||||||
const index = row * cols + col;
|
const index = row * cols + col;
|
||||||
if (counts[index]) max = Math.max(max, sums[index] / counts[index]);
|
if (counts[index]) {
|
||||||
|
const value = sums[index] / counts[index];
|
||||||
|
max = Math.max(max, value);
|
||||||
|
magnitudeMax = Math.max(magnitudeMax, Math.abs(value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
maxByCol[col] = max;
|
maxByCol[col] = max;
|
||||||
|
magnitudeMaxByCol[col] = magnitudeMax;
|
||||||
maxValue = 0;
|
maxValue = 0;
|
||||||
|
magnitudeMaxValue = 0;
|
||||||
for (let c = 0; c < cols; c++) {
|
for (let c = 0; c < cols; c++) {
|
||||||
maxValue = Math.max(maxValue, maxByCol[c]);
|
maxValue = Math.max(maxValue, maxByCol[c]);
|
||||||
|
magnitudeMaxValue = Math.max(magnitudeMaxValue, magnitudeMaxByCol[c]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +127,14 @@ export function createAverageGrid({
|
|||||||
}
|
}
|
||||||
if (!dirty) return undefined;
|
if (!dirty) return undefined;
|
||||||
const previousMax = maxValue;
|
const previousMax = maxValue;
|
||||||
|
const previousMagnitudeMax = magnitudeMaxValue;
|
||||||
updateColumnMax(col);
|
updateColumnMax(col);
|
||||||
return { col, maxChanged: maxValue !== previousMax };
|
return {
|
||||||
|
col,
|
||||||
|
maxChanged:
|
||||||
|
maxValue !== previousMax ||
|
||||||
|
magnitudeMaxValue !== previousMagnitudeMax,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
getValue(col, row) {
|
getValue(col, row) {
|
||||||
if (col < 0 || col >= cols || row < 0 || row >= rows) {
|
if (col < 0 || col >= cols || row < 0 || row >= rows) {
|
||||||
@@ -131,8 +147,15 @@ export function createAverageGrid({
|
|||||||
if (col < 0 || col >= cols || row < 0 || row >= rows) return 0;
|
if (col < 0 || col >= cols || row < 0 || row >= rows) return 0;
|
||||||
return counts[row * cols + col];
|
return counts[row * cols + col];
|
||||||
},
|
},
|
||||||
getMaxValue() {
|
getMaxValue(col) {
|
||||||
return maxValue;
|
if (col === undefined) return maxValue;
|
||||||
|
if (col < 0 || col >= cols) return 0;
|
||||||
|
return maxByCol[col];
|
||||||
|
},
|
||||||
|
getMagnitudeMaxValue(col) {
|
||||||
|
if (col === undefined) return magnitudeMaxValue;
|
||||||
|
if (col < 0 || col >= cols) return 0;
|
||||||
|
return magnitudeMaxByCol[col];
|
||||||
},
|
},
|
||||||
getDateIndexRange(col) {
|
getDateIndexRange(col) {
|
||||||
if (col < 0 || col >= cols || dates.length === 0) {
|
if (col < 0 || col >= cols || dates.length === 0) {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function init() {
|
|||||||
yMin = range.yMin;
|
yMin = range.yMin;
|
||||||
yMax = range.yMax;
|
yMax = range.yMax;
|
||||||
hideTooltip();
|
hideTooltip();
|
||||||
rebuildGrid();
|
rebuildAndLoadVisibleDates();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,12 +102,18 @@ export function setOption(option) {
|
|||||||
function resizeAndRebuild() {
|
function resizeAndRebuild() {
|
||||||
if (!canvas || !renderer) return;
|
if (!canvas || !renderer) return;
|
||||||
const { width, height } = canvas.getBoundingClientRect();
|
const { width, height } = canvas.getBoundingClientRect();
|
||||||
if (renderer.resize(width, height)) rebuildGrid();
|
if (renderer.resize(width, height)) rebuildAndLoadVisibleDates();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadRange() {
|
function loadRange() {
|
||||||
if (!currentOption || !loader) return;
|
if (!currentOption || !loader) return;
|
||||||
loader.load({ option: currentOption, from, to });
|
loader.setRange({ option: currentOption, from, to });
|
||||||
|
rebuildAndLoadVisibleDates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildAndLoadVisibleDates() {
|
||||||
|
rebuildGrid();
|
||||||
|
loadVisibleDates();
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildGrid() {
|
function rebuildGrid() {
|
||||||
@@ -132,14 +138,41 @@ function rebuildGrid() {
|
|||||||
yMax,
|
yMax,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < dates.length; i++) {
|
for (const dateIndex of getVisibleDateIndexes(currentGrid)) {
|
||||||
const points = loader.getPoint(dates[i]);
|
const points = loader.getPoint(dates[dateIndex]);
|
||||||
if (points) currentGrid.add(i, points);
|
if (points) currentGrid.add(dateIndex, points);
|
||||||
}
|
}
|
||||||
|
|
||||||
paint();
|
paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadVisibleDates() {
|
||||||
|
if (!currentOption || !loader || !currentGrid) return;
|
||||||
|
loader.load({
|
||||||
|
option: currentOption,
|
||||||
|
dateIndexes: getVisibleDateIndexes(currentGrid),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HeatmapGrid} grid
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
function getVisibleDateIndexes(grid) {
|
||||||
|
/** @type {number[]} */
|
||||||
|
const indexes = [];
|
||||||
|
let previousDateIndex = -1;
|
||||||
|
for (let col = 0; col < grid.cols; col++) {
|
||||||
|
const dateIndex = grid.getDateIndexRange(col).end;
|
||||||
|
if (!Number.isInteger(dateIndex) || dateIndex === previousDateIndex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
previousDateIndex = dateIndex;
|
||||||
|
indexes.push(dateIndex);
|
||||||
|
}
|
||||||
|
return indexes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} dateIndex
|
* @param {number} dateIndex
|
||||||
* @param {HeatmapPoints} points
|
* @param {HeatmapPoints} points
|
||||||
|
|||||||
@@ -37,23 +37,41 @@ export function createHeatmapLoader({ addDateToGrid, rebuildGrid, paint }) {
|
|||||||
* @param {string} args.from
|
* @param {string} args.from
|
||||||
* @param {string} args.to
|
* @param {string} args.to
|
||||||
*/
|
*/
|
||||||
load({ option, from, to }) {
|
setRange({ option, from, to }) {
|
||||||
|
abortController?.abort();
|
||||||
|
generation += 1;
|
||||||
|
activeOption = option;
|
||||||
|
dates = dateRange(from, to);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param {Object} args
|
||||||
|
* @param {HeatmapOption} args.option
|
||||||
|
* @param {readonly number[]} args.dateIndexes
|
||||||
|
*/
|
||||||
|
load({ option, dateIndexes }) {
|
||||||
abortController?.abort();
|
abortController?.abort();
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const currentGeneration = ++generation;
|
const currentGeneration = ++generation;
|
||||||
activeOption = option;
|
activeOption = option;
|
||||||
abortController = controller;
|
abortController = controller;
|
||||||
dates = dateRange(from, to);
|
|
||||||
|
|
||||||
/** @type {{ date: string, dateIndex: number }[]} */
|
/** @type {{ date: string, dateIndex: number }[]} */
|
||||||
const missing = [];
|
const missing = [];
|
||||||
for (let dateIndex = 0; dateIndex < dates.length; dateIndex++) {
|
let previousDateIndex = -1;
|
||||||
|
for (const dateIndex of dateIndexes) {
|
||||||
|
if (
|
||||||
|
dateIndex === previousDateIndex ||
|
||||||
|
dateIndex < 0 ||
|
||||||
|
dateIndex >= dates.length
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
previousDateIndex = dateIndex;
|
||||||
const date = dates[dateIndex];
|
const date = dates[dateIndex];
|
||||||
if (!pointsByDate.has(date)) missing.push({ date, dateIndex });
|
if (!pointsByDate.has(date)) missing.push({ date, dateIndex });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!missing.length) {
|
if (!missing.length) {
|
||||||
rebuildGrid();
|
|
||||||
abortController = undefined;
|
abortController = undefined;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -90,8 +108,6 @@ export function createHeatmapLoader({ addDateToGrid, rebuildGrid, paint }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
rebuildGrid();
|
|
||||||
|
|
||||||
void Promise.all(workers).then(() => {
|
void Promise.all(workers).then(() => {
|
||||||
if (isCurrentLoad(option, controller, currentGeneration)) {
|
if (isCurrentLoad(option, controller, currentGeneration)) {
|
||||||
if (needsRebuild) {
|
if (needsRebuild) {
|
||||||
|
|||||||
+106
-1
@@ -12,6 +12,25 @@ const INFERNO_STOPS = [
|
|||||||
|
|
||||||
export const INFERNO_LUT = createColorLut(INFERNO_STOPS);
|
export const INFERNO_LUT = createColorLut(INFERNO_STOPS);
|
||||||
|
|
||||||
|
const DIVERGING_NEGATIVE_STOPS = [
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0.25, 60, 0, 0],
|
||||||
|
[0.5, 140, 10, 0],
|
||||||
|
[0.75, 200, 30, 10],
|
||||||
|
[1, 240, 60, 20],
|
||||||
|
];
|
||||||
|
|
||||||
|
const DIVERGING_POSITIVE_STOPS = [
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0.25, 0, 40, 0],
|
||||||
|
[0.5, 0, 110, 10],
|
||||||
|
[0.75, 10, 180, 20],
|
||||||
|
[1, 30, 230, 50],
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DIVERGING_NEGATIVE_LUT = createColorLut(DIVERGING_NEGATIVE_STOPS);
|
||||||
|
export const DIVERGING_POSITIVE_LUT = createColorLut(DIVERGING_POSITIVE_STOPS);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ArrayLike<number>} lut
|
* @param {ArrayLike<number>} lut
|
||||||
* @returns {HeatmapColorFn}
|
* @returns {HeatmapColorFn}
|
||||||
@@ -31,7 +50,7 @@ export function intensityColor(lut) {
|
|||||||
export function logIntensityColor(lut) {
|
export function logIntensityColor(lut) {
|
||||||
return (value, context) => {
|
return (value, context) => {
|
||||||
if (!Number.isFinite(value) || value <= 0) return 0x00000000;
|
if (!Number.isFinite(value) || value <= 0) return 0x00000000;
|
||||||
const max = context.grid.getMaxValue();
|
const max = context.grid.getMaxValue(context.col);
|
||||||
if (max <= 0) return 0x00000000;
|
if (max <= 0) return 0x00000000;
|
||||||
const t = Math.log2(value + 1) / Math.log2(max + 1);
|
const t = Math.log2(value + 1) / Math.log2(max + 1);
|
||||||
const index = Math.min(255, Math.max(0, Math.round(t * 255)));
|
const index = Math.min(255, Math.max(0, Math.round(t * 255)));
|
||||||
@@ -39,6 +58,92 @@ export function logIntensityColor(lut) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ArrayLike<number>} lut
|
||||||
|
* @returns {HeatmapColorFn}
|
||||||
|
*/
|
||||||
|
export function linearIntensityColor(lut) {
|
||||||
|
return (value, context) => {
|
||||||
|
if (!Number.isFinite(value) || value <= 0) return 0x00000000;
|
||||||
|
const cap = context.grid.getMaxValue(context.col);
|
||||||
|
if (cap <= 0) return 0x00000000;
|
||||||
|
const t = Math.min(1, value / cap);
|
||||||
|
const index = Math.min(255, Math.max(0, Math.round(t * 255)));
|
||||||
|
return lut[index] ?? 0x00000000;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ArrayLike<number>} lut
|
||||||
|
* @param {number} [exponent]
|
||||||
|
* @returns {HeatmapColorFn}
|
||||||
|
*/
|
||||||
|
export function powerIntensityColor(lut, exponent = 0.4) {
|
||||||
|
return (value, context) => {
|
||||||
|
if (!Number.isFinite(value) || value <= 0) return 0x00000000;
|
||||||
|
const cap = context.grid.getMaxValue(context.col);
|
||||||
|
if (cap <= 0) return 0x00000000;
|
||||||
|
const t = Math.pow(Math.min(1, value / cap), exponent);
|
||||||
|
const index = Math.min(255, Math.max(0, Math.round(t * 255)));
|
||||||
|
return lut[index] ?? 0x00000000;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ArrayLike<number>} negativeLut
|
||||||
|
* @param {ArrayLike<number>} positiveLut
|
||||||
|
* @param {number} [exponent]
|
||||||
|
* @returns {HeatmapColorFn}
|
||||||
|
*/
|
||||||
|
export function divergingPowerIntensityColor(
|
||||||
|
negativeLut,
|
||||||
|
positiveLut,
|
||||||
|
exponent = 0.4,
|
||||||
|
) {
|
||||||
|
return (value, context) => {
|
||||||
|
if (!Number.isFinite(value) || value === 0) return 0x00000000;
|
||||||
|
const cap = context.grid.getMagnitudeMaxValue(context.col);
|
||||||
|
if (cap <= 0) return 0x00000000;
|
||||||
|
const t = Math.pow(Math.min(1, Math.abs(value) / cap), exponent);
|
||||||
|
const index = Math.min(255, Math.max(0, Math.round(t * 255)));
|
||||||
|
const lut = value < 0 ? negativeLut : positiveLut;
|
||||||
|
return lut[index] ?? 0x00000000;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ArrayLike<number>} lut
|
||||||
|
* @param {{ knee?: number, max?: number }} [options]
|
||||||
|
* @returns {HeatmapColorFn}
|
||||||
|
*/
|
||||||
|
export function softIntensityColor(lut, { knee = 0.15, max = 1 } = {}) {
|
||||||
|
return (value, context) => {
|
||||||
|
if (!Number.isFinite(value) || value <= 0) return 0x00000000;
|
||||||
|
const cap = context.grid.getMaxValue(context.col);
|
||||||
|
if (cap <= 0) return 0x00000000;
|
||||||
|
const ratio = Math.min(1, value / cap);
|
||||||
|
const t = (ratio / (ratio + knee)) * max;
|
||||||
|
const index = Math.min(255, Math.max(0, Math.round(t * 255)));
|
||||||
|
return lut[index] ?? 0x00000000;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ArrayLike<number>} lut
|
||||||
|
* @returns {HeatmapColorFn}
|
||||||
|
*/
|
||||||
|
export function smoothLogIntensityColor(lut) {
|
||||||
|
return (value, context) => {
|
||||||
|
if (!Number.isFinite(value) || value <= 0) return 0x00000000;
|
||||||
|
const cap = context.grid.getMaxValue(context.col);
|
||||||
|
if (cap <= 0) return 0x00000000;
|
||||||
|
const u = Math.log1p(value) / Math.log1p(cap);
|
||||||
|
const t = u * u * (3 - 2 * u);
|
||||||
|
const index = Math.min(255, Math.max(0, Math.round(t * 255)));
|
||||||
|
return lut[index] ?? 0x00000000;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number[][]} stops - Tuples of [position, red, green, blue].
|
* @param {number[][]} stops - Tuples of [position, red, green, blue].
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,28 +7,28 @@ const BINS = 2400;
|
|||||||
const MIN_LOG = -8;
|
const MIN_LOG = -8;
|
||||||
const BINS_PER_DECADE = 200;
|
const BINS_PER_DECADE = 200;
|
||||||
const AMOUNT_CHOICES = [
|
const AMOUNT_CHOICES = [
|
||||||
{ label: "1 sat", value: -8 },
|
{ label: "1 sat", key: "1sat", value: -8 },
|
||||||
{ label: "10 sats", value: -7 },
|
{ label: "10 sats", key: "10sats", value: -7 },
|
||||||
{ label: "100 sats", value: -6 },
|
{ label: "100 sats", key: "100sats", value: -6 },
|
||||||
{ label: "1k sats", value: -5 },
|
{ label: "1k sats", key: "1ksats", value: -5 },
|
||||||
{ label: "10k sats", value: -4 },
|
{ label: "10k sats", key: "10ksats", value: -4 },
|
||||||
{ label: "100k sats", value: -3 },
|
{ label: "100k sats", key: "100ksats", value: -3 },
|
||||||
{ label: "0.01 BTC", value: -2 },
|
{ label: "0.01 BTC", key: "0.01btc", value: -2 },
|
||||||
{ label: "0.1 BTC", value: -1 },
|
{ label: "0.1 BTC", key: "0.1btc", value: -1 },
|
||||||
{ label: "1 BTC", value: 0 },
|
{ label: "1 BTC", key: "1btc", value: 0 },
|
||||||
{ label: "10 BTC", value: 1 },
|
{ label: "10 BTC", key: "10btc", value: 1 },
|
||||||
{ label: "100 BTC", value: 2 },
|
{ label: "100 BTC", key: "100btc", value: 2 },
|
||||||
{ label: "1k BTC", value: 3 },
|
{ label: "1k BTC", key: "1kbtc", value: 3 },
|
||||||
{ label: "10k BTC", value: 4 },
|
{ label: "10k BTC", key: "10kbtc", value: 4 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const oracleOutputsHeatmapOption = createOracleHeatmapOption(
|
export const oracleOutputsHeatmapOption = createOracleHeatmapOption(
|
||||||
"outputs",
|
"outputs",
|
||||||
"outputs",
|
"All",
|
||||||
);
|
);
|
||||||
export const oraclePaymentsHeatmapOption = createOracleHeatmapOption(
|
export const oraclePaymentsHeatmapOption = createOracleHeatmapOption(
|
||||||
"payments",
|
"payments",
|
||||||
"payments",
|
"Payments",
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +41,7 @@ function createOracleHeatmapOption(mode, name) {
|
|||||||
kind: "heatmap",
|
kind: "heatmap",
|
||||||
name,
|
name,
|
||||||
title:
|
title:
|
||||||
mode === "outputs" ? "Output Value Histogram" : "Payment Value Histogram",
|
mode === "outputs" ? "All Output Values" : "Payment Output Values",
|
||||||
points: {
|
points: {
|
||||||
fetch: (date, signal, onPoints) =>
|
fetch: (date, signal, onPoints) =>
|
||||||
fetchOraclePoints(mode, date, signal, onPoints),
|
fetchOraclePoints(mode, date, signal, onPoints),
|
||||||
@@ -74,8 +74,8 @@ function createOracleHeatmapOption(mode, name) {
|
|||||||
},
|
},
|
||||||
tooltip: defaultTooltip(
|
tooltip: defaultTooltip(
|
||||||
mode === "outputs"
|
mode === "outputs"
|
||||||
? { valueLabel: "Outputs", averageLabel: "Avg outputs" }
|
? { valueLabel: "Outputs" }
|
||||||
: { valueLabel: "Payment signal", averageLabel: "Avg payment signal" },
|
: { valueLabel: "Payment signal" },
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import { numberToShortUSFormat } from "../../../scripts/utils/format.js";
|
|||||||
/**
|
/**
|
||||||
* @param {Object} [args]
|
* @param {Object} [args]
|
||||||
* @param {string} [args.valueLabel]
|
* @param {string} [args.valueLabel]
|
||||||
* @param {string} [args.averageLabel]
|
|
||||||
* @param {(value: number) => string} [args.formatValue]
|
* @param {(value: number) => string} [args.formatValue]
|
||||||
* @returns {HeatmapTooltipFn}
|
* @returns {HeatmapTooltipFn}
|
||||||
*/
|
*/
|
||||||
export function defaultTooltip({
|
export function defaultTooltip({
|
||||||
valueLabel = "Value",
|
valueLabel = "Value",
|
||||||
averageLabel = "Avg value",
|
|
||||||
formatValue = formatNumber,
|
formatValue = formatNumber,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return ({ option, grid, col, row }) => {
|
return ({ option, grid, col, row }) => {
|
||||||
@@ -18,16 +16,13 @@ export function defaultTooltip({
|
|||||||
const value = grid.getValue(col, row);
|
const value = grid.getValue(col, row);
|
||||||
const yLabel = option.axis?.y?.label ?? "y";
|
const yLabel = option.axis?.y?.label ?? "y";
|
||||||
const formatY = option.axis?.y?.format ?? formatNumber;
|
const formatY = option.axis?.y?.format ?? formatNumber;
|
||||||
const label = grid.getCount(col, row) > 1 ? averageLabel : valueLabel;
|
|
||||||
|
|
||||||
const from = grid.dates[dateRange.start] ?? "";
|
const date = grid.dates[dateRange.end] ?? grid.dates[dateRange.start] ?? "";
|
||||||
const to = grid.dates[dateRange.end] ?? from;
|
|
||||||
const date = from === to ? from : `${from} to ${to}`;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
date,
|
date,
|
||||||
`${capitalize(yLabel)}: ${formatY(yRange.start)} to ${formatY(yRange.end)}`,
|
`${capitalize(yLabel)}: ${formatY(yRange.start)} to ${formatY(yRange.end)}`,
|
||||||
`${label}: ${formatValue(value)}`,
|
`${valueLabel}: ${formatValue(value)}`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,8 @@
|
|||||||
* @property {(dateIndex: number, points: HeatmapPoints) => HeatmapGridAddResult | undefined} add
|
* @property {(dateIndex: number, points: HeatmapPoints) => HeatmapGridAddResult | undefined} add
|
||||||
* @property {(col: number, row: number) => number} getValue
|
* @property {(col: number, row: number) => number} getValue
|
||||||
* @property {(col: number, row: number) => number} getCount
|
* @property {(col: number, row: number) => number} getCount
|
||||||
* @property {() => number} getMaxValue
|
* @property {(col?: number) => number} getMaxValue
|
||||||
|
* @property {(col?: number) => number} getMagnitudeMaxValue
|
||||||
* @property {(col: number) => HeatmapRange} getDateIndexRange
|
* @property {(col: number) => HeatmapRange} getDateIndexRange
|
||||||
* @property {(row: number) => HeatmapRange} getYRange
|
* @property {(row: number) => HeatmapRange} getYRange
|
||||||
*
|
*
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
*
|
*
|
||||||
* @typedef {Object} HeatmapAxisChoice
|
* @typedef {Object} HeatmapAxisChoice
|
||||||
* @property {string} label
|
* @property {string} label
|
||||||
|
* @property {string} [key]
|
||||||
* @property {number} value
|
* @property {number} value
|
||||||
*
|
*
|
||||||
* @typedef {Object} HeatmapAxis
|
* @typedef {Object} HeatmapAxis
|
||||||
|
|||||||
+205
-57
@@ -1,96 +1,230 @@
|
|||||||
import { brk } from "../../scripts/utils/client.js";
|
import { brk } from "../../scripts/utils/client.js";
|
||||||
import { numberToShortUSFormat } from "../../scripts/utils/format.js";
|
import { numberToShortUSFormat } from "../../scripts/utils/format.js";
|
||||||
import { createAverageGrid } from "./grid.js";
|
import { createAverageGrid } from "./grid.js";
|
||||||
import { INFERNO_LUT, logIntensityColor } from "./lut.js";
|
import {
|
||||||
|
DIVERGING_NEGATIVE_LUT,
|
||||||
|
DIVERGING_POSITIVE_LUT,
|
||||||
|
INFERNO_LUT,
|
||||||
|
divergingPowerIntensityColor,
|
||||||
|
powerIntensityColor,
|
||||||
|
} from "./lut.js";
|
||||||
import { defaultTooltip } from "./tooltip/index.js";
|
import { defaultTooltip } from "./tooltip/index.js";
|
||||||
|
|
||||||
const COHORT = "all";
|
/** @typedef {Brk.Cohort} UrpdCohort */
|
||||||
const AGGREGATION = "raw";
|
/**
|
||||||
|
* @typedef {Object} UrpdMetric
|
||||||
|
* @property {string} name
|
||||||
|
* @property {string} title
|
||||||
|
* @property {(bucket: Urpd["buckets"][number]) => number} getValue
|
||||||
|
* @property {HeatmapColorFn} color
|
||||||
|
* @property {{ valueLabel: string, formatValue: (value: number) => string }} tooltip
|
||||||
|
*/
|
||||||
|
/** @typedef {{ name: string, cohort: UrpdCohort }} UrpdCohortFolder */
|
||||||
|
|
||||||
|
const AGGREGATION = "log2000";
|
||||||
const MIN_LOG = -2;
|
const MIN_LOG = -2;
|
||||||
const MAX_LOG = 6;
|
const MAX_LOG = 6;
|
||||||
const DEFAULT_MIN_LOG = Math.log10(1_000);
|
const DEFAULT_MIN_LOG = Math.log10(1_000);
|
||||||
const DEFAULT_MAX_LOG = Math.log10(250_000);
|
const DEFAULT_MAX_LOG = Math.log10(250_000);
|
||||||
const PRICE_CHOICES = [
|
const PRICE_CHOICES = [
|
||||||
{ label: "$0.01", value: Math.log10(0.01) },
|
{ label: "$0.01", key: "0.01", value: Math.log10(0.01) },
|
||||||
{ label: "$0.1", value: Math.log10(0.1) },
|
{ label: "$0.1", key: "0.1", value: Math.log10(0.1) },
|
||||||
{ label: "$1", value: 0 },
|
{ label: "$1", key: "1", value: 0 },
|
||||||
{ label: "$10", value: 1 },
|
{ label: "$10", key: "10", value: 1 },
|
||||||
{ label: "$100", value: 2 },
|
{ label: "$100", key: "100", value: 2 },
|
||||||
{ label: "$250", value: Math.log10(250) },
|
{ label: "$250", key: "250", value: Math.log10(250) },
|
||||||
{ label: "$1k", value: Math.log10(1_000) },
|
{ label: "$1k", key: "1k", value: Math.log10(1_000) },
|
||||||
{ label: "$2.5k", value: Math.log10(2_500) },
|
{ label: "$2.5k", key: "2.5k", value: Math.log10(2_500) },
|
||||||
{ label: "$5k", value: Math.log10(5_000) },
|
{ label: "$5k", key: "5k", value: Math.log10(5_000) },
|
||||||
{ label: "$10k", value: Math.log10(10_000) },
|
{ label: "$10k", key: "10k", value: Math.log10(10_000) },
|
||||||
{ label: "$25k", value: Math.log10(25_000) },
|
{ label: "$25k", key: "25k", value: Math.log10(25_000) },
|
||||||
{ label: "$50k", value: Math.log10(50_000) },
|
{ label: "$50k", key: "50k", value: Math.log10(50_000) },
|
||||||
{ label: "$100k", value: Math.log10(100_000) },
|
{ label: "$100k", key: "100k", value: Math.log10(100_000) },
|
||||||
{ label: "$250k", value: Math.log10(250_000) },
|
{ label: "$250k", key: "250k", value: Math.log10(250_000) },
|
||||||
{ label: "$500k", value: Math.log10(500_000) },
|
{ label: "$500k", key: "500k", value: Math.log10(500_000) },
|
||||||
{ label: "$1M", value: Math.log10(1_000_000) },
|
{ label: "$1M", key: "1M", value: Math.log10(1_000_000) },
|
||||||
];
|
];
|
||||||
|
const VALUE_COLOR = powerIntensityColor(INFERNO_LUT, 0.4);
|
||||||
|
const PNL_COLOR = divergingPowerIntensityColor(
|
||||||
|
DIVERGING_NEGATIVE_LUT,
|
||||||
|
DIVERGING_POSITIVE_LUT,
|
||||||
|
0.4,
|
||||||
|
);
|
||||||
|
|
||||||
/** @satisfies {PartialHeatmapOption} */
|
/** @type {UrpdMetric[]} */
|
||||||
export const urpdSupplyHeatmapOption = {
|
const METRICS = [
|
||||||
kind: "heatmap",
|
{
|
||||||
name: "Supply",
|
name: "supply",
|
||||||
title: "URPD Supply",
|
title: "Supply",
|
||||||
points: {
|
getValue: (bucket) => bucket.supply,
|
||||||
fetch: (date, signal, onPoints) =>
|
color: VALUE_COLOR,
|
||||||
fetchUrpdSupplyPoints(date, signal, onPoints),
|
tooltip: {
|
||||||
},
|
valueLabel: "Supply",
|
||||||
grid: createAverageGrid({
|
formatValue: formatBitcoin,
|
||||||
yMin: MIN_LOG,
|
|
||||||
yMax: MAX_LOG,
|
|
||||||
}),
|
|
||||||
color: logIntensityColor(INFERNO_LUT),
|
|
||||||
axis: {
|
|
||||||
y: {
|
|
||||||
label: "price",
|
|
||||||
choices: PRICE_CHOICES,
|
|
||||||
format: formatPrice,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaults: {
|
{
|
||||||
from: "2017",
|
name: "capital",
|
||||||
to: "today",
|
title: "Capital",
|
||||||
yMin: DEFAULT_MIN_LOG,
|
getValue: (bucket) => bucket.realizedCap,
|
||||||
yMax: DEFAULT_MAX_LOG,
|
color: VALUE_COLOR,
|
||||||
|
tooltip: {
|
||||||
|
valueLabel: "Realized cap",
|
||||||
|
formatValue: formatDollar,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tooltip: defaultTooltip({
|
{
|
||||||
valueLabel: "Supply",
|
name: "profitability",
|
||||||
averageLabel: "Avg supply",
|
title: "Profitability",
|
||||||
formatValue: formatBitcoin,
|
getValue: (bucket) => bucket.unrealizedPnl,
|
||||||
}),
|
color: PNL_COLOR,
|
||||||
};
|
tooltip: {
|
||||||
|
valueLabel: "Unrealized PnL",
|
||||||
|
formatValue: formatSignedDollar,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @type {UrpdCohortFolder[]} */
|
||||||
|
const AGE_BANDS = [
|
||||||
|
{ name: "Up to 1h", cohort: "utxos_under_1h_old" },
|
||||||
|
{ name: "1h to 1d", cohort: "utxos_1h_to_1d_old" },
|
||||||
|
{ name: "1d to 1w", cohort: "utxos_1d_to_1w_old" },
|
||||||
|
{ name: "1w to 1m", cohort: "utxos_1w_to_1m_old" },
|
||||||
|
{ name: "1m to 2m", cohort: "utxos_1m_to_2m_old" },
|
||||||
|
{ name: "2m to 3m", cohort: "utxos_2m_to_3m_old" },
|
||||||
|
{ name: "3m to 4m", cohort: "utxos_3m_to_4m_old" },
|
||||||
|
{ name: "4m to 5m", cohort: "utxos_4m_to_5m_old" },
|
||||||
|
{ name: "5m to 6m", cohort: "utxos_5m_to_6m_old" },
|
||||||
|
{ name: "6m to 1y", cohort: "utxos_6m_to_1y_old" },
|
||||||
|
{ name: "1y to 2y", cohort: "utxos_1y_to_2y_old" },
|
||||||
|
{ name: "2y to 3y", cohort: "utxos_2y_to_3y_old" },
|
||||||
|
{ name: "3y to 4y", cohort: "utxos_3y_to_4y_old" },
|
||||||
|
{ name: "4y to 5y", cohort: "utxos_4y_to_5y_old" },
|
||||||
|
{ name: "5y to 6y", cohort: "utxos_5y_to_6y_old" },
|
||||||
|
{ name: "6y to 7y", cohort: "utxos_6y_to_7y_old" },
|
||||||
|
{ name: "7y to 8y", cohort: "utxos_7y_to_8y_old" },
|
||||||
|
{ name: "8y to 10y", cohort: "utxos_8y_to_10y_old" },
|
||||||
|
{ name: "10y to 12y", cohort: "utxos_10y_to_12y_old" },
|
||||||
|
{ name: "12y to 15y", cohort: "utxos_12y_to_15y_old" },
|
||||||
|
{ name: "Over 15y", cohort: "utxos_over_15y_old" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const urpdAllHeatmapOptions = createCohortHeatmapOptions({
|
||||||
|
cohort: "all",
|
||||||
|
});
|
||||||
|
export const urpdSthHeatmapOptions = createCohortHeatmapOptions({
|
||||||
|
cohort: "sth",
|
||||||
|
titlePrefix: "STH",
|
||||||
|
});
|
||||||
|
export const urpdLthHeatmapOptions = createCohortHeatmapOptions({
|
||||||
|
cohort: "lth",
|
||||||
|
titlePrefix: "LTH",
|
||||||
|
});
|
||||||
|
export const urpdAgeBandHeatmapFolders = AGE_BANDS.map(({ name, cohort }) => ({
|
||||||
|
name,
|
||||||
|
tree: createCohortHeatmapOptions({ cohort, titlePrefix: name }),
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param {Object} args
|
||||||
|
* @param {UrpdCohort} args.cohort
|
||||||
|
* @param {string} [args.titlePrefix]
|
||||||
|
* @returns {PartialHeatmapOption[]}
|
||||||
|
*/
|
||||||
|
function createCohortHeatmapOptions({ cohort, titlePrefix }) {
|
||||||
|
return METRICS.map((metric) => {
|
||||||
|
const title = titlePrefix
|
||||||
|
? `${titlePrefix} ${metric.title} Distribution`
|
||||||
|
: `${metric.title} Distribution`;
|
||||||
|
|
||||||
|
return createUrpdHeatmapOption({
|
||||||
|
...metric,
|
||||||
|
cohort,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} args
|
||||||
|
* @param {UrpdCohort} args.cohort
|
||||||
|
* @param {string} args.name
|
||||||
|
* @param {string} args.title
|
||||||
|
* @param {(bucket: Urpd["buckets"][number]) => number} args.getValue
|
||||||
|
* @param {HeatmapColorFn} args.color
|
||||||
|
* @param {{ valueLabel?: string, formatValue?: (value: number) => string }} args.tooltip
|
||||||
|
* @returns {PartialHeatmapOption}
|
||||||
|
*/
|
||||||
|
function createUrpdHeatmapOption({
|
||||||
|
cohort,
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
getValue,
|
||||||
|
color,
|
||||||
|
tooltip,
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
kind: "heatmap",
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
points: {
|
||||||
|
fetch: (date, signal, onPoints) =>
|
||||||
|
fetchUrpdPoints(cohort, date, signal, getValue, onPoints),
|
||||||
|
},
|
||||||
|
grid: createAverageGrid({
|
||||||
|
yMin: MIN_LOG,
|
||||||
|
yMax: MAX_LOG,
|
||||||
|
}),
|
||||||
|
color,
|
||||||
|
axis: {
|
||||||
|
y: {
|
||||||
|
label: "price",
|
||||||
|
choices: PRICE_CHOICES,
|
||||||
|
format: formatPrice,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
from: "2017",
|
||||||
|
to: "today",
|
||||||
|
yMin: DEFAULT_MIN_LOG,
|
||||||
|
yMax: DEFAULT_MAX_LOG,
|
||||||
|
},
|
||||||
|
tooltip: defaultTooltip(tooltip),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UrpdCohort} cohort
|
||||||
* @param {string} date
|
* @param {string} date
|
||||||
* @param {AbortSignal} signal
|
* @param {AbortSignal} signal
|
||||||
|
* @param {(bucket: Urpd["buckets"][number]) => number} getValue
|
||||||
* @param {(points: HeatmapPoints) => void} [onPoints]
|
* @param {(points: HeatmapPoints) => void} [onPoints]
|
||||||
* @returns {Promise<HeatmapPoints>}
|
* @returns {Promise<HeatmapPoints>}
|
||||||
*/
|
*/
|
||||||
async function fetchUrpdSupplyPoints(date, signal, onPoints) {
|
async function fetchUrpdPoints(cohort, date, signal, getValue, onPoints) {
|
||||||
/** @type {HeatmapPoints | undefined} */
|
/** @type {HeatmapPoints | undefined} */
|
||||||
let points;
|
let points;
|
||||||
const urpd = await brk.getUrpdAt(COHORT, date, AGGREGATION, {
|
const urpd = await brk.getUrpdAt(cohort, date, AGGREGATION, {
|
||||||
signal,
|
signal,
|
||||||
cache: false,
|
cache: false,
|
||||||
onValue: onPoints
|
onValue: onPoints
|
||||||
? (value) => {
|
? (urpd) => {
|
||||||
points = toSupplyPoints(value);
|
points = toPoints(urpd, getValue);
|
||||||
onPoints(points);
|
onPoints(points);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return points ?? toSupplyPoints(urpd);
|
return points ?? toPoints(urpd, getValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Urpd} urpd
|
* @param {Urpd} urpd
|
||||||
|
* @param {(bucket: Urpd["buckets"][number]) => number} getValue
|
||||||
* @returns {HeatmapPoints}
|
* @returns {HeatmapPoints}
|
||||||
*/
|
*/
|
||||||
function toSupplyPoints(urpd) {
|
function toPoints(urpd, getValue) {
|
||||||
const buckets = urpd.buckets;
|
const buckets = urpd.buckets;
|
||||||
const y = new Float64Array(buckets.length);
|
const y = new Float64Array(buckets.length);
|
||||||
const values = new Float64Array(buckets.length);
|
const values = new Float64Array(buckets.length);
|
||||||
@@ -98,9 +232,10 @@ function toSupplyPoints(urpd) {
|
|||||||
|
|
||||||
for (let i = 0; i < buckets.length; i++) {
|
for (let i = 0; i < buckets.length; i++) {
|
||||||
const bucket = buckets[i];
|
const bucket = buckets[i];
|
||||||
if (bucket.priceFloor <= 0 || !Number.isFinite(bucket.supply)) continue;
|
const pointValue = getValue(bucket);
|
||||||
|
if (bucket.priceFloor <= 0 || !Number.isFinite(pointValue)) continue;
|
||||||
y[length] = Math.log10(bucket.priceFloor);
|
y[length] = Math.log10(bucket.priceFloor);
|
||||||
values[length] = bucket.supply;
|
values[length] = pointValue;
|
||||||
length++;
|
length++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +265,19 @@ function formatBitcoin(value) {
|
|||||||
return `${numberToShortUSFormat(value)} BTC`;
|
return `${numberToShortUSFormat(value)} BTC`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {number} value */
|
||||||
|
function formatDollar(value) {
|
||||||
|
return `$${numberToShortUSFormat(value)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} value */
|
||||||
|
function formatSignedDollar(value) {
|
||||||
|
const formatted = `$${numberToShortUSFormat(Math.abs(value))}`;
|
||||||
|
if (value > 0) return `+${formatted}`;
|
||||||
|
if (value < 0) return `-${formatted}`;
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
/** @param {number} value */
|
/** @param {number} value */
|
||||||
function formatCompact(value) {
|
function formatCompact(value) {
|
||||||
if (value >= 1000) return `${formatNumber(value / 1000)}k`;
|
if (value >= 1000) return `${formatNumber(value / 1000)}k`;
|
||||||
|
|||||||
Reference in New Issue
Block a user