@oliasoft-open-source/charts-library 2.13.5 → 2.14.0

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 (33) hide show
  1. package/package.json +2 -2
  2. package/release-notes.md +3 -0
  3. package/src/components/controls/axes-options/axes-options.jsx +25 -4
  4. package/src/components/line-chart/{line-chart-consts.js → constants/line-chart-consts.js} +1 -0
  5. package/src/components/line-chart/controls/axes-options/action-types.js +5 -0
  6. package/src/components/{controls → line-chart/controls}/axes-options/axes-options-form-state.js +18 -19
  7. package/src/components/{controls → line-chart/controls}/controls.jsx +88 -33
  8. package/src/components/line-chart/controls/drag-options.jsx +104 -0
  9. package/src/components/line-chart/controls/legend-options.jsx +32 -0
  10. package/src/components/{controls → line-chart/controls}/line-options.jsx +14 -5
  11. package/src/components/line-chart/hooks/use-chart-functions.js +257 -0
  12. package/src/components/line-chart/hooks/use-chart-options.js +155 -0
  13. package/src/components/line-chart/hooks/use-chart-plugins.js +26 -0
  14. package/src/components/line-chart/hooks/use-toggle-handler.js +33 -0
  15. package/src/components/line-chart/line-chart.jsx +49 -410
  16. package/src/components/line-chart/state/initial-state.js +7 -8
  17. package/src/components/line-chart/state/line-chart-reducer.js +68 -86
  18. package/src/components/line-chart/state/use-chart-state.js +2 -1
  19. package/src/components/line-chart/{axis-scales → utils/axis-scales}/axis-scales.js +3 -3
  20. package/src/components/line-chart/{datalabels-alignment → utils/datalabels-alignment}/get-datalabels-position.js +1 -1
  21. package/src/components/line-chart/utils/generate-line-chart-datasets.js +108 -0
  22. package/src/components/line-chart/{get-line-chart-data-labels.js → utils/get-line-chart-data-labels.js} +2 -2
  23. package/src/components/line-chart/{get-line-chart-scales.js → utils/get-line-chart-scales.js} +11 -11
  24. package/src/components/line-chart/{get-line-chart-tooltips.js → utils/get-line-chart-tooltips.js} +6 -3
  25. package/src/helpers/chart-utils.js +3 -5
  26. package/src/helpers/enums.js +9 -0
  27. package/src/components/controls/drag-options.jsx +0 -98
  28. package/src/components/controls/legend-options.jsx +0 -25
  29. /package/src/components/{controls → line-chart/controls}/controls.module.less +0 -0
  30. /package/src/components/line-chart/{datalabels-alignment → utils/datalabels-alignment}/get-alignment-condition.js +0 -0
  31. /package/src/components/line-chart/{datalabels-alignment → utils/datalabels-alignment}/get-alignment-data.js +0 -0
  32. /package/src/components/line-chart/{get-axes-ranges-from-chart.js → utils/get-axes-ranges-from-chart.js} +0 -0
  33. /package/src/components/line-chart/{line-chart-utils.js → utils/line-chart-utils.js} +0 -0
