@internetstiftelsen/charts 0.9.2 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -3
- package/dist/area.d.ts +2 -0
- package/dist/area.js +39 -31
- package/dist/bar.d.ts +20 -1
- package/dist/bar.js +395 -519
- package/dist/base-chart.d.ts +21 -1
- package/dist/base-chart.js +166 -93
- package/dist/chart-group.d.ts +137 -0
- package/dist/chart-group.js +1155 -0
- package/dist/chart-interface.d.ts +1 -1
- package/dist/donut-center-content.d.ts +1 -0
- package/dist/donut-center-content.js +21 -38
- package/dist/donut-chart.js +30 -15
- package/dist/gauge-chart.d.ts +20 -0
- package/dist/gauge-chart.js +229 -133
- package/dist/legend-state.d.ts +19 -0
- package/dist/legend-state.js +81 -0
- package/dist/legend.d.ts +5 -2
- package/dist/legend.js +45 -38
- package/dist/line.js +3 -1
- package/dist/pie-chart.d.ts +3 -0
- package/dist/pie-chart.js +45 -19
- package/dist/scatter.d.ts +16 -0
- package/dist/scatter.js +165 -0
- package/dist/tooltip.d.ts +2 -1
- package/dist/tooltip.js +21 -25
- package/dist/types.d.ts +19 -1
- package/dist/utils.js +11 -19
- package/dist/validation.d.ts +4 -0
- package/dist/validation.js +19 -0
- package/dist/x-axis.d.ts +10 -0
- package/dist/x-axis.js +190 -149
- package/dist/xy-chart.d.ts +40 -1
- package/dist/xy-chart.js +488 -165
- package/dist/y-axis.d.ts +7 -2
- package/dist/y-axis.js +99 -10
- package/docs/chart-group.md +213 -0
- package/docs/components.md +321 -0
- package/docs/donut-chart.md +193 -0
- package/docs/gauge-chart.md +175 -0
- package/docs/getting-started.md +311 -0
- package/docs/pie-chart.md +123 -0
- package/docs/theming.md +162 -0
- package/docs/word-cloud-chart.md +98 -0
- package/docs/xy-chart.md +517 -0
- package/package.json +6 -4
package/dist/xy-chart.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { max, min, scaleBand, scaleLinear, scaleLog, scaleTime, } from 'd3';
|
|
2
2
|
import { BaseChart, } from './base-chart.js';
|
|
3
|
-
import { ChartValidator } from './validation.js';
|
|
3
|
+
import { ChartValidationError, ChartValidator } from './validation.js';
|
|
4
4
|
import { GROUPED_GAP_TICK_PREFIX, GROUPED_GROUP_LABEL_KEY, } from './grouped-data.js';
|
|
5
5
|
import { resolveScaleValue } from './scale-utils.js';
|
|
6
|
+
import { mergeDeep } from './utils.js';
|
|
6
7
|
const DEFAULT_SERIES_COLOR = '#8884d8';
|
|
7
8
|
function isXYSeries(component) {
|
|
8
9
|
return (component.type === 'line' ||
|
|
10
|
+
component.type === 'scatter' ||
|
|
9
11
|
component.type === 'bar' ||
|
|
10
12
|
component.type === 'area');
|
|
11
13
|
}
|
|
@@ -48,6 +50,12 @@ export class XYChart extends BaseChart {
|
|
|
48
50
|
writable: true,
|
|
49
51
|
value: void 0
|
|
50
52
|
});
|
|
53
|
+
Object.defineProperty(this, "scaleConfigOverride", {
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
writable: true,
|
|
57
|
+
value: null
|
|
58
|
+
});
|
|
51
59
|
this.orientation = config.orientation ?? 'vertical';
|
|
52
60
|
this.barStackMode = config.barStack?.mode ?? 'normal';
|
|
53
61
|
this.barStackGap = config.barStack?.gap ?? 0.1;
|
|
@@ -82,7 +90,7 @@ export class XYChart extends BaseChart {
|
|
|
82
90
|
width: this.configuredWidth,
|
|
83
91
|
height: this.configuredHeight,
|
|
84
92
|
theme: this.theme,
|
|
85
|
-
scales: this.
|
|
93
|
+
scales: this.resolvedScaleConfig,
|
|
86
94
|
responsive: this.responsiveConfig,
|
|
87
95
|
orientation: this.orientation,
|
|
88
96
|
barStack: {
|
|
@@ -106,6 +114,7 @@ export class XYChart extends BaseChart {
|
|
|
106
114
|
prepareLayout(context) {
|
|
107
115
|
super.prepareLayout(context);
|
|
108
116
|
this.xAxis?.clearEstimatedSpace?.();
|
|
117
|
+
this.yAxis?.clearEstimatedSpace?.();
|
|
109
118
|
if (this.xAxis) {
|
|
110
119
|
const xKey = this.getXKey();
|
|
111
120
|
const labelKey = this.xAxis.labelKey;
|
|
@@ -117,32 +126,59 @@ export class XYChart extends BaseChart {
|
|
|
117
126
|
});
|
|
118
127
|
this.xAxis.estimateLayoutSpace?.(labels, this.renderTheme, context.svgNode);
|
|
119
128
|
}
|
|
129
|
+
if (this.yAxis) {
|
|
130
|
+
this.yAxis.estimateLayoutSpace?.(this.getYAxisEstimateLabels(), this.renderTheme, context.svgNode);
|
|
131
|
+
}
|
|
120
132
|
}
|
|
121
|
-
|
|
122
|
-
this.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const typeName = series.type === 'line'
|
|
136
|
-
? 'Line'
|
|
137
|
-
: series.type === 'bar'
|
|
138
|
-
? 'Bar'
|
|
139
|
-
: 'Area';
|
|
140
|
-
ChartValidator.validatePositiveData(this.data, series.dataKey, typeName);
|
|
133
|
+
getYAxisEstimateLabels() {
|
|
134
|
+
if (!this.yAxis) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const yAxisConfig = this.yAxis.getExportConfig();
|
|
138
|
+
const { y: yConfig } = this.getResolvedAxisConfigs();
|
|
139
|
+
const tickFormat = yAxisConfig.tickFormat;
|
|
140
|
+
if (yConfig.type === 'band') {
|
|
141
|
+
const tickValues = this.resolveScaleDomain(yConfig, this.isHorizontalOrientation() ? this.getXKey() : null);
|
|
142
|
+
return tickValues.map((value) => {
|
|
143
|
+
if (typeof tickFormat === 'function') {
|
|
144
|
+
return tickFormat(value);
|
|
145
|
+
}
|
|
146
|
+
return String(value);
|
|
141
147
|
});
|
|
142
148
|
}
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
const scale = this.createContinuousScaleForLayoutEstimate(yConfig);
|
|
150
|
+
const tickValues = scale.ticks(5);
|
|
151
|
+
if (typeof tickFormat === 'function') {
|
|
152
|
+
return tickValues.map((value) => tickFormat(value));
|
|
145
153
|
}
|
|
154
|
+
const tickFormatter = scale.tickFormat(5, typeof tickFormat === 'string' ? tickFormat : undefined);
|
|
155
|
+
return tickValues.map((value) => tickFormatter(value));
|
|
156
|
+
}
|
|
157
|
+
createContinuousScaleForLayoutEstimate(config) {
|
|
158
|
+
const domain = this.resolveScaleDomain(config, null);
|
|
159
|
+
switch (config.type) {
|
|
160
|
+
case 'linear': {
|
|
161
|
+
const scale = scaleLinear()
|
|
162
|
+
.domain(domain)
|
|
163
|
+
.rangeRound([100, 0]);
|
|
164
|
+
return config.nice === false ? scale : scale.nice();
|
|
165
|
+
}
|
|
166
|
+
case 'time': {
|
|
167
|
+
const scale = scaleTime()
|
|
168
|
+
.domain(domain)
|
|
169
|
+
.range([100, 0]);
|
|
170
|
+
return config.nice === false ? scale : scale.nice();
|
|
171
|
+
}
|
|
172
|
+
case 'log': {
|
|
173
|
+
const scale = scaleLog()
|
|
174
|
+
.domain(domain)
|
|
175
|
+
.rangeRound([100, 0]);
|
|
176
|
+
return config.nice === false ? scale : scale.nice();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
renderChart({ svg, plotGroup, plotArea, }) {
|
|
181
|
+
this.validateRenderState();
|
|
146
182
|
const xKey = this.getXKey();
|
|
147
183
|
const categoryScaleType = this.getCategoryScaleType();
|
|
148
184
|
const visibleSeries = this.getVisibleSeries();
|
|
@@ -152,20 +188,8 @@ export class XYChart extends BaseChart {
|
|
|
152
188
|
this.grid.render(plotGroup, this.x, this.y, this.renderTheme, this.isHorizontalOrientation());
|
|
153
189
|
}
|
|
154
190
|
this.renderSeries(visibleSeries);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.xAxis.render(svg, this.x, this.renderTheme, plotArea.bottom, this.data);
|
|
158
|
-
}
|
|
159
|
-
if (this.yAxis) {
|
|
160
|
-
this.yAxis.render(svg, this.y, this.renderTheme, plotArea.left);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (this.tooltip && this.x && this.y) {
|
|
164
|
-
const visibleAreaSeries = visibleSeries.filter((series) => series.type === 'area');
|
|
165
|
-
const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, visibleAreaSeries);
|
|
166
|
-
this.tooltip.initialize(this.renderTheme);
|
|
167
|
-
this.tooltip.attachToArea(svg, this.data, visibleSeries, xKey, this.x, this.y, this.renderTheme, plotArea, this.parseValue.bind(this), this.isHorizontalOrientation(), categoryScaleType, (series, dataPoint) => this.getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries));
|
|
168
|
-
}
|
|
191
|
+
this.renderAxes(svg, plotArea);
|
|
192
|
+
this.attachTooltip(svg, plotArea, visibleSeries, xKey, categoryScaleType);
|
|
169
193
|
this.renderInlineLegend(svg);
|
|
170
194
|
}
|
|
171
195
|
getXKey() {
|
|
@@ -177,7 +201,7 @@ export class XYChart extends BaseChart {
|
|
|
177
201
|
getLegendSeries() {
|
|
178
202
|
const displaySeries = this.getDisplaySeries();
|
|
179
203
|
return displaySeries.map((series) => {
|
|
180
|
-
if (series.type === 'line') {
|
|
204
|
+
if (series.type === 'line' || series.type === 'scatter') {
|
|
181
205
|
return {
|
|
182
206
|
dataKey: series.dataKey,
|
|
183
207
|
stroke: series.stroke,
|
|
@@ -190,7 +214,45 @@ export class XYChart extends BaseChart {
|
|
|
190
214
|
});
|
|
191
215
|
}
|
|
192
216
|
getCategoryScaleType() {
|
|
193
|
-
return this.
|
|
217
|
+
return this.resolvedScaleConfig.x?.type || 'band';
|
|
218
|
+
}
|
|
219
|
+
getOrientation() {
|
|
220
|
+
return this.orientation;
|
|
221
|
+
}
|
|
222
|
+
getValueAxisScaleType() {
|
|
223
|
+
if (this.orientation !== 'vertical') {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return this.resolvedScaleConfig.y?.type ?? 'linear';
|
|
227
|
+
}
|
|
228
|
+
getValueAxisDomain() {
|
|
229
|
+
return this.resolveValueAxisDomain(this.resolvedScaleConfig);
|
|
230
|
+
}
|
|
231
|
+
getBaseValueAxisDomain() {
|
|
232
|
+
return this.resolveValueAxisDomain(this.scaleConfig);
|
|
233
|
+
}
|
|
234
|
+
setScaleConfigOverride(override, rerender = true) {
|
|
235
|
+
this.scaleConfigOverride = override;
|
|
236
|
+
if (rerender) {
|
|
237
|
+
this.rerender();
|
|
238
|
+
}
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
resolveValueAxisDomain(scaleConfig) {
|
|
242
|
+
if (this.orientation !== 'vertical') {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const yConfig = this.getAxisConfigsForScaleConfig(scaleConfig).y;
|
|
246
|
+
if (yConfig.type !== 'linear' && yConfig.type !== 'log') {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
const domain = this.resolveScaleDomain(yConfig, null);
|
|
250
|
+
if (domain.length < 2 ||
|
|
251
|
+
typeof domain[0] !== 'number' ||
|
|
252
|
+
typeof domain[1] !== 'number') {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return [domain[0], domain[1]];
|
|
194
256
|
}
|
|
195
257
|
getVisibleSeries() {
|
|
196
258
|
return this.filterVisibleItems(this.getDisplaySeries(), (series) => {
|
|
@@ -228,6 +290,13 @@ export class XYChart extends BaseChart {
|
|
|
228
290
|
: series.stroke,
|
|
229
291
|
});
|
|
230
292
|
}
|
|
293
|
+
if (series.type === 'scatter') {
|
|
294
|
+
return this.cloneSeriesWithOverride(series, {
|
|
295
|
+
stroke: this.shouldReplaceSeriesColor(series.stroke)
|
|
296
|
+
? paletteColor
|
|
297
|
+
: series.stroke,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
231
300
|
if (series.type === 'bar') {
|
|
232
301
|
return this.cloneSeriesWithOverride(series, {
|
|
233
302
|
fill: this.shouldReplaceSeriesColor(series.fill)
|
|
@@ -253,23 +322,59 @@ export class XYChart extends BaseChart {
|
|
|
253
322
|
}
|
|
254
323
|
setupScales() {
|
|
255
324
|
const xKey = this.getXKey();
|
|
256
|
-
const isHorizontal = this.
|
|
257
|
-
|
|
258
|
-
let yConfig;
|
|
259
|
-
if (isHorizontal) {
|
|
260
|
-
xConfig = this.scaleConfig.y || { type: 'linear' };
|
|
261
|
-
yConfig = this.scaleConfig.x || { type: 'band' };
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
xConfig = this.scaleConfig.x || { type: 'band' };
|
|
265
|
-
yConfig = this.scaleConfig.y || { type: 'linear' };
|
|
266
|
-
}
|
|
325
|
+
const isHorizontal = this.isHorizontalOrientation();
|
|
326
|
+
const { x: xConfig, y: yConfig } = this.getResolvedAxisConfigs();
|
|
267
327
|
this.x = this.createScale(xConfig, isHorizontal ? null : xKey, 'x');
|
|
268
328
|
this.y = this.createScale(yConfig, isHorizontal ? xKey : null, 'y');
|
|
269
329
|
}
|
|
330
|
+
get resolvedScaleConfig() {
|
|
331
|
+
if (!this.scaleConfigOverride) {
|
|
332
|
+
return this.scaleConfig;
|
|
333
|
+
}
|
|
334
|
+
return mergeDeep(this.scaleConfig, this.scaleConfigOverride);
|
|
335
|
+
}
|
|
336
|
+
getResolvedAxisConfigs() {
|
|
337
|
+
return this.getAxisConfigsForScaleConfig(this.resolvedScaleConfig);
|
|
338
|
+
}
|
|
339
|
+
getAxisConfigsForScaleConfig(scaleConfig) {
|
|
340
|
+
if (this.isHorizontalOrientation()) {
|
|
341
|
+
return {
|
|
342
|
+
x: {
|
|
343
|
+
type: 'linear',
|
|
344
|
+
...(scaleConfig.y ?? {}),
|
|
345
|
+
},
|
|
346
|
+
y: {
|
|
347
|
+
type: 'band',
|
|
348
|
+
...(scaleConfig.x ?? {}),
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
x: {
|
|
354
|
+
type: 'band',
|
|
355
|
+
...(scaleConfig.x ?? {}),
|
|
356
|
+
},
|
|
357
|
+
y: {
|
|
358
|
+
type: 'linear',
|
|
359
|
+
...(scaleConfig.y ?? {}),
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
270
363
|
isHorizontalOrientation() {
|
|
271
364
|
return this.orientation === 'horizontal';
|
|
272
365
|
}
|
|
366
|
+
getSeriesTypeName(series) {
|
|
367
|
+
switch (series.type) {
|
|
368
|
+
case 'line':
|
|
369
|
+
return 'Line';
|
|
370
|
+
case 'scatter':
|
|
371
|
+
return 'Scatter';
|
|
372
|
+
case 'bar':
|
|
373
|
+
return 'Bar';
|
|
374
|
+
case 'area':
|
|
375
|
+
return 'Area';
|
|
376
|
+
}
|
|
377
|
+
}
|
|
273
378
|
validateSeriesOrientation() {
|
|
274
379
|
if (this.orientation !== 'horizontal') {
|
|
275
380
|
return;
|
|
@@ -277,10 +382,13 @@ export class XYChart extends BaseChart {
|
|
|
277
382
|
const hasLineSeries = this.series.some((series) => {
|
|
278
383
|
return series.type === 'line';
|
|
279
384
|
});
|
|
385
|
+
const hasScatterSeries = this.series.some((series) => {
|
|
386
|
+
return series.type === 'scatter';
|
|
387
|
+
});
|
|
280
388
|
const hasAreaSeries = this.series.some((series) => {
|
|
281
389
|
return series.type === 'area';
|
|
282
390
|
});
|
|
283
|
-
if (hasLineSeries || hasAreaSeries) {
|
|
391
|
+
if (hasLineSeries || hasScatterSeries || hasAreaSeries) {
|
|
284
392
|
throw new Error('XYChart: horizontal orientation currently supports Bar series only');
|
|
285
393
|
}
|
|
286
394
|
}
|
|
@@ -296,6 +404,35 @@ export class XYChart extends BaseChart {
|
|
|
296
404
|
});
|
|
297
405
|
return values;
|
|
298
406
|
}
|
|
407
|
+
getBarPercentDomain(positiveTotalData, negativeTotalData) {
|
|
408
|
+
const hasPositive = Array.from(positiveTotalData.values()).some((value) => value > 0);
|
|
409
|
+
const hasNegative = Array.from(negativeTotalData.values()).some((value) => value > 0);
|
|
410
|
+
if (hasPositive && hasNegative) {
|
|
411
|
+
return [-100, 100];
|
|
412
|
+
}
|
|
413
|
+
if (hasNegative) {
|
|
414
|
+
return [-100, 0];
|
|
415
|
+
}
|
|
416
|
+
return [0, 100];
|
|
417
|
+
}
|
|
418
|
+
getBarValueDomain(xKey, barSeries) {
|
|
419
|
+
if (barSeries.length === 0) {
|
|
420
|
+
return [0, 0];
|
|
421
|
+
}
|
|
422
|
+
const { positiveTotalData, negativeTotalData, rawValuesBySeriesIndex } = this.computeStackingData(this.data, xKey, barSeries);
|
|
423
|
+
if (this.barStackMode === 'percent') {
|
|
424
|
+
return this.getBarPercentDomain(positiveTotalData, negativeTotalData);
|
|
425
|
+
}
|
|
426
|
+
if (this.barStackMode === 'normal') {
|
|
427
|
+
const maxPositiveTotal = max(Array.from(positiveTotalData.values())) ?? 0;
|
|
428
|
+
const maxNegativeTotal = max(Array.from(negativeTotalData.values())) ?? 0;
|
|
429
|
+
return [-maxNegativeTotal, maxPositiveTotal];
|
|
430
|
+
}
|
|
431
|
+
const rawValues = Array.from(rawValuesBySeriesIndex.values()).flatMap((seriesValues) => Array.from(seriesValues.values()));
|
|
432
|
+
const minValue = min(rawValues) ?? 0;
|
|
433
|
+
const maxValue = max(rawValues) ?? 0;
|
|
434
|
+
return [Math.min(minValue, 0), Math.max(maxValue, 0)];
|
|
435
|
+
}
|
|
299
436
|
getStackedAreaGroups(areaSeries) {
|
|
300
437
|
const groups = new Map();
|
|
301
438
|
if (this.areaStackMode === 'none') {
|
|
@@ -346,149 +483,292 @@ export class XYChart extends BaseChart {
|
|
|
346
483
|
});
|
|
347
484
|
return domain;
|
|
348
485
|
}
|
|
349
|
-
|
|
486
|
+
getScaleRange(axis, scaleType, reverse) {
|
|
350
487
|
if (!this.plotArea) {
|
|
351
488
|
throw new Error('Plot area not calculated');
|
|
352
489
|
}
|
|
353
|
-
const scaleType = config.type || (axis === 'x' ? 'band' : 'linear');
|
|
354
|
-
const isXAxis = axis === 'x';
|
|
355
490
|
const plotPadding = 10;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (config.domain) {
|
|
362
|
-
domain = config.domain;
|
|
491
|
+
let rangeStart;
|
|
492
|
+
let rangeEnd;
|
|
493
|
+
if (axis === 'x') {
|
|
494
|
+
rangeStart = this.plotArea.left + plotPadding;
|
|
495
|
+
rangeEnd = this.plotArea.right;
|
|
363
496
|
}
|
|
364
|
-
else if (scaleType === 'band'
|
|
365
|
-
|
|
497
|
+
else if (scaleType === 'band') {
|
|
498
|
+
rangeStart = this.plotArea.top;
|
|
499
|
+
rangeEnd = this.plotArea.bottom - plotPadding;
|
|
366
500
|
}
|
|
367
|
-
else
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
max(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
|
|
371
|
-
];
|
|
501
|
+
else {
|
|
502
|
+
rangeStart = this.plotArea.bottom - plotPadding;
|
|
503
|
+
rangeEnd = this.plotArea.top;
|
|
372
504
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const maxVal = config.max ?? max(values) ?? 100;
|
|
379
|
-
domain = [minVal, maxVal];
|
|
505
|
+
return reverse ? [rangeEnd, rangeStart] : [rangeStart, rangeEnd];
|
|
506
|
+
}
|
|
507
|
+
createScale(config, dataKey, axis) {
|
|
508
|
+
if (!this.plotArea) {
|
|
509
|
+
throw new Error('Plot area not calculated');
|
|
380
510
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
511
|
+
const scaleType = config.type;
|
|
512
|
+
const [rangeStart, rangeEnd] = this.getScaleRange(axis, scaleType, config.reverse ?? false);
|
|
513
|
+
const domain = this.resolveScaleDomain(config, dataKey);
|
|
514
|
+
this.validateExplicitBarValueDomain(config, dataKey, scaleType, domain);
|
|
515
|
+
this.validateScaleDomain(scaleType, domain);
|
|
516
|
+
return this.buildScale(scaleType, domain, [rangeStart, rangeEnd], config);
|
|
517
|
+
}
|
|
518
|
+
resolveScaleDomain(config, dataKey) {
|
|
519
|
+
const buckets = this.getSeriesBuckets();
|
|
520
|
+
const stackedAreaGroups = this.getStackedAreaGroups(buckets.areaSeries);
|
|
521
|
+
if (this.isBarValueScale(config.type, dataKey, buckets.barSeries) &&
|
|
522
|
+
config.type === 'log') {
|
|
523
|
+
throw new ChartValidationError('XYChart: bar series require a linear value axis because bars need a zero baseline');
|
|
524
|
+
}
|
|
525
|
+
const domain = this.resolveRawScaleDomain(config, dataKey, buckets, stackedAreaGroups);
|
|
526
|
+
return this.applyNiceDomain(config, domain);
|
|
527
|
+
}
|
|
528
|
+
getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries) {
|
|
529
|
+
const rawValue = dataPoint[series.dataKey];
|
|
530
|
+
if (rawValue === null || rawValue === undefined) {
|
|
531
|
+
return NaN;
|
|
532
|
+
}
|
|
533
|
+
const parsedValue = this.parseValue(rawValue);
|
|
534
|
+
if (!Number.isFinite(parsedValue)) {
|
|
535
|
+
return NaN;
|
|
536
|
+
}
|
|
537
|
+
if (series.type === 'bar') {
|
|
538
|
+
return series.getRenderedValue(parsedValue, this.orientation);
|
|
539
|
+
}
|
|
540
|
+
if (series.type !== 'area') {
|
|
541
|
+
return parsedValue;
|
|
542
|
+
}
|
|
543
|
+
return this.getAreaTooltipValue(dataPoint, xKey, parsedValue, areaStackingContextBySeries.get(series));
|
|
544
|
+
}
|
|
545
|
+
validateRenderState() {
|
|
546
|
+
this.validateSeriesOrientation();
|
|
547
|
+
this.validateSeriesData();
|
|
548
|
+
this.validateValueScaleRequirements();
|
|
549
|
+
this.validateXAxisDataKey();
|
|
550
|
+
}
|
|
551
|
+
validateSeriesData() {
|
|
552
|
+
this.series.forEach((series) => {
|
|
553
|
+
const typeName = this.getSeriesTypeName(series);
|
|
554
|
+
ChartValidator.validateDataKey(this.data, series.dataKey, typeName);
|
|
555
|
+
ChartValidator.validateNumericData(this.data, series.dataKey, typeName);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
validateValueScaleRequirements() {
|
|
559
|
+
if ((this.resolvedScaleConfig.y?.type ?? 'linear') !== 'log') {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
this.series.forEach((series) => {
|
|
563
|
+
ChartValidator.validatePositiveData(this.data, series.dataKey, this.getSeriesTypeName(series));
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
validateXAxisDataKey() {
|
|
567
|
+
if (this.xAxis?.dataKey) {
|
|
568
|
+
ChartValidator.validateDataKey(this.data, this.xAxis.dataKey, 'XAxis');
|
|
431
569
|
}
|
|
570
|
+
}
|
|
571
|
+
renderAxes(svg, plotArea) {
|
|
572
|
+
if (!this.x || !this.y) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (this.xAxis) {
|
|
576
|
+
this.xAxis.render(svg, this.x, this.renderTheme, plotArea.bottom, this.data);
|
|
577
|
+
}
|
|
578
|
+
if (this.yAxis) {
|
|
579
|
+
this.yAxis.render(svg, this.y, this.renderTheme, plotArea.left);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
attachTooltip(svg, plotArea, visibleSeries, xKey, categoryScaleType) {
|
|
583
|
+
if (!this.tooltip || !this.x || !this.y) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const visibleAreaSeries = visibleSeries.filter((series) => series.type === 'area');
|
|
587
|
+
const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, visibleAreaSeries);
|
|
588
|
+
this.tooltip.initialize(this.renderTheme);
|
|
589
|
+
this.tooltip.attachToArea(svg, this.data, visibleSeries, xKey, this.x, this.y, this.renderTheme, plotArea, this.parseValue.bind(this), this.isHorizontalOrientation(), categoryScaleType, (series, dataPoint) => this.getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries));
|
|
590
|
+
}
|
|
591
|
+
validateExplicitBarValueDomain(config, dataKey, scaleType, domain) {
|
|
592
|
+
if (dataKey === null &&
|
|
593
|
+
this.series.some((series) => series.type === 'bar') &&
|
|
594
|
+
(scaleType === 'linear' || scaleType === 'log') &&
|
|
595
|
+
(config.domain !== undefined ||
|
|
596
|
+
config.min !== undefined ||
|
|
597
|
+
config.max !== undefined)) {
|
|
598
|
+
ChartValidator.validateBarDomainIncludesZero(domain);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
validateScaleDomain(scaleType, domain) {
|
|
432
602
|
if (scaleType === 'log') {
|
|
433
603
|
ChartValidator.validateScaleConfig(scaleType, domain);
|
|
434
604
|
}
|
|
605
|
+
}
|
|
606
|
+
buildScale(scaleType, domain, range, config) {
|
|
435
607
|
switch (scaleType) {
|
|
436
|
-
case 'band':
|
|
437
|
-
|
|
608
|
+
case 'band':
|
|
609
|
+
return scaleBand()
|
|
438
610
|
.domain(domain)
|
|
439
|
-
.rangeRound(
|
|
440
|
-
|
|
441
|
-
scale.paddingInner(config.padding);
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
scale.paddingInner(0.1);
|
|
445
|
-
}
|
|
446
|
-
return scale;
|
|
447
|
-
}
|
|
611
|
+
.rangeRound(range)
|
|
612
|
+
.paddingInner(config.padding ?? 0.1);
|
|
448
613
|
case 'linear': {
|
|
449
614
|
const scale = scaleLinear()
|
|
450
615
|
.domain(domain)
|
|
451
|
-
.rangeRound(
|
|
452
|
-
|
|
453
|
-
scale.nice();
|
|
454
|
-
}
|
|
455
|
-
return scale;
|
|
616
|
+
.rangeRound(range);
|
|
617
|
+
return config.nice === false ? scale : scale.nice();
|
|
456
618
|
}
|
|
457
619
|
case 'time': {
|
|
458
620
|
const scale = scaleTime()
|
|
459
621
|
.domain(domain)
|
|
460
|
-
.range(
|
|
461
|
-
|
|
462
|
-
scale.nice();
|
|
463
|
-
}
|
|
464
|
-
return scale;
|
|
622
|
+
.range(range);
|
|
623
|
+
return config.nice === false ? scale : scale.nice();
|
|
465
624
|
}
|
|
466
625
|
case 'log': {
|
|
467
626
|
const scale = scaleLog()
|
|
468
627
|
.domain(domain)
|
|
469
|
-
.rangeRound(
|
|
470
|
-
|
|
471
|
-
scale.nice();
|
|
472
|
-
}
|
|
473
|
-
return scale;
|
|
628
|
+
.rangeRound(range);
|
|
629
|
+
return config.nice === false ? scale : scale.nice();
|
|
474
630
|
}
|
|
475
631
|
default:
|
|
476
632
|
throw new Error(`Unsupported scale type: ${scaleType}`);
|
|
477
633
|
}
|
|
478
634
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
635
|
+
getSeriesBuckets() {
|
|
636
|
+
return {
|
|
637
|
+
barSeries: this.series.filter((s) => s.type === 'bar'),
|
|
638
|
+
lineSeries: this.series.filter((s) => s.type === 'line'),
|
|
639
|
+
scatterSeries: this.series.filter((s) => s.type === 'scatter'),
|
|
640
|
+
areaSeries: this.series.filter((s) => s.type === 'area'),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
isBarValueScale(scaleType, dataKey, barSeries) {
|
|
644
|
+
return (dataKey === null &&
|
|
645
|
+
barSeries.length > 0 &&
|
|
646
|
+
(scaleType === 'linear' || scaleType === 'log'));
|
|
647
|
+
}
|
|
648
|
+
resolveRawScaleDomain(config, dataKey, buckets, stackedAreaGroups) {
|
|
649
|
+
if (config.domain) {
|
|
650
|
+
return config.domain;
|
|
483
651
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return NaN;
|
|
652
|
+
if (config.type === 'band' && dataKey) {
|
|
653
|
+
return this.buildBandDomainWithGroupGaps(dataKey, config.groupGap ?? 0);
|
|
487
654
|
}
|
|
488
|
-
if (
|
|
489
|
-
return
|
|
655
|
+
if (config.type === 'time' && dataKey) {
|
|
656
|
+
return this.resolveTimeDomain(dataKey);
|
|
657
|
+
}
|
|
658
|
+
if ((config.type === 'linear' || config.type === 'log') && dataKey) {
|
|
659
|
+
return this.resolveNumericDataKeyDomain(config, dataKey);
|
|
660
|
+
}
|
|
661
|
+
return this.resolveSeriesValueDomain(config, buckets, stackedAreaGroups);
|
|
662
|
+
}
|
|
663
|
+
resolveTimeDomain(dataKey) {
|
|
664
|
+
return [
|
|
665
|
+
min(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
|
|
666
|
+
max(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
|
|
667
|
+
];
|
|
668
|
+
}
|
|
669
|
+
resolveNumericDataKeyDomain(config, dataKey) {
|
|
670
|
+
const values = this.data
|
|
671
|
+
.map((d) => this.parseValue(d[dataKey]))
|
|
672
|
+
.filter((value) => Number.isFinite(value));
|
|
673
|
+
return [
|
|
674
|
+
config.min ?? min(values) ?? 0,
|
|
675
|
+
config.max ?? max(values) ?? 100,
|
|
676
|
+
];
|
|
677
|
+
}
|
|
678
|
+
resolveSeriesValueDomain(config, buckets, stackedAreaGroups) {
|
|
679
|
+
if (this.hasPercentValueDomain(buckets.barSeries, stackedAreaGroups)) {
|
|
680
|
+
return this.resolvePercentValueDomain(buckets.barSeries, stackedAreaGroups);
|
|
681
|
+
}
|
|
682
|
+
return this.resolveStandardValueDomain(config, buckets, stackedAreaGroups);
|
|
683
|
+
}
|
|
684
|
+
hasPercentValueDomain(barSeries, stackedAreaGroups) {
|
|
685
|
+
return ((this.barStackMode === 'percent' && barSeries.length > 0) ||
|
|
686
|
+
(this.areaStackMode === 'percent' && stackedAreaGroups.size > 0));
|
|
687
|
+
}
|
|
688
|
+
resolvePercentValueDomain(barSeries, stackedAreaGroups) {
|
|
689
|
+
let minDomain = 0;
|
|
690
|
+
let maxDomain = 100;
|
|
691
|
+
if (this.barStackMode === 'percent' && barSeries.length > 0) {
|
|
692
|
+
[minDomain, maxDomain] = this.getBarValueDomain(this.getXKey(), barSeries);
|
|
693
|
+
}
|
|
694
|
+
if (this.areaStackMode === 'percent' && stackedAreaGroups.size > 0) {
|
|
695
|
+
minDomain = Math.min(minDomain, 0);
|
|
696
|
+
maxDomain = Math.max(maxDomain, 100);
|
|
697
|
+
}
|
|
698
|
+
return [minDomain, maxDomain];
|
|
699
|
+
}
|
|
700
|
+
resolveStandardValueDomain(config, buckets, stackedAreaGroups) {
|
|
701
|
+
const values = this.collectSeriesValues([
|
|
702
|
+
...buckets.lineSeries,
|
|
703
|
+
...buckets.scatterSeries,
|
|
704
|
+
...buckets.areaSeries,
|
|
705
|
+
]);
|
|
706
|
+
const minCandidates = [...values];
|
|
707
|
+
const maxCandidates = [...values];
|
|
708
|
+
const stackedValues = this.collectStackedAreaTotals(stackedAreaGroups);
|
|
709
|
+
if (buckets.barSeries.length > 0) {
|
|
710
|
+
const [barMin, barMax] = this.getBarValueDomain(this.getXKey(), buckets.barSeries);
|
|
711
|
+
minCandidates.push(barMin);
|
|
712
|
+
maxCandidates.push(barMax);
|
|
713
|
+
}
|
|
714
|
+
this.addAreaBaselines(minCandidates, buckets.areaSeries, stackedAreaGroups);
|
|
715
|
+
return [
|
|
716
|
+
config.min ?? min(minCandidates) ?? 0,
|
|
717
|
+
config.max ?? max([...maxCandidates, ...stackedValues]) ?? 100,
|
|
718
|
+
];
|
|
719
|
+
}
|
|
720
|
+
collectStackedAreaTotals(stackedAreaGroups) {
|
|
721
|
+
const stackedValues = [];
|
|
722
|
+
stackedAreaGroups.forEach((stackSeries) => {
|
|
723
|
+
this.data.forEach((dataPoint) => {
|
|
724
|
+
const total = stackSeries.reduce((sum, series) => {
|
|
725
|
+
const value = this.parseValue(dataPoint[series.dataKey]);
|
|
726
|
+
return Number.isFinite(value) ? sum + value : sum;
|
|
727
|
+
}, 0);
|
|
728
|
+
stackedValues.push(total);
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
return stackedValues;
|
|
732
|
+
}
|
|
733
|
+
addAreaBaselines(minCandidates, areaSeries, stackedAreaGroups) {
|
|
734
|
+
const stackedAreaKeys = new Set();
|
|
735
|
+
stackedAreaGroups.forEach((stackSeries) => {
|
|
736
|
+
stackSeries.forEach((series) => {
|
|
737
|
+
stackedAreaKeys.add(series.dataKey);
|
|
738
|
+
});
|
|
739
|
+
minCandidates.push(0);
|
|
740
|
+
});
|
|
741
|
+
areaSeries
|
|
742
|
+
.filter((series) => !stackedAreaKeys.has(series.dataKey))
|
|
743
|
+
.forEach((series) => {
|
|
744
|
+
minCandidates.push(series.baseline);
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
applyNiceDomain(config, domain) {
|
|
748
|
+
if (config.nice === false) {
|
|
749
|
+
return domain;
|
|
750
|
+
}
|
|
751
|
+
if (config.type === 'linear') {
|
|
752
|
+
return scaleLinear()
|
|
753
|
+
.domain(domain)
|
|
754
|
+
.nice()
|
|
755
|
+
.domain();
|
|
756
|
+
}
|
|
757
|
+
if (config.type === 'time') {
|
|
758
|
+
return scaleTime()
|
|
759
|
+
.domain(domain)
|
|
760
|
+
.nice()
|
|
761
|
+
.domain();
|
|
762
|
+
}
|
|
763
|
+
if (config.type === 'log') {
|
|
764
|
+
return scaleLog()
|
|
765
|
+
.domain(domain)
|
|
766
|
+
.nice()
|
|
767
|
+
.domain();
|
|
490
768
|
}
|
|
491
|
-
|
|
769
|
+
return domain;
|
|
770
|
+
}
|
|
771
|
+
getAreaTooltipValue(dataPoint, xKey, parsedValue, stackingContext) {
|
|
492
772
|
if (!stackingContext || stackingContext.mode === 'none') {
|
|
493
773
|
return parsedValue;
|
|
494
774
|
}
|
|
@@ -512,12 +792,13 @@ export class XYChart extends BaseChart {
|
|
|
512
792
|
const barSeries = visibleSeries.filter((s) => s.type === 'bar');
|
|
513
793
|
const areaSeries = visibleSeries.filter((s) => s.type === 'area');
|
|
514
794
|
const lineSeries = visibleSeries.filter((s) => s.type === 'line');
|
|
795
|
+
const scatterSeries = visibleSeries.filter((s) => s.type === 'scatter');
|
|
515
796
|
const areaValueLabelLayer = areaSeries.length > 0
|
|
516
797
|
? this.plotGroup
|
|
517
798
|
.append('g')
|
|
518
799
|
.attr('class', 'area-value-label-layer')
|
|
519
800
|
: null;
|
|
520
|
-
const { cumulativeDataBySeriesIndex, totalData, rawValuesBySeriesIndex, } = this.computeStackingData(this.data, xKey, barSeries);
|
|
801
|
+
const { cumulativeDataBySeriesIndex, positiveCumulativeDataBySeriesIndex, negativeCumulativeDataBySeriesIndex, totalData, positiveTotalData, negativeTotalData, rawValuesBySeriesIndex, } = this.computeStackingData(this.data, xKey, barSeries);
|
|
521
802
|
const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, areaSeries);
|
|
522
803
|
barSeries.forEach((series, barIndex) => {
|
|
523
804
|
const nextLayerData = this.barStackMode === 'layer'
|
|
@@ -529,6 +810,12 @@ export class XYChart extends BaseChart {
|
|
|
529
810
|
totalSeries: barSeries.length,
|
|
530
811
|
cumulativeData: cumulativeDataBySeriesIndex.get(barIndex) ?? new Map(),
|
|
531
812
|
totalData,
|
|
813
|
+
positiveCumulativeData: positiveCumulativeDataBySeriesIndex.get(barIndex) ??
|
|
814
|
+
new Map(),
|
|
815
|
+
negativeCumulativeData: negativeCumulativeDataBySeriesIndex.get(barIndex) ??
|
|
816
|
+
new Map(),
|
|
817
|
+
positiveTotalData,
|
|
818
|
+
negativeTotalData,
|
|
532
819
|
gap: this.barStackGap,
|
|
533
820
|
nextLayerData,
|
|
534
821
|
};
|
|
@@ -540,21 +827,37 @@ export class XYChart extends BaseChart {
|
|
|
540
827
|
lineSeries.forEach((series) => {
|
|
541
828
|
series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme);
|
|
542
829
|
});
|
|
830
|
+
scatterSeries.forEach((series) => {
|
|
831
|
+
series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme);
|
|
832
|
+
});
|
|
543
833
|
if (areaValueLabelLayer) {
|
|
544
834
|
areaValueLabelLayer.raise();
|
|
545
835
|
}
|
|
546
836
|
}
|
|
547
837
|
computeStackingData(data, xKey, barSeries) {
|
|
548
838
|
const cumulativeDataBySeriesIndex = new Map();
|
|
839
|
+
const positiveCumulativeDataBySeriesIndex = new Map();
|
|
840
|
+
const negativeCumulativeDataBySeriesIndex = new Map();
|
|
549
841
|
const rawValuesBySeriesIndex = new Map();
|
|
550
842
|
const totalData = new Map();
|
|
843
|
+
const positiveTotalData = new Map();
|
|
844
|
+
const negativeTotalData = new Map();
|
|
551
845
|
data.forEach((dataPoint) => {
|
|
552
846
|
const categoryKey = String(dataPoint[xKey]);
|
|
553
847
|
let total = 0;
|
|
848
|
+
let positiveTotal = 0;
|
|
849
|
+
let negativeTotal = 0;
|
|
554
850
|
barSeries.forEach((series, seriesIndex) => {
|
|
555
|
-
const
|
|
851
|
+
const rawValue = this.parseValue(dataPoint[series.dataKey]);
|
|
852
|
+
const value = series.getRenderedValue(rawValue, this.orientation);
|
|
556
853
|
if (Number.isFinite(value)) {
|
|
557
854
|
total += value;
|
|
855
|
+
if (value > 0) {
|
|
856
|
+
positiveTotal += value;
|
|
857
|
+
}
|
|
858
|
+
else if (value < 0) {
|
|
859
|
+
negativeTotal += Math.abs(value);
|
|
860
|
+
}
|
|
558
861
|
}
|
|
559
862
|
// Build per-series raw value maps (used for layer next-layer data)
|
|
560
863
|
let rawMap = rawValuesBySeriesIndex.get(seriesIndex);
|
|
@@ -567,25 +870,45 @@ export class XYChart extends BaseChart {
|
|
|
567
870
|
}
|
|
568
871
|
});
|
|
569
872
|
totalData.set(categoryKey, total);
|
|
873
|
+
positiveTotalData.set(categoryKey, positiveTotal);
|
|
874
|
+
negativeTotalData.set(categoryKey, negativeTotal);
|
|
570
875
|
});
|
|
571
876
|
barSeries.forEach((_, seriesIndex) => {
|
|
572
877
|
const cumulativeForSeries = new Map();
|
|
878
|
+
const positiveCumulativeForSeries = new Map();
|
|
879
|
+
const negativeCumulativeForSeries = new Map();
|
|
573
880
|
data.forEach((dataPoint) => {
|
|
574
881
|
const categoryKey = String(dataPoint[xKey]);
|
|
575
882
|
let cumulative = 0;
|
|
883
|
+
let positiveCumulative = 0;
|
|
884
|
+
let negativeCumulative = 0;
|
|
576
885
|
for (let i = 0; i < seriesIndex; i++) {
|
|
577
|
-
const value = this.parseValue(dataPoint[barSeries[i].dataKey]);
|
|
886
|
+
const value = barSeries[i].getRenderedValue(this.parseValue(dataPoint[barSeries[i].dataKey]), this.orientation);
|
|
578
887
|
if (Number.isFinite(value)) {
|
|
579
888
|
cumulative += value;
|
|
889
|
+
if (value > 0) {
|
|
890
|
+
positiveCumulative += value;
|
|
891
|
+
}
|
|
892
|
+
else if (value < 0) {
|
|
893
|
+
negativeCumulative += Math.abs(value);
|
|
894
|
+
}
|
|
580
895
|
}
|
|
581
896
|
}
|
|
582
897
|
cumulativeForSeries.set(categoryKey, cumulative);
|
|
898
|
+
positiveCumulativeForSeries.set(categoryKey, positiveCumulative);
|
|
899
|
+
negativeCumulativeForSeries.set(categoryKey, negativeCumulative);
|
|
583
900
|
});
|
|
584
901
|
cumulativeDataBySeriesIndex.set(seriesIndex, cumulativeForSeries);
|
|
902
|
+
positiveCumulativeDataBySeriesIndex.set(seriesIndex, positiveCumulativeForSeries);
|
|
903
|
+
negativeCumulativeDataBySeriesIndex.set(seriesIndex, negativeCumulativeForSeries);
|
|
585
904
|
});
|
|
586
905
|
return {
|
|
587
906
|
cumulativeDataBySeriesIndex,
|
|
907
|
+
positiveCumulativeDataBySeriesIndex,
|
|
908
|
+
negativeCumulativeDataBySeriesIndex,
|
|
588
909
|
totalData,
|
|
910
|
+
positiveTotalData,
|
|
911
|
+
negativeTotalData,
|
|
589
912
|
rawValuesBySeriesIndex,
|
|
590
913
|
};
|
|
591
914
|
}
|