diff --git a/website_next/index.html b/website_next/index.html
index bdade82db..7cb6131f3 100644
--- a/website_next/index.html
+++ b/website_next/index.html
@@ -103,6 +103,7 @@
+
diff --git a/website_next/learn/charts/area/index.js b/website_next/learn/charts/area/index.js
new file mode 100644
index 000000000..7721abda3
--- /dev/null
+++ b/website_next/learn/charts/area/index.js
@@ -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 */
diff --git a/website_next/learn/charts/area/style.css b/website_next/learn/charts/area/style.css
new file mode 100644
index 000000000..da877ff94
--- /dev/null
+++ b/website_next/learn/charts/area/style.css
@@ -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;
+ }
+ }
+}
diff --git a/website_next/learn/charts/bar/index.js b/website_next/learn/charts/bar/index.js
index a6f60d1ec..1a1c6cd71 100644
--- a/website_next/learn/charts/bar/index.js
+++ b/website_next/learn/charts/bar/index.js
@@ -35,21 +35,21 @@ function createBarPathData(points, width) {
* @param {LoadedSeries[]} loadedSeries
* @param {number} height
* @param {SeriesHighlight} highlight
- * @param {{ reversed: boolean }} options
* @param {import("../scale.js").ChartScale} scale
+ * @param {import("../order.js").ChartOrder} order
*/
export function renderBarPlot(
group,
loadedSeries,
height,
highlight,
- options,
scale,
+ order,
) {
const { lineIndexes, plottedSeries, stackIndexes } = createStackedSeries(
loadedSeries,
height,
- options.reversed,
+ order,
scale,
);
diff --git a/website_next/learn/charts/dots/index.js b/website_next/learn/charts/dots/index.js
index 1ce93f633..9fe6a78c1 100644
--- a/website_next/learn/charts/dots/index.js
+++ b/website_next/learn/charts/dots/index.js
@@ -1,5 +1,6 @@
import { formatCoordinate } from "../path.js";
import { appendSeriesPath } from "../series-path.js";
+import { createOrderedIndexes } from "../order.js";
import { createLineSeries } from "../line/series.js";
const radius = 1;
@@ -22,11 +23,21 @@ function createDotsPathData(points) {
* @param {number} height
* @param {SeriesHighlight} highlight
* @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 indexes = createOrderedIndexes(plottedSeries.length, order);
- plottedSeries.forEach(({ color, points }, index) => {
+ for (const index of indexes) {
+ const { color, points } = plottedSeries[index];
appendSeriesPath({
group,
highlight,
@@ -35,7 +46,7 @@ export function renderDotsPlot(group, loadedSeries, height, highlight, scale) {
color,
d: createDotsPathData(points),
});
- });
+ }
return plottedSeries;
}
diff --git a/website_next/learn/charts/index.js b/website_next/learn/charts/index.js
index d165b5bbf..232ad7c77 100644
--- a/website_next/learn/charts/index.js
+++ b/website_next/learn/charts/index.js
@@ -1,6 +1,11 @@
import { createFullscreenButton } from "./fullscreen.js";
import { onChartVisibility } from "./intersection.js";
import { createLegend } from "./legend/index.js";
+import {
+ createOrderControl,
+ getDefaultOrder,
+ saveOrder,
+} from "./order.js";
import { createChartRenderer } from "./renderer.js";
import {
createScaleControl,
@@ -20,6 +25,36 @@ import {
} from "./views.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 */
export function createChart(chart) {
const figure = document.createElement("figure");
@@ -33,6 +68,7 @@ export function createChart(chart) {
let currentTimeframe = getDefaultTimeframe(chartKey);
let currentView = getDefaultView(chartKey, chart.defaultType);
let currentScale = getDefaultScale(chartKey, chart.defaultScale);
+ let currentOrder = getDefaultOrder(chartKey);
const { legend, menu, items, readout } = createLegend(chart);
figure.dataset.chart = "series";
@@ -40,6 +76,7 @@ export function createChart(chart) {
figure.dataset.timeframe = currentTimeframe;
figure.dataset.view = currentView;
figure.dataset.scale = currentScale;
+ figure.dataset.order = currentOrder;
svg.setAttribute(
"viewBox",
`0 0 ${VIEWBOX_WIDTH} ${FALLBACK_VIEWBOX_HEIGHT}`,
@@ -59,19 +96,44 @@ export function createChart(chart) {
chart,
getView: () => currentView,
getScale: () => currentScale,
+ getOrder: () => currentOrder,
getTimeframe: () => currentTimeframe,
});
- const viewControl = createViewControl(currentView, (view) => {
- currentView = view;
- saveView(chartKey, view);
- figure.dataset.view = view;
- renderer.renderCurrent();
+ const viewControl = createChartSettingControl({
+ currentValue: currentView,
+ createControl: createViewControl,
+ save: saveView,
+ chartKey,
+ figure,
+ dataKey: "view",
+ setValue: (view) => {
+ currentView = view;
+ },
+ render: renderer.renderCurrent,
});
- const scaleControl = createScaleControl(currentScale, (scale) => {
- currentScale = scale;
- saveScale(chartKey, scale);
- figure.dataset.scale = scale;
- renderer.renderCurrent();
+ const scaleControl = createChartSettingControl({
+ currentValue: currentScale,
+ createControl: createScaleControl,
+ save: saveScale,
+ 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(
currentTimeframe,
@@ -82,7 +144,7 @@ export function createChart(chart) {
void renderer.loadCurrent();
},
);
- chartControls.append(viewControl, scaleControl);
+ chartControls.append(viewControl, scaleControl, orderControl);
timeControls.append(timeframeControl, createFullscreenButton(figure));
controls.append(chartControls, timeControls);
plot.append(svg, status);
diff --git a/website_next/learn/charts/line/index.js b/website_next/learn/charts/line/index.js
index 4b0f9b198..cb8a440ff 100644
--- a/website_next/learn/charts/line/index.js
+++ b/website_next/learn/charts/line/index.js
@@ -1,5 +1,6 @@
import { createLinePathData } from "../path.js";
import { appendSeriesPath } from "../series-path.js";
+import { createOrderedIndexes } from "../order.js";
import { createLineSeries } from "./series.js";
/**
@@ -8,11 +9,21 @@ import { createLineSeries } from "./series.js";
* @param {number} height
* @param {SeriesHighlight} highlight
* @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 indexes = createOrderedIndexes(plottedSeries.length, order);
- plottedSeries.forEach(({ color, points }, index) => {
+ for (const index of indexes) {
+ const { color, points } = plottedSeries[index];
appendSeriesPath({
group,
highlight,
@@ -21,7 +32,7 @@ export function renderLinePlot(group, loadedSeries, height, highlight, scale) {
color,
d: createLinePathData(points),
});
- });
+ }
return plottedSeries;
}
diff --git a/website_next/learn/charts/line/series.js b/website_next/learn/charts/line/series.js
index 493490a00..059d53028 100644
--- a/website_next/learn/charts/line/series.js
+++ b/website_next/learn/charts/line/series.js
@@ -1,21 +1,17 @@
import { VIEWBOX_WIDTH } from "../viewbox.js";
-import { scaleY } from "../scale.js";
+import { createBounds, includeBoundValue, scaleY } from "../scale.js";
/** @param {LoadedSeries[]} series */
function createValueBounds(series) {
- let min = Infinity;
- let max = -Infinity;
- let minPositive = Infinity;
+ const bounds = createBounds();
for (const { entries } of series) {
for (const { value } of entries) {
- min = Math.min(min, value);
- max = Math.max(max, value);
- if (value > 0) minPositive = Math.min(minPositive, value);
+ includeBoundValue(bounds, value);
}
}
- return { min, max, minPositive };
+ return bounds;
}
/**
diff --git a/website_next/learn/charts/order.js b/website_next/learn/charts/order.js
new file mode 100644
index 000000000..13af19886
--- /dev/null
+++ b/website_next/learn/charts/order.js
@@ -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 */
diff --git a/website_next/learn/charts/plot.js b/website_next/learn/charts/plot.js
index 9a33a2fdc..8b9a07f68 100644
--- a/website_next/learn/charts/plot.js
+++ b/website_next/learn/charts/plot.js
@@ -1,3 +1,4 @@
+import { renderAreaPlot } from "./area/index.js";
import { renderBarPlot } from "./bar/index.js";
import { renderDotsPlot } from "./dots/index.js";
import { renderLinePlot } from "./line/index.js";
@@ -10,36 +11,61 @@ import { renderStackedPlot } from "./stacked/index.js";
* @param {number} height
* @param {SeriesHighlight} highlight
* @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) {
case "line":
- return renderLinePlot(group, loadedSeries, height, highlight, scale);
- case "bar":
- case "bar-reversed":
- return renderBarPlot(
+ return renderLinePlot(
group,
loadedSeries,
height,
highlight,
- { reversed: view === "bar-reversed" },
scale,
+ order,
);
+ case "area":
+ return renderAreaPlot(
+ group,
+ loadedSeries,
+ height,
+ highlight,
+ scale,
+ order,
+ );
+ case "bar":
+ return renderBarPlot(group, loadedSeries, height, highlight, scale, order);
case "dots":
- return renderDotsPlot(group, loadedSeries, height, highlight, scale);
- default:
+ return renderDotsPlot(
+ group,
+ loadedSeries,
+ height,
+ highlight,
+ scale,
+ order,
+ );
+ case "stacked":
return renderStackedPlot(
group,
loadedSeries,
height,
highlight,
- { reversed: view === "stacked-reversed" },
scale,
+ order,
);
}
}
/** @typedef {import("./highlight.js").SeriesHighlight} SeriesHighlight */
/** @typedef {import("./index.js").LoadedSeries} LoadedSeries */
+/** @typedef {import("./order.js").ChartOrder} ChartOrder */
/** @typedef {import("./scale.js").ChartScale} ChartScale */
/** @typedef {import("./views.js").ChartView} ChartView */
diff --git a/website_next/learn/charts/renderer.js b/website_next/learn/charts/renderer.js
index bbd1437d0..370f65853 100644
--- a/website_next/learn/charts/renderer.js
+++ b/website_next/learn/charts/renderer.js
@@ -15,6 +15,7 @@ import { getViewBoxHeight, VIEWBOX_WIDTH } from "./viewbox.js";
* @param {Chart} args.chart
* @param {() => ChartView} args.getView
* @param {() => ChartScale} args.getScale
+ * @param {() => ChartOrder} args.getOrder
* @param {() => TimeframeValue} args.getTimeframe
*/
export function createChartRenderer({
@@ -26,6 +27,7 @@ export function createChartRenderer({
chart,
getView,
getScale,
+ getOrder,
getTimeframe,
}) {
const group = createSvgElement("g");
@@ -68,6 +70,7 @@ export function createChartRenderer({
height,
highlight,
getScale(),
+ getOrder(),
),
height,
);
@@ -132,6 +135,7 @@ export function createChartRenderer({
/** @typedef {import("./index.js").Chart} Chart */
/** @typedef {import("./index.js").LoadedSeries} LoadedSeries */
/** @typedef {import("./legend/index.js").Readout} Readout */
+/** @typedef {import("./order.js").ChartOrder} ChartOrder */
/** @typedef {import("./scale.js").ChartScale} ChartScale */
/** @typedef {import("./timeframes.js").TimeframeValue} TimeframeValue */
/** @typedef {import("./views.js").ChartView} ChartView */
diff --git a/website_next/learn/charts/scale.js b/website_next/learn/charts/scale.js
index 5d249eaaa..4c12a7bce 100644
--- a/website_next/learn/charts/scale.js
+++ b/website_next/learn/charts/scale.js
@@ -36,6 +36,24 @@ export function createScaleControl(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 {ScaleBounds} bounds
diff --git a/website_next/learn/charts/stacked/index.js b/website_next/learn/charts/stacked/index.js
index 374b551e1..bc3df5e1b 100644
--- a/website_next/learn/charts/stacked/index.js
+++ b/website_next/learn/charts/stacked/index.js
@@ -7,21 +7,21 @@ import { createStackedSeries } from "./series.js";
* @param {LoadedSeries[]} loadedSeries
* @param {number} height
* @param {SeriesHighlight} highlight
- * @param {{ reversed: boolean }} options
* @param {import("../scale.js").ChartScale} scale
+ * @param {import("../order.js").ChartOrder} order
*/
export function renderStackedPlot(
group,
loadedSeries,
height,
highlight,
- options,
scale,
+ order,
) {
const { lineIndexes, plottedSeries, stackIndexes } = createStackedSeries(
loadedSeries,
height,
- options.reversed,
+ order,
scale,
);
diff --git a/website_next/learn/charts/stacked/series.js b/website_next/learn/charts/stacked/series.js
index 5199921e9..331c4c0f0 100644
--- a/website_next/learn/charts/stacked/series.js
+++ b/website_next/learn/charts/stacked/series.js
@@ -1,42 +1,40 @@
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 {number[]} stackIndexes
+ * @param {number[]} stackOrder
* @param {number[]} lineIndexes
*/
-function createStackBounds(series, stackIndexes, lineIndexes) {
+function createStackBounds(series, stackOrder, lineIndexes) {
+ const bounds = createBounds();
const length = series[0].entries.length;
- let min = 0;
- let max = 0;
- let minPositive = Infinity;
+
+ includeBoundValue(bounds, 0);
for (let index = 0; index < length; index += 1) {
let negative = 0;
let positive = 0;
- for (const seriesIndex of stackIndexes) {
+ for (const seriesIndex of stackOrder) {
const value = series[seriesIndex].entries[index].value;
+ const end = value < 0 ? negative + value : positive + value;
- if (value < 0) negative += value;
- else positive += value;
+ if (value < 0) negative = end;
+ 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) {
const value = series[seriesIndex].entries[index].value;
- min = Math.min(min, value);
- max = Math.max(max, value);
- if (value > 0) minPositive = Math.min(minPositive, value);
+ includeBoundValue(bounds, value);
}
}
- return { min, max, minPositive };
+ return bounds;
}
/** @returns {StackedPoint[]} */
@@ -47,36 +45,36 @@ function createStackedPoints() {
/**
* @param {LoadedSeries[]} loadedSeries
* @param {number} height
- * @param {boolean} reversed
+ * @param {import("../order.js").ChartOrder} order
* @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 lineIndexes = indexes.filter(
- (index) => loadedSeries[index].series.role === "line",
+ const lineIndexes = orderIndexes(
+ indexes.filter((index) => loadedSeries[index].series.role === "line"),
+ order,
);
- const stackIndexes = indexes.filter(
- (index) => loadedSeries[index].series.role !== "line",
+ const stackIndexes = orderIndexes(
+ indexes.filter((index) => loadedSeries[index].series.role !== "line"),
+ order,
);
- const bounds = createStackBounds(loadedSeries, stackIndexes, lineIndexes);
const length = loadedSeries[0].entries.length;
const xScale = VIEWBOX_WIDTH / (length - 1);
- const order = [...stackIndexes];
const plottedSeries = loadedSeries.map(({ series, color }) => ({
series,
color,
points: createStackedPoints(),
}));
- if (reversed) order.reverse();
+ const bounds = createStackBounds(loadedSeries, stackIndexes, lineIndexes);
for (let index = 0; index < length; index += 1) {
let negative = 0;
let positive = 0;
const x = index * xScale;
- for (const seriesIndex of order) {
+ for (const seriesIndex of stackIndexes) {
const { date, value } = loadedSeries[seriesIndex].entries[index];
const start = value < 0 ? negative : positive;
const end = start + value;
diff --git a/website_next/learn/charts/views.js b/website_next/learn/charts/views.js
index 68732f48d..82e034a66 100644
--- a/website_next/learn/charts/views.js
+++ b/website_next/learn/charts/views.js
@@ -2,18 +2,16 @@ import { createChartSetting } from "./setting.js";
export const viewTypes = /** @type {const} */ ({
line: "line",
+ area: "area",
stacked: "stacked",
- stackedReversed: "stacked-reversed",
bar: "bar",
- barReversed: "bar-reversed",
dots: "dots",
});
const views = /** @type {const} */ ([
{ value: viewTypes.line, label: "Line" },
- { value: viewTypes.stacked, label: "Stack↑" },
- { value: viewTypes.stackedReversed, label: "Stack↓" },
- { value: viewTypes.bar, label: "Bars↑" },
- { value: viewTypes.barReversed, label: "Bars↓" },
+ { value: viewTypes.area, label: "Area" },
+ { value: viewTypes.stacked, label: "Stack" },
+ { value: viewTypes.bar, label: "Bars" },
{ value: viewTypes.dots, label: "Dots" },
]);
const defaultView = viewTypes.stacked;