mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 06:01:57 -07:00
heatmaps: part 1
This commit is contained in:
Generated
+8
-8
@@ -700,9 +700,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.2"
|
||||
version = "8.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
|
||||
checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
@@ -711,9 +711,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "5.0.0"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
|
||||
checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
@@ -2082,9 +2082,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
@@ -2859,9 +2859,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.95.0"
|
||||
channel = "1.96.0"
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<link rel="stylesheet" href="/styles/reset.css" />
|
||||
<link rel="stylesheet" href="/styles/search.css" />
|
||||
<link rel="stylesheet" href="/styles/variables.css" />
|
||||
<link rel="stylesheet" href="/src/heatmap/style.css" />
|
||||
<!-- /IMPORTMAP -->
|
||||
|
||||
<!-- ------- -->
|
||||
@@ -149,6 +150,7 @@
|
||||
<aside id="aside">
|
||||
<div id="explorer" hidden></div>
|
||||
<div id="chart" hidden></div>
|
||||
<div id="heatmap" hidden></div>
|
||||
</aside>
|
||||
<footer>
|
||||
<fieldset id="frame-selectors">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*
|
||||
* @import { Color } from "./utils/colors.js"
|
||||
*
|
||||
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddrCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, FetchedDotsBaselineSeriesBlueprint, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, PatternWithoutRelative, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortWithoutRelative, CohortAddr, CohortLongTerm, CohortAgeRange, CohortAgeRangeWithMatured, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupWithoutRelative, CohortGroupAddr, UtxoCohortGroupObject, AddrCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern, AnyValuePattern } from "./options/partial.js"
|
||||
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddrCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, FetchedDotsBaselineSeriesBlueprint, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, PatternWithoutRelative, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortWithoutRelative, CohortAddr, CohortLongTerm, CohortAgeRange, CohortAgeRangeWithMatured, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupWithoutRelative, CohortGroupAddr, UtxoCohortGroupObject, AddrCohortGroupObject, FetchedDotsSeriesBlueprint, HeatmapOption, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern, AnyValuePattern } from "./options/partial.js"
|
||||
*
|
||||
*
|
||||
* @import { UnitObject as Unit } from "./utils/units.js"
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "./panes/chart.js";
|
||||
import { init as initExplorer } from "./explorer/index.js";
|
||||
import { init as initSearch } from "./panes/search.js";
|
||||
import { init as initHeatmap } from "../src/heatmap/index.js";
|
||||
import { readStored, removeStored, writeToStorage } from "./utils/storage.js";
|
||||
import {
|
||||
asideElement,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
navLabelElement,
|
||||
searchElement,
|
||||
layoutButtonElement,
|
||||
heatmapElement,
|
||||
style,
|
||||
} from "./utils/elements.js";
|
||||
import { idle } from "./utils/timing.js";
|
||||
@@ -125,12 +127,15 @@ function initSelected() {
|
||||
|
||||
let previousElement = /** @type {HTMLElement | undefined} */ (undefined);
|
||||
let firstTimeLoadingChart = true;
|
||||
let firstTimeLoadingHeatmap = true;
|
||||
let firstTimeLoadingExplorer = true;
|
||||
|
||||
options.selected.onChange((option) => {
|
||||
/** @type {HTMLElement | undefined} */
|
||||
let element;
|
||||
|
||||
console.log(option);
|
||||
|
||||
switch (option.kind) {
|
||||
case "explorer": {
|
||||
element = explorerElement;
|
||||
@@ -142,7 +147,21 @@ function initSelected() {
|
||||
|
||||
break;
|
||||
}
|
||||
case "heatmap": {
|
||||
console.log("heatmap");
|
||||
|
||||
element = heatmapElement;
|
||||
|
||||
if (firstTimeLoadingHeatmap) {
|
||||
initHeatmap(option);
|
||||
}
|
||||
firstTimeLoadingHeatmap = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case "chart": {
|
||||
console.log("chart");
|
||||
|
||||
element = chartElement;
|
||||
|
||||
if (firstTimeLoadingChart) {
|
||||
|
||||
@@ -311,6 +311,14 @@ export function initOptions() {
|
||||
option.kind = anyPartial.kind;
|
||||
option.path = [];
|
||||
option.name = name;
|
||||
} else if ("kind" in anyPartial && anyPartial.kind === "heatmap") {
|
||||
Object.assign(
|
||||
option,
|
||||
/** @satisfies {HeatmapOption} */ ({
|
||||
...anyPartial,
|
||||
path,
|
||||
}),
|
||||
);
|
||||
} else if ("url" in anyPartial) {
|
||||
Object.assign(
|
||||
option,
|
||||
|
||||
@@ -63,6 +63,7 @@ export function createPartialOptions() {
|
||||
kind: "explorer",
|
||||
title: "Explorer",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Charts",
|
||||
tree: [
|
||||
@@ -295,6 +296,17 @@ export function createPartialOptions() {
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Heatmaps",
|
||||
tree: [
|
||||
{
|
||||
kind: "heatmap",
|
||||
name: "name",
|
||||
title: "name",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "API",
|
||||
url: () => "/api",
|
||||
|
||||
@@ -104,6 +104,14 @@
|
||||
*
|
||||
* @typedef {Required<Omit<PartialChartOption, "top" | "bottom">> & ProcessedChartOptionAddons & ProcessedOptionAddons} ChartOption
|
||||
*
|
||||
* @typedef {Object} PartialHeatmapOptionSpecific
|
||||
* @property {"heatmap"} kind
|
||||
* @property {string} title
|
||||
*
|
||||
* @typedef {PartialOption & PartialHeatmapOptionSpecific} PartialHeatmapOption
|
||||
*
|
||||
* @typedef {Required<PartialHeatmapOption> & ProcessedOptionAddons} HeatmapOption
|
||||
*
|
||||
* @typedef {Object} PartialUrlOptionSpecific
|
||||
* @property {"link"} [kind]
|
||||
* @property {() => string} url
|
||||
@@ -114,9 +122,9 @@
|
||||
*
|
||||
* @typedef {Required<PartialUrlOption> & ProcessedOptionAddons} UrlOption
|
||||
*
|
||||
* @typedef {PartialExplorerOption | PartialChartOption | PartialUrlOption} AnyPartialOption
|
||||
* @typedef {PartialExplorerOption | PartialChartOption | PartialUrlOption | PartialHeatmapOption} AnyPartialOption
|
||||
*
|
||||
* @typedef {ExplorerOption | ChartOption | UrlOption} Option
|
||||
* @typedef {ExplorerOption | ChartOption | UrlOption | HeatmapOption} Option
|
||||
*
|
||||
* @typedef {(AnyPartialOption | PartialOptionsGroup)[]} PartialOptionsTree
|
||||
*
|
||||
@@ -324,7 +332,6 @@
|
||||
*
|
||||
* @typedef {UtxoCohortObject | AddrCohortObject | CohortWithoutRelative} CohortObject
|
||||
*
|
||||
*
|
||||
* @typedef {Object} AddrCohortGroupObject
|
||||
* @property {string} name
|
||||
* @property {string} title
|
||||
|
||||
@@ -11,6 +11,7 @@ export const searchElement = getElementById("search");
|
||||
export const navElement = getElementById("nav");
|
||||
export const chartElement = getElementById("chart");
|
||||
export const explorerElement = getElementById("explorer");
|
||||
export const heatmapElement = getElementById("heatmap");
|
||||
|
||||
export const asideLabelElement = getElementById("aside-selector-label");
|
||||
export const navLabelElement = getElementById(`nav-selector-label`);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { createHeader } from "../../scripts/utils/dom.js";
|
||||
import { heatmapElement } from "../../scripts/utils/elements.js";
|
||||
import { debounce, next } from "../../scripts/utils/timing.js";
|
||||
|
||||
/**
|
||||
* @param {HeatmapOption} option
|
||||
*/
|
||||
export async function init(option) {
|
||||
const { headerElement, headingElement } = createHeader();
|
||||
headingElement.innerHTML = option.title;
|
||||
heatmapElement.append(headerElement);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
heatmapElement.append(canvas);
|
||||
await next();
|
||||
|
||||
let renderer = createRenderer(canvas);
|
||||
|
||||
function randomColor() {
|
||||
// Inferno-ish: just random for now
|
||||
const r = (Math.random() * 255) | 0;
|
||||
const g = (Math.random() * 100) | 0;
|
||||
const b = (Math.random() * 200) | 0;
|
||||
return 0xff000000 | (b << 16) | (g << 8) | r; // ABGR
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} col
|
||||
* @param {number} row
|
||||
*/
|
||||
function getColor(col, row) {
|
||||
return randomColor();
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderer.paint(100, 100, getColor);
|
||||
}
|
||||
|
||||
const { width, height } = canvas.getBoundingClientRect();
|
||||
if (renderer.resize(width, height)) {
|
||||
render();
|
||||
}
|
||||
|
||||
new ResizeObserver(
|
||||
debounce(() => {
|
||||
const { width, height } = canvas.getBoundingClientRect();
|
||||
if (width && height && renderer.resize(width, height)) {
|
||||
render();
|
||||
}
|
||||
}, 1000),
|
||||
).observe(heatmapElement);
|
||||
}
|
||||
|
||||
/** @param {HTMLCanvasElement} */
|
||||
export function createRenderer(canvas) {
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) throw "Expected context from canvas";
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let imageData = new ImageData(1, 1);
|
||||
let buffer = new Uint32Array();
|
||||
|
||||
return {
|
||||
/**
|
||||
* @param {number} w
|
||||
* @param {number} h
|
||||
* @returns {boolean} wether the canvas was actually resized (true) or not (false)
|
||||
*/
|
||||
resize(w, h) {
|
||||
if (w == width && h == height) return false;
|
||||
const bound = canvas.getBoundingClientRect();
|
||||
width = Math.floor(Math.min(w, bound.width));
|
||||
height = Math.floor(Math.min(h, bound.height));
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
imageData = context.createImageData(width, height);
|
||||
buffer = new Uint32Array(imageData.data.buffer);
|
||||
return true;
|
||||
},
|
||||
|
||||
get width() {
|
||||
return width;
|
||||
},
|
||||
get height() {
|
||||
return height;
|
||||
},
|
||||
|
||||
/**
|
||||
* Full repaint: iterate all cells, colorize, one blit.
|
||||
* @param {number} cols
|
||||
* @param {number} rows
|
||||
* @param {(col: number, row: number) => number} getColor - returns ABGR uint32
|
||||
*/
|
||||
paint(cols, rows, getColor) {
|
||||
const colX = new Int32Array(cols + 1);
|
||||
for (let c = 0; c <= cols; c++) colX[c] = ((c * width) / cols + 0.5) | 0;
|
||||
const rowY = new Int32Array(rows + 1);
|
||||
for (let r = 0; r <= rows; r++) rowY[r] = ((r * height) / rows + 0.5) | 0;
|
||||
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const x0 = colX[c];
|
||||
const x1 = colX[c + 1];
|
||||
for (let r = 0; r < rows; r++) {
|
||||
const color = getColor(c, r);
|
||||
const y0 = rowY[r];
|
||||
const y1 = rowY[r + 1];
|
||||
for (let y = y0; y < y1; y++) {
|
||||
buffer.fill(color, y * width + x0, y * width + x1);
|
||||
}
|
||||
}
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Incremental repaint: only dirty columns, blit each separately.
|
||||
* @param {number} cols
|
||||
* @param {number} rows
|
||||
* @param {Iterable<number>} dirty
|
||||
* @param {(col: number, row: number) => number} getColor
|
||||
*/
|
||||
paintCols(cols, rows, dirty, getColor) {
|
||||
const colW = width / cols;
|
||||
const rowH = height / rows;
|
||||
for (const c of dirty) {
|
||||
const x0 = (c * colW) | 0;
|
||||
const x1 = Math.min(width, ((c + 1) * colW) | 0);
|
||||
for (let r = 0; r < rows; r++) {
|
||||
const color = getColor(c, r);
|
||||
const y0 = (r * rowH) | 0;
|
||||
const y1 = Math.min(height, ((r + 1) * rowH) | 0);
|
||||
for (let y = y0; y < y1; y++) {
|
||||
buffer.fill(color, y * width + x0, y * width + x1);
|
||||
}
|
||||
}
|
||||
context.putImageData(imageData, 0, 0, x0, 0, x1 - x0, height);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#heatmap {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user