@datarailsshared/dr_renderer 1.4.46 → 1.4.52

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 CHANGED
@@ -21,3 +21,4 @@ To be able to compile types with `npm run build:types` you need to install types
21
21
 
22
22
  ## Publish to npm
23
23
  Just merge to prod branch
24
+ ###
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarailsshared/dr_renderer",
3
- "version": "1.4.46",
3
+ "version": "1.4.52",
4
4
  "description": "DataRails charts and tables renderer",
5
5
  "keywords": [
6
6
  "datarails",
@@ -89,10 +89,15 @@ function DrGaugeChart(pivotData, opts, isDynamicGoal) {
89
89
  }
90
90
 
91
91
  this.mergeOptions = function (options) {
92
+ const clone = (o) =>
93
+ o && typeof o === 'object'
94
+ ? (globalThis.structuredClone ? structuredClone(o) : JSON.parse(JSON.stringify(o)))
95
+ : o;
96
+
92
97
  return helpers.mergeDeep(
93
- JSON.parse(JSON.stringify(GAUGE_OPTIONS_DEFAULT)),
94
- this.getDefaultValueForChart(DrGaugeChart.highchartsRenderer.CHART_TYPES.GAUGE_CHART_ENHANCED),
95
- options
98
+ clone(GAUGE_OPTIONS_DEFAULT),
99
+ clone(this.getDefaultValueForChart(DrGaugeChart.highchartsRenderer.CHART_TYPES.GAUGE_CHART_ENHANCED)),
100
+ clone(options || {})
96
101
  );
97
102
  };
98
103
 
@@ -246,45 +251,75 @@ function DrGaugeChart(pivotData, opts, isDynamicGoal) {
246
251
  };
247
252
 
248
253
  this.getPaneDimensions = function (chart, options) {
249
- const { renderer } = chart;
250
- const valueLabel = this.createValueLabel(chart, this.options);
254
+ const valueLabel = this.createValueLabel(chart, options);
255
+ const { height: labelH } = valueLabel.getBBox ? valueLabel.getBBox() : { height: 0 };
256
+ valueLabel.destroy && valueLabel.destroy();
257
+
251
258
  const { offset } = options.gauge;
252
- const { height: labelH } = valueLabel.getBBox();
259
+ const offsetTop = Array.isArray(offset) ? offset[0] : 0;
260
+ const offsetRight = Array.isArray(offset) ? offset[1] : 0;
261
+ const offsetBottomG= Array.isArray(offset) ? offset[2] : 0;
262
+ const offsetLeft = Array.isArray(offset) ? offset[3] : 0;
263
+
264
+ const vOff = options.gauge.valueOffset || [0,0,0,0];
265
+
266
+ const offsetBottom = (labelH || 0) + (vOff[0] || 0) + (vOff[2] || 0) + (offsetBottomG || 0);
267
+
268
+ const safeDiv = (num, den) => {
269
+ const v = num / den;
270
+ return Number.isFinite(v) ? v : Infinity;
271
+ };
253
272
 
254
- const offsetBottom = labelH + options.gauge.valueOffset[0] + options.gauge.valueOffset[2] + offset[2];
255
- valueLabel.destroy();
273
+ const radiuses = [
274
+ chart.chartWidth / 2 - Math.max(offsetRight || 0, offsetLeft || 0),
275
+ chart.chartHeight - offsetBottom - (offsetTop || 0),
276
+ ];
256
277
 
257
- const radiuses = [chart.chartWidth / 2 - Math.max(offset[1], offset[3]), chart.chartHeight - offsetBottom - offset[0]];
258
278
  if (options.label.show) {
259
279
  this.ticks.forEach((tick) => {
260
- const label = renderer.label(this.formatTickLabel(tick, options), 0, 0, null, null, null, true).add();
280
+ const label = chart.renderer
281
+ .label(this.formatTickLabel(tick, options), 0, 0, null, null, null, true)
282
+ .add();
283
+
261
284
  const angle = this.getAngleForValue(tick);
262
- // depends on label width
263
- radiuses.push(
264
- (chart.chartWidth / 2 - label.bBox.width - Math.max(offset[1], offset[3])) /
265
- Math.sin(Math.abs(Math.PI / 2 - angle))
266
- );
267
- // depends on label height
285
+ const s = Math.sin(Math.abs(Math.PI / 2 - angle));
286
+ const c = Math.cos(Math.abs(Math.PI / 2 - angle));
287
+
268
288
  radiuses.push(
269
- (chart.chartHeight - offsetBottom - label.bBox.height / 2 - offset[0]) /
270
- Math.cos(Math.abs(Math.PI / 2 - angle))
289
+ safeDiv(
290
+ chart.chartWidth / 2 - label.bBox.width - Math.max(offsetRight || 0, offsetLeft || 0),
291
+ s
292
+ ),
293
+ safeDiv(
294
+ chart.chartHeight - offsetBottom - label.bBox.height / 2 - (offsetTop || 0),
295
+ c
296
+ )
271
297
  );
272
- label.destroy();
298
+
299
+ label.destroy && label.destroy();
273
300
  });
274
301
  } else {
275
- // reserve space for the goal icon
276
302
  const angle = this.getAngleForValue(options.goal.value);
277
- const [iconW, iconH] = options.gauge.goalIconSize;
278
- radiuses.push((chart.chartWidth / 2 - iconW / 2 - offset[1]) / Math.sin(Math.abs(Math.PI / 2 - angle)));
279
- radiuses.push((chart.chartHeight - offsetBottom - iconH / 2 - offset[0]) / Math.cos(Math.abs(Math.PI / 2 - angle)));
303
+ const [iconW, iconH] = options.gauge.goalIconSize || [0, 0];
304
+ const s = Math.sin(Math.abs(Math.PI / 2 - angle));
305
+ const c = Math.cos(Math.abs(Math.PI / 2 - angle));
306
+
307
+ radiuses.push(
308
+ safeDiv(chart.chartWidth / 2 - iconW / 2 - (offsetRight || 0), s),
309
+ safeDiv(chart.chartHeight - offsetBottom - iconH / 2 - (offsetTop || 0), c)
310
+ );
280
311
  }
281
312
 
313
+ const finite = radiuses.filter(Number.isFinite);
314
+ const radius = finite.length ? Math.min(...finite) : 0;
315
+
282
316
  return {
283
- radius: Math.min(...radiuses),
317
+ radius,
284
318
  center: [chart.chartWidth / 2, chart.chartHeight - offsetBottom],
285
319
  };
286
320
  };
287
321
 
322
+
288
323
  this.setTicksStyles = function (chart, options) {
289
324
  const ticks = chart.yAxis[0].ticks;
290
325
  Object.keys(ticks).forEach((i) => {
@@ -398,14 +433,26 @@ function DrGaugeChart(pivotData, opts, isDynamicGoal) {
398
433
 
399
434
  this.setPane = function (chart, options) {
400
435
  const { radius, center } = this.getPaneDimensions(chart, options);
401
- chart.pane[0].options.size = 2 * radius;
436
+
437
+ const MIN_RADIUS = Math.max(8, options.gauge.thickness / 2 + 1);
438
+ const R = Math.max(MIN_RADIUS, radius);
439
+
440
+ chart.pane[0].options.size = 2 * R;
402
441
  chart.pane[0].options.center = center;
442
+
443
+ const effectiveTick = Math.min(options.gauge.tickLength, Math.floor(R * 0.8));
444
+ chart.yAxis[0].options.tickLength = effectiveTick;
445
+
403
446
  chart.yAxis[0].options.plotBands.forEach((band) => {
404
- band.outerRadius = radius - (options.gauge.tickLength - options.gauge.thickness) / 2;
447
+ band.outerRadius = Math.max(1, R - (effectiveTick - options.gauge.thickness) / 2);
405
448
  });
406
- chart.series[0].options.dial.radius = Math.round((100 * (radius - options.gauge.tickLength - 10)) / radius) + "%";
449
+
450
+ chart.series[0].options.dial.radius = R > 0
451
+ ? Math.round((100 * (R - effectiveTick - 10)) / R) + "%"
452
+ : "0%";
407
453
  };
408
454
 
455
+
409
456
  this.setCustomElements = function (chart, options) {
410
457
  chart.label = this.createValueLabel(chart, options);
411
458
  chart.startBorder = DrGaugeChart.createBorder(chart, options, "start");
@@ -18,25 +18,32 @@ function isNumber(n) {
18
18
  }
19
19
 
20
20
  function mergeDeep(target, ...sources) {
21
- const isObject = (obj) => obj && typeof obj === 'object' && !Array.isArray(obj);
21
+ const deepClone = (val) => {
22
+ if (Array.isArray(val)) return val.map(deepClone);
23
+ if (_.isPlainObject(val)) {
24
+ const out = {};
25
+ for (const k of Object.keys(val)) out[k] = deepClone(val[k]);
26
+ return out;
27
+ }
28
+ return val;
29
+ };
22
30
 
23
- if (!isObject(target)) return target;
31
+ if (!_.isPlainObject(target)) return target;
24
32
 
25
- sources.forEach((source) => {
26
- if (!isObject(source)) return;
33
+ for (const source of sources) {
34
+ if (!_.isPlainObject(source)) continue;
27
35
 
28
- Object.keys(source).forEach((key) => {
29
- const targetValue = target[key];
30
- const sourceValue = source[key];
36
+ for (const key of Object.keys(source)) {
37
+ const tVal = target[key];
38
+ const sVal = source[key];
31
39
 
32
- if (isObject(targetValue) && isObject(sourceValue)) {
33
- target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue);
40
+ if (_.isPlainObject(tVal) && _.isPlainObject(sVal)) {
41
+ target[key] = mergeDeep({}, tVal, sVal);
34
42
  } else {
35
- target[key] = sourceValue;
43
+ target[key] = deepClone(sVal);
36
44
  }
37
- });
38
- });
39
-
45
+ }
46
+ }
40
47
  return target;
41
48
  }
42
49
 
@@ -6017,21 +6017,30 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
6017
6017
  ];
6018
6018
 
6019
6019
  highchartsRenderer.getDefaultValueForChart = function (type, existing_options) {
6020
- const chartOpt = type === highchartsRenderer.richTextSubType.type
6020
+ const chartOpt = (type === highchartsRenderer.richTextSubType.type)
6021
6021
  ? highchartsRenderer.richTextSubType
6022
6022
  : highchartsRenderer.getChartOptionsBySubType(type);
6023
- let valToReturn = {};
6024
- if (chartOpt) {
6025
- lodash.forEach(chartOpt.suboptions, (suboption) => {
6026
- valToReturn[suboption.category_type] = highchartsRenderer.getDefaultValueForSubOptions(suboption, existing_options, type);
6027
- });
6028
- }
6029
6023
 
6030
- if (chartOpt.hasOwnProperty('default_options') && chartOpt.default_options) {
6031
- valToReturn = lodash.extend(valToReturn, chartOpt.default_options);
6032
- }
6024
+ if (!chartOpt) return {};
6033
6025
 
6034
- return valToReturn;
6026
+ const clone = (v) => (globalThis.structuredClone ? structuredClone(v) : lodash.cloneDeep(v));
6027
+
6028
+ const subValues = {};
6029
+ lodash.forEach(chartOpt.suboptions, (suboption) => {
6030
+ subValues[suboption.category_type] = clone(
6031
+ highchartsRenderer.getDefaultValueForSubOptions(suboption, existing_options, type)
6032
+ );
6033
+ });
6034
+
6035
+ const baseDefaults = clone(chartOpt.default_options || {});
6036
+
6037
+ const overwriteArrays = (objValue, srcValue) => {
6038
+ if (Array.isArray(objValue) || Array.isArray(srcValue)) {
6039
+ return clone(srcValue);
6040
+ }
6041
+ };
6042
+
6043
+ return lodash.mergeWith({}, subValues, baseDefaults, overwriteArrays);
6035
6044
  };
6036
6045
 
6037
6046
  highchartsRenderer.getCommonChartOptions = function(additionalOptions) {
@@ -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]
@@ -3054,6 +3054,115 @@ describe('highcharts_renderer', () => {
3054
3054
  });
3055
3055
 
3056
3056
  describe('function getDefaultValueForChart', () => {
3057
+ let originalRich, originalGetter, originalSubDefaults;
3058
+
3059
+ beforeEach(() => {
3060
+ originalRich = highchartsRenderer.richTextSubType;
3061
+ originalGetter = highchartsRenderer.getChartOptionsBySubType;
3062
+ originalSubDefaults = highchartsRenderer.getDefaultValueForSubOptions;
3063
+
3064
+ highchartsRenderer.richTextSubType = { type: 'rich_text', suboptions: [], default_options: null };
3065
+ });
3066
+
3067
+ afterEach(() => {
3068
+ highchartsRenderer.richTextSubType = originalRich;
3069
+ highchartsRenderer.getChartOptionsBySubType = originalGetter;
3070
+ highchartsRenderer.getDefaultValueForSubOptions = originalSubDefaults;
3071
+ });
3072
+
3073
+ it('returns {} for unknown subtype', () => {
3074
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => null);
3075
+ expect(highchartsRenderer.getDefaultValueForChart('unknown-type', {})).toEqual({});
3076
+ });
3077
+
3078
+ it('deep-clones suboptions return value (no shared refs)', () => {
3079
+ const shared = { arr: [1, 2, 3], nested: { x: 1 } };
3080
+ highchartsRenderer.getDefaultValueForSubOptions = jest.fn(() => shared);
3081
+
3082
+ const chartOpt = {
3083
+ suboptions: [{ category_type: 'segments' }, { category_type: 'label' }],
3084
+ default_options: {},
3085
+ };
3086
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => chartOpt);
3087
+
3088
+ const res = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3089
+ expect(res.segments).toEqual(shared);
3090
+ expect(res.segments).not.toBe(shared);
3091
+
3092
+ res.segments.arr.push(99);
3093
+ res.segments.nested.x = 42;
3094
+ expect(shared.arr).toEqual([1, 2, 3]);
3095
+ expect(shared.nested.x).toBe(1);
3096
+ });
3097
+
3098
+ it('does not mutate chartOpt.default_options (base defaults stay immutable)', () => {
3099
+ const baseDefaults = {
3100
+ segments: [10, 20],
3101
+ label: { font_size: 12, style: { weight: 400 } },
3102
+ };
3103
+ highchartsRenderer.getDefaultValueForSubOptions = jest.fn(() => ({ label: { font_size: 8 } }));
3104
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => ({
3105
+ suboptions: [{ category_type: 'label' }],
3106
+ default_options: baseDefaults,
3107
+ }));
3108
+
3109
+ const res = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3110
+ res.label.style.weight = 700;
3111
+ res.segments.push(30);
3112
+
3113
+ expect(baseDefaults).toEqual({
3114
+ segments: [10, 20],
3115
+ label: { font_size: 12, style: { weight: 400 } },
3116
+ });
3117
+ });
3118
+
3119
+ it('default_options override suboptions (old semantics) and arrays are replaced, not merged', () => {
3120
+ highchartsRenderer.getDefaultValueForSubOptions = jest.fn((sub) => {
3121
+ if (sub.category_type === 'segments') return [1, 2, 3];
3122
+ if (sub.category_type === 'label') return { font_size: 8 };
3123
+ return {};
3124
+ });
3125
+
3126
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => ({
3127
+ suboptions: [{ category_type: 'segments' }, { category_type: 'label' }],
3128
+ default_options: {
3129
+ segments: [100],
3130
+ label: { font_size: 12, color: '#000' },
3131
+ },
3132
+ }));
3133
+
3134
+ const res = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3135
+ expect(res.segments).toEqual([100]); // массив заменён целиком
3136
+ expect(res.label).toEqual({ font_size: 12, color: '#000' }); // приоритет у default_options
3137
+ });
3138
+
3139
+ it('fresh object on each call (no cross-call leakage)', () => {
3140
+ highchartsRenderer.getDefaultValueForSubOptions = jest.fn(() => ({ a: 1 }));
3141
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => ({
3142
+ suboptions: [{ category_type: 'cfg' }],
3143
+ default_options: {},
3144
+ }));
3145
+
3146
+ const res1 = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3147
+ const res2 = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3148
+ res1.cfg.a = 99;
3149
+
3150
+ expect(res2.cfg.a).toBe(1);
3151
+ expect(res1.cfg).not.toBe(res2.cfg);
3152
+ });
3153
+
3154
+ it('handles non-plain values inside defaults (e.g., Date) without throwing', () => {
3155
+ const dt = new Date('2025-01-01T00:00:00.000Z');
3156
+ highchartsRenderer.getDefaultValueForSubOptions = jest.fn(() => ({}));
3157
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => ({
3158
+ suboptions: [],
3159
+ default_options: { createdAt: dt },
3160
+ }));
3161
+
3162
+ const res = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3163
+ expect(new Date(res.createdAt).getTime()).toBe(dt.getTime());
3164
+ });
3165
+
3057
3166
  it('should return empty value for rich_text type', () => {
3058
3167
  expect(highchartsRenderer.getDefaultValueForChart('rich_text', {})).toEqual({});
3059
3168
  });