diff --git a/package.json b/package.json index 2d8ede07..bb7d823e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@netdata/charts", - "version": "6.12.0", + "version": "6.12.1", "description": "Netdata frontend SDK and chart utilities", "main": "dist/index.js", "module": "dist/es6/index.js", diff --git a/src/chartLibraries/dygraph/divergingStack.js b/src/chartLibraries/dygraph/divergingStack.js new file mode 100644 index 00000000..fe966bd8 --- /dev/null +++ b/src/chartLibraries/dygraph/divergingStack.js @@ -0,0 +1,112 @@ +import DefaultHandler from "dygraphs/src/datahandler/default" + +const stackBaseKey = "netdataStackBase" +const stackEndKey = "netdataStackEnd" + +export const getDivergingStackBounds = point => { + const base = point?.[stackBaseKey] + const end = point?.[stackEndKey] + + return Number.isFinite(base) && Number.isFinite(end) ? { base, end } : null +} + +export const findDivergingStackedPoint = (points, offsetY, toDomYCoord) => { + let closestPoint + let closestDistance = Infinity + + points.forEach(point => { + const bounds = getDivergingStackBounds(point) + if (!bounds) return + + const baseY = toDomYCoord(bounds.base) + const endY = toDomYCoord(bounds.end) + if (!Number.isFinite(baseY) || !Number.isFinite(endY)) return + + const top = Math.min(baseY, endY) + const bottom = Math.max(baseY, endY) + const distance = offsetY < top ? top - offsetY : offsetY > bottom ? offsetY - bottom : 0 + + if (distance >= closestDistance) return + + closestPoint = point + closestDistance = distance + }) + + if (!closestPoint) return {} + + return { + point: closestPoint, + row: closestPoint.idx, + seriesName: closestPoint.name, + } +} + +export const makeDivergingStackedDataHandler = chart => { + return class DivergingStackedDataHandler extends DefaultHandler { + constructor() { + super() + + const dimensionIds = chart.getPayloadDimensionIds() + const payloadLabels = chart.getPayload().labels || [] + const series = dimensionIds.map((dimensionId, index) => ({ + dimensionId, + name: payloadLabels[index + 1] ?? dimensionId, + })) + const selectedDimensions = chart.getAttribute("selectedLegendDimensions") + const visibleSeries = selectedDimensions?.length + ? series.filter(({ dimensionId }) => chart.isDimensionVisible(dimensionId)) + : series + + this.seriesNames = new Set(series.map(({ name }) => name)) + this.firstStackedSeries = visibleSeries[visibleSeries.length - 1]?.name + this.positiveStack = [] + this.negativeStack = [] + this.pendingExtremes = null + } + + getExtremeYValues(series, dateWindow, stepPlot) { + this.pendingExtremes = super.getExtremeYValues(series, dateWindow, stepPlot) + return this.pendingExtremes + } + + seriesToPoints(series, setName, boundaryIdStart) { + const points = super.seriesToPoints(series, setName, boundaryIdStart) + if (!this.seriesNames.has(setName)) return points + + if (setName === this.firstStackedSeries) { + this.positiveStack = [] + this.negativeStack = [] + } + + let min = null + let max = null + + points.forEach(point => { + const value = point.yval + if (!Number.isFinite(value)) { + point[stackBaseKey] = null + point[stackEndKey] = null + return + } + + const stack = value < 0 ? this.negativeStack : this.positiveStack + const base = stack[point.idx] ?? 0 + const end = base + value + + stack[point.idx] = end + point[stackBaseKey] = base + point[stackEndKey] = end + + if (min === null || end < min) min = end + if (max === null || end > max) max = end + }) + + if (this.pendingExtremes) { + this.pendingExtremes[0] = min + this.pendingExtremes[1] = max + } + + return points + } + } +} diff --git a/src/chartLibraries/dygraph/divergingStack.test.js b/src/chartLibraries/dygraph/divergingStack.test.js new file mode 100644 index 00000000..50da6802 --- /dev/null +++ b/src/chartLibraries/dygraph/divergingStack.test.js @@ -0,0 +1,192 @@ +import Dygraph from "dygraphs" +import { makeHeatmapPayload, makeTestChart } from "@jest/testUtilities" +import { + findDivergingStackedPoint, + getDivergingStackBounds, + makeDivergingStackedDataHandler, +} from "./divergingStack" + +const loadStackedPayload = async (chart, ids, rows, labels = ids) => { + const payload = makeHeatmapPayload(ids, rows) + + payload.view.chart_type = "stacked" + payload.view.dimensions.grouped_by = [] + payload.view.dimensions.names = labels + payload.result.labels = ["time", ...labels] + + chart.doneFetch(payload) + await new Promise(resolve => setTimeout(resolve, 0)) +} + +const makeDygraph = chart => { + const element = document.createElement("div") + Object.defineProperties(element, { + clientWidth: { value: 800 }, + clientHeight: { value: 400 }, + }) + element.style.padding = "0px" + document.body.appendChild(element) + + const dimensionIds = chart.getPayloadDimensionIds() + const selectedDimensions = chart.getAttribute("selectedLegendDimensions") + const visibility = [ + ...dimensionIds.map(selectedDimensions.length ? chart.isDimensionVisible : () => true), + true, + true, + ] + + return new Dygraph(element, chart.getPayload().data, { + labels: chart.getPayload().labels, + visibility, + dataHandler: makeDivergingStackedDataHandler(chart), + }) +} + +const gather = dygraph => dygraph.gatherDatasets_(dygraph.rolledSeries_, null) + +describe("diverging stacked data handler", () => { + const dygraphs = [] + + afterEach(() => { + dygraphs.forEach(dygraph => dygraph.destroy()) + dygraphs.length = 0 + document.body.innerHTML = "" + }) + + const createChart = async (ids, rows, attributes = {}, labels = ids) => { + const { chart } = makeTestChart({ + attributes: { + chartType: "stacked", + groupBy: [], + selectedLegendDimensions: [], + ...attributes, + }, + }) + + await loadStackedPayload(chart, ids, rows, labels) + + const dygraph = makeDygraph(chart) + dygraphs.push(dygraph) + + return { chart, dygraph } + } + + it("stacks positive and negative values independently at every point", async () => { + const { chart, dygraph } = await createChart( + ["positive", "negative", "crossing"], + [ + [2, -1, 0.5], + [-2, 1, -0.25], + ] + ) + const rawData = chart.getPayload().data.map(row => [...row]) + const { points, extremes } = gather(dygraph) + + expect(points[1].map(getDivergingStackBounds)).toEqual([ + { base: 0.5, end: 2.5 }, + { base: -0.25, end: -2.25 }, + ]) + expect(points[2].map(getDivergingStackBounds)).toEqual([ + { base: 0, end: -1 }, + { base: 0, end: 1 }, + ]) + expect(points[3].map(getDivergingStackBounds)).toEqual([ + { base: 0, end: 0.5 }, + { base: 0, end: -0.25 }, + ]) + expect(extremes.positive).toEqual([-2.25, 2.5]) + expect(extremes.negative).toEqual([-1, 1]) + expect(extremes.crossing).toEqual([-0.25, 0.5]) + expect(chart.getPayload().data).toEqual(rawData) + }) + + it("preserves the existing reverse series order for positive stacks", async () => { + const { dygraph } = await createChart(["top", "middle", "bottom"], [[2, 3, 4]]) + const { points } = gather(dygraph) + + expect(getDivergingStackBounds(points[3][0])).toEqual({ base: 0, end: 4 }) + expect(getDivergingStackBounds(points[2][0])).toEqual({ base: 4, end: 7 }) + expect(getDivergingStackBounds(points[1][0])).toEqual({ base: 7, end: 9 }) + }) + + it("preserves the existing reverse series order for negative stacks", async () => { + const { dygraph } = await createChart(["top", "middle", "bottom"], [[-2, -3, -4]]) + const { points } = gather(dygraph) + + expect(getDivergingStackBounds(points[3][0])).toEqual({ base: 0, end: -4 }) + expect(getDivergingStackBounds(points[2][0])).toEqual({ base: -4, end: -7 }) + expect(getDivergingStackBounds(points[1][0])).toEqual({ base: -7, end: -9 }) + }) + + it("maps display labels to dimension ids by column", async () => { + const ids = ['{"PROTOCOL":"6"}', '{"PROTOCOL":"17"}'] + const labels = ["Protocol=TCP", "Protocol=UDP"] + const { dygraph } = await createChart(ids, [[2, 4]], {}, labels) + const { points } = gather(dygraph) + + expect(getDivergingStackBounds(points[2][0])).toEqual({ base: 0, end: 4 }) + expect(getDivergingStackBounds(points[1][0])).toEqual({ base: 4, end: 6 }) + }) + + it("maps a tile sparkline label to its synthetic dimension", async () => { + const { dygraph } = await createChart( + ["source dimension"], + [[5]], + { sparkline: true }, + ["selected"] + ) + const { points } = gather(dygraph) + + expect(getDivergingStackBounds(points[1][0])).toEqual({ base: 0, end: 5 }) + }) + + it("leaves null values as gaps without changing adjacent stack totals", async () => { + const { dygraph } = await createChart(["top", "gap", "bottom"], [[2, null, 4]]) + const { points } = gather(dygraph) + + expect(getDivergingStackBounds(points[3][0])).toEqual({ base: 0, end: 4 }) + expect(getDivergingStackBounds(points[2][0])).toBeNull() + expect(getDivergingStackBounds(points[1][0])).toEqual({ base: 4, end: 6 }) + }) + + it("rebases a selected dimension at zero", async () => { + const ids = ["top-id", "middle-id", "bottom-id"] + const labels = ["top", "middle", "bottom"] + const { chart, dygraph } = await createChart(ids, [[2, 3, 4]], {}, labels) + chart.updateAttribute("selectedLegendDimensions", ["middle"]) + await new Promise(resolve => setTimeout(resolve, 0)) + + dygraph.updateOptions({ + visibility: [...ids.map(chart.isDimensionVisible), true, true], + dataHandler: makeDivergingStackedDataHandler(chart), + }) + + const { points } = gather(dygraph) + expect(points[1]).toBeUndefined() + expect(getDivergingStackBounds(points[2][0])).toEqual({ base: 0, end: 3 }) + expect(points[3]).toBeUndefined() + }) + + it("resets accumulators for every zoom-range gather", async () => { + const { dygraph } = await createChart(["top", "bottom"], [[2, 4]]) + + const first = gather(dygraph) + const second = gather(dygraph) + + expect(getDivergingStackBounds(first.points[1][0])).toEqual({ base: 4, end: 6 }) + expect(getDivergingStackBounds(second.points[1][0])).toEqual({ base: 4, end: 6 }) + }) + + it("finds the signed band under the cursor", async () => { + const { dygraph } = await createChart( + ["positive", "negative", "crossing"], + [[2, -1, 0.5]] + ) + const { points } = gather(dygraph) + const rowPoints = points.slice(1, 4).map(series => series[0]) + const toDomYCoord = value => 100 - value * 10 + + expect(findDivergingStackedPoint(rowPoints, 105, toDomYCoord).seriesName).toBe("negative") + expect(findDivergingStackedPoint(rowPoints, 98, toDomYCoord).seriesName).toBe("crossing") + }) +}) diff --git a/src/chartLibraries/dygraph/hoverX.js b/src/chartLibraries/dygraph/hoverX.js index 868fd6a3..60ba47e5 100644 --- a/src/chartLibraries/dygraph/hoverX.js +++ b/src/chartLibraries/dygraph/hoverX.js @@ -1,5 +1,6 @@ import getOffsets from "@/helpers/eventOffset" import { isHeatmap } from "@/helpers/heatmap" +import { findDivergingStackedPoint } from "./divergingStack" const shouldFindMaxValue = { stacked: true, @@ -45,20 +46,11 @@ export default chartUI => { } if (shouldFindMaxValue[chartUI.chart.getAttribute("chartType")]) { - let closestPoint = _dygraph.findStackedPoint(offsetX, offsetY) + const closestPoint = findDivergingStackedPoint(points, offsetY, value => + _dygraph.toDomYCoord(value) + ) - if (closestPoint.point?.canvasy > offsetY) { - closestPoint = points.reduce((max, p) => (p.yval > max.yval ? p : max), { - yval: -Infinity, - }) - - closestPoint = { - point: closestPoint, - row: closestPoint.idx, - seriesName: closestPoint.name, - } - } - return closestPoint + return closestPoint.seriesName ? closestPoint : _dygraph.findClosestPoint(offsetX, offsetY) } return _dygraph.findClosestPoint(offsetX, offsetY) diff --git a/src/chartLibraries/dygraph/index.js b/src/chartLibraries/dygraph/index.js index f2544808..5f0a4a23 100644 --- a/src/chartLibraries/dygraph/index.js +++ b/src/chartLibraries/dygraph/index.js @@ -5,6 +5,7 @@ import makeExecuteLatest from "@/helpers/makeExecuteLatest" import { isHeatmap } from "@/helpers/heatmap" import { makeLinePlotter, + makeStackedAreaPlotter, makeStackedBarPlotter, makeMultiColumnBarPlotter, makeHeatmapPlotter, @@ -16,11 +17,13 @@ import makeNavigation from "./navigation" import makeHoverX from "./hoverX" import makeOverlays from "./overlays" import crosshair from "./crosshair" +import { makeDivergingStackedDataHandler } from "./divergingStack" const touchEvents = ["touchstart", "touchmove", "touchend"] export default (sdk, chart) => { const chartUI = makeChartUI(sdk, chart) + const DivergingStackedDataHandler = makeDivergingStackedDataHandler(chart) let dygraph = null let listeners = [] let navigation = null @@ -213,6 +216,7 @@ export default (sdk, chart) => { const plotterByChartType = { line: makeLinePlotter(chartUI), + stacked: makeStackedAreaPlotter(chartUI), stackedBar: makeStackedBarPlotter(chartUI), multiBar: makeMultiColumnBarPlotter(chartUI), heatmap: makeHeatmapPlotter(chartUI), @@ -230,6 +234,7 @@ export default (sdk, chart) => { stackedGraph: false, forceIncludeZero: false, errorBars: false, + dataHandler: null, makeYAxisLabelFormatter: () => (y, granularity, opts, d) => { const dataMin = chart.getAttribute("min") const dataMax = chart.getAttribute("max") @@ -272,9 +277,10 @@ export default (sdk, chart) => { ...defaultOptions, strokeWidth: 0.1, fillAlpha: 0.8, - fillGraph: true, - stackedGraph: true, + fillGraph: false, + stackedGraph: false, forceIncludeZero: true, + dataHandler: DivergingStackedDataHandler, }, area: { ...defaultOptions, @@ -283,8 +289,9 @@ export default (sdk, chart) => { }, stackedBar: { ...defaultOptions, - stackedGraph: true, + stackedGraph: false, forceIncludeZero: true, + dataHandler: DivergingStackedDataHandler, }, heatmap: { ...defaultOptions, @@ -317,7 +324,7 @@ export default (sdk, chart) => { const { chartType, includeZero, enabledXAxis, enabledYAxis, yAxisLabelWidth, stepPlot } = chart.getAttributes() - const plotter = stepPlot + const plotter = stepPlot && chartType === "line" ? plotterByChartType.default : plotterByChartType[chartType] || plotterByChartType.default @@ -332,6 +339,7 @@ export default (sdk, chart) => { makeYTicker, highlightCircleSize, yRangePad, + dataHandler, } = optionsByChartType[chartType] || optionsByChartType.default const yAxisLabelFormatter = makeYAxisLabelFormatter(labels) @@ -361,6 +369,7 @@ export default (sdk, chart) => { plotter, stepPlot, errorBars, + dataHandler, axes: { x: enabledXAxis ? { diff --git a/src/chartLibraries/dygraph/plotters/index.js b/src/chartLibraries/dygraph/plotters/index.js index af6202ba..7cf99001 100644 --- a/src/chartLibraries/dygraph/plotters/index.js +++ b/src/chartLibraries/dygraph/plotters/index.js @@ -1,4 +1,5 @@ export { default as makeLinePlotter } from "./linePlotter" +export { default as makeStackedAreaPlotter } from "./stackedArea" export { default as makeStackedBarPlotter } from "./stackedBar" export { default as makeMultiColumnBarPlotter } from "./multiColumnBar" export { default as makeHeatmapPlotter } from "./heatmap" diff --git a/src/chartLibraries/dygraph/plotters/stackedArea.js b/src/chartLibraries/dygraph/plotters/stackedArea.js new file mode 100644 index 00000000..089bfd60 --- /dev/null +++ b/src/chartLibraries/dygraph/plotters/stackedArea.js @@ -0,0 +1,57 @@ +import Dygraph from "dygraphs" +import { getDivergingStackBounds } from "../divergingStack" + +const makeFillPlotter = () => plotter => { + const { drawingContext: ctx, dygraph, points, setName } = plotter + const stepPlot = dygraph.getBooleanOption("stepPlot", setName) + + ctx.fillStyle = plotter.color + ctx.globalAlpha = dygraph.getNumericOption("fillAlpha", setName) + ctx.beginPath() + + let previous + + points.forEach(point => { + const bounds = getDivergingStackBounds(point) + if (!bounds) { + previous = null + return + } + + const current = { + x: point.canvasx, + baseY: dygraph.toDomYCoord(bounds.base), + endY: dygraph.toDomYCoord(bounds.end), + } + + if (previous) { + ctx.moveTo(previous.x, previous.endY) + + if (stepPlot) { + ctx.lineTo(current.x, previous.endY) + ctx.lineTo(current.x, current.endY) + } else { + ctx.lineTo(current.x, current.endY) + } + + ctx.lineTo(current.x, current.baseY) + ctx.lineTo(previous.x, previous.baseY) + ctx.closePath() + } + + previous = current + }) + + ctx.fill() +} + +const makeLinePlotter = () => plotter => { + plotter.points.forEach(point => { + const bounds = getDivergingStackBounds(point) + if (bounds) point.canvasy = plotter.dygraph.toDomYCoord(bounds.end) + }) + + Dygraph.Plotters.linePlotter(plotter) +} + +export default () => [makeFillPlotter(), makeLinePlotter()] diff --git a/src/chartLibraries/dygraph/plotters/stackedBar.js b/src/chartLibraries/dygraph/plotters/stackedBar.js index 3ccd5da7..1a951f44 100644 --- a/src/chartLibraries/dygraph/plotters/stackedBar.js +++ b/src/chartLibraries/dygraph/plotters/stackedBar.js @@ -1,20 +1,52 @@ +import { darkenColor } from "./helpers" +import { getDivergingStackBounds } from "../divergingStack" + +const getBarWidth = ({ points, plotArea }) => { + let minSeparation = Infinity + + for (let index = 1; index < points.length; index++) { + minSeparation = Math.min(minSeparation, points[index].canvasx - points[index - 1].canvasx) + } + + const separation = Number.isFinite(minSeparation) + ? minSeparation + : plotArea.w / Math.max(points.length, 1) + + return Math.max(1, Math.floor((2 / 3) * separation)) +} + +export const getDivergingBarRect = (point, barWidth, toDomYCoord) => { + const bounds = getDivergingStackBounds(point) + if (!bounds) return null + + const baseY = toDomYCoord(bounds.base) + const endY = toDomYCoord(bounds.end) + + return { + x: point.canvasx - barWidth / 2, + y: Math.min(baseY, endY), + width: barWidth, + height: Math.abs(baseY - endY), + } +} + export default () => plotter => { const ctx = plotter.drawingContext const points = plotter.points - const y_bottom = plotter.dygraph.toDomYCoord(0) + const barWidth = getBarWidth(plotter) ctx.fillStyle = plotter.color + ctx.strokeStyle = darkenColor(plotter.color) - const min_sep = points[1].canvasx - points[0].canvasx - - const bar_width = Math.floor((2.0 / 3) * min_sep) - - // Do the actual plotting. points.forEach(p => { - const center_x = p.canvasx + const rect = getDivergingBarRect(p, barWidth, value => + plotter.dygraph.toDomYCoord(value) + ) + if (!rect) return - ctx.fillRect(center_x - bar_width / 2, p.canvasy, bar_width, y_bottom - p.canvasy) + p.canvasy = plotter.dygraph.toDomYCoord(getDivergingStackBounds(p).end) - ctx.strokeRect(center_x - bar_width / 2, p.canvasy, bar_width, y_bottom - p.canvasy) + ctx.fillRect(rect.x, rect.y, rect.width, rect.height) + ctx.strokeRect(rect.x, rect.y, rect.width, rect.height) }) } diff --git a/src/chartLibraries/dygraph/plotters/stackedBar.test.js b/src/chartLibraries/dygraph/plotters/stackedBar.test.js index 4a20dcec..e9d60ca4 100644 --- a/src/chartLibraries/dygraph/plotters/stackedBar.test.js +++ b/src/chartLibraries/dygraph/plotters/stackedBar.test.js @@ -1,98 +1,31 @@ -import stackedBarPlotter from "./stackedBar" +import { getDivergingBarRect } from "./stackedBar" -describe("stackedBar plotter", () => { - let mockPlotter - let mockCtx +describe("diverging stacked bar geometry", () => { + const toDomYCoord = value => 100 - value * 10 - beforeEach(() => { - mockCtx = { - fillStyle: "blue", - strokeStyle: "darkblue", - fillRect: jest.fn(), - strokeRect: jest.fn(), - } + it("draws positive values from their positive stack baseline", () => { + const point = { canvasx: 20, netdataStackBase: 2, netdataStackEnd: 5 } - mockPlotter = { - drawingContext: mockCtx, - color: "#ff0000", - points: [ - { canvasx: 10, canvasy: 20 }, - { canvasx: 30, canvasy: 40 }, - ], - dygraph: { - toDomYCoord: jest.fn(() => 100), - }, - } + expect(getDivergingBarRect(point, 10, toDomYCoord)).toEqual({ + x: 15, + y: 50, + width: 10, + height: 30, + }) }) - it("renders stacked bars", () => { - const plotter = stackedBarPlotter() - plotter(mockPlotter) + it("draws negative values from their negative stack baseline", () => { + const point = { canvasx: 20, netdataStackBase: -1, netdataStackEnd: -4 } - expect(mockCtx.fillRect).toHaveBeenCalledTimes(2) - expect(mockCtx.strokeRect).toHaveBeenCalledTimes(2) + expect(getDivergingBarRect(point, 10, toDomYCoord)).toEqual({ + x: 15, + y: 110, + width: 10, + height: 30, + }) }) - it("uses plotter color for fill style", () => { - const plotter = stackedBarPlotter() - plotter(mockPlotter) - - expect(mockCtx.fillStyle).toBe("#ff0000") - }) - - it("calculates bar width from point separation", () => { - const plotter = stackedBarPlotter() - plotter(mockPlotter) - - // Bar width should be 2/3 of separation (30-10 = 20, so 2/3 * 20 = 13.33, floored = 13) - expect(mockCtx.fillRect).toHaveBeenCalledWith( - 3.5, // center_x - bar_width/2 (10 - 13/2 = 3.5) - 20, // canvasy - 13, // bar_width - 80 // y_bottom - canvasy (100 - 20) - ) - }) - - it("positions bars correctly based on canvas coordinates", () => { - const plotter = stackedBarPlotter() - plotter(mockPlotter) - - expect(mockCtx.fillRect).toHaveBeenNthCalledWith( - 1, - 3.5, // first bar x position - 20, // first bar y position - 13, // width - 80 // height - ) - - expect(mockCtx.fillRect).toHaveBeenNthCalledWith( - 2, - 23.5, // second bar x position (30 - 13/2) - 40, // second bar y position - 13, // width - 60 // height (100 - 40) - ) - }) - - it("draws stroke rectangles with same dimensions", () => { - const plotter = stackedBarPlotter() - plotter(mockPlotter) - - expect(mockCtx.strokeRect).toHaveBeenCalledWith(3.5, 20, 13, 80) - expect(mockCtx.strokeRect).toHaveBeenCalledWith(23.5, 40, 13, 60) - }) - - it("uses y_bottom from dygraph", () => { - mockPlotter.dygraph.toDomYCoord = jest.fn(() => 150) - - const plotter = stackedBarPlotter() - plotter(mockPlotter) - - expect(mockCtx.fillRect).toHaveBeenCalledWith( - expect.any(Number), - 20, - expect.any(Number), - 130 // 150 - 20 - ) + it("ignores points without diverging stack bounds", () => { + expect(getDivergingBarRect({ canvasx: 20 }, 10, toDomYCoord)).toBeNull() }) }) diff --git a/src/unitsScalingChartTypes.stories.js b/src/unitsScalingChartTypes.stories.js index e130ab4b..9faaa8f4 100644 --- a/src/unitsScalingChartTypes.stories.js +++ b/src/unitsScalingChartTypes.stories.js @@ -132,7 +132,7 @@ const makePayload = ({ title, unit, dimensions, chartType = "line" }) => { totals: {}, functions: [], result: { - labels: ["time", ...ids], + labels: ["time", ...names], point: { value: 0, arp: 1, pa: 2 }, data: rows, }, @@ -172,17 +172,17 @@ const makePayload = ({ title, unit, dimensions, chartType = "line" }) => { const makeSignedRateDimensions = () => [ { - id: "positive traffic", + id: "traffic_positive", name: "positive traffic", values: makeWave({ center: 1.8e6, amplitude: 650000, phase: 0.2 }), }, { - id: "negative traffic", + id: "traffic_negative", name: "negative traffic", values: makeWave({ center: -1.5e6, amplitude: 720000, phase: 1.4 }), }, { - id: "crossing zero", + id: "traffic_crossing_zero", name: "crossing zero", values: makeWave({ center: 0, amplitude: 480000, cycles: 3.4, phase: 0.7 }), },