@datarailsshared/dr_renderer 1.3.59 → 1.4.5

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.
@@ -30,7 +30,7 @@ function setInitialPointStyles(opts, series) {
30
30
  const isSelected =
31
31
  item && opts.selectedPoint &&
32
32
  item.initialName === opts.selectedPoint.initialName &&
33
- item.y.toFixed(2) === opts.selectedPoint.y.toFixed(2);
33
+ item.y != null && item.y.toFixed(2) === opts.selectedPoint.y != null && opts.selectedPoint.y.toFixed(2);
34
34
  item = Object.assign(item, getSeriesPointStyles(isSelected));
35
35
  });
36
36
  }
@@ -5,37 +5,40 @@ const DR_SCENARIO = {
5
5
  Forecast: 'Forecast',
6
6
  };
7
7
 
8
- function createSingleDataSeriesForForecast(chart_series, chartOptions, pivotData) {
8
+ function createSingleDataSeriesForForecast(chart_series, chartOptions, pivotData, isChartCombiLine) {
9
+ if (!chartOptions || !chartOptions.chart) return null;
10
+
9
11
  const { actuals, forecast, smart_query } = chartOptions.chart;
10
12
  const input = pivotData.input;
11
13
 
12
- const hasSQActuals = input.some(item => item.Scenario && item.Scenario.indexOf(DR_SCENARIO.SQ_Actuals) !== -1);
14
+ const hasSQActuals = lodash.some(input, item => lodash.includes(item.Scenario, DR_SCENARIO.SQ_Actuals));
13
15
  chartOptions.isSmartQueriesEnabled = hasSQActuals;
14
16
  if (!smart_query || !hasSQActuals) return null;
15
17
 
16
18
  const midMonthOffset = 0.5
17
- return chart_series.length === 1
18
- ? buildChartSeriesFromPivotInputOnly(input, actuals, forecast, midMonthOffset)
19
- : buildChartSeriesFromSeries(chart_series, actuals, forecast, midMonthOffset);
19
+ return chart_series.length === 1
20
+ ? buildChartSeriesFromPivotInputOnly(input, actuals, forecast, midMonthOffset, chart_series[0].name)
21
+ : (isChartCombiLine ?
22
+ buildChartSeriesForMultipleSeriesCombinedLine(chart_series, input, actuals, forecast, midMonthOffset) :
23
+ buildChartSeriesFromSeries(chart_series, actuals, forecast, midMonthOffset));
20
24
  }
21
25
 
22
- function buildChartSeriesFromPivotInputOnly(input, actuals, forecast, midMonthOffset) {
26
+ function buildChartSeriesFromPivotInputOnly(input, actuals, forecast, midMonthOffset, name) {
23
27
  const filtered = input.filter(item =>
24
- (item.Scenario === DR_SCENARIO.SQ_Actuals || item.Scenario === DR_SCENARIO.Forecast) &&
25
- item.Amount !== 0
28
+ (item.Scenario === DR_SCENARIO.SQ_Actuals || item.Scenario === DR_SCENARIO.Forecast) && !!item['Reporting Month']
26
29
  );
27
30
 
28
- const data = filtered.map(item => ({
31
+ const data = lodash.map(filtered, item => ({
29
32
  y: item.Amount,
30
- name: item.Reporting_Month,
31
- initialName: item.Reporting_Month,
33
+ name: item['Reporting Month'],
34
+ initialName: item['Reporting Month'],
32
35
  type: item.Scenario,
33
- })).sort((a, b) => new Date(a.name) - new Date(b.name));
36
+ })).sort((a, b) => sortRowValuesByName(a, b));
34
37
 
35
- const sqCount = input.filter(item => item.Scenario === DR_SCENARIO.SQ_Actuals).length;
38
+ const sqCount = lodash.filter(input, item => item.Scenario === DR_SCENARIO.SQ_Actuals).length;
36
39
 
37
40
  return {
38
- name: "Forecast Smart Query",
41
+ name,
39
42
  data,
40
43
  zoneAxis: "x",
41
44
  zones: [
@@ -45,6 +48,51 @@ function buildChartSeriesFromPivotInputOnly(input, actuals, forecast, midMonthOf
45
48
  };
46
49
  }
47
50
 
51
+ function buildChartSeriesForMultipleSeriesCombinedLine(chart_series, input, actuals, forecast, midMonthOffset) {
52
+ const resultingSeries = [];
53
+ for (let i = 0; i < chart_series.length; i++) {
54
+ const series = chart_series[i];
55
+ if (!series || !series.data || !series.data.length) continue;
56
+
57
+ const data = lodash.chain(input)
58
+ .filter(item => item['Scenario Cycle'] === series.name &&
59
+ (item['Scenario'] === DR_SCENARIO.SQ_Actuals || item['Scenario'] === DR_SCENARIO.Forecast) && !!item['Reporting Month'])
60
+ .map(item => ({
61
+ y: item.Amount,
62
+ name: item['Reporting Month'],
63
+ initialName: item['Reporting Month'],
64
+ type: item.Scenario,
65
+ }))
66
+ .value().sort((a, b) => {
67
+ return sortRowValuesByName(a, b);
68
+ });
69
+ if (!data.length) {
70
+ resultingSeries.push(series);
71
+ continue;
72
+ }
73
+
74
+ const sqCount = lodash.filter(data, item => item.type === DR_SCENARIO.SQ_Actuals).length;
75
+ resultingSeries.push(
76
+ {
77
+ name: series.name,
78
+ data,
79
+ zoneAxis: "x",
80
+ zones: [
81
+ { value: sqCount - midMonthOffset, dashStyle: actuals },
82
+ { dashStyle: forecast },
83
+ ],
84
+ }
85
+ )
86
+ }
87
+ return resultingSeries;
88
+ }
89
+
90
+ function sortRowValuesByName(rowValueA, rowValueB) {
91
+ const aDate = new Date(rowValueA.name);
92
+ const bDate = new Date(rowValueB.name);
93
+ return !isNaN(aDate.getTime()) && !isNaN(bDate.getTime()) ? aDate - bDate : rowValueA.name.localeCompare(rowValueB.name);
94
+ }
95
+
48
96
  function buildChartSeriesFromSeries(chart_series, actuals, forecast, midMonthOffset) {
49
97
  const seriesA = lodash.find(chart_series, s => s.name && lodash.includes(s.name, DR_SCENARIO.SQ_Actuals));
50
98
  const seriesB = lodash.find(chart_series, s => s.name && lodash.includes(s.name, DR_SCENARIO.Forecast));
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Enum for dr renderer error codes.
3
+ */
4
+ export type RendererErrorCodes = number;
5
+ export namespace RendererErrorCodes {
6
+ let NoDataError: number;
7
+ let TooMuchDataError: number;
8
+ let DataConflictError: number;
9
+ let GaugeConfigurationError: number;
10
+ let GenericRenderingError: number;
11
+ let GenericComputationalError: number;
12
+ }
13
+ /**
14
+ * Base error class for all renderer-related errors.
15
+ * @class BaseRendererError
16
+ * @extends Error
17
+ */
18
+ export class BaseRendererError extends Error {
19
+ /**
20
+ * Creates a new BaseRendererError instance.
21
+ * @param {Object} config - Error configuration object
22
+ * @param {number} config.code - Unique error code
23
+ * @param {string} config.title - Error title/message
24
+ * @param {Object} [config.options={}] - Additional error options or context
25
+ */
26
+ constructor({ code, title, options }: {
27
+ code: number;
28
+ title: string;
29
+ options?: Object | undefined;
30
+ });
31
+ /**
32
+ * Error code for identification purposes
33
+ * @type {number}
34
+ */
35
+ code: number;
36
+ /**
37
+ * Human-readable error title
38
+ * @type {string}
39
+ */
40
+ title: string;
41
+ /**
42
+ * Additional options or context for the error
43
+ * @type {Object}
44
+ */
45
+ options: Object;
46
+ }
47
+ /**
48
+ * Error thrown when there is too much data to render efficiently.
49
+ * @class TooMuchDataError
50
+ * @extends BaseRendererError
51
+ */
52
+ export class TooMuchDataError extends BaseRendererError {
53
+ /**
54
+ * Creates a new TooMuchDataError instance.
55
+ * This error is thrown when the dataset exceeds the renderer's capacity
56
+ * and requires the user to edit the widget.
57
+ */
58
+ constructor();
59
+ }
60
+ /**
61
+ * Error thrown when no data is available.
62
+ * @class NoDataError
63
+ * @extends BaseRendererError
64
+ */
65
+ export class NoDataError extends BaseRendererError {
66
+ /**
67
+ * Creates a new NoDataError instance.
68
+ * This error is thrown when a widget or component has no available data.
69
+ */
70
+ constructor();
71
+ }
72
+ /**
73
+ * Error thrown when there are conflicts in the data being processed.
74
+ * @class DataConflictError
75
+ * @extends BaseRendererError
76
+ */
77
+ export class DataConflictError extends BaseRendererError {
78
+ /**
79
+ * Creates a new DataConflictError instance.
80
+ * @param {Object} [options] - Additional context about the data conflict.
81
+ */
82
+ constructor(options?: Object);
83
+ }
84
+ /**
85
+ * Error thrown when a gauge chart is missing required info.
86
+ * @class GaugeConfigurationError
87
+ * @extends BaseRendererError
88
+ */
89
+ export class GaugeConfigurationError extends BaseRendererError {
90
+ /**
91
+ * Creates a new GaugeConfigurationError instance.
92
+ */
93
+ constructor();
94
+ }
95
+ /**
96
+ * Generic error for rendering failures in PivotTable components.
97
+ * @class GenericRenderingError
98
+ * @extends BaseRendererError
99
+ */
100
+ export class GenericRenderingError extends BaseRendererError {
101
+ /**
102
+ * Creates a new GenericRenderingError instance.
103
+ * This error is thrown when an unexpected error occurs during
104
+ * the rendering process of PivotTable results.
105
+ */
106
+ constructor();
107
+ }
108
+ /**
109
+ * Generic error for computational failures in PivotTable components.
110
+ * @class GenericComputationalError
111
+ * @extends BaseRendererError
112
+ */
113
+ export class GenericComputationalError extends BaseRendererError {
114
+ /**
115
+ * Creates a new GenericComputationalError instance.
116
+ * This error is thrown when an unexpected error occurs during
117
+ * the computation process of PivotTable results.
118
+ */
119
+ constructor();
120
+ }
@@ -0,0 +1,2 @@
1
+ export * from './graph-table-renderer';
2
+ export * from './errors'
@@ -0,0 +1,41 @@
1
+ const lodash = require('lodash');
2
+
3
+ function getAggregatorPercentageValueIfRequired(value, render_options, data, rowKey, colKey) {
4
+ const deltaColumn = lodash.get(render_options, 'chartOptions.delta_column', null);
5
+ const isPercentage = lodash.get(render_options, 'comboOptions.secondaryAxisSettings.is_percentage', false) || deltaColumn && deltaColumn.is_percentage;
6
+ const currentRowName = rowKey && rowKey.length ? String(rowKey[0]) : '';
7
+ const isVariance = deltaColumn && currentRowName.replace('_', '').toLowerCase() === deltaColumn.name.replace('_', '').toLowerCase();
8
+ const baseRowKey = data && data.rowKeys && data.rowKeys.length ? data.rowKeys[0] : null;
9
+ const currentColKey = colKey ? colKey : [];
10
+ const agg = data && baseRowKey ? data.getAggregator(baseRowKey, currentColKey) : null;
11
+
12
+ if (isPercentage && isVariance && baseRowKey && agg) {
13
+ if (deltaColumn && isAbsoluteValue(deltaColumn.formula)) {
14
+ value = getRelatedValue(value, agg.value());
15
+ }
16
+
17
+ return Math.round(value * 100) + '%';
18
+ }
19
+
20
+ return null;
21
+ };
22
+
23
+ function getRelatedValue(value, baseValue) {
24
+ if (!baseValue)
25
+ return value < 0 ? -1 : 1;
26
+
27
+ return value / baseValue;
28
+ };
29
+
30
+ function isAbsoluteValue(formula) {
31
+ if (!formula)
32
+ return false;
33
+
34
+ return !lodash.includes(formula.replace(/\s+/g, ''), '/');
35
+ };
36
+
37
+ module.exports = {
38
+ getAggregatorPercentageValueIfRequired,
39
+ getRelatedValue,
40
+ isAbsoluteValue
41
+ };
@@ -73,6 +73,39 @@ describe('dr-renderer-helpers', () => {
73
73
  expect(drRendererHelpers.mergeDeep({ a: 1 }, 'string')).toEqual({ a: 1 });
74
74
  });
75
75
 
76
+ it('does not mutate sources', () => {
77
+ const src1 = { a: { x: 1 }, arr: [1,2] };
78
+ const src2 = { a: { y: 2 }, arr: [3] };
79
+ const res = drRendererHelpers.mergeDeep({}, src1, src2);
80
+
81
+ res.a.x = 42;
82
+ res.arr.push(99);
83
+
84
+ expect(src1).toEqual({ a: { x: 1 }, arr: [1,2] });
85
+ expect(src2).toEqual({ a: { y: 2 }, arr: [3] });
86
+ });
87
+
88
+ it('replaces arrays by clone, not by reference', () => {
89
+ const src = { a: [1,2,3] };
90
+ const res = drRendererHelpers.mergeDeep({}, src);
91
+ expect(res.a).not.toBe(src.a);
92
+ });
93
+
94
+ it('ignores non-plain objects', () => {
95
+ const d = new Date();
96
+ const res = drRendererHelpers.mergeDeep({}, { when: d });
97
+ expect(res.when).toBe(d);
98
+ });
99
+
100
+ it('last source wins (and arrays are replaced)', () => {
101
+ const res = drRendererHelpers.mergeDeep(
102
+ { a: { x: 1 }, arr: [1] },
103
+ { a: { y: 2 }, arr: [2] },
104
+ { a: { z: 3 }, arr: [3] },
105
+ );
106
+ expect(res).toEqual({ a: { x:1, y:2, z:3 }, arr: [3] });
107
+ });
108
+
76
109
  it('should not merge arrays and replace instead', () => {
77
110
  expect(drRendererHelpers.mergeDeep({
78
111
  a: [1, 2, 3]
@@ -23,6 +23,7 @@ DrGaugeChart.highchartsRenderer = {
23
23
  }),
24
24
  disableChartAnimations: jest.fn((value) => disableChartAnimation = value),
25
25
  chartAnimationsDisabled: jest.fn(() => disableChartAnimation),
26
+ hasFeature: jest.fn().mockReturnValue(false),
26
27
  };
27
28
 
28
29
  const mockAggregationValue = 1000;
@@ -309,6 +310,93 @@ describe("DrGaugeChart", () => {
309
310
  },
310
311
  ]);
311
312
  });
313
+
314
+ it("scales by needle magnitude when goal is 0 in percentage mode", () => {
315
+ chart.value = 250;
316
+ expect(
317
+ chart.createPlotBands({
318
+ isAbsoluteValue: false,
319
+ gauge: { thickness: 10 },
320
+ goal: { value: 0 },
321
+ segments: [
322
+ { from: 0, to: 50, color: "red", title: "Title 1" },
323
+ { from: 50, to: 100, color: "blue", title: "Title 2" },
324
+ ],
325
+ })
326
+ ).toEqual([
327
+ { from: 0, to: 125, color: "red", thickness: 10, title: "Title 1" },
328
+ { from: 125, to: 250, color: "blue", thickness: 10, title: "Title 2" },
329
+ ]);
330
+ });
331
+
332
+ it("scales by absolute needle magnitude when goal is not a number", () => {
333
+ const featureSpy = jest
334
+ .spyOn(DrGaugeChart.highchartsRenderer, "hasFeature")
335
+ .mockReturnValue(true);
336
+
337
+ chart.value = -300;
338
+ const res = chart.createPlotBands({
339
+ isAbsoluteValue: false,
340
+ gauge: { thickness: 8 },
341
+ goal: { value: undefined },
342
+ segments: [
343
+ { from: 0, to: 50, color: "#1", title: "A" },
344
+ { from: 50, to: 100, color: "#2", title: "B" },
345
+ ],
346
+ });
347
+
348
+ expect(res).toEqual([
349
+ { from: 0, to: 150, color: "#1", thickness: 8, title: "A" },
350
+ { from: 150, to: 300, color: "#2", thickness: 8, title: "B" },
351
+ ]);
352
+
353
+ featureSpy.mockRestore();
354
+ });
355
+
356
+ it("normalizes bands when goal is negative in percentage mode", () => {
357
+ const res = chart.createPlotBands({
358
+ isAbsoluteValue: false,
359
+ gauge: { thickness: 12 },
360
+ goal: { value: -1000 },
361
+ segments: [
362
+ { from: 0, to: 50, color: "r", title: "S1" },
363
+ { from: 50, to: 100, color: "b", title: "S2" },
364
+ ],
365
+ });
366
+
367
+ expect(res[0].from).toBe(-500);
368
+ expect(Math.abs(res[0].to)).toBe(0);
369
+ expect(res[0].color).toBe("r");
370
+ expect(res[0].thickness).toBe(12);
371
+ expect(res[0].title).toBe("S1");
372
+
373
+ expect(res[1]).toEqual({ from: -1000, to: -500, color: "b", thickness: 12, title: "S2" });
374
+ });
375
+
376
+ it("does not clamp last segment when dynamic goal feature is enabled", () => {
377
+ const featureSpy = jest
378
+ .spyOn(DrGaugeChart.highchartsRenderer, "hasFeature")
379
+ .mockReturnValue(true);
380
+
381
+ const res = chart.createPlotBands({
382
+ isAbsoluteValue: true,
383
+ gauge: { thickness: 10 },
384
+ goal: { value: 1800 },
385
+ segments: [
386
+ { from: 100, to: 200, color: "red", title: "Title 1" },
387
+ { from: 200, to: 400, color: "blue", title: "Title 2" },
388
+ { from: 400, to: 800, color: "green", title: "Title 3" },
389
+ ],
390
+ });
391
+
392
+ expect(res).toEqual([
393
+ { from: 100, to: 200, color: "red", thickness: 10, title: "Title 1" },
394
+ { from: 200, to: 400, color: "blue", thickness: 10, title: "Title 2" },
395
+ { from: 400, to: 800, color: "green", thickness: 10, title: "Title 3" },
396
+ ]);
397
+
398
+ featureSpy.mockRestore();
399
+ });
312
400
  });
313
401
 
314
402
  describe("createTicks", () => {
@@ -0,0 +1,157 @@
1
+ const {
2
+ BaseRendererError,
3
+ NoDataError,
4
+ TooMuchDataError,
5
+ DataConflictError,
6
+ GaugeConfigurationError,
7
+ GenericRenderingError,
8
+ GenericComputationalError
9
+ } = require('../src/errors');
10
+
11
+ describe('Error Classes', () => {
12
+ describe('BaseRendererError', () => {
13
+ it('should create instance with provided code and title', () => {
14
+ const error = new BaseRendererError({ code: 5, title: 'Test Error' });
15
+
16
+ expect(error.code).toBe(5);
17
+ expect(error.title).toBe('Test Error');
18
+ });
19
+
20
+ it('should create instance with options parameter', () => {
21
+ const options = { isBreakdown: true, minCategories: 5 };
22
+ const error = new BaseRendererError({ code: 5, title: 'Test Error', options });
23
+
24
+ expect(error.code).toBe(5);
25
+ expect(error.title).toBe('Test Error');
26
+ expect(error.options).toEqual(options);
27
+ });
28
+
29
+ it('should set empty options object when no options provided', () => {
30
+ const error = new BaseRendererError({ code: 1, title: 'Test' });
31
+
32
+ expect(error.options).toEqual({});
33
+ });
34
+
35
+ it('should be instance of BaseRendererError', () => {
36
+ const error = new BaseRendererError({ code: 1, title: 'Test' });
37
+ expect(error).toBeInstanceOf(BaseRendererError);
38
+ });
39
+
40
+ it('should be instance of Error', () => {
41
+ const error = new BaseRendererError({ code: 1, title: 'Test' });
42
+ expect(error).toBeInstanceOf(Error);
43
+ });
44
+ });
45
+
46
+ describe('NoDataError', () => {
47
+ it('should create instance with correct code and title', () => {
48
+ const error = new NoDataError();
49
+
50
+ expect(error.code).toBe(1);
51
+ expect(error.title).toBe('No Data Available');
52
+ });
53
+
54
+ it('should be instance of BaseRendererError', () => {
55
+ const error = new NoDataError();
56
+ expect(error).toBeInstanceOf(BaseRendererError);
57
+ });
58
+ });
59
+
60
+ describe('TooMuchDataError', () => {
61
+ it('should create instance with correct code and title', () => {
62
+ const error = new TooMuchDataError();
63
+
64
+ expect(error.code).toBe(3);
65
+ expect(error.title).toBe('There is too much data. Please edit this widget');
66
+ });
67
+
68
+ it('should be instance of BaseRendererError', () => {
69
+ const error = new TooMuchDataError();
70
+ expect(error).toBeInstanceOf(BaseRendererError);
71
+ });
72
+ });
73
+
74
+ describe('DataConflictError', () => {
75
+ it('should create instance with correct code and title', () => {
76
+ const error = new DataConflictError();
77
+
78
+ expect(error.code).toBe(5);
79
+ expect(error.title).toBe('Data Conflict');
80
+ });
81
+
82
+ it('should create instance with options parameter', () => {
83
+ const options = {
84
+ isBreakdown: true,
85
+ uniqueCategories: ['A', 'B'],
86
+ minCategories: 5,
87
+ maxCategories: 10
88
+ };
89
+ const error = new DataConflictError(options);
90
+
91
+ expect(error.code).toBe(5);
92
+ expect(error.title).toBe('Data Conflict');
93
+ expect(error.options).toEqual(options);
94
+ });
95
+
96
+ it('should create instance with empty options when none provided', () => {
97
+ const error = new DataConflictError();
98
+
99
+ expect(error.options).toEqual({});
100
+ });
101
+ });
102
+
103
+ describe('GaugeConfigurationError', () => {
104
+ it('should create instance with correct code and title', () => {
105
+ const error = new GaugeConfigurationError();
106
+
107
+ expect(error.code).toBe(6);
108
+ expect(error.title).toBe('Please configure goal and needle');
109
+ });
110
+
111
+ it('should create instance with empty options', () => {
112
+ const error = new GaugeConfigurationError();
113
+
114
+ expect(error.options).toEqual({});
115
+ });
116
+ });
117
+
118
+ describe('GenericRenderingError', () => {
119
+ it('should create instance with correct code and title', () => {
120
+ const error = new GenericRenderingError();
121
+
122
+ expect(error.code).toBe(7);
123
+ expect(error.title).toBe('An error occurred rendering the PivotTable results.');
124
+ });
125
+
126
+ it('should create instance with empty options', () => {
127
+ const error = new GenericRenderingError();
128
+
129
+ expect(error.options).toEqual({});
130
+ });
131
+
132
+ it('should be instance of BaseRendererError', () => {
133
+ const error = new GenericRenderingError();
134
+ expect(error).toBeInstanceOf(BaseRendererError);
135
+ });
136
+ });
137
+
138
+ describe('GenericComputationalError', () => {
139
+ it('should create instance with correct code and title', () => {
140
+ const error = new GenericComputationalError();
141
+
142
+ expect(error.code).toBe(8);
143
+ expect(error.title).toBe('An error occurred computing the PivotTable results.');
144
+ });
145
+
146
+ it('should create instance with empty options', () => {
147
+ const error = new GenericComputationalError();
148
+
149
+ expect(error.options).toEqual({});
150
+ });
151
+
152
+ it('should be instance of BaseRendererError', () => {
153
+ const error = new GenericComputationalError();
154
+ expect(error).toBeInstanceOf(BaseRendererError);
155
+ });
156
+ });
157
+ });