mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-30 04:08:11 -07:00
website: redesign part 23
This commit is contained in:
@@ -103,6 +103,7 @@
|
|||||||
<link rel="stylesheet" href="/learn/charts/controls/style.css" />
|
<link rel="stylesheet" href="/learn/charts/controls/style.css" />
|
||||||
<link rel="stylesheet" href="/learn/charts/legend/style.css" />
|
<link rel="stylesheet" href="/learn/charts/legend/style.css" />
|
||||||
<link rel="stylesheet" href="/learn/charts/scrubber/style.css" />
|
<link rel="stylesheet" href="/learn/charts/scrubber/style.css" />
|
||||||
|
<link rel="stylesheet" href="/learn/charts/area/style.css" />
|
||||||
<link rel="stylesheet" href="/learn/charts/bar/style.css" />
|
<link rel="stylesheet" href="/learn/charts/bar/style.css" />
|
||||||
<link rel="stylesheet" href="/learn/charts/line/style.css" />
|
<link rel="stylesheet" href="/learn/charts/line/style.css" />
|
||||||
<link rel="stylesheet" href="/learn/charts/dots/style.css" />
|
<link rel="stylesheet" href="/learn/charts/dots/style.css" />
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { createAreaPathData, createLinePathData } from "../path.js";
|
||||||
|
import { appendSeriesPath } from "../series-path.js";
|
||||||
|
import { createOrderedIndexes } from "../order.js";
|
||||||
|
import { createLineSeries } from "../line/series.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} height
|
||||||
|
* @param {{ date: Date, value: number, x: number, y: number }[]} points
|
||||||
|
*/
|
||||||
|
function createAreaPoints(height, points) {
|
||||||
|
return points.map((point) => ({
|
||||||
|
...point,
|
||||||
|
y0: height,
|
||||||
|
y1: point.y,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SVGGElement} group
|
||||||
|
* @param {LoadedSeries[]} loadedSeries
|
||||||
|
* @param {number} height
|
||||||
|
* @param {SeriesHighlight} highlight
|
||||||
|
* @param {import("../scale.js").ChartScale} scale
|
||||||
|
* @param {import("../order.js").ChartOrder} order
|
||||||
|
*/
|
||||||
|
export function renderAreaPlot(
|
||||||
|
group,
|
||||||
|
loadedSeries,
|
||||||
|
height,
|
||||||
|
highlight,
|
||||||
|
scale,
|
||||||
|
order,
|
||||||
|
) {
|
||||||
|
const plottedSeries = createLineSeries(loadedSeries, height, scale);
|
||||||
|
const indexes = createOrderedIndexes(plottedSeries.length, order);
|
||||||
|
|
||||||
|
for (const index of indexes) {
|
||||||
|
const { color, points } = plottedSeries[index];
|
||||||
|
appendSeriesPath({
|
||||||
|
group,
|
||||||
|
highlight,
|
||||||
|
index,
|
||||||
|
chart: "area",
|
||||||
|
color,
|
||||||
|
d: createAreaPathData(createAreaPoints(height, points)),
|
||||||
|
});
|
||||||
|
|
||||||
|
appendSeriesPath({
|
||||||
|
group,
|
||||||
|
highlight,
|
||||||
|
index,
|
||||||
|
chart: "line",
|
||||||
|
color,
|
||||||
|
d: createLinePathData(points),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return plottedSeries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @typedef {import("../highlight.js").SeriesHighlight} SeriesHighlight */
|
||||||
|
/** @typedef {import("../index.js").LoadedSeries} LoadedSeries */
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
main.learn {
|
||||||
|
figure[data-chart="series"] {
|
||||||
|
path[data-chart="area"] {
|
||||||
|
fill: var(--color, var(--orange));
|
||||||
|
fill-opacity: 0.5;
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,21 +35,21 @@ function createBarPathData(points, width) {
|
|||||||
* @param {LoadedSeries[]} loadedSeries
|
* @param {LoadedSeries[]} loadedSeries
|
||||||
* @param {number} height
|
* @param {number} height
|
||||||
* @param {SeriesHighlight} highlight
|
* @param {SeriesHighlight} highlight
|
||||||
* @param {{ reversed: boolean }} options
|
|
||||||
* @param {import("../scale.js").ChartScale} scale
|
* @param {import("../scale.js").ChartScale} scale
|
||||||
|
* @param {import("../order.js").ChartOrder} order
|
||||||
*/
|
*/
|
||||||
export function renderBarPlot(
|
export function renderBarPlot(
|
||||||
group,
|
group,
|
||||||
loadedSeries,
|
loadedSeries,
|
||||||
height,
|
height,
|
||||||
highlight,
|
highlight,
|
||||||
options,
|
|
||||||
scale,
|
scale,
|
||||||
|
order,
|
||||||
) {
|
) {
|
||||||
const { lineIndexes, plottedSeries, stackIndexes } = createStackedSeries(
|
const { lineIndexes, plottedSeries, stackIndexes } = createStackedSeries(
|
||||||
loadedSeries,
|
loadedSeries,
|
||||||
height,
|
height,
|
||||||
options.reversed,
|
order,
|
||||||
scale,
|
scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { formatCoordinate } from "../path.js";
|
import { formatCoordinate } from "../path.js";
|
||||||
import { appendSeriesPath } from "../series-path.js";
|
import { appendSeriesPath } from "../series-path.js";
|
||||||
|
import { createOrderedIndexes } from "../order.js";
|
||||||
import { createLineSeries } from "../line/series.js";
|
import { createLineSeries } from "../line/series.js";
|
||||||
|
|
||||||
const radius = 1;
|
const radius = 1;
|
||||||
@@ -22,11 +23,21 @@ function createDotsPathData(points) {
|
|||||||
* @param {number} height
|
* @param {number} height
|
||||||
* @param {SeriesHighlight} highlight
|
* @param {SeriesHighlight} highlight
|
||||||
* @param {import("../scale.js").ChartScale} scale
|
* @param {import("../scale.js").ChartScale} scale
|
||||||
|
* @param {import("../order.js").ChartOrder} order
|
||||||
*/
|
*/
|
||||||
export function renderDotsPlot(group, loadedSeries, height, highlight, scale) {
|
export function renderDotsPlot(
|
||||||
|
group,
|
||||||
|
loadedSeries,
|
||||||
|
height,
|
||||||
|
highlight,
|
||||||
|
scale,
|
||||||
|
order,
|
||||||
|
) {
|
||||||
const plottedSeries = createLineSeries(loadedSeries, height, scale);
|
const plottedSeries = createLineSeries(loadedSeries, height, scale);
|
||||||
|
const indexes = createOrderedIndexes(plottedSeries.length, order);
|
||||||
|
|
||||||
plottedSeries.forEach(({ color, points }, index) => {
|
for (const index of indexes) {
|
||||||
|
const { color, points } = plottedSeries[index];
|
||||||
appendSeriesPath({
|
appendSeriesPath({
|
||||||
group,
|
group,
|
||||||
highlight,
|
highlight,
|
||||||
@@ -35,7 +46,7 @@ export function renderDotsPlot(group, loadedSeries, height, highlight, scale) {
|
|||||||
color,
|
color,
|
||||||
d: createDotsPathData(points),
|
d: createDotsPathData(points),
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return plottedSeries;
|
return plottedSeries;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { createFullscreenButton } from "./fullscreen.js";
|
import { createFullscreenButton } from "./fullscreen.js";
|
||||||
import { onChartVisibility } from "./intersection.js";
|
import { onChartVisibility } from "./intersection.js";
|
||||||
import { createLegend } from "./legend/index.js";
|
import { createLegend } from "./legend/index.js";
|
||||||
|
import {
|
||||||
|
createOrderControl,
|
||||||
|
getDefaultOrder,
|
||||||
|
saveOrder,
|
||||||
|
} from "./order.js";
|
||||||
import { createChartRenderer } from "./renderer.js";
|
import { createChartRenderer } from "./renderer.js";
|
||||||
import {
|
import {
|
||||||
createScaleControl,
|
createScaleControl,
|
||||||
@@ -20,6 +25,36 @@ import {
|
|||||||
} from "./views.js";
|
} from "./views.js";
|
||||||
import { FALLBACK_VIEWBOX_HEIGHT, VIEWBOX_WIDTH } from "./viewbox.js";
|
import { FALLBACK_VIEWBOX_HEIGHT, VIEWBOX_WIDTH } from "./viewbox.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {string} T
|
||||||
|
* @param {Object} args
|
||||||
|
* @param {T} args.currentValue
|
||||||
|
* @param {(currentValue: T, onChange: (value: T) => void) => HTMLFieldSetElement} args.createControl
|
||||||
|
* @param {(chartKey: string, value: T) => void} args.save
|
||||||
|
* @param {string} args.chartKey
|
||||||
|
* @param {HTMLElement} args.figure
|
||||||
|
* @param {string} args.dataKey
|
||||||
|
* @param {(value: T) => void} args.setValue
|
||||||
|
* @param {() => void} args.render
|
||||||
|
*/
|
||||||
|
function createChartSettingControl({
|
||||||
|
currentValue,
|
||||||
|
createControl,
|
||||||
|
save,
|
||||||
|
chartKey,
|
||||||
|
figure,
|
||||||
|
dataKey,
|
||||||
|
setValue,
|
||||||
|
render,
|
||||||
|
}) {
|
||||||
|
return createControl(currentValue, (value) => {
|
||||||
|
setValue(value);
|
||||||
|
save(chartKey, value);
|
||||||
|
figure.dataset[dataKey] = value;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** @param {Chart} chart */
|
/** @param {Chart} chart */
|
||||||
export function createChart(chart) {
|
export function createChart(chart) {
|
||||||
const figure = document.createElement("figure");
|
const figure = document.createElement("figure");
|
||||||
@@ -33,6 +68,7 @@ export function createChart(chart) {
|
|||||||
let currentTimeframe = getDefaultTimeframe(chartKey);
|
let currentTimeframe = getDefaultTimeframe(chartKey);
|
||||||
let currentView = getDefaultView(chartKey, chart.defaultType);
|
let currentView = getDefaultView(chartKey, chart.defaultType);
|
||||||
let currentScale = getDefaultScale(chartKey, chart.defaultScale);
|
let currentScale = getDefaultScale(chartKey, chart.defaultScale);
|
||||||
|
let currentOrder = getDefaultOrder(chartKey);
|
||||||
const { legend, menu, items, readout } = createLegend(chart);
|
const { legend, menu, items, readout } = createLegend(chart);
|
||||||
|
|
||||||
figure.dataset.chart = "series";
|
figure.dataset.chart = "series";
|
||||||
@@ -40,6 +76,7 @@ export function createChart(chart) {
|
|||||||
figure.dataset.timeframe = currentTimeframe;
|
figure.dataset.timeframe = currentTimeframe;
|
||||||
figure.dataset.view = currentView;
|
figure.dataset.view = currentView;
|
||||||
figure.dataset.scale = currentScale;
|
figure.dataset.scale = currentScale;
|
||||||
|
figure.dataset.order = currentOrder;
|
||||||
svg.setAttribute(
|
svg.setAttribute(
|
||||||
"viewBox",
|
"viewBox",
|
||||||
`0 0 ${VIEWBOX_WIDTH} ${FALLBACK_VIEWBOX_HEIGHT}`,
|
`0 0 ${VIEWBOX_WIDTH} ${FALLBACK_VIEWBOX_HEIGHT}`,
|
||||||
@@ -59,19 +96,44 @@ export function createChart(chart) {
|
|||||||
chart,
|
chart,
|
||||||
getView: () => currentView,
|
getView: () => currentView,
|
||||||
getScale: () => currentScale,
|
getScale: () => currentScale,
|
||||||
|
getOrder: () => currentOrder,
|
||||||
getTimeframe: () => currentTimeframe,
|
getTimeframe: () => currentTimeframe,
|
||||||
});
|
});
|
||||||
const viewControl = createViewControl(currentView, (view) => {
|
const viewControl = createChartSettingControl({
|
||||||
currentView = view;
|
currentValue: currentView,
|
||||||
saveView(chartKey, view);
|
createControl: createViewControl,
|
||||||
figure.dataset.view = view;
|
save: saveView,
|
||||||
renderer.renderCurrent();
|
chartKey,
|
||||||
|
figure,
|
||||||
|
dataKey: "view",
|
||||||
|
setValue: (view) => {
|
||||||
|
currentView = view;
|
||||||
|
},
|
||||||
|
render: renderer.renderCurrent,
|
||||||
});
|
});
|
||||||
const scaleControl = createScaleControl(currentScale, (scale) => {
|
const scaleControl = createChartSettingControl({
|
||||||
currentScale = scale;
|
currentValue: currentScale,
|
||||||
saveScale(chartKey, scale);
|
createControl: createScaleControl,
|
||||||
figure.dataset.scale = scale;
|
save: saveScale,
|
||||||
renderer.renderCurrent();
|
chartKey,
|
||||||
|
figure,
|
||||||
|
dataKey: "scale",
|
||||||
|
setValue: (scale) => {
|
||||||
|
currentScale = scale;
|
||||||
|
},
|
||||||
|
render: renderer.renderCurrent,
|
||||||
|
});
|
||||||
|
const orderControl = createChartSettingControl({
|
||||||
|
currentValue: currentOrder,
|
||||||
|
createControl: createOrderControl,
|
||||||
|
save: saveOrder,
|
||||||
|
chartKey,
|
||||||
|
figure,
|
||||||
|
dataKey: "order",
|
||||||
|
setValue: (order) => {
|
||||||
|
currentOrder = order;
|
||||||
|
},
|
||||||
|
render: renderer.renderCurrent,
|
||||||
});
|
});
|
||||||
const timeframeControl = createTimeframeControl(
|
const timeframeControl = createTimeframeControl(
|
||||||
currentTimeframe,
|
currentTimeframe,
|
||||||
@@ -82,7 +144,7 @@ export function createChart(chart) {
|
|||||||
void renderer.loadCurrent();
|
void renderer.loadCurrent();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
chartControls.append(viewControl, scaleControl);
|
chartControls.append(viewControl, scaleControl, orderControl);
|
||||||
timeControls.append(timeframeControl, createFullscreenButton(figure));
|
timeControls.append(timeframeControl, createFullscreenButton(figure));
|
||||||
controls.append(chartControls, timeControls);
|
controls.append(chartControls, timeControls);
|
||||||
plot.append(svg, status);
|
plot.append(svg, status);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createLinePathData } from "../path.js";
|
import { createLinePathData } from "../path.js";
|
||||||
import { appendSeriesPath } from "../series-path.js";
|
import { appendSeriesPath } from "../series-path.js";
|
||||||
|
import { createOrderedIndexes } from "../order.js";
|
||||||
import { createLineSeries } from "./series.js";
|
import { createLineSeries } from "./series.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,11 +9,21 @@ import { createLineSeries } from "./series.js";
|
|||||||
* @param {number} height
|
* @param {number} height
|
||||||
* @param {SeriesHighlight} highlight
|
* @param {SeriesHighlight} highlight
|
||||||
* @param {import("../scale.js").ChartScale} scale
|
* @param {import("../scale.js").ChartScale} scale
|
||||||
|
* @param {import("../order.js").ChartOrder} order
|
||||||
*/
|
*/
|
||||||
export function renderLinePlot(group, loadedSeries, height, highlight, scale) {
|
export function renderLinePlot(
|
||||||
|
group,
|
||||||
|
loadedSeries,
|
||||||
|
height,
|
||||||
|
highlight,
|
||||||
|
scale,
|
||||||
|
order,
|
||||||
|
) {
|
||||||
const plottedSeries = createLineSeries(loadedSeries, height, scale);
|
const plottedSeries = createLineSeries(loadedSeries, height, scale);
|
||||||
|
const indexes = createOrderedIndexes(plottedSeries.length, order);
|
||||||
|
|
||||||
plottedSeries.forEach(({ color, points }, index) => {
|
for (const index of indexes) {
|
||||||
|
const { color, points } = plottedSeries[index];
|
||||||
appendSeriesPath({
|
appendSeriesPath({
|
||||||
group,
|
group,
|
||||||
highlight,
|
highlight,
|
||||||
@@ -21,7 +32,7 @@ export function renderLinePlot(group, loadedSeries, height, highlight, scale) {
|
|||||||
color,
|
color,
|
||||||
d: createLinePathData(points),
|
d: createLinePathData(points),
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return plottedSeries;
|
return plottedSeries;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { VIEWBOX_WIDTH } from "../viewbox.js";
|
import { VIEWBOX_WIDTH } from "../viewbox.js";
|
||||||
import { scaleY } from "../scale.js";
|
import { createBounds, includeBoundValue, scaleY } from "../scale.js";
|
||||||
|
|
||||||
/** @param {LoadedSeries[]} series */
|
/** @param {LoadedSeries[]} series */
|
||||||
function createValueBounds(series) {
|
function createValueBounds(series) {
|
||||||
let min = Infinity;
|
const bounds = createBounds();
|
||||||
let max = -Infinity;
|
|
||||||
let minPositive = Infinity;
|
|
||||||
|
|
||||||
for (const { entries } of series) {
|
for (const { entries } of series) {
|
||||||
for (const { value } of entries) {
|
for (const { value } of entries) {
|
||||||
min = Math.min(min, value);
|
includeBoundValue(bounds, value);
|
||||||
max = Math.max(max, value);
|
|
||||||
if (value > 0) minPositive = Math.min(minPositive, value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { min, max, minPositive };
|
return bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { createChartSetting } from "./setting.js";
|
||||||
|
|
||||||
|
const orders = /** @type {const} */ ([
|
||||||
|
{ value: "ascending", label: "Asc" },
|
||||||
|
{ value: "descending", label: "Dsc" },
|
||||||
|
]);
|
||||||
|
const defaultOrder = "ascending";
|
||||||
|
const setting = createChartSetting({
|
||||||
|
storageKey: "order",
|
||||||
|
legend: "Order",
|
||||||
|
options: orders,
|
||||||
|
defaultValue: defaultOrder,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @param {string} chartKey */
|
||||||
|
export function getDefaultOrder(chartKey) {
|
||||||
|
return setting.get(chartKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} chartKey
|
||||||
|
* @param {ChartOrder} order
|
||||||
|
*/
|
||||||
|
export function saveOrder(chartKey, order) {
|
||||||
|
setting.save(chartKey, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ChartOrder} currentOrder
|
||||||
|
* @param {(order: ChartOrder) => void} onChange
|
||||||
|
*/
|
||||||
|
export function createOrderControl(currentOrder, onChange) {
|
||||||
|
return setting.create(currentOrder, onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number[]} indexes
|
||||||
|
* @param {ChartOrder} order
|
||||||
|
*/
|
||||||
|
export function orderIndexes(indexes, order) {
|
||||||
|
const orderedIndexes = [...indexes];
|
||||||
|
|
||||||
|
if (order === "descending") orderedIndexes.reverse();
|
||||||
|
|
||||||
|
return orderedIndexes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} length
|
||||||
|
* @param {ChartOrder} order
|
||||||
|
*/
|
||||||
|
export function createOrderedIndexes(length, order) {
|
||||||
|
return orderIndexes(
|
||||||
|
Array.from({ length }, (_, index) => index),
|
||||||
|
order,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @typedef {(typeof orders)[number]["value"]} ChartOrder */
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { renderAreaPlot } from "./area/index.js";
|
||||||
import { renderBarPlot } from "./bar/index.js";
|
import { renderBarPlot } from "./bar/index.js";
|
||||||
import { renderDotsPlot } from "./dots/index.js";
|
import { renderDotsPlot } from "./dots/index.js";
|
||||||
import { renderLinePlot } from "./line/index.js";
|
import { renderLinePlot } from "./line/index.js";
|
||||||
@@ -10,36 +11,61 @@ import { renderStackedPlot } from "./stacked/index.js";
|
|||||||
* @param {number} height
|
* @param {number} height
|
||||||
* @param {SeriesHighlight} highlight
|
* @param {SeriesHighlight} highlight
|
||||||
* @param {ChartScale} scale
|
* @param {ChartScale} scale
|
||||||
|
* @param {ChartOrder} order
|
||||||
*/
|
*/
|
||||||
export function renderPlot(view, group, loadedSeries, height, highlight, scale) {
|
export function renderPlot(
|
||||||
|
view,
|
||||||
|
group,
|
||||||
|
loadedSeries,
|
||||||
|
height,
|
||||||
|
highlight,
|
||||||
|
scale,
|
||||||
|
order,
|
||||||
|
) {
|
||||||
switch (view) {
|
switch (view) {
|
||||||
case "line":
|
case "line":
|
||||||
return renderLinePlot(group, loadedSeries, height, highlight, scale);
|
return renderLinePlot(
|
||||||
case "bar":
|
|
||||||
case "bar-reversed":
|
|
||||||
return renderBarPlot(
|
|
||||||
group,
|
group,
|
||||||
loadedSeries,
|
loadedSeries,
|
||||||
height,
|
height,
|
||||||
highlight,
|
highlight,
|
||||||
{ reversed: view === "bar-reversed" },
|
|
||||||
scale,
|
scale,
|
||||||
|
order,
|
||||||
);
|
);
|
||||||
|
case "area":
|
||||||
|
return renderAreaPlot(
|
||||||
|
group,
|
||||||
|
loadedSeries,
|
||||||
|
height,
|
||||||
|
highlight,
|
||||||
|
scale,
|
||||||
|
order,
|
||||||
|
);
|
||||||
|
case "bar":
|
||||||
|
return renderBarPlot(group, loadedSeries, height, highlight, scale, order);
|
||||||
case "dots":
|
case "dots":
|
||||||
return renderDotsPlot(group, loadedSeries, height, highlight, scale);
|
return renderDotsPlot(
|
||||||
default:
|
group,
|
||||||
|
loadedSeries,
|
||||||
|
height,
|
||||||
|
highlight,
|
||||||
|
scale,
|
||||||
|
order,
|
||||||
|
);
|
||||||
|
case "stacked":
|
||||||
return renderStackedPlot(
|
return renderStackedPlot(
|
||||||
group,
|
group,
|
||||||
loadedSeries,
|
loadedSeries,
|
||||||
height,
|
height,
|
||||||
highlight,
|
highlight,
|
||||||
{ reversed: view === "stacked-reversed" },
|
|
||||||
scale,
|
scale,
|
||||||
|
order,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @typedef {import("./highlight.js").SeriesHighlight} SeriesHighlight */
|
/** @typedef {import("./highlight.js").SeriesHighlight} SeriesHighlight */
|
||||||
/** @typedef {import("./index.js").LoadedSeries} LoadedSeries */
|
/** @typedef {import("./index.js").LoadedSeries} LoadedSeries */
|
||||||
|
/** @typedef {import("./order.js").ChartOrder} ChartOrder */
|
||||||
/** @typedef {import("./scale.js").ChartScale} ChartScale */
|
/** @typedef {import("./scale.js").ChartScale} ChartScale */
|
||||||
/** @typedef {import("./views.js").ChartView} ChartView */
|
/** @typedef {import("./views.js").ChartView} ChartView */
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { getViewBoxHeight, VIEWBOX_WIDTH } from "./viewbox.js";
|
|||||||
* @param {Chart} args.chart
|
* @param {Chart} args.chart
|
||||||
* @param {() => ChartView} args.getView
|
* @param {() => ChartView} args.getView
|
||||||
* @param {() => ChartScale} args.getScale
|
* @param {() => ChartScale} args.getScale
|
||||||
|
* @param {() => ChartOrder} args.getOrder
|
||||||
* @param {() => TimeframeValue} args.getTimeframe
|
* @param {() => TimeframeValue} args.getTimeframe
|
||||||
*/
|
*/
|
||||||
export function createChartRenderer({
|
export function createChartRenderer({
|
||||||
@@ -26,6 +27,7 @@ export function createChartRenderer({
|
|||||||
chart,
|
chart,
|
||||||
getView,
|
getView,
|
||||||
getScale,
|
getScale,
|
||||||
|
getOrder,
|
||||||
getTimeframe,
|
getTimeframe,
|
||||||
}) {
|
}) {
|
||||||
const group = createSvgElement("g");
|
const group = createSvgElement("g");
|
||||||
@@ -68,6 +70,7 @@ export function createChartRenderer({
|
|||||||
height,
|
height,
|
||||||
highlight,
|
highlight,
|
||||||
getScale(),
|
getScale(),
|
||||||
|
getOrder(),
|
||||||
),
|
),
|
||||||
height,
|
height,
|
||||||
);
|
);
|
||||||
@@ -132,6 +135,7 @@ export function createChartRenderer({
|
|||||||
/** @typedef {import("./index.js").Chart} Chart */
|
/** @typedef {import("./index.js").Chart} Chart */
|
||||||
/** @typedef {import("./index.js").LoadedSeries} LoadedSeries */
|
/** @typedef {import("./index.js").LoadedSeries} LoadedSeries */
|
||||||
/** @typedef {import("./legend/index.js").Readout} Readout */
|
/** @typedef {import("./legend/index.js").Readout} Readout */
|
||||||
|
/** @typedef {import("./order.js").ChartOrder} ChartOrder */
|
||||||
/** @typedef {import("./scale.js").ChartScale} ChartScale */
|
/** @typedef {import("./scale.js").ChartScale} ChartScale */
|
||||||
/** @typedef {import("./timeframes.js").TimeframeValue} TimeframeValue */
|
/** @typedef {import("./timeframes.js").TimeframeValue} TimeframeValue */
|
||||||
/** @typedef {import("./views.js").ChartView} ChartView */
|
/** @typedef {import("./views.js").ChartView} ChartView */
|
||||||
|
|||||||
@@ -36,6 +36,24 @@ export function createScaleControl(currentScale, onChange) {
|
|||||||
return setting.create(currentScale, onChange);
|
return setting.create(currentScale, onChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createBounds() {
|
||||||
|
return {
|
||||||
|
min: Infinity,
|
||||||
|
max: -Infinity,
|
||||||
|
minPositive: Infinity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ScaleBounds} bounds
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
export function includeBoundValue(bounds, value) {
|
||||||
|
bounds.min = Math.min(bounds.min, value);
|
||||||
|
bounds.max = Math.max(bounds.max, value);
|
||||||
|
if (value > 0) bounds.minPositive = Math.min(bounds.minPositive, value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} value
|
* @param {number} value
|
||||||
* @param {ScaleBounds} bounds
|
* @param {ScaleBounds} bounds
|
||||||
|
|||||||
@@ -7,21 +7,21 @@ import { createStackedSeries } from "./series.js";
|
|||||||
* @param {LoadedSeries[]} loadedSeries
|
* @param {LoadedSeries[]} loadedSeries
|
||||||
* @param {number} height
|
* @param {number} height
|
||||||
* @param {SeriesHighlight} highlight
|
* @param {SeriesHighlight} highlight
|
||||||
* @param {{ reversed: boolean }} options
|
|
||||||
* @param {import("../scale.js").ChartScale} scale
|
* @param {import("../scale.js").ChartScale} scale
|
||||||
|
* @param {import("../order.js").ChartOrder} order
|
||||||
*/
|
*/
|
||||||
export function renderStackedPlot(
|
export function renderStackedPlot(
|
||||||
group,
|
group,
|
||||||
loadedSeries,
|
loadedSeries,
|
||||||
height,
|
height,
|
||||||
highlight,
|
highlight,
|
||||||
options,
|
|
||||||
scale,
|
scale,
|
||||||
|
order,
|
||||||
) {
|
) {
|
||||||
const { lineIndexes, plottedSeries, stackIndexes } = createStackedSeries(
|
const { lineIndexes, plottedSeries, stackIndexes } = createStackedSeries(
|
||||||
loadedSeries,
|
loadedSeries,
|
||||||
height,
|
height,
|
||||||
options.reversed,
|
order,
|
||||||
scale,
|
scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,40 @@
|
|||||||
import { VIEWBOX_WIDTH } from "../viewbox.js";
|
import { VIEWBOX_WIDTH } from "../viewbox.js";
|
||||||
import { scaleY } from "../scale.js";
|
import { orderIndexes } from "../order.js";
|
||||||
|
import { createBounds, includeBoundValue, scaleY } from "../scale.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {LoadedSeries[]} series
|
* @param {LoadedSeries[]} series
|
||||||
* @param {number[]} stackIndexes
|
* @param {number[]} stackOrder
|
||||||
* @param {number[]} lineIndexes
|
* @param {number[]} lineIndexes
|
||||||
*/
|
*/
|
||||||
function createStackBounds(series, stackIndexes, lineIndexes) {
|
function createStackBounds(series, stackOrder, lineIndexes) {
|
||||||
|
const bounds = createBounds();
|
||||||
const length = series[0].entries.length;
|
const length = series[0].entries.length;
|
||||||
let min = 0;
|
|
||||||
let max = 0;
|
includeBoundValue(bounds, 0);
|
||||||
let minPositive = Infinity;
|
|
||||||
|
|
||||||
for (let index = 0; index < length; index += 1) {
|
for (let index = 0; index < length; index += 1) {
|
||||||
let negative = 0;
|
let negative = 0;
|
||||||
let positive = 0;
|
let positive = 0;
|
||||||
|
|
||||||
for (const seriesIndex of stackIndexes) {
|
for (const seriesIndex of stackOrder) {
|
||||||
const value = series[seriesIndex].entries[index].value;
|
const value = series[seriesIndex].entries[index].value;
|
||||||
|
const end = value < 0 ? negative + value : positive + value;
|
||||||
|
|
||||||
if (value < 0) negative += value;
|
if (value < 0) negative = end;
|
||||||
else positive += value;
|
else positive = end;
|
||||||
|
|
||||||
|
includeBoundValue(bounds, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
min = Math.min(min, negative);
|
|
||||||
max = Math.max(max, positive);
|
|
||||||
if (positive > 0) minPositive = Math.min(minPositive, positive);
|
|
||||||
|
|
||||||
for (const seriesIndex of lineIndexes) {
|
for (const seriesIndex of lineIndexes) {
|
||||||
const value = series[seriesIndex].entries[index].value;
|
const value = series[seriesIndex].entries[index].value;
|
||||||
|
|
||||||
min = Math.min(min, value);
|
includeBoundValue(bounds, value);
|
||||||
max = Math.max(max, value);
|
|
||||||
if (value > 0) minPositive = Math.min(minPositive, value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { min, max, minPositive };
|
return bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {StackedPoint[]} */
|
/** @returns {StackedPoint[]} */
|
||||||
@@ -47,36 +45,36 @@ function createStackedPoints() {
|
|||||||
/**
|
/**
|
||||||
* @param {LoadedSeries[]} loadedSeries
|
* @param {LoadedSeries[]} loadedSeries
|
||||||
* @param {number} height
|
* @param {number} height
|
||||||
* @param {boolean} reversed
|
* @param {import("../order.js").ChartOrder} order
|
||||||
* @param {import("../scale.js").ChartScale} scale
|
* @param {import("../scale.js").ChartScale} scale
|
||||||
*/
|
*/
|
||||||
export function createStackedSeries(loadedSeries, height, reversed, scale) {
|
export function createStackedSeries(loadedSeries, height, order, scale) {
|
||||||
const indexes = loadedSeries.map((_, index) => index);
|
const indexes = loadedSeries.map((_, index) => index);
|
||||||
const lineIndexes = indexes.filter(
|
const lineIndexes = orderIndexes(
|
||||||
(index) => loadedSeries[index].series.role === "line",
|
indexes.filter((index) => loadedSeries[index].series.role === "line"),
|
||||||
|
order,
|
||||||
);
|
);
|
||||||
const stackIndexes = indexes.filter(
|
const stackIndexes = orderIndexes(
|
||||||
(index) => loadedSeries[index].series.role !== "line",
|
indexes.filter((index) => loadedSeries[index].series.role !== "line"),
|
||||||
|
order,
|
||||||
);
|
);
|
||||||
|
|
||||||
const bounds = createStackBounds(loadedSeries, stackIndexes, lineIndexes);
|
|
||||||
const length = loadedSeries[0].entries.length;
|
const length = loadedSeries[0].entries.length;
|
||||||
const xScale = VIEWBOX_WIDTH / (length - 1);
|
const xScale = VIEWBOX_WIDTH / (length - 1);
|
||||||
const order = [...stackIndexes];
|
|
||||||
const plottedSeries = loadedSeries.map(({ series, color }) => ({
|
const plottedSeries = loadedSeries.map(({ series, color }) => ({
|
||||||
series,
|
series,
|
||||||
color,
|
color,
|
||||||
points: createStackedPoints(),
|
points: createStackedPoints(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (reversed) order.reverse();
|
const bounds = createStackBounds(loadedSeries, stackIndexes, lineIndexes);
|
||||||
|
|
||||||
for (let index = 0; index < length; index += 1) {
|
for (let index = 0; index < length; index += 1) {
|
||||||
let negative = 0;
|
let negative = 0;
|
||||||
let positive = 0;
|
let positive = 0;
|
||||||
const x = index * xScale;
|
const x = index * xScale;
|
||||||
|
|
||||||
for (const seriesIndex of order) {
|
for (const seriesIndex of stackIndexes) {
|
||||||
const { date, value } = loadedSeries[seriesIndex].entries[index];
|
const { date, value } = loadedSeries[seriesIndex].entries[index];
|
||||||
const start = value < 0 ? negative : positive;
|
const start = value < 0 ? negative : positive;
|
||||||
const end = start + value;
|
const end = start + value;
|
||||||
|
|||||||
@@ -2,18 +2,16 @@ import { createChartSetting } from "./setting.js";
|
|||||||
|
|
||||||
export const viewTypes = /** @type {const} */ ({
|
export const viewTypes = /** @type {const} */ ({
|
||||||
line: "line",
|
line: "line",
|
||||||
|
area: "area",
|
||||||
stacked: "stacked",
|
stacked: "stacked",
|
||||||
stackedReversed: "stacked-reversed",
|
|
||||||
bar: "bar",
|
bar: "bar",
|
||||||
barReversed: "bar-reversed",
|
|
||||||
dots: "dots",
|
dots: "dots",
|
||||||
});
|
});
|
||||||
const views = /** @type {const} */ ([
|
const views = /** @type {const} */ ([
|
||||||
{ value: viewTypes.line, label: "Line" },
|
{ value: viewTypes.line, label: "Line" },
|
||||||
{ value: viewTypes.stacked, label: "Stack↑" },
|
{ value: viewTypes.area, label: "Area" },
|
||||||
{ value: viewTypes.stackedReversed, label: "Stack↓" },
|
{ value: viewTypes.stacked, label: "Stack" },
|
||||||
{ value: viewTypes.bar, label: "Bars↑" },
|
{ value: viewTypes.bar, label: "Bars" },
|
||||||
{ value: viewTypes.barReversed, label: "Bars↓" },
|
|
||||||
{ value: viewTypes.dots, label: "Dots" },
|
{ value: viewTypes.dots, label: "Dots" },
|
||||||
]);
|
]);
|
||||||
const defaultView = viewTypes.stacked;
|
const defaultView = viewTypes.stacked;
|
||||||
|
|||||||
Reference in New Issue
Block a user