@oliasoft-open-source/charts-library 2.14.1 → 2.16.0-beta-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/package.json +1 -1
- package/release-notes.md +2 -2
- package/src/components/line-chart/hooks/use-chart-options.js +20 -0
- package/src/components/line-chart/line-chart-prop-types.js +27 -2
- package/src/components/line-chart/line-chart.jsx +3 -8
- package/src/components/line-chart/plugins/chart-area-text-plugin.js +117 -0
- package/src/components/line-chart/state/use-chart-state.js +9 -23
- package/src/helpers/chart-interface.ts +8 -4
- package/src/helpers/chart-utils.js +1 -1
- package/src/helpers/get-chart-annotation.js +7 -44
- /package/src/components/line-chart/{line-chart.minor-gridlines-plugin.js → plugins/line-chart.minor-gridlines-plugin.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oliasoft-open-source/charts-library",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0-beta-1",
|
|
4
4
|
"description": "React Chart Library (based on Chart.js and react-chart-js-2)",
|
|
5
5
|
"homepage": "https://gitlab.com/oliasoft-open-source/charts-library",
|
|
6
6
|
"bugs": {
|
package/release-notes.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Charts Library Release Notes
|
|
2
2
|
|
|
3
|
-
## 2.
|
|
4
|
-
-
|
|
3
|
+
## 2.16.0
|
|
4
|
+
- Added common chart area text plugin
|
|
5
5
|
|
|
6
6
|
## 2.14.0
|
|
7
7
|
- Refactored line chart, for better performance, readable and reduce not needed re-renders
|
|
@@ -38,6 +38,17 @@ export const useChartOptions = ({
|
|
|
38
38
|
}) => {
|
|
39
39
|
const {
|
|
40
40
|
interactions: { onAnimationComplete },
|
|
41
|
+
annotations: {
|
|
42
|
+
labelAnnotation: {
|
|
43
|
+
showLabel,
|
|
44
|
+
text,
|
|
45
|
+
fontSize,
|
|
46
|
+
xOffset,
|
|
47
|
+
yOffset,
|
|
48
|
+
maxWidth,
|
|
49
|
+
lineHeight,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
41
52
|
} = options;
|
|
42
53
|
|
|
43
54
|
const {
|
|
@@ -122,6 +133,15 @@ export const useChartOptions = ({
|
|
|
122
133
|
chartAreaBorder: {
|
|
123
134
|
borderColor: BORDER_COLOR,
|
|
124
135
|
},
|
|
136
|
+
chartAreaText: {
|
|
137
|
+
showLabel,
|
|
138
|
+
text,
|
|
139
|
+
fontSize,
|
|
140
|
+
xOffset,
|
|
141
|
+
yOffset,
|
|
142
|
+
maxWidth,
|
|
143
|
+
lineHeight,
|
|
144
|
+
},
|
|
125
145
|
...dragData,
|
|
126
146
|
};
|
|
127
147
|
|
|
@@ -72,7 +72,18 @@ export const LineChartPropTypes = {
|
|
|
72
72
|
showMinorGridlines: PropTypes.bool,
|
|
73
73
|
}),
|
|
74
74
|
annotations: PropTypes.shape({
|
|
75
|
-
labelAnnotation: PropTypes.
|
|
75
|
+
labelAnnotation: PropTypes.shape({
|
|
76
|
+
showLabel: PropTypes.bool,
|
|
77
|
+
text: PropTypes.oneOfType([
|
|
78
|
+
PropTypes.string,
|
|
79
|
+
PropTypes.arrayOf(PropTypes.string),
|
|
80
|
+
]),
|
|
81
|
+
fontSize: PropTypes.number,
|
|
82
|
+
xOffset: PropTypes.number,
|
|
83
|
+
yOffset: PropTypes.number,
|
|
84
|
+
maxWidth: PropTypes.number,
|
|
85
|
+
lineHeight: PropTypes.number,
|
|
86
|
+
}),
|
|
76
87
|
showAnnotations: PropTypes.bool,
|
|
77
88
|
controlAnnotation: PropTypes.bool,
|
|
78
89
|
annotationsData: PropTypes.arrayOf(
|
|
@@ -201,7 +212,21 @@ export const getDefaultProps = (props) => {
|
|
|
201
212
|
props.chart.options.graph.showMinorGridlines || false,
|
|
202
213
|
},
|
|
203
214
|
annotations: {
|
|
204
|
-
labelAnnotation:
|
|
215
|
+
labelAnnotation: {
|
|
216
|
+
showLabel:
|
|
217
|
+
props.chart.options.annotations.labelAnnotation?.showLabel ?? false,
|
|
218
|
+
text: props.chart.options.annotations.labelAnnotation?.text ?? '',
|
|
219
|
+
fontSize:
|
|
220
|
+
props.chart.options.annotations.labelAnnotation?.fontSize ?? 12,
|
|
221
|
+
xOffset:
|
|
222
|
+
props.chart.options.annotations.labelAnnotation?.xOffset ?? 5,
|
|
223
|
+
yOffset:
|
|
224
|
+
props.chart.options.annotations.labelAnnotation?.yOffset ?? 10,
|
|
225
|
+
maxWidth:
|
|
226
|
+
props.chart.options.annotations.labelAnnotation?.maxWidth ?? 300,
|
|
227
|
+
lineHeight:
|
|
228
|
+
props.chart.options.annotations.labelAnnotation?.lineHeight ?? 12,
|
|
229
|
+
},
|
|
205
230
|
showAnnotations:
|
|
206
231
|
props.chart.options.annotations.showAnnotations ?? false,
|
|
207
232
|
controlAnnotation:
|
|
@@ -29,6 +29,7 @@ import { useChartOptions } from './hooks/use-chart-options';
|
|
|
29
29
|
import { useChartPlugins } from './hooks/use-chart-plugins';
|
|
30
30
|
import { generateKey } from './utils/line-chart-utils';
|
|
31
31
|
import { useChartState } from './state/use-chart-state';
|
|
32
|
+
import { chartAreaTextPlugin } from './plugins/chart-area-text-plugin';
|
|
32
33
|
|
|
33
34
|
ChartJS.register(
|
|
34
35
|
LinearScale,
|
|
@@ -44,6 +45,7 @@ ChartJS.register(
|
|
|
44
45
|
dataLabelsPlugin,
|
|
45
46
|
annotationPlugin,
|
|
46
47
|
dragDataPlugin,
|
|
48
|
+
chartAreaTextPlugin,
|
|
47
49
|
);
|
|
48
50
|
|
|
49
51
|
/**
|
|
@@ -80,14 +82,7 @@ const LineChart = (props) => {
|
|
|
80
82
|
}, [state.lineEnabled, state.pointsEnabled, axes, annotations, graph]);
|
|
81
83
|
|
|
82
84
|
// Call the custom hooks.
|
|
83
|
-
useChartState({
|
|
84
|
-
chartRef,
|
|
85
|
-
options,
|
|
86
|
-
state,
|
|
87
|
-
generatedDatasets,
|
|
88
|
-
dispatch,
|
|
89
|
-
persistenceId,
|
|
90
|
-
});
|
|
85
|
+
useChartState({ chartRef, options, state, dispatch, persistenceId });
|
|
91
86
|
|
|
92
87
|
const { resetZoom, handleDownload, handleKeyDown, handleKeyUp } =
|
|
93
88
|
useChartFunctions({
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const WORD_SEPARATOR = ' ';
|
|
2
|
+
const TRANSPARENT = 'rgba(0, 0, 0, 0.5)';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Splits the input text into words based on the predefined WORD_SEPARATOR.
|
|
6
|
+
* If the input is an array, it first joins the array and then splits it.
|
|
7
|
+
*
|
|
8
|
+
* @param {string | string[]} text - The text to split into words.
|
|
9
|
+
* @returns {string[]} An array of words.
|
|
10
|
+
*/
|
|
11
|
+
const getWords = (text) => {
|
|
12
|
+
return Array.isArray(text)
|
|
13
|
+
? text.join(WORD_SEPARATOR).split(WORD_SEPARATOR)
|
|
14
|
+
: text.split(WORD_SEPARATOR);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Counts the number of lines required to render the words within the specified maxWidth.
|
|
19
|
+
* It takes into consideration the context (ctx) provided to calculate the text width.
|
|
20
|
+
*
|
|
21
|
+
* @param {string[]} words - The array of words to be processed.
|
|
22
|
+
* @param {number} maxWidth - The maximum width allowed for the text.
|
|
23
|
+
* @param {CanvasRenderingContext2D} ctx - The canvas rendering context to measure the text width.
|
|
24
|
+
* @returns {number} The number of lines required.
|
|
25
|
+
*/
|
|
26
|
+
const countLines = (words, maxWidth, ctx) => {
|
|
27
|
+
let line = '';
|
|
28
|
+
let lines = 0;
|
|
29
|
+
|
|
30
|
+
for (const word of words) {
|
|
31
|
+
const testLine = `${line}${word}${WORD_SEPARATOR}`;
|
|
32
|
+
const { width: testWidth } = ctx.measureText(testLine);
|
|
33
|
+
|
|
34
|
+
if (testWidth > maxWidth) {
|
|
35
|
+
line = `${word}${WORD_SEPARATOR}`;
|
|
36
|
+
lines++;
|
|
37
|
+
} else {
|
|
38
|
+
line = testLine;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
lines++; // Add the last line
|
|
42
|
+
return lines;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const drawText = (ctx, lines, lineHeight, x, y) => {
|
|
46
|
+
lines.forEach((line, index) => {
|
|
47
|
+
ctx.fillText(line, x, y - (lines.length - 1 - index) * lineHeight);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const chartAreaTextPlugin = {
|
|
52
|
+
id: 'chartAreaText',
|
|
53
|
+
|
|
54
|
+
beforeLayout: (chart, args, options) => {
|
|
55
|
+
const { showLabel, text, fontStyle, fontSize, maxWidth, lineHeight } =
|
|
56
|
+
options;
|
|
57
|
+
const { ctx } = chart;
|
|
58
|
+
|
|
59
|
+
if (!showLabel || !text) return;
|
|
60
|
+
|
|
61
|
+
ctx.save();
|
|
62
|
+
ctx.font = `${fontStyle} ${fontSize}px Arial`;
|
|
63
|
+
|
|
64
|
+
const words = getWords(text);
|
|
65
|
+
const lines = countLines(words, maxWidth, ctx);
|
|
66
|
+
|
|
67
|
+
// Calculate and set the padding needed at the bottom of the chart
|
|
68
|
+
const paddingNeeded = lines * lineHeight + lineHeight;
|
|
69
|
+
chart.options.layout.padding.bottom = paddingNeeded;
|
|
70
|
+
|
|
71
|
+
ctx.restore();
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
beforeDraw: (chart, args, options) => {
|
|
75
|
+
const { showLabel, text, fontSize, xOffset, yOffset, lineHeight } = options;
|
|
76
|
+
const { ctx, chartArea } = chart;
|
|
77
|
+
|
|
78
|
+
if (!showLabel || !text) return;
|
|
79
|
+
|
|
80
|
+
// Determine the maxWidth based on chartArea width
|
|
81
|
+
const maxWidth =
|
|
82
|
+
chartArea.width < 500
|
|
83
|
+
? 100
|
|
84
|
+
: chartArea.width < 700
|
|
85
|
+
? 200
|
|
86
|
+
: options.maxWidth;
|
|
87
|
+
|
|
88
|
+
// Split the text into words and calculate the number of lines
|
|
89
|
+
const words = getWords(text);
|
|
90
|
+
let line = '';
|
|
91
|
+
let lines = [];
|
|
92
|
+
for (let i = 0; i < words.length; i++) {
|
|
93
|
+
const testLine = `${line}${words[i]}${WORD_SEPARATOR}`;
|
|
94
|
+
const { width: testWidth } = ctx.measureText(testLine);
|
|
95
|
+
if (testWidth > maxWidth) {
|
|
96
|
+
lines.push(line.trim());
|
|
97
|
+
line = `${words[i]}${WORD_SEPARATOR}`;
|
|
98
|
+
} else {
|
|
99
|
+
line = testLine;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
lines.push(line.trim()); // Add the last line
|
|
103
|
+
|
|
104
|
+
// Wrap the text based on the maxWidth and draw it on the canvas
|
|
105
|
+
ctx.save();
|
|
106
|
+
ctx.font = `${fontSize}px Arial`;
|
|
107
|
+
ctx.fillStyle = TRANSPARENT;
|
|
108
|
+
ctx.textAlign = 'right';
|
|
109
|
+
|
|
110
|
+
const y = chartArea.bottom - yOffset;
|
|
111
|
+
const x = chartArea.right - xOffset; // Subtract margin on the right
|
|
112
|
+
|
|
113
|
+
drawText(ctx, lines, lineHeight, x, y);
|
|
114
|
+
|
|
115
|
+
ctx.restore();
|
|
116
|
+
},
|
|
117
|
+
};
|
|
@@ -75,38 +75,26 @@ const useUpdateAxesRanges = (range, dispatch) => {
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
78
|
+
* Hook for saving the initial axes ranges in the chart state.
|
|
79
79
|
*
|
|
80
|
-
* @param
|
|
81
|
-
* @param
|
|
82
|
-
* @param
|
|
83
|
-
* @param {Function} dispatch - A dispatch function from the useContext hook for managing the state.
|
|
80
|
+
* @param chartRef - The reference to the chart.
|
|
81
|
+
* @param axes - The axes data.
|
|
82
|
+
* @param dispatch - The dispatch function for state management.
|
|
84
83
|
*/
|
|
85
|
-
const useSaveInitialAxesRanges = (
|
|
86
|
-
chartRef,
|
|
87
|
-
axes,
|
|
88
|
-
generatedDatasets,
|
|
89
|
-
dispatch,
|
|
90
|
-
) => {
|
|
91
|
-
const prevGeneratedDatasets = useRef();
|
|
92
|
-
|
|
84
|
+
const useSaveInitialAxesRanges = (chartRef, axes, dispatch) => {
|
|
93
85
|
const saveInitialAxesRanges = useCallback(() => {
|
|
94
|
-
if (
|
|
95
|
-
chartRef &&
|
|
96
|
-
!isEqual(generatedDatasets, prevGeneratedDatasets.current)
|
|
97
|
-
) {
|
|
86
|
+
if (chartRef) {
|
|
98
87
|
const initialAxesRanges = getAxesRangesFromChart(chartRef, axes);
|
|
99
88
|
dispatch({
|
|
100
89
|
type: SAVE_INITIAL_AXES_RANGES,
|
|
101
90
|
payload: { initialAxesRanges },
|
|
102
91
|
});
|
|
103
|
-
prevGeneratedDatasets.current = generatedDatasets;
|
|
104
92
|
}
|
|
105
|
-
}, [chartRef
|
|
93
|
+
}, [chartRef]);
|
|
106
94
|
|
|
107
95
|
useEffect(() => {
|
|
108
96
|
saveInitialAxesRanges();
|
|
109
|
-
}, [saveInitialAxesRanges
|
|
97
|
+
}, [saveInitialAxesRanges]);
|
|
110
98
|
};
|
|
111
99
|
|
|
112
100
|
/**
|
|
@@ -115,7 +103,6 @@ const useSaveInitialAxesRanges = (
|
|
|
115
103
|
* @param chartRef - The reference to the chart.
|
|
116
104
|
* @param options - The chart options.
|
|
117
105
|
* @param state - The chart state.
|
|
118
|
-
* @param {Array} generatedDatasets - An array of generated datasets for the chart.
|
|
119
106
|
* @param dispatch - The dispatch function for state management.
|
|
120
107
|
* @param persistenceId - The chart persistenceId.
|
|
121
108
|
*/
|
|
@@ -123,7 +110,6 @@ export const useChartState = ({
|
|
|
123
110
|
chartRef,
|
|
124
111
|
options,
|
|
125
112
|
state,
|
|
126
|
-
generatedDatasets,
|
|
127
113
|
dispatch,
|
|
128
114
|
persistenceId,
|
|
129
115
|
}) => {
|
|
@@ -137,5 +123,5 @@ export const useChartState = ({
|
|
|
137
123
|
useStoreChartStateInStorage(memoState, persistenceId);
|
|
138
124
|
useToggleCustomLegendVisibility(memoState, memoOptions);
|
|
139
125
|
useUpdateAxesRanges(range, dispatch);
|
|
140
|
-
useSaveInitialAxesRanges(chartRef, axes,
|
|
126
|
+
useSaveInitialAxesRanges(chartRef, axes, dispatch);
|
|
141
127
|
};
|
|
@@ -26,12 +26,16 @@ export interface IChartAnnotationsData {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export interface ILabelAnnotation {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
showLabel: boolean;
|
|
30
|
+
text: string | string[];
|
|
31
|
+
fontSize?: number;
|
|
32
|
+
xOffset: number
|
|
33
|
+
yOffset: number
|
|
34
|
+
maxWidth: number
|
|
35
|
+
lineHeight: number
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
|
|
35
39
|
export interface IChartAnnotations {
|
|
36
40
|
showAnnotations: boolean;
|
|
37
41
|
controlAnnotation: boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { round } from '@oliasoft-open-source/units';
|
|
2
2
|
import { defaults } from 'chart.js';
|
|
3
3
|
import cx from 'classnames';
|
|
4
|
-
import { chartMinorGridlinesPlugin } from '../components/line-chart/line-chart.minor-gridlines-plugin';
|
|
4
|
+
import { chartMinorGridlinesPlugin } from '../components/line-chart/plugins/line-chart.minor-gridlines-plugin';
|
|
5
5
|
import {
|
|
6
6
|
BORDER_COLOR,
|
|
7
7
|
CUSTOM_LEGEND_PLUGIN_NAME,
|
|
@@ -86,37 +86,6 @@ const generateAnnotations = (options, state) => {
|
|
|
86
86
|
return annotations;
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
-
/**
|
|
90
|
-
* @param {import('../components/bar-chart/bar-chart.interface').IBarChartOptions |
|
|
91
|
-
* import('../components/line-chart/line-chart.interface').ILineChartOptions} options - chart options object
|
|
92
|
-
* @return {{label1: {}}}
|
|
93
|
-
*/
|
|
94
|
-
const getLabelAnnotation = (options) => {
|
|
95
|
-
const { annotations = {} } = options;
|
|
96
|
-
const { labelAnnotation = {} } = annotations;
|
|
97
|
-
const isDarkModeOn = options.chartStyling.darkMode || false;
|
|
98
|
-
const {
|
|
99
|
-
content = [],
|
|
100
|
-
xAdjust = -200,
|
|
101
|
-
yAdjust = 120,
|
|
102
|
-
fontSize = 12,
|
|
103
|
-
} = labelAnnotation;
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
label1: {
|
|
107
|
-
type: 'label',
|
|
108
|
-
xAdjust,
|
|
109
|
-
yAdjust,
|
|
110
|
-
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
111
|
-
color: isDarkModeOn && 'rgba(255, 255, 255, 1)',
|
|
112
|
-
content,
|
|
113
|
-
font: {
|
|
114
|
-
size: fontSize,
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
};
|
|
119
|
-
|
|
120
89
|
/**
|
|
121
90
|
* @param {import('../components/bar-chart/bar-chart.interface').IBarChartOptions |
|
|
122
91
|
* import('../components/line-chart/line-chart.interface').ILineChartOptions} options - chart options object
|
|
@@ -124,20 +93,14 @@ const getLabelAnnotation = (options) => {
|
|
|
124
93
|
* @return {{annotations: []}}
|
|
125
94
|
*/
|
|
126
95
|
const getAnnotation = (options, state) => {
|
|
127
|
-
const {
|
|
128
|
-
const
|
|
129
|
-
const isAnnotationDataProvided = annotations?.annotationsData?.length;
|
|
96
|
+
const { showAnnotations, annotationsData } = options.annotations || {};
|
|
97
|
+
const shouldGenerateAnnotations = showAnnotations && annotationsData?.length;
|
|
130
98
|
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
...getLabelAnnotation(options),
|
|
135
|
-
...generateAnnotations(options, state),
|
|
136
|
-
}
|
|
137
|
-
: null;
|
|
99
|
+
const annotations = shouldGenerateAnnotations
|
|
100
|
+
? generateAnnotations(options, state)
|
|
101
|
+
: null;
|
|
138
102
|
|
|
139
|
-
return {
|
|
140
|
-
annotations: formAnnotation,
|
|
141
|
-
};
|
|
103
|
+
return { annotations };
|
|
142
104
|
};
|
|
105
|
+
|
|
143
106
|
export default getAnnotation;
|