@datarailsshared/dr_renderer 1.4.50 → 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/package.json
CHANGED
@@ -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
|
-
|
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
|
250
|
-
const
|
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
|
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
|
255
|
-
|
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
|
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
|
-
|
263
|
-
|
264
|
-
|
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
|
-
(
|
270
|
-
|
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
|
-
|
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
|
-
|
279
|
-
|
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
|
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
|
-
|
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 =
|
447
|
+
band.outerRadius = Math.max(1, R - (effectiveTick - options.gauge.thickness) / 2);
|
405
448
|
});
|
406
|
-
|
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
|
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 (!
|
31
|
+
if (!_.isPlainObject(target)) return target;
|
24
32
|
|
25
|
-
|
26
|
-
if (!
|
33
|
+
for (const source of sources) {
|
34
|
+
if (!_.isPlainObject(source)) continue;
|
27
35
|
|
28
|
-
Object.keys(source)
|
29
|
-
const
|
30
|
-
const
|
36
|
+
for (const key of Object.keys(source)) {
|
37
|
+
const tVal = target[key];
|
38
|
+
const sVal = source[key];
|
31
39
|
|
32
|
-
if (
|
33
|
-
target[key] = mergeDeep(
|
40
|
+
if (_.isPlainObject(tVal) && _.isPlainObject(sVal)) {
|
41
|
+
target[key] = mergeDeep({}, tVal, sVal);
|
34
42
|
} else {
|
35
|
-
target[key] =
|
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
|
6031
|
-
valToReturn = lodash.extend(valToReturn, chartOpt.default_options);
|
6032
|
-
}
|
6024
|
+
if (!chartOpt) return {};
|
6033
6025
|
|
6034
|
-
|
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
|
});
|