@ons/design-system 72.8.0 → 72.9.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.
- package/components/chart/_chart.scss +5 -0
- package/components/chart/_macro.njk +34 -30
- package/components/chart/_macro.spec.js +169 -0
- package/components/chart/annotations-options.js +1 -1
- package/components/chart/area-chart.js +26 -0
- package/components/chart/bar-chart.js +2 -2
- package/components/chart/chart-constants.js +84 -3
- package/components/chart/chart.js +38 -12
- package/components/chart/column-chart.js +33 -24
- package/components/chart/common-chart-options.js +6 -33
- package/components/chart/example-area-chart.njk +68 -0
- package/components/chart/example-column-with-line-chart.njk +3 -1
- package/components/chart/example-line-chart-with-markers.njk +138 -0
- package/components/chart/example-scatter-chart.njk +94 -0
- package/components/chart/line-chart.js +18 -23
- package/components/chart/scatter-chart.js +34 -0
- package/css/main.css +1 -1
- package/package.json +1 -1
- package/scripts/main.es5.js +1 -1
- package/scripts/main.js +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{% from "components/list/_macro.njk" import onsList %}
|
|
2
2
|
|
|
3
3
|
{% macro onsChart(params) %}
|
|
4
|
-
{% set supportedChartTypes = ['line', 'bar', 'column'] %}
|
|
4
|
+
{% set supportedChartTypes = ['line', 'bar', 'column', 'scatter', 'area'] %}
|
|
5
5
|
|
|
6
6
|
{% if params.headingLevel and params.headingLevel >= 1 and params.headingLevel <= 4 %}
|
|
7
7
|
{% set headingLevel = params.headingLevel %}
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
data-highcharts-title="{{ params.title }}"
|
|
23
23
|
data-highcharts-id="{{ params.id }}"
|
|
24
24
|
{% if params.useStackedLayout %}data-highcharts-use-stacked-layout="{{ params.useStackedLayout }}"{% endif %}
|
|
25
|
-
id="{{ params.id }}"
|
|
26
25
|
{% if params.percentageHeightDesktop and params.chartType != 'bar' %}
|
|
27
26
|
data-highcharts-percentage-height-desktop="{{ params.percentageHeightDesktop }}"
|
|
28
27
|
{% endif %}
|
|
@@ -42,32 +41,34 @@
|
|
|
42
41
|
data-highcharts-y-axis-tick-interval-desktop="{{ params.yAxis.tickIntervalDesktop }}"
|
|
43
42
|
{% endif %}
|
|
44
43
|
>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
<figure class="ons-chart">
|
|
45
|
+
{{ openingTitleTag | safe }} class="ons-chart__title">{{ params.title }}{{ closingTitleTag | safe }}
|
|
46
|
+
{{ openingSubtitleTag | safe }}
|
|
47
|
+
class="ons-chart__subtitle">{{ params.subtitle }}{{ closingSubtitleTag | safe }}
|
|
48
|
+
{% if params.description %}
|
|
49
|
+
<p class="ons-u-vh">{{ params.description }}</p>
|
|
50
|
+
{% endif %}
|
|
51
|
+
{% if params.chartType in supportedChartTypes %}
|
|
53
52
|
<div data-highcharts-chart></div>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
53
|
+
{% else %}
|
|
54
|
+
<p data-invalid-chart-type><b>Chart type "{{ params.chartType }}" is not supported</b></p>
|
|
55
|
+
{% endif %}
|
|
56
|
+
{#
|
|
57
|
+
Footnotes for the annotations at mobile
|
|
58
|
+
Hidden from screen readers because the full text will be read out where they appear in the chart
|
|
59
|
+
#}
|
|
60
|
+
<ul class="ons-chart__footnotes" aria-hidden="true">
|
|
61
|
+
{% for annotation in params.annotations %}
|
|
62
|
+
<li class="ons-chart__footnote_item">
|
|
63
|
+
<span class="ons-chart__footnote-number">{{ loop.index }}</span>
|
|
64
|
+
{{ annotation.text }}
|
|
65
|
+
</li>
|
|
66
|
+
{% endfor %}
|
|
67
|
+
</ul>
|
|
68
|
+
{% if params.caption %}
|
|
69
|
+
<figcaption class="ons-chart__caption">{{ params.caption }}</figcaption>
|
|
70
|
+
{% endif %}
|
|
71
|
+
</figure>
|
|
71
72
|
|
|
72
73
|
{% if params.download.title and params.download.itemsList | length > 0 %}
|
|
73
74
|
{{ openingDownloadTitleTag | safe }}
|
|
@@ -79,6 +80,7 @@
|
|
|
79
80
|
})
|
|
80
81
|
}}
|
|
81
82
|
{% endif %}
|
|
83
|
+
{% if params.chartType in supportedChartTypes %}
|
|
82
84
|
{% set series = [] %}
|
|
83
85
|
{% for item in params.series %}
|
|
84
86
|
{%
|
|
@@ -91,6 +93,7 @@
|
|
|
91
93
|
"dataLabels": {
|
|
92
94
|
"enabled": item.dataLabels if item.dataLabels else false
|
|
93
95
|
},
|
|
96
|
+
"connectNulls": item.connectNulls if item.connectNulls else false,
|
|
94
97
|
"type": item.type if item.type and item.type == 'line' else params.chartType
|
|
95
98
|
}
|
|
96
99
|
%}
|
|
@@ -126,9 +129,10 @@
|
|
|
126
129
|
|
|
127
130
|
<!-- Set scripts to pass the config values as json to the javascript -->
|
|
128
131
|
<!-- prettier-ignore-start -->
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
<script type="application/json" data-highcharts-config--{{ params.id }}>
|
|
133
|
+
{{ config | tojson }}
|
|
134
|
+
</script>
|
|
135
|
+
{% endif %}
|
|
132
136
|
{% if params.annotations %}
|
|
133
137
|
<script type="application/json" data-highcharts-annotations--{{ params.id }}>
|
|
134
138
|
{{ params.annotations | tojson }}
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
EXAMPLE_COLUMN_CHART_WITH_ANNOTATIONS_PARAMS,
|
|
16
16
|
EXAMPLE_BAR_WITH_LINE_CHART_PARAMS,
|
|
17
17
|
EXAMPLE_COLUMN_WITH_LINE_CHART_PARAMS,
|
|
18
|
+
EXAMPLE_SCATTER_CHART_PARAMS,
|
|
19
|
+
EXAMPLE_AREA_CHART_PARAMS,
|
|
20
|
+
EXAMPLE_INVALID_CHART_PARAMS,
|
|
18
21
|
} from './_test-examples';
|
|
19
22
|
|
|
20
23
|
describe('Macro: Chart', () => {
|
|
@@ -28,6 +31,10 @@ describe('Macro: Chart', () => {
|
|
|
28
31
|
expect(results).toHaveNoViolations();
|
|
29
32
|
});
|
|
30
33
|
|
|
34
|
+
test('THEN: it renders the title', () => {
|
|
35
|
+
expect($('.ons-chart__title').text()).toBe('Example Line Chart');
|
|
36
|
+
});
|
|
37
|
+
|
|
31
38
|
test('THEN: it renders the subtitle', () => {
|
|
32
39
|
expect($('.ons-chart__subtitle').text()).toBe('A sample subtitle');
|
|
33
40
|
});
|
|
@@ -153,6 +160,9 @@ describe('Macro: Chart', () => {
|
|
|
153
160
|
expect(configScript).toContain('"data":[5,15,25]');
|
|
154
161
|
expect(configScript).toContain('"name":"Category 2"');
|
|
155
162
|
expect(configScript).toContain('"data":[10,20,30]');
|
|
163
|
+
expect(configScript).toContain('"connectNulls":true');
|
|
164
|
+
expect(configScript).toContain('"marker":{"enabled":true}');
|
|
165
|
+
expect(configScript).toContain('"dataLabels":{"enabled":true}');
|
|
156
166
|
});
|
|
157
167
|
});
|
|
158
168
|
});
|
|
@@ -854,4 +864,163 @@ describe('Macro: Chart', () => {
|
|
|
854
864
|
});
|
|
855
865
|
});
|
|
856
866
|
});
|
|
867
|
+
|
|
868
|
+
describe('FOR: Scatter chart', () => {
|
|
869
|
+
describe('GIVEN: Params: required', () => {
|
|
870
|
+
describe('WHEN: required params are provided', () => {
|
|
871
|
+
const $ = cheerio.load(renderComponent('chart', EXAMPLE_SCATTER_CHART_PARAMS));
|
|
872
|
+
|
|
873
|
+
test('THEN: it passes jest-axe checks', async () => {
|
|
874
|
+
const results = await axe($.html());
|
|
875
|
+
expect(results).toHaveNoViolations();
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test('THEN: it renders the chart container with correct data attributes', () => {
|
|
879
|
+
expect($('[data-highcharts-base-chart]').attr('data-highcharts-type')).toBe('scatter');
|
|
880
|
+
expect($('[data-highcharts-base-chart]').attr('data-highcharts-theme')).toBe('primary');
|
|
881
|
+
expect($('[data-highcharts-base-chart]').attr('data-highcharts-title')).toBe('Example Scatter Chart');
|
|
882
|
+
expect($('[data-highcharts-base-chart]').attr('data-highcharts-id')).toBe('scatter-chart-123');
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
describe('FOR: Area Chart', () => {
|
|
889
|
+
describe('GIVEN: Params: required', () => {
|
|
890
|
+
describe('WHEN: required params are provided', () => {
|
|
891
|
+
const $ = cheerio.load(renderComponent('chart', EXAMPLE_AREA_CHART_PARAMS));
|
|
892
|
+
const configScript = $(`script[data-highcharts-config--area-chart-123]`).html();
|
|
893
|
+
|
|
894
|
+
test('THEN: it passes jest-axe checks', async () => {
|
|
895
|
+
const results = await axe($.html());
|
|
896
|
+
expect(results).toHaveNoViolations();
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
test('THEN: it includes at least one area series', () => {
|
|
900
|
+
expect(configScript).toContain('"type":"area"');
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
test('THEN: it renders the chart container with correct data attributes', () => {
|
|
904
|
+
expect($('[data-highcharts-base-chart]').attr('data-highcharts-type')).toBe('area');
|
|
905
|
+
expect($('[data-highcharts-base-chart]').attr('data-highcharts-theme')).toBe('primary');
|
|
906
|
+
expect($('[data-highcharts-base-chart]').attr('data-highcharts-title')).toBe('Example Area Chart');
|
|
907
|
+
expect($('[data-highcharts-base-chart]').attr('data-highcharts-id')).toBe('area-chart-123');
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
test('THEN: it includes the Highcharts JSON config', () => {
|
|
911
|
+
expect(configScript).toContain('"text":"X Axis Title"');
|
|
912
|
+
expect(configScript).toContain('"text":"Y Axis Title"');
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
describe('GIVEN: Params: Legend', () => {
|
|
918
|
+
describe('WHEN: legend is enabled', () => {
|
|
919
|
+
const $ = cheerio.load(renderComponent('chart', { ...EXAMPLE_AREA_CHART_PARAMS, legend: false }));
|
|
920
|
+
|
|
921
|
+
test('THEN: it renders the legend', () => {
|
|
922
|
+
const configScript = $(`script[data-highcharts-config--area-chart-123]`).html();
|
|
923
|
+
expect(configScript).toContain('"enabled":false');
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
describe('GIVEN: Params: Caption', () => {
|
|
929
|
+
describe('WHEN: caption is provided', () => {
|
|
930
|
+
const $ = cheerio.load(
|
|
931
|
+
renderComponent('chart', {
|
|
932
|
+
...EXAMPLE_AREA_CHART_PARAMS,
|
|
933
|
+
caption: 'This is an example caption for the chart.',
|
|
934
|
+
}),
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
test('THEN: it renders the caption when provided', () => {
|
|
938
|
+
expect($('figcaption').text()).toBe('This is an example caption for the chart.');
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
describe('GIVEN: Params: Description', () => {
|
|
944
|
+
describe('WHEN: description is provided', () => {
|
|
945
|
+
const $ = cheerio.load(
|
|
946
|
+
renderComponent('chart', {
|
|
947
|
+
...EXAMPLE_AREA_CHART_PARAMS,
|
|
948
|
+
description: 'An accessible description for screen readers.',
|
|
949
|
+
}),
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
test('THEN: it renders the description for accessibility', () => {
|
|
953
|
+
expect($('.ons-u-vh').text()).toBe('An accessible description for screen readers.');
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
describe('GIVEN: Params: Download', () => {
|
|
959
|
+
describe('WHEN: download object are provided', () => {
|
|
960
|
+
const $ = cheerio.load(
|
|
961
|
+
renderComponent('chart', {
|
|
962
|
+
...EXAMPLE_AREA_CHART_PARAMS,
|
|
963
|
+
download: {
|
|
964
|
+
title: 'Download Chart Data',
|
|
965
|
+
itemsList: [
|
|
966
|
+
{ text: 'Download as PNG', url: 'https://example.com/chart.png' },
|
|
967
|
+
{ text: 'Download as CSV', url: 'https://example.com/chart.csv' },
|
|
968
|
+
],
|
|
969
|
+
},
|
|
970
|
+
}),
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
test('THEN: it renders the download section correctly', () => {
|
|
974
|
+
expect($('.ons-chart__download-title').text()).toBe('Download Chart Data');
|
|
975
|
+
|
|
976
|
+
const downloadLinks = $('.ons-chart__download-title').next().find('li a');
|
|
977
|
+
expect(downloadLinks.eq(0).text()).toBe('Download as PNG');
|
|
978
|
+
expect(downloadLinks.eq(0).attr('href')).toBe('https://example.com/chart.png');
|
|
979
|
+
expect(downloadLinks.eq(1).text()).toBe('Download as CSV');
|
|
980
|
+
expect(downloadLinks.eq(1).attr('href')).toBe('https://example.com/chart.csv');
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
describe('FOR: Invalid Chart', () => {
|
|
987
|
+
describe('GIVEN: Invalid chart type', () => {
|
|
988
|
+
describe('WHEN: an invalid chart type is provided', () => {
|
|
989
|
+
const $ = cheerio.load(renderComponent('chart', EXAMPLE_INVALID_CHART_PARAMS));
|
|
990
|
+
|
|
991
|
+
test('THEN: it does not render the chart', () => {
|
|
992
|
+
expect($('[data-highcharts-chart]').length).toBe(0);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
test('THEN: it renders the error message', () => {
|
|
996
|
+
expect($('[data-invalid-chart-type]').text()).toBe('Chart type "invalid" is not supported');
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
test('THEN: it does not include the Highcharts JSON config', () => {
|
|
1000
|
+
const configScript = $(`script[data-highcharts-config--invalid-chart-123]`).html();
|
|
1001
|
+
expect(configScript).toBeNull();
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test('THEN: it still renders the title', () => {
|
|
1005
|
+
expect($('.ons-chart__title').text()).toBe('Example Invalid Chart');
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
test('THEN: it still renders the subtitle', () => {
|
|
1009
|
+
expect($('.ons-chart__subtitle').text()).toBe('A sample subtitle');
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
test('THEN: it still renders the description', () => {
|
|
1013
|
+
expect($('.ons-u-vh').text()).toBe('A detailed description');
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
test('THEN: it still renders the caption', () => {
|
|
1017
|
+
expect($('figcaption').text()).toBe('A detailed caption');
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
test('THEN: it still renders the download', () => {
|
|
1021
|
+
expect($('.ons-chart__download-title').text()).toBe('Download this chart');
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
857
1026
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class AreaChart {
|
|
2
|
+
getAreaChartOptions = () => {
|
|
3
|
+
return {
|
|
4
|
+
legend: {
|
|
5
|
+
symbolWidth: 12,
|
|
6
|
+
symbolHeight: 12,
|
|
7
|
+
},
|
|
8
|
+
plotOptions: {
|
|
9
|
+
area: {
|
|
10
|
+
fillOpacity: 1,
|
|
11
|
+
// Use a circle instead of the default blob plus line icon
|
|
12
|
+
// 'rectangle' counterintuitively gives a circle, because the legend icon has a border radius of half it's height by default
|
|
13
|
+
legendSymbol: 'rectangle',
|
|
14
|
+
stacking: 'normal',
|
|
15
|
+
},
|
|
16
|
+
series: {
|
|
17
|
+
marker: {
|
|
18
|
+
enabled: false,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default AreaChart;
|
|
@@ -26,7 +26,7 @@ class BarChart {
|
|
|
26
26
|
// The design system does not include a semibold font weight, so we use 700 (bold) as an alternative.
|
|
27
27
|
fontWeight: '700',
|
|
28
28
|
color: this.constants.labelColor,
|
|
29
|
-
fontSize: this.constants.
|
|
29
|
+
fontSize: this.constants.defaultFontSize,
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
32
|
},
|
|
@@ -119,7 +119,7 @@ class BarChart {
|
|
|
119
119
|
// The design system does not include a semibold font weight, so we use 700 (bold) as an alternative.
|
|
120
120
|
fontWeight: '700',
|
|
121
121
|
color: this.constants.labelColor,
|
|
122
|
-
fontSize: this.constants.
|
|
122
|
+
fontSize: this.constants.defaultFontSize,
|
|
123
123
|
},
|
|
124
124
|
});
|
|
125
125
|
|
|
@@ -9,9 +9,90 @@ class ChartConstants {
|
|
|
9
9
|
categoryLabelColor: '#414042',
|
|
10
10
|
gridLineColor: '#d9d9d9',
|
|
11
11
|
zeroLineColor: '#b3b3b3',
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
defaultFontSize: '0.875rem', // 14px
|
|
13
|
+
lineMarkerStyles: [
|
|
14
|
+
{
|
|
15
|
+
radius: 4,
|
|
16
|
+
symbol: 'circle',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
radius: 4,
|
|
20
|
+
symbol: 'square',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
radius: 5,
|
|
24
|
+
symbol: 'diamond',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
radius: 5,
|
|
28
|
+
symbol: 'triangle',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
radius: 5,
|
|
32
|
+
symbol: 'triangle-down',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
radius: 4,
|
|
36
|
+
symbol: 'circle',
|
|
37
|
+
fillColor: 'white',
|
|
38
|
+
lineWidth: 2.5,
|
|
39
|
+
lineColor: null,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
radius: 4,
|
|
43
|
+
symbol: 'square',
|
|
44
|
+
fillColor: 'white',
|
|
45
|
+
lineWidth: 2.5,
|
|
46
|
+
lineColor: null,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
radius: 5,
|
|
50
|
+
symbol: 'diamond',
|
|
51
|
+
fillColor: 'white',
|
|
52
|
+
lineWidth: 2.5,
|
|
53
|
+
lineColor: null,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
radius: 5,
|
|
57
|
+
symbol: 'triangle',
|
|
58
|
+
fillColor: 'white',
|
|
59
|
+
lineWidth: 2.5,
|
|
60
|
+
lineColor: null,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
radius: 5,
|
|
64
|
+
symbol: 'triangle-down',
|
|
65
|
+
fillColor: 'white',
|
|
66
|
+
lineWidth: 2.5,
|
|
67
|
+
lineColor: null,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
scatterMarkerStyles: [
|
|
71
|
+
{
|
|
72
|
+
radius: 5,
|
|
73
|
+
symbol: 'circle',
|
|
74
|
+
lineWidth: 1,
|
|
75
|
+
lineColor: '#ffffff',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
radius: 5,
|
|
79
|
+
symbol: 'square',
|
|
80
|
+
lineWidth: 1,
|
|
81
|
+
lineColor: '#ffffff',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
radius: 6,
|
|
85
|
+
symbol: 'diamond',
|
|
86
|
+
lineWidth: 1,
|
|
87
|
+
lineColor: '#ffffff',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
radius: 6,
|
|
91
|
+
symbol: 'triangle',
|
|
92
|
+
lineWidth: 1,
|
|
93
|
+
lineColor: '#ffffff',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
15
96
|
};
|
|
16
97
|
|
|
17
98
|
return constants;
|
|
@@ -7,7 +7,9 @@ import SpecificChartOptions from './specific-chart-options';
|
|
|
7
7
|
import LineChart from './line-chart';
|
|
8
8
|
import BarChart from './bar-chart';
|
|
9
9
|
import ColumnChart from './column-chart';
|
|
10
|
+
import ScatterChart from './scatter-chart';
|
|
10
11
|
import AnnotationsOptions from './annotations-options';
|
|
12
|
+
import AreaChart from './area-chart';
|
|
11
13
|
|
|
12
14
|
class HighchartsBaseChart {
|
|
13
15
|
static selector() {
|
|
@@ -19,6 +21,10 @@ class HighchartsBaseChart {
|
|
|
19
21
|
this.chartType = this.node.dataset.highchartsType;
|
|
20
22
|
this.theme = this.node.dataset.highchartsTheme;
|
|
21
23
|
const chartNode = this.node.querySelector('[data-highcharts-chart]');
|
|
24
|
+
if (!chartNode) {
|
|
25
|
+
console.error('No chart node found');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
22
28
|
this.id = this.node.dataset.highchartsId;
|
|
23
29
|
this.useStackedLayout = this.node.hasAttribute('data-highcharts-use-stacked-layout');
|
|
24
30
|
this.config = JSON.parse(this.node.querySelector(`[data-highcharts-config--${this.id}]`).textContent);
|
|
@@ -37,6 +43,8 @@ class HighchartsBaseChart {
|
|
|
37
43
|
this.lineChart = new LineChart();
|
|
38
44
|
this.barChart = new BarChart();
|
|
39
45
|
this.columnChart = new ColumnChart();
|
|
46
|
+
this.areaChart = new AreaChart();
|
|
47
|
+
this.scatterChart = new ScatterChart();
|
|
40
48
|
this.extraLines = this.checkForExtraLines();
|
|
41
49
|
if (window.isCommonChartOptionsDefined === undefined) {
|
|
42
50
|
this.setCommonChartOptions();
|
|
@@ -100,7 +108,9 @@ class HighchartsBaseChart {
|
|
|
100
108
|
const specificChartOptions = this.specificChartOptions.getOptions();
|
|
101
109
|
const lineChartOptions = this.lineChart.getLineChartOptions();
|
|
102
110
|
const barChartOptions = this.barChart.getBarChartOptions(this.useStackedLayout);
|
|
103
|
-
const columnChartOptions = this.columnChart.getColumnChartOptions(this.useStackedLayout);
|
|
111
|
+
const columnChartOptions = this.columnChart.getColumnChartOptions(this.config, this.useStackedLayout, this.extraLines);
|
|
112
|
+
const areaChartOptions = this.areaChart.getAreaChartOptions();
|
|
113
|
+
const scatterChartOptions = this.scatterChart.getScatterChartOptions();
|
|
104
114
|
// Merge specificChartOptions with the existing config
|
|
105
115
|
this.config = this.mergeConfigs(this.config, specificChartOptions);
|
|
106
116
|
|
|
@@ -117,6 +127,14 @@ class HighchartsBaseChart {
|
|
|
117
127
|
// Merge the column chart options with the existing config
|
|
118
128
|
this.config = this.mergeConfigs(this.config, columnChartOptions);
|
|
119
129
|
}
|
|
130
|
+
if (this.chartType === 'area') {
|
|
131
|
+
// Merge the area chart options with the existing config
|
|
132
|
+
this.config = this.mergeConfigs(this.config, areaChartOptions);
|
|
133
|
+
}
|
|
134
|
+
if (this.chartType === 'scatter') {
|
|
135
|
+
// Merge the scatter chart options with the existing config
|
|
136
|
+
this.config = this.mergeConfigs(this.config, scatterChartOptions);
|
|
137
|
+
}
|
|
120
138
|
|
|
121
139
|
if (this.extraLines > 0) {
|
|
122
140
|
this.config = this.mergeConfigs(this.config, this.lineChart.getLineChartOptions());
|
|
@@ -142,27 +160,34 @@ class HighchartsBaseChart {
|
|
|
142
160
|
// Note this is not the same as the viewport width
|
|
143
161
|
// All responsive rules should be defined here to avoid overriding existing rules
|
|
144
162
|
setResponsiveOptions = () => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this.
|
|
148
|
-
|
|
163
|
+
let mobileChartOptions = this.commonChartOptions.getMobileOptions(this.xAxisTickIntervalMobile, this.yAxisTickIntervalMobile);
|
|
164
|
+
if (this.chartType === 'column') {
|
|
165
|
+
const mobileColumnChartOptions = this.columnChart.getColumnChartMobileOptions(
|
|
166
|
+
this.config,
|
|
167
|
+
this.useStackedLayout,
|
|
168
|
+
this.extraLines,
|
|
169
|
+
);
|
|
170
|
+
mobileChartOptions = this.mergeConfigs(mobileChartOptions, mobileColumnChartOptions);
|
|
171
|
+
}
|
|
172
|
+
|
|
149
173
|
if (!this.config.responsive) {
|
|
150
174
|
this.config.responsive = {};
|
|
151
175
|
}
|
|
152
|
-
|
|
176
|
+
|
|
153
177
|
let rules = [
|
|
154
178
|
{
|
|
155
179
|
condition: {
|
|
156
180
|
maxWidth: 400,
|
|
157
181
|
},
|
|
158
182
|
chartOptions: {
|
|
159
|
-
...
|
|
183
|
+
...mobileChartOptions,
|
|
160
184
|
},
|
|
161
185
|
},
|
|
162
|
-
// We are using a slightly wider breakpoint for annotations
|
|
163
|
-
// to try and reduce the likelihood of them being automatically
|
|
164
|
-
// hidden by Highcharts
|
|
165
186
|
{
|
|
187
|
+
// If these conditions change, the styling for the footnotes container query in _chart.scss needs to be updated
|
|
188
|
+
// We are using a slightly wider breakpoint for annotations
|
|
189
|
+
// to try and reduce the likelihood of them being automatically
|
|
190
|
+
// hidden by Highcharts
|
|
166
191
|
condition: {
|
|
167
192
|
maxWidth: 600,
|
|
168
193
|
},
|
|
@@ -203,9 +228,11 @@ class HighchartsBaseChart {
|
|
|
203
228
|
}
|
|
204
229
|
}
|
|
205
230
|
if (this.chartType === 'column') {
|
|
206
|
-
this.columnChart.updatePointPadding(this.config, currentChart, this.useStackedLayout, this.extraLines);
|
|
207
231
|
this.commonChartOptions.hideDataLabels(currentChart.series);
|
|
208
232
|
}
|
|
233
|
+
if (this.chartType === 'scatter') {
|
|
234
|
+
this.scatterChart.updateMarkers(currentChart);
|
|
235
|
+
}
|
|
209
236
|
if (this.chartType != 'bar') {
|
|
210
237
|
this.commonChartOptions.adjustChartHeight(currentChart, this.percentageHeightDesktop, this.percentageHeightMobile);
|
|
211
238
|
}
|
|
@@ -215,7 +242,6 @@ class HighchartsBaseChart {
|
|
|
215
242
|
if (this.extraLines > 0) {
|
|
216
243
|
currentChart.series.forEach((series) => {
|
|
217
244
|
if (series.type === 'line') {
|
|
218
|
-
this.lineChart.updateLastPointMarker([series]);
|
|
219
245
|
this.commonChartOptions.hideDataLabels([series]);
|
|
220
246
|
}
|
|
221
247
|
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
class ColumnChart {
|
|
2
|
-
getColumnChartOptions = (useStackedLayout) => {
|
|
2
|
+
getColumnChartOptions = (config, useStackedLayout, extraLines) => {
|
|
3
3
|
return {
|
|
4
4
|
plotOptions: {
|
|
5
5
|
column: {
|
|
6
|
-
|
|
7
|
-
groupPadding: 0,
|
|
6
|
+
...this.getPointPadding(config, useStackedLayout, extraLines, false),
|
|
8
7
|
borderRadius: 0,
|
|
9
8
|
borderWidth: 0,
|
|
10
9
|
},
|
|
@@ -15,33 +14,43 @@ class ColumnChart {
|
|
|
15
14
|
};
|
|
16
15
|
};
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
getColumnChartMobileOptions = (config, useStackedLayout, extraLines) => {
|
|
18
|
+
return {
|
|
19
|
+
plotOptions: {
|
|
20
|
+
column: {
|
|
21
|
+
...this.getPointPadding(config, useStackedLayout, extraLines, true),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Set spacing between bars based on screen size and number of categories:
|
|
28
|
+
// - For charts with enough categories (≥ 10 on mobile, ≥ 20 on desktop), use 10% spacing (0.1), i.e. 20% total gap between bars
|
|
29
|
+
// - For charts with fewer categories, use 20% spacing (0.2), i.e. 40% total gap
|
|
30
|
+
// - For non-clustered or stacked charts, spacing is applied as pointPadding
|
|
31
|
+
// - For clustered charts, spacing is applied as groupPadding
|
|
32
|
+
// - Max bar width: 75px (desktop), 55px (mobile)
|
|
33
|
+
getPointPadding = (config, stackedLayout, numberOfExtraLines, isMobile) => {
|
|
34
|
+
const numberOfCategories = config.xAxis.categories ? config.xAxis.categories.length : 0;
|
|
35
|
+
const numberOfSeries = config.series.length - numberOfExtraLines; // Get number of column series
|
|
36
|
+
|
|
37
|
+
const categoryThreshold = isMobile ? 10 : 20;
|
|
38
|
+
const maxPointWidth = isMobile ? 55 : 75;
|
|
39
|
+
|
|
24
40
|
let pointPadding = 0;
|
|
25
41
|
let groupPadding = 0;
|
|
42
|
+
let spacing = numberOfCategories >= categoryThreshold ? 0.1 : 0.2;
|
|
43
|
+
|
|
26
44
|
// non-clustered charts or stacked charts
|
|
27
45
|
if (numberOfSeries === 1 || stackedLayout === true) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} else {
|
|
34
|
-
// clustered charts
|
|
35
|
-
groupPadding = 0.04;
|
|
46
|
+
pointPadding = spacing;
|
|
47
|
+
}
|
|
48
|
+
// clustered charts
|
|
49
|
+
else {
|
|
50
|
+
groupPadding = spacing;
|
|
36
51
|
}
|
|
37
52
|
|
|
38
|
-
|
|
39
|
-
currentChart.series.forEach((series) => {
|
|
40
|
-
series.update({
|
|
41
|
-
pointPadding: pointPadding,
|
|
42
|
-
groupPadding: groupPadding,
|
|
43
|
-
});
|
|
44
|
-
});
|
|
53
|
+
return { pointPadding: pointPadding, groupPadding: groupPadding, maxPointWidth: maxPointWidth };
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
|