@@ -1,4 +1,4 @@
1
- import React, { useReducer, useRef, useState } from 'react';
1
+ import React, { useMemo, useReducer, useRef } from 'react';
2
2
  import {
3
3
  CategoryScale,
4
4
  Chart as ChartJS,
@@ -16,63 +16,18 @@ import zoomPlugin from 'chartjs-plugin-zoom';
16
16
  import dataLabelsPlugin from 'chartjs-plugin-datalabels';
17
17
  import annotationPlugin from 'chartjs-plugin-annotation';
18
18
  import dragDataPlugin from 'chartjs-plugin-dragdata';
19
- import { triggerBase64Download } from 'react-base64-downloader';
20
19
  import styles from './line-chart.module.less';
21
20
  import { reducer } from './state/line-chart-reducer';
22
21
  import initialState from './state/initial-state';
23
- import {
24
- DISABLE_DRAG_OPTIONS,
25
- RESET_AXES_RANGES,
26
- TOGGLE_ANNOTATION,
27
- TOGGLE_DRAG_POINTS,
28
- TOGGLE_LEGEND,
29
- TOGGLE_LINE,
30
- TOGGLE_PAN,
31
- TOGGLE_POINTS,
32
- TOGGLE_TABLE,
33
- TOGGLE_ZOOM,
34
- UPDATE_AXES_RANGES,
35
- } from './state/action-types';
36
- import Controls from '../controls/controls';
22
+ import Controls from './controls/controls';
37
23
  import { getDefaultProps, LineChartPropTypes } from './line-chart-prop-types';
38
- import getLineChartToolTips from './get-line-chart-tooltips';
39
- import getLineChartDataLabels from './get-line-chart-data-labels';
40
- import {
41
- BORDER_JOIN_STYLE,
42
- DEFAULT_BACKGROUND_COLOR,
43
- DEFAULT_BORDER_WIDTH,
44
- DEFAULT_HOVER_RADIUS,
45
- DEFAULT_LINE_POINT_RADIUS,
46
- DEFAULT_POINT_RADIUS,
47
- ZOOM_BOX_BACKGROUND_COLOR,
48
- } from './line-chart-consts';
49
-
50
- import getAnnotation from '../../helpers/get-chart-annotation';
51
- import {
52
- generateRandomColor,
53
- getChartFileName,
54
- getClassName,
55
- getLegend,
56
- getPlugins,
57
- setDefaultTheme,
58
- } from '../../helpers/chart-utils';
59
- import {
60
- ANIMATION_DURATION,
61
- AUTO,
62
- BORDER_COLOR,
63
- COLORS,
64
- CUSTOM_LEGEND_PLUGIN_NAME,
65
- } from '../../helpers/chart-consts';
66
- import {
67
- AxisType,
68
- ChartHoverMode,
69
- Key,
70
- PanZoomMode,
71
- PointStyle,
72
- } from '../../helpers/enums';
73
- import getDraggableData from '../../helpers/get-draggableData';
74
- import { generateAxisId, generateKey } from './line-chart-utils';
75
- import { autoScale } from './axis-scales/axis-scales';
24
+ import { getClassName, setDefaultTheme } from '../../helpers/chart-utils';
25
+ import { AUTO } from '../../helpers/chart-consts';
26
+ import { generateLineChartDatasets } from './utils/generate-line-chart-datasets';
27
+ import { useChartFunctions } from './hooks/use-chart-functions';
28
+ import { useChartOptions } from './hooks/use-chart-options';
29
+ import { useChartPlugins } from './hooks/use-chart-plugins';
30
+ import { generateKey } from './utils/line-chart-utils';
76
31
  import { useChartState } from './state/use-chart-state';
77
32
 
78
33
  ChartJS.register(
@@ -98,21 +53,15 @@ ChartJS.register(
98
53
  const LineChart = (props) => {
99
54
  setDefaultTheme();
100
55
  const chartRef = useRef(null);
101
- const [hoveredPoint, setHoveredPoint] = useState(null);
56
+ const { table } = props;
102
57
  const chart = getDefaultProps(props);
103
- const { options, testId, persistenceId } = chart;
104
- const { headerComponent, subheaderComponent, table } = props;
105
58
  const {
106
- additionalAxesOptions,
107
- annotations,
108
- axes,
109
- chartStyling,
110
- graph,
111
- interactions,
112
- legend,
113
- depthType,
114
- dragData,
115
- } = options;
59
+ data: { datasets },
60
+ options,
61
+ testId,
62
+ persistenceId,
63
+ } = chart;
64
+ const { annotations, axes, chartStyling, graph } = options;
116
65
 
117
66
  /**
118
67
  * @type {[object, import('react').Dispatch<{type: String, payload: any}>]} useReducer
@@ -126,241 +75,39 @@ const LineChart = (props) => {
126
75
  initialState,
127
76
  );
128
77
 
129
- // Call the custom hook.
130
- useChartState({ chartRef, options, state, dispatch, persistenceId });
131
-
132
- const generateLineChartDatasets = (datasets) => {
133
- const copyDataset = [...datasets];
134
-
135
- // Add annotations to dataset to have them appear in legend.
136
- if (
137
- annotations.controlAnnotation &&
138
- annotations.showAnnotations &&
139
- annotations.annotationsData?.length
140
- ) {
141
- annotations.annotationsData.forEach((annotation, index) => {
142
- const color = annotation.color || COLORS[index];
143
- copyDataset.push({
144
- isAnnotation: true,
145
- label: annotation.label,
146
- annotationIndex: index,
147
- backgroundColor: color,
148
- pointBackgroundColor: color,
149
- borderColor: color,
150
- data: [{}],
151
- });
152
- });
153
- }
154
-
155
- const generatedDatasets = copyDataset.map((line, i) => {
156
- if (line.formation) {
157
- const axesMin = state.axes[0]?.min;
158
- const axesMax = state.axes[0]?.max;
159
- // line with formation flag has 3 points: start point, mid-point with label, and end point.
160
- const [startPoint, midPointWithLabel, endPoint] = line.data;
161
-
162
- if (axesMin && startPoint?.x) {
163
- line.data[0].x = axesMin < startPoint?.x ? axesMin : startPoint?.x;
164
- }
165
-
166
- if (axesMax && endPoint?.x) {
167
- line.data[2].x = axesMax > endPoint?.x ? axesMax : endPoint?.x;
168
- }
169
- }
170
- /*
171
- Remove invalid falsy data points OW-9855
172
- Points should be an object of {x, y} pairs
173
- This is an extra guard to prevent crashes if parent apps pass bad inputs
174
- */
175
- line.data = line?.data?.filter(Boolean) || [];
176
-
177
- line.showLine = state.lineEnabled;
178
- const linePointRadius = line.pointRadius
179
- ? parseFloat(line.pointRadius)
180
- : DEFAULT_POINT_RADIUS;
181
- const pointHoverRadius = line.pointHoverRadius
182
- ? parseFloat(line.pointHoverRadius)
183
- : DEFAULT_HOVER_RADIUS;
184
- const indexedColor = COLORS[i];
185
-
186
- return {
187
- ...line,
188
- lineTension: graph.lineTension,
189
- spanGaps: graph.spanGaps,
190
- borderWidth: parseFloat(line.borderWidth) || DEFAULT_BORDER_WIDTH,
191
- borderDash: line.borderDash || [],
192
- borderJoinStyle: BORDER_JOIN_STYLE,
193
- borderColor:
194
- line.borderColor || indexedColor || generateRandomColor(COLORS),
195
- backgroundColor: line.backgroundColor || DEFAULT_BACKGROUND_COLOR,
196
- pointBackgroundColor:
197
- line.pointBackgroundColor ||
198
- indexedColor ||
199
- generateRandomColor(COLORS),
200
- pointRadius:
201
- state.pointsEnabled === true
202
- ? linePointRadius
203
- : DEFAULT_LINE_POINT_RADIUS,
204
- pointHoverRadius,
205
- pointHitRadius: line.pointHitRadius || pointHoverRadius,
206
- };
207
- });
208
- return generatedDatasets;
209
- };
210
-
211
- const generatedDatasets = generateLineChartDatasets(chart.data.datasets);
212
-
213
- const legendClick = (e, legendItem) => {
214
- const index = legendItem.datasetIndex;
215
- const chartInstance = chartRef.current;
216
- const { datasets } = chartInstance.data;
217
- const dataset = datasets[index];
218
- const meta = chartInstance.getDatasetMeta(index);
219
- meta.hidden = meta.hidden === null ? !dataset.hidden : null;
220
-
221
- if (annotations.controlAnnotation && dataset.isAnnotation) {
222
- const { annotationIndex } = dataset;
223
- dispatch({ type: TOGGLE_ANNOTATION, payload: { annotationIndex } });
224
- }
225
-
226
- // Show/hide entire display group
227
- if (dataset.displayGroup) {
228
- datasets.forEach((ds, ix) => {
229
- if (ds.displayGroup !== dataset.displayGroup) {
230
- return;
231
- }
232
- chartInstance.getDatasetMeta(ix).hidden = meta.hidden;
233
- });
234
- }
78
+ const generatedDatasets = useMemo(() => {
79
+ return generateLineChartDatasets(datasets, state, options);
80
+ }, [state.lineEnabled, state.pointsEnabled, axes, annotations, graph]);
235
81
 
236
- if (interactions.onLegendClick) {
237
- interactions.onLegendClick(legendItem?.text, legendItem.hidden);
238
- }
239
-
240
- chartInstance.update();
241
- };
242
-
243
- const resetZoom = () => {
244
- const chartInstance = chartRef.current;
245
- chartInstance.resetZoom();
246
- dispatch({ type: RESET_AXES_RANGES });
247
- };
248
-
249
- const onHover = (evt, hoveredItems) => {
250
- if (!hoveredItems?.length && interactions.onPointUnhover && hoveredPoint) {
251
- setHoveredPoint(null);
252
- interactions.onPointUnhover(evt);
253
- }
254
-
255
- if (hoveredItems?.length && interactions.onPointHover) {
256
- const { index, datasetIndex } = hoveredItems[0];
257
- const dataset = generatedDatasets[datasetIndex];
258
- const point = dataset?.data[index];
259
-
260
- if (point && hoveredPoint !== point) {
261
- setHoveredPoint(point);
262
- interactions.onPointHover(evt, datasetIndex, index, generatedDatasets);
263
- }
264
- }
265
- };
266
-
267
- const handleDownload = () => {
268
- const chart = chartRef.current;
269
- // Add temporary canvas background
270
- const { ctx } = chart;
271
- ctx.save();
272
- ctx.globalCompositeOperation = 'destination-over';
273
- ctx.fillStyle = 'white';
274
- ctx.fillRect(0, 0, chart.width, chart.height);
275
- ctx.restore();
276
-
277
- const base64Image = chart.toBase64Image();
278
- const fileName = getChartFileName(state.axes);
279
-
280
- triggerBase64Download(base64Image, fileName);
281
- };
282
-
283
- const handleKeyDown = (evt) => {
284
- if (evt.key === Key.Shift) {
285
- const chart = chartRef.current;
286
- chart.config.options.plugins.zoom.zoom.mode = PanZoomMode.Y;
287
- chart.config.options.plugins.zoom.pan.mode = PanZoomMode.Y;
288
- chart.update();
289
- }
290
- };
291
-
292
- const handleKeyUp = (evt) => {
293
- if (evt.key === Key.Shift) {
294
- const chart = chartRef.current;
295
- chart.config.options.plugins.zoom.zoom.mode = PanZoomMode.Z;
296
- chart.config.options.plugins.zoom.pan.mode = PanZoomMode.Z;
297
- chart.update();
298
- }
299
- };
82
+ // Call the custom hooks.
83
+ useChartState({ chartRef, options, state, dispatch, persistenceId });
300
84
 
301
- const getControlsAxes = () => {
302
- return state.axes.map((axis, i) => {
303
- const axisType = i ? AxisType.Y : AxisType.X; // only first element is 'x' - rest is 'y'
304
- const min = axis.min ?? additionalAxesOptions?.range?.[axisType]?.min;
305
- const max = axis.max ?? additionalAxesOptions?.range?.[axisType]?.max;
306
- // we need to get all axes element to array to set unit.
307
- const axesArray = axes.x.concat(axes.y);
308
- const unit = axesArray?.[i]?.unit;
309
- return {
310
- ...axis,
311
- //only add min and max properties if they are defined:
312
- ...(min ? { min } : {}),
313
- ...(max ? { max } : {}),
314
- ...(unit ? { unit } : {}),
315
- };
85
+ const { resetZoom, handleDownload, handleKeyDown, handleKeyUp } =
86
+ useChartFunctions({
87
+ chartRef,
88
+ state,
89
+ options,
90
+ dispatch,
91
+ generatedDatasets,
316
92
  });
317
- };
318
- const controlsAxes = getControlsAxes(axes);
319
-
320
- const getControlsAxesLabels = (propsAxes) => {
321
- if (!Object.keys(propsAxes)?.length) {
322
- return [];
323
- }
324
-
325
- const getAxesLabels = (axes, axisType) => {
326
- if (!axes[axisType] || !axes[axisType]?.length) {
327
- return [];
328
- } else {
329
- return axes[axisType].map((axisObj, index) => {
330
- return {
331
- id: generateAxisId(axisType, index, axes[axisType].length > 1),
332
- label: axisObj?.label || '',
333
- };
334
- });
335
- }
336
- };
337
93
 
338
- const axesLabels = [
339
- ...getAxesLabels(propsAxes, AxisType.X),
340
- ...getAxesLabels(propsAxes, AxisType.Y),
341
- ];
342
- return axesLabels;
343
- };
94
+ const useOptions = useChartOptions({
95
+ chartRef,
96
+ state,
97
+ options,
98
+ dispatch,
99
+ generatedDatasets,
100
+ });
344
101
 
345
- const controlsAxesLabels = getControlsAxesLabels(props.chart.options.axes);
346
-
347
- const updateAxesRangesFromChart = (chart) => {
348
- const { scales = {} } = chart || {};
349
- const axes = Object.entries(scales).map(([key, { min, max }]) => {
350
- return {
351
- id: key,
352
- min: min ?? 0,
353
- max: max ?? 0,
354
- };
355
- });
356
- dispatch({
357
- type: UPDATE_AXES_RANGES,
358
- payload: { axes },
359
- });
360
- };
102
+ const usePlugins = useChartPlugins({ options, resetZoom });
361
103
 
362
104
  return (
363
105
  <div
106
+ key={generateKey([
107
+ state.enableDragPoints,
108
+ state.zoomEnabled,
109
+ state.panEnabled,
110
+ ])}
364
111
  className={getClassName(chartStyling, styles)}
365
112
  style={{
366
113
  width: chartStyling.width || AUTO,
@@ -372,132 +119,24 @@ const LineChart = (props) => {
372
119
  data-testid={testId}
373
120
  >
374
121
  <Controls
375
- axes={controlsAxes}
376
- controlsAxesLabels={controlsAxesLabels}
377
- chart={chart}
378
- headerComponent={headerComponent}
379
- legendEnabled={state.legendEnabled}
380
- lineEnabled={state.lineEnabled}
381
- onDownload={handleDownload}
382
- onResetAxes={() => {
383
- dispatch({ type: RESET_AXES_RANGES });
384
- }}
385
- onUpdateAxes={({ axes }) => {
386
- dispatch({ type: UPDATE_AXES_RANGES, payload: { axes } });
387
- }}
388
- onToggleLegend={() => dispatch({ type: TOGGLE_LEGEND })}
389
- onToggleLine={() => dispatch({ type: TOGGLE_LINE })}
390
- onTogglePan={() => dispatch({ type: TOGGLE_PAN })}
391
- onTogglePoints={() => dispatch({ type: TOGGLE_POINTS })}
392
- onToggleTable={() => dispatch({ type: TOGGLE_TABLE })}
393
- onToggleZoom={() => dispatch({ type: TOGGLE_ZOOM })}
394
- panEnabled={state.panEnabled}
395
- pointsEnabled={state.pointsEnabled}
396
- initialAxesRanges={state.initialAxesRanges}
397
- showTable={state.showTable}
398
- subheaderComponent={subheaderComponent}
399
- table={table}
400
- zoomEnabled={state.zoomEnabled}
401
- depthType={depthType}
402
- enableDragPoints={state.enableDragPoints}
403
- isDragDataAllowed={dragData.enableDragData}
404
- onToggleDragPoints={() => dispatch({ type: TOGGLE_DRAG_POINTS })}
405
- onDisableDragOptions={() => dispatch({ type: DISABLE_DRAG_OPTIONS })}
122
+ props={props}
123
+ chartRef={chartRef}
124
+ state={state}
125
+ options={options}
126
+ dispatch={dispatch}
127
+ generatedDatasets={generatedDatasets}
406
128
  />
407
129
  {table && state.showTable ? (
408
130
  <div className={styles.table}>{table}</div>
409
131
  ) : (
410
132
  <div className={styles.canvas}>
411
133
  <Line
412
- key={generateKey([
413
- state.enableDragPoints,
414
- state.zoomEnabled,
415
- state.panEnabled,
416
- ])}
417
134
  ref={chartRef}
418
135
  data={{
419
136
  datasets: generatedDatasets,
420
137
  }}
421
- options={{
422
- onHover,
423
- maintainAspectRatio: chartStyling.maintainAspectRatio,
424
- aspectRatio: chartStyling.squareAspectRatio ? 1 : null, // 1 equals square aspect ratio
425
- animation: chartStyling.performanceMode
426
- ? false
427
- : {
428
- duration: ANIMATION_DURATION.FAST,
429
- onComplete: interactions.onAnimationComplete,
430
- },
431
- hover: {
432
- mode: ChartHoverMode.Nearest,
433
- intersect: true,
434
- },
435
- elements: {
436
- line: {
437
- pointStyle: PointStyle.Circle,
438
- showLine: state.lineEnabled,
439
- },
440
- },
441
- scales: autoScale(options, state, generatedDatasets),
442
- plugins: {
443
- // title: getTitle(options),
444
- datalabels: getLineChartDataLabels(options),
445
- annotation: getAnnotation(options, state),
446
- zoom: {
447
- pan: {
448
- enabled: state.panEnabled,
449
- mode: PanZoomMode.XY,
450
- onPanComplete({ chart }) {
451
- updateAxesRangesFromChart(chart);
452
- },
453
- },
454
- zoom: {
455
- mode: PanZoomMode.XY,
456
- drag: {
457
- enabled: state.zoomEnabled,
458
- threshold: 3,
459
- backgroundColor: ZOOM_BOX_BACKGROUND_COLOR,
460
- borderColor: BORDER_COLOR,
461
- borderWidth: 1,
462
- },
463
- onZoomComplete({ chart }) {
464
- updateAxesRangesFromChart(chart);
465
- },
466
- },
467
- },
468
- tooltip: getLineChartToolTips(options),
469
- legend: getLegend(options, legendClick, state),
470
- [CUSTOM_LEGEND_PLUGIN_NAME]: options.legend.customLegend
471
- .customLegendPlugin && {
472
- containerID:
473
- options.legend.customLegend.customLegendContainerID,
474
- },
475
- chartAreaBorder: {
476
- borderColor: BORDER_COLOR,
477
- },
478
- ...(state.enableDragPoints ? getDraggableData(options) : {}),
479
- },
480
- events: [
481
- 'mousemove',
482
- 'mouseout',
483
- 'click',
484
- 'touchstart',
485
- 'touchmove',
486
- 'dblclick',
487
- ],
488
- }}
489
- plugins={[
490
- ...getPlugins(graph, legend, state),
491
- {
492
- id: 'customEventCatcher',
493
- beforeEvent(chart, args, pluginOptions) {
494
- const { event } = args;
495
- if (event.type === 'dblclick') {
496
- resetZoom();
497
- }
498
- },
499
- },
500
- ]}
138
+ options={useOptions}
139
+ plugins={usePlugins}
501
140
  />
502
141
  </div>
503
142
  )}
@@ -1,24 +1,23 @@
1
1
  import { AxisType } from '../../../helpers/enums';
2
2
  import { setAnnotations } from '../../../helpers/chart-utils';
3
- import { generateAxisId } from '../line-chart-utils';
3
+ import { generateAxisId } from '../utils/line-chart-utils';
4
4
  import { getChartStateFromStorage } from './manage-state-in-local-storage';
5
5
 
6
6
  /**
7
-
8
7
  Initial chart state for the line chart.
8
+
9
9
  @param {Object} options - The chart options.
10
10
  @return {Object} The initial chart state.
11
11
  */
12
12
  const initialState = ({ options, persistenceId }) => {
13
13
  const {
14
14
  additionalAxesOptions: { range: customAxesRange },
15
- annotations,
15
+ annotations: { annotationsData },
16
16
  axes,
17
- chartOptions,
18
- legend,
17
+ chartOptions: { enableZoom, enablePan, showPoints, showLine },
18
+ legend: { display },
19
19
  dragData,
20
20
  } = options;
21
- const { enableZoom, enablePan, showPoints, showLine } = chartOptions;
22
21
  /**
23
22
  * getStateAxesByType
24
23
  * @param {'x'|'y'} axisType
@@ -79,9 +78,9 @@ const initialState = ({ options, persistenceId }) => {
79
78
  panEnabled: panEnabled ?? enablePan,
80
79
  pointsEnabled: pointsEnabled ?? showPoints,
81
80
  lineEnabled: lineEnabled ?? showLine,
82
- legendEnabled: legendEnabled ?? legend.display,
81
+ legendEnabled: legendEnabled ?? display,
83
82
  axes: stateAxes,
84
- showAnnotationLineIndex: setAnnotations(annotations.annotationsData),
83
+ showAnnotationLineIndex: setAnnotations(annotationsData),
85
84
  showTable: false,
86
85
  enableDragPoints: dragData.enableDragData && enableDragPoints,
87
86
  };