@oliasoft-open-source/charts-library 2.1.0 → 2.1.2

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.
Files changed (29) hide show
  1. package/.storybook/main.js +18 -22
  2. package/.storybook/preview.js +37 -0
  3. package/.storybook/storybook.less +8 -0
  4. package/package.json +2 -1
  5. package/src/components/bar-chart/bar-chart-prop-types.js +7 -0
  6. package/src/components/bar-chart/bar-chart.jsx +7 -2
  7. package/src/components/bar-chart/{basic.stories.jsx → bar-chart.stories.jsx} +108 -371
  8. package/src/components/line-chart/Controls/Controls.jsx +2 -0
  9. package/src/components/line-chart/Controls/Layer.jsx +52 -49
  10. package/src/components/line-chart/line-chart-prop-types.js +10 -0
  11. package/src/components/line-chart/line-chart.jsx +91 -10
  12. package/src/components/line-chart/line-chart.module.less +6 -0
  13. package/src/components/line-chart/line-chart.stories.jsx +393 -0
  14. package/src/components/line-chart/state/line-chart-reducer.js +26 -15
  15. package/src/components/pie-chart/pie-chart.stories.jsx +234 -0
  16. package/src/components/scatter-chart/{scatter.stories.jsx → scatter-chart.stories.jsx} +25 -79
  17. package/src/helpers/chart-consts.js +2 -0
  18. package/src/helpers/chart-interface.ts +9 -0
  19. package/src/helpers/chart-utils.js +24 -4
  20. package/src/helpers/get-custom-legend-plugin-example.js +81 -0
  21. package/src/components/bar-chart/charts.stories.jsx +0 -119
  22. package/src/components/line-chart/basic.stories.jsx +0 -735
  23. package/src/components/line-chart/charts.stories.jsx +0 -264
  24. package/src/components/line-chart/stories/shapes/cubes.stories.jsx +0 -326
  25. package/src/components/line-chart/stories/shapes/pyramid.stories.jsx +0 -189
  26. package/src/components/line-chart/stories/shapes/round.stories.jsx +0 -339
  27. package/src/components/line-chart/stories/shapes/triangle.stories.jsx +0 -166
  28. package/src/components/pie-chart/basic.stories.jsx +0 -390
  29. package/src/components/pie-chart/charts.stories.jsx +0 -66
@@ -50,6 +50,7 @@ export const LineChartPropTypes = {
50
50
  staticChartHeight: PropTypes.bool,
51
51
  performanceMode: PropTypes.bool,
52
52
  darkMode: PropTypes.bool,
53
+ squareAspectRatio: PropTypes.bool,
53
54
  }),
54
55
  tooltip: PropTypes.shape({
55
56
  tooltips: PropTypes.bool,
@@ -79,6 +80,10 @@ export const LineChartPropTypes = {
79
80
  display: PropTypes.bool,
80
81
  position: PropTypes.oneOf(['top', 'bottom', 'right']),
81
82
  align: PropTypes.oneOf(['start', 'center', 'end']),
83
+ customLegend: PropTypes.shape({
84
+ customLegendPlugin: PropTypes.object,
85
+ customLegendContainerID: PropTypes.string,
86
+ }),
82
87
  }),
83
88
  chartOptions: PropTypes.shape({
84
89
  showPoints: PropTypes.bool,
@@ -106,6 +111,8 @@ export const getDefaultProps = (props) => {
106
111
  props.chart.options.graph = props.chart.options.graph || {};
107
112
  props.chart.options.annotations = props.chart.options.annotations || {};
108
113
  props.chart.options.legend = props.chart.options.legend || {};
114
+ props.chart.options.legend.customLegend = props.chart.options.legend
115
+ .customLegend || { customLegendPlugin: null, customLegendContainerID: '' };
109
116
  props.chart.options.chartOptions = props.chart.options.chartOptions || {};
110
117
  props.chart.options.interactions = props.chart.options.interactions || {};
111
118
  // Set defaults for missing properties
@@ -141,6 +148,8 @@ export const getDefaultProps = (props) => {
141
148
  performanceMode:
142
149
  props.chart.options.chartStyling.performanceMode ?? true,
143
150
  darkMode: props.chart.options.chartStyling.darkMode || false,
151
+ squareAspectRatio:
152
+ props.chart.options.chartStyling.squareAspectRatio || false,
144
153
  },
145
154
  tooltip: {
146
155
  tooltips: props.chart.options.tooltip.tooltips ?? true,
@@ -167,6 +176,7 @@ export const getDefaultProps = (props) => {
167
176
  display: props.chart.options.legend.display ?? true,
168
177
  position: props.chart.options.legend.position || 'bottom',
169
178
  align: props.chart.options.legend.align || 'center',
179
+ customLegend: props.chart.options.legend.customLegend,
170
180
  },
171
181
  chartOptions: {
172
182
  showPoints: props.chart.options.chartOptions.showPoints ?? true,
@@ -60,6 +60,7 @@ import {
60
60
  ANIMATION_DURATION,
61
61
  AUTO,
62
62
  COLORS,
63
+ CUSTOM_LEGEND_PLUGIN_NAME,
63
64
  DARK_MODE_COLORS,
64
65
  DEFAULT_COLOR,
65
66
  DEFAULT_DARK_MODE_BORDER_COLOR,
@@ -102,7 +103,6 @@ defaults.darkModeBorderColor = DEFAULT_DARK_MODE_BORDER_COLOR;
102
103
  */
103
104
  const LineChart = (props) => {
104
105
  const chartRef = useRef(null);
105
- const [shiftPressed, setShiftPressed] = useState(false);
106
106
  let pointHover = false;
107
107
  const chart = getDefaultProps(props);
108
108
  const { options } = chart;
@@ -150,6 +150,23 @@ const LineChart = (props) => {
150
150
  });
151
151
  }, [showLine, showPoints, enableZoom, enablePan]);
152
152
 
153
+ /**
154
+ * Toggle custom legends visibility.
155
+ * Needed because they are rendered in a html element separate from the chart.
156
+ */
157
+ useEffect(() => {
158
+ if (options.legend.customLegend.customLegendPlugin) {
159
+ const parent = document.getElementById(
160
+ options.legend.customLegend.customLegendContainerID,
161
+ );
162
+ if (state.legendEnabled) {
163
+ parent.style.visibility = 'visible';
164
+ } else {
165
+ parent.style.visibility = 'hidden';
166
+ }
167
+ }
168
+ }, [state.legendEnabled]);
169
+
153
170
  const generateLineChartDatasets = (datasets) => {
154
171
  const copyDataset = [...datasets];
155
172
 
@@ -303,14 +320,20 @@ const LineChart = (props) => {
303
320
  };
304
321
 
305
322
  const handleKeyDown = (evt) => {
306
- if (evt.key === Key.Shift && !shiftPressed) {
307
- setShiftPressed(true);
323
+ if (evt.key === Key.Shift) {
324
+ const chart = chartRef.current;
325
+ chart.config.options.plugins.zoom.zoom.mode = PanZoomMode.Y;
326
+ chart.config.options.plugins.zoom.pan.mode = PanZoomMode.Y;
327
+ chart.update();
308
328
  }
309
329
  };
310
330
 
311
331
  const handleKeyUp = (evt) => {
312
332
  if (evt.key === Key.Shift) {
313
- setShiftPressed(false);
333
+ const chart = chartRef.current;
334
+ chart.config.options.plugins.zoom.zoom.mode = PanZoomMode.Z;
335
+ chart.config.options.plugins.zoom.pan.mode = PanZoomMode.Z;
336
+ chart.update();
314
337
  }
315
338
  };
316
339
 
@@ -329,19 +352,66 @@ const LineChart = (props) => {
329
352
  };
330
353
  const controlsAxes = getControlsAxes();
331
354
 
355
+ const setAxisValuesInSettings = (chart) => {
356
+ dispatch({
357
+ type: SET_AXIS_VALUE,
358
+ payload: [
359
+ {
360
+ name: 'min',
361
+ value: chart.scales?.x?.min ? chart.scales.x.min.toFixed(1) : 0,
362
+ id: 'x',
363
+ },
364
+ {
365
+ name: 'max',
366
+ value: chart.scales?.x?.max ? chart.scales.x.max.toFixed(1) : 0,
367
+ id: 'x',
368
+ },
369
+ {
370
+ name: 'min',
371
+ value: chart.scales?.y?.min ? chart.scales.y.min.toFixed(1) : 0,
372
+ id: 'y',
373
+ },
374
+ {
375
+ name: 'max',
376
+ value: chart.scales?.y?.max ? chart.scales.y.max.toFixed(1) : 0,
377
+ id: 'y',
378
+ },
379
+ ],
380
+ });
381
+ };
382
+
383
+ const getScalesMaxMin = () => {
384
+ const chart = chartRef.current;
385
+ return {
386
+ x: {
387
+ min: chart?.scales?.x?.min,
388
+ max: chart?.scales?.x?.max,
389
+ },
390
+ y: {
391
+ min: chart?.scales?.y?.min,
392
+ max: chart?.scales?.y?.max,
393
+ },
394
+ };
395
+ };
396
+
332
397
  return (
333
398
  <div
334
399
  className={getClassName(chartStyling, styles)}
335
- style={{
336
- width: chartStyling.width || AUTO,
337
- height: chartStyling.height || AUTO,
338
- }}
400
+ style={
401
+ chartStyling.squareAspectRatio
402
+ ? {}
403
+ : {
404
+ width: chartStyling.width || AUTO,
405
+ height: chartStyling.height || AUTO,
406
+ }
407
+ }
339
408
  tabIndex={0} //eslint-disable-line jsx-a11y/no-noninteractive-tabindex
340
409
  onKeyDown={handleKeyDown}
341
410
  onKeyUp={handleKeyUp}
342
411
  >
343
412
  <div className={styles.zoomForm}>
344
413
  <Controls
414
+ scalesMaxMin={getScalesMaxMin()}
345
415
  axes={controlsAxes}
346
416
  legendEnabled={state.legendEnabled}
347
417
  lineEnabled={state.lineEnabled}
@@ -372,6 +442,7 @@ const LineChart = (props) => {
372
442
  onClick,
373
443
  onHover,
374
444
  maintainAspectRatio: chartStyling.maintainAspectRatio,
445
+ aspectRatio: chartStyling.squareAspectRatio ? 1 : null, // 1 equals square aspect ratio
375
446
  animation: {
376
447
  duration: chartStyling.performanceMode
377
448
  ? ANIMATION_DURATION.NO
@@ -398,7 +469,10 @@ const LineChart = (props) => {
398
469
  zoom: {
399
470
  pan: {
400
471
  enabled: state.panEnabled,
401
- mode: shiftPressed ? PanZoomMode.Y : PanZoomMode.X,
472
+ mode: PanZoomMode.X,
473
+ onPanComplete({ chart }) {
474
+ setAxisValuesInSettings(chart);
475
+ },
402
476
  },
403
477
  zoom: {
404
478
  mode: PanZoomMode.X,
@@ -406,13 +480,20 @@ const LineChart = (props) => {
406
480
  enabled: state.zoomEnabled,
407
481
  threshold: 3,
408
482
  },
483
+ onZoomComplete({ chart }) {
484
+ setAxisValuesInSettings(chart);
485
+ },
409
486
  },
410
487
  },
411
488
  tooltip: getLineChartToolTips(options),
412
489
  legend: getLegend(options, legendClick, state),
490
+ [CUSTOM_LEGEND_PLUGIN_NAME]: options.legend.customLegend
491
+ .customLegendPlugin && {
492
+ containerID: options.legend.customLegend.customLegendContainerID,
493
+ },
413
494
  },
414
495
  }}
415
- plugins={getPlugins(graph)}
496
+ plugins={getPlugins(graph, legend, state)}
416
497
  />
417
498
  </div>
418
499
  );
@@ -22,6 +22,12 @@
22
22
  height: 100%;
23
23
  }
24
24
 
25
+ // TODO: fix square plot scaling
26
+ &.squareAspectRatio {
27
+ max-height: 450px;
28
+ max-width: 450px;
29
+ }
30
+
25
31
  &:focus {
26
32
  border: 1px solid #85b7d9;
27
33
  outline: none; // Remove dotted outline on FF
@@ -0,0 +1,393 @@
1
+ import React from 'react';
2
+ import { LineChart } from './line-chart';
3
+ import { getCustomLegendPlugin } from '../../helpers/get-custom-legend-plugin-example';
4
+
5
+ const dataset1 = {
6
+ label: 'Dataset 1',
7
+ data: [
8
+ {
9
+ x: 0,
10
+ y: 0,
11
+ },
12
+ {
13
+ x: 5,
14
+ y: 25,
15
+ },
16
+ {
17
+ x: 10,
18
+ y: 60,
19
+ },
20
+ {
21
+ x: 15,
22
+ y: 90,
23
+ },
24
+ {
25
+ x: 20,
26
+ y: 120,
27
+ },
28
+ {
29
+ x: 25,
30
+ y: 150,
31
+ },
32
+ ],
33
+ };
34
+
35
+ const datasetLabelled1 = {
36
+ ...dataset1,
37
+ data: dataset1.data.map((item) => ({ ...item, label: ['Label'] })),
38
+ };
39
+
40
+ const dataset2 = {
41
+ label: 'Dataset 2',
42
+ data: [
43
+ {
44
+ x: 0,
45
+ y: 0,
46
+ },
47
+ {
48
+ x: 3,
49
+ y: 15,
50
+ },
51
+ {
52
+ x: 6,
53
+ y: 30,
54
+ },
55
+ {
56
+ x: 9,
57
+ y: 60,
58
+ },
59
+ {
60
+ x: 12,
61
+ y: 120,
62
+ },
63
+ {
64
+ x: 15,
65
+ y: 240,
66
+ },
67
+ ],
68
+ };
69
+
70
+ const datasetLabelled2 = {
71
+ ...dataset2,
72
+ data: dataset2.data.map((item) => ({ ...item, label: ['Label'] })),
73
+ };
74
+
75
+ const basicChart = {
76
+ data: {
77
+ datasets: [dataset1, dataset2],
78
+ },
79
+ options: {
80
+ title: 'Chart title',
81
+ chartStyling: {
82
+ height: 400,
83
+ },
84
+ },
85
+ };
86
+
87
+ export default {
88
+ title: 'LineChart',
89
+ component: LineChart,
90
+ args: {
91
+ chart: basicChart,
92
+ },
93
+ };
94
+
95
+ const Template = (args) => {
96
+ return (
97
+ <LineChart
98
+ // eslint-disable-next-line react/jsx-props-no-spreading
99
+ {...args}
100
+ />
101
+ );
102
+ };
103
+
104
+ const customLegendContainerID = 'custom-legend-container';
105
+
106
+ const TemplateWithCustomLegendContainer = (args) => {
107
+ return (
108
+ <>
109
+ <LineChart
110
+ // eslint-disable-next-line react/jsx-props-no-spreading
111
+ {...args}
112
+ />
113
+ <div id={customLegendContainerID}></div>
114
+ </>
115
+ );
116
+ };
117
+
118
+ export const Default = Template.bind({});
119
+
120
+ export const MinorGridlines = Template.bind({});
121
+ MinorGridlines.args = {
122
+ chart: {
123
+ ...basicChart,
124
+ options: {
125
+ ...basicChart.options,
126
+ graph: {
127
+ showMinorGridlines: true,
128
+ },
129
+ },
130
+ },
131
+ };
132
+
133
+ export const AxesLabels = Template.bind({});
134
+ AxesLabels.args = {
135
+ chart: {
136
+ ...basicChart,
137
+ options: {
138
+ ...basicChart.options,
139
+ axes: {
140
+ x: [
141
+ {
142
+ label: 'x-axis title here',
143
+ },
144
+ ],
145
+ y: [
146
+ {
147
+ label: 'y-axis title here',
148
+ },
149
+ ],
150
+ },
151
+ },
152
+ },
153
+ };
154
+
155
+ export const MultipleXAxes = Template.bind({});
156
+ MultipleXAxes.args = {
157
+ chart: {
158
+ ...basicChart,
159
+ options: {
160
+ ...basicChart.options,
161
+ axes: {
162
+ x: [
163
+ {
164
+ label: 'The X',
165
+ position: 'bottom',
166
+ },
167
+ {
168
+ label: 'The X 2',
169
+ position: 'top',
170
+ },
171
+ ],
172
+ },
173
+ },
174
+ },
175
+ };
176
+
177
+ export const ReversedYAxis = Template.bind({});
178
+ ReversedYAxis.args = {
179
+ chart: {
180
+ ...basicChart,
181
+ options: {
182
+ ...basicChart.options,
183
+ additionalAxesOptions: {
184
+ reverse: true,
185
+ },
186
+ },
187
+ },
188
+ };
189
+
190
+ export const LogarithmicScale = Template.bind({});
191
+ LogarithmicScale.args = {
192
+ chart: {
193
+ ...basicChart,
194
+ options: {
195
+ ...basicChart.options,
196
+ additionalAxesOptions: {
197
+ chartScaleType: 'logarithmic',
198
+ },
199
+ },
200
+ },
201
+ };
202
+
203
+ export const PresetRange = Template.bind({});
204
+ PresetRange.args = {
205
+ chart: {
206
+ ...basicChart,
207
+ options: {
208
+ ...basicChart.options,
209
+ additionalAxesOptions: {
210
+ range: {
211
+ x: {
212
+ min: -10,
213
+ max: 40,
214
+ },
215
+ y: {
216
+ min: -10,
217
+ max: 180,
218
+ },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ };
224
+
225
+ export const DataLabels = Template.bind({});
226
+ DataLabels.args = {
227
+ chart: {
228
+ ...basicChart,
229
+ data: {
230
+ datasets: [datasetLabelled1, datasetLabelled2],
231
+ },
232
+ options: {
233
+ ...basicChart.options,
234
+ graph: {
235
+ showDataLabels: true,
236
+ },
237
+ },
238
+ },
239
+ };
240
+
241
+ export const DataLabelsInTooltips = Template.bind({});
242
+ DataLabelsInTooltips.args = {
243
+ chart: {
244
+ ...basicChart,
245
+ data: {
246
+ datasets: [datasetLabelled1, datasetLabelled2],
247
+ },
248
+ options: {
249
+ ...basicChart.options,
250
+ tooltip: {
251
+ showLabelsInTooltips: true,
252
+ },
253
+ },
254
+ },
255
+ };
256
+
257
+ export const LegendOnRight = Template.bind({});
258
+ LegendOnRight.args = {
259
+ chart: {
260
+ ...basicChart,
261
+ options: {
262
+ ...basicChart.options,
263
+ legend: {
264
+ position: 'right',
265
+ },
266
+ },
267
+ },
268
+ };
269
+
270
+ export const Annotations = Template.bind({});
271
+ Annotations.args = {
272
+ chart: {
273
+ ...basicChart,
274
+ options: {
275
+ ...basicChart.options,
276
+ annotations: {
277
+ showAnnotations: true,
278
+ annotationsData: [
279
+ {
280
+ annotationAxis: 'y',
281
+ label: 'Annotation on y-axis',
282
+ value: 50,
283
+ },
284
+ {
285
+ annotationAxis: 'x',
286
+ label: 'Annotation on x-axis',
287
+ value: 10,
288
+ },
289
+ {
290
+ annotationAxis: 'y',
291
+ label: 'Diagonal annotation',
292
+ value: 20,
293
+ endValue: 170,
294
+ },
295
+ ],
296
+ },
297
+ },
298
+ },
299
+ };
300
+
301
+ export const AnnotationsInLegend = Template.bind({});
302
+ AnnotationsInLegend.args = {
303
+ chart: {
304
+ ...basicChart,
305
+ options: {
306
+ ...basicChart.options,
307
+ annotations: {
308
+ controlAnnotation: true,
309
+ showAnnotations: true,
310
+ annotationsData: [
311
+ {
312
+ annotationAxis: 'x',
313
+ label: 'Annotation',
314
+ value: 20,
315
+ },
316
+ ],
317
+ },
318
+ },
319
+ },
320
+ };
321
+
322
+ export const CustomLegend = TemplateWithCustomLegendContainer.bind({});
323
+ CustomLegend.args = {
324
+ chart: {
325
+ ...basicChart,
326
+ options: {
327
+ ...basicChart.options,
328
+ title: 'Custom HTML legend',
329
+ legend: {
330
+ customLegend: {
331
+ customLegendPlugin: getCustomLegendPlugin(customLegendContainerID),
332
+ customLegendContainerID,
333
+ },
334
+ },
335
+ },
336
+ },
337
+ };
338
+
339
+ export const Animation = Template.bind({});
340
+ Animation.args = {
341
+ chart: {
342
+ ...basicChart,
343
+ options: {
344
+ ...basicChart.options,
345
+ chartStyling: {
346
+ ...basicChart.options.chartStyling,
347
+ performanceMode: false,
348
+ },
349
+ },
350
+ },
351
+ };
352
+
353
+ export const CloseControlsOnOutsideClick = Template.bind({});
354
+ CloseControlsOnOutsideClick.args = {
355
+ chart: {
356
+ ...basicChart,
357
+ options: {
358
+ ...basicChart.options,
359
+ chartOptions: {
360
+ closeOnOutsideClick: true,
361
+ },
362
+ },
363
+ },
364
+ };
365
+
366
+ export const SquareAspectRatio = Template.bind({});
367
+ SquareAspectRatio.args = {
368
+ chart: {
369
+ ...basicChart,
370
+ options: {
371
+ ...basicChart.options,
372
+ title: 'Square aspect ratio',
373
+ chartStyling: {
374
+ squareAspectRatio: true,
375
+ maintainAspectRatio: true,
376
+ },
377
+ },
378
+ },
379
+ };
380
+
381
+ export const DarkMode = Template.bind({});
382
+ DarkMode.args = {
383
+ chart: {
384
+ ...basicChart,
385
+ options: {
386
+ ...basicChart.options,
387
+ chartStyling: {
388
+ ...basicChart.options.chartStyling,
389
+ darkMode: true,
390
+ },
391
+ },
392
+ },
393
+ };
@@ -71,21 +71,32 @@ export const reducer = (state, action) => {
71
71
  };
72
72
  }
73
73
  case SET_AXIS_VALUE: {
74
- const { name, value: nextInputValue, id } = action.payload;
75
- const nextValue = isNaN(nextInputValue)
76
- ? cleanNumStr(nextInputValue)
77
- : toNum(nextInputValue);
78
- const axis = newState.axes.find((a) => a.id === id);
79
- axis.min = getAxisValue(name === 'min' ? nextValue : axis.min?.value);
80
- axis.max = getAxisValue(name === 'max' ? nextValue : axis.max?.value);
81
- axis.min.valid =
82
- validNumber(axis.min.inputValue ?? '') &&
83
- isLessThanMax(axis.min.value, axis.max.value);
84
- axis.min.displayValue = axis.min.valid ? axis.min.value : undefined;
85
- axis.max.valid =
86
- validNumber(axis.max.inputValue ?? '') &&
87
- isGreaterThanMin(axis.max.value, axis.min.value);
88
- axis.max.displayValue = axis.max.valid ? axis.max.value : undefined;
74
+ const valiateElement = (name, nextInputValue, id) => {
75
+ const nextValue = isNaN(nextInputValue)
76
+ ? cleanNumStr(nextInputValue)
77
+ : toNum(nextInputValue);
78
+ const axis = newState.axes.find((a) => a.id === id);
79
+ axis.min = getAxisValue(name === 'min' ? nextValue : axis.min?.value);
80
+ axis.max = getAxisValue(name === 'max' ? nextValue : axis.max?.value);
81
+ axis.min.valid =
82
+ validNumber(axis.min.inputValue ?? '') &&
83
+ isLessThanMax(axis.min.value, axis.max.value);
84
+ axis.min.displayValue = axis.min.valid ? axis.min.value : undefined;
85
+ axis.max.valid =
86
+ validNumber(axis.max.inputValue ?? '') &&
87
+ isGreaterThanMin(axis.max.value, axis.min.value);
88
+ axis.max.displayValue = axis.max.valid ? axis.max.value : undefined;
89
+ };
90
+ if (Array.isArray(action.payload)) {
91
+ action.payload.forEach((element) => {
92
+ const { name, value: nextInputValue, id } = element;
93
+ valiateElement(name, nextInputValue, id);
94
+ });
95
+ } else {
96
+ const { name, value: nextInputValue, id } = action.payload;
97
+ valiateElement(name, nextInputValue, id);
98
+ }
99
+
89
100
  return newState;
90
101
  }
91
102
  case SET_POINTS_ZOOM_DEFAULTS: