@react-magma/charts 13.1.0 → 13.1.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/dist/charts.js +222 -114
- package/dist/charts.js.map +1 -1
- package/dist/charts.modern.module.js +222 -114
- package/dist/charts.modern.module.js.map +1 -1
- package/dist/charts.umd.js +604 -306
- package/dist/charts.umd.js.map +1 -1
- package/package.json +3 -3
- package/src/components/CarbonChart/CarbonChart.test.js +164 -0
- package/src/components/CarbonChart/CarbonChart.tsx +252 -119
- package/src/components/CarbonChart/CarbonChartArea.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartAreaStacked.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBar.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBarFloating.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBarGrouped.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBarStacked.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBoxplot.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBubble.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBullet.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartCombo.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartDonut.stories.tsx +3 -1
- package/src/components/CarbonChart/CarbonChartGauge.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartHistogram.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartLine.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartLollipop.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartMeter.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartPie.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartRadar.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartScatter.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartSparkline.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartStep.stories.tsx +1 -1
- package/src/components/ChartTable/ChartMoreOptionsButton.tsx +1 -0
- package/dist/components/ChartTable/ChartTable.test.d.ts +0 -1
- /package/src/components/ChartTable/{ChartTable.test.tsx → ChartTable.test.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-magma/charts",
|
|
3
|
-
"version": "13.1.
|
|
3
|
+
"version": "13.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"identity-obj-proxy": "^3.0.0",
|
|
48
48
|
"react": "^17.0.2",
|
|
49
49
|
"react-dom": "^17.0.2",
|
|
50
|
-
"react-magma-dom": "^4.
|
|
50
|
+
"react-magma-dom": "^4.14.0",
|
|
51
51
|
"react-magma-icons": "^3.2.5",
|
|
52
52
|
"rollup": "^4.52.4",
|
|
53
53
|
"rollup-plugin-postcss": "^4.0.2"
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@emotion/styled": "^11.13.0",
|
|
58
58
|
"react": "^17.0.2",
|
|
59
59
|
"react-dom": "^17.0.2",
|
|
60
|
-
"react-magma-dom": "^4.
|
|
60
|
+
"react-magma-dom": "^4.14.0",
|
|
61
61
|
"react-magma-icons": "^3.2.5"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
3
|
import { act, render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
4
5
|
import { ThemeContext, magma, DropdownMenuItem } from 'react-magma-dom';
|
|
5
6
|
|
|
6
7
|
import { CarbonChart, CarbonChartType } from '.';
|
|
@@ -588,6 +589,32 @@ describe('CarbonChart', () => {
|
|
|
588
589
|
}
|
|
589
590
|
);
|
|
590
591
|
});
|
|
592
|
+
|
|
593
|
+
it('should move focus back after closing the More options dropdown and chart toolbar', () => {
|
|
594
|
+
const testId = 'modal-footer-border-test';
|
|
595
|
+
const { getByTestId, getByText } = render(
|
|
596
|
+
<ThemeContext.Provider value={magma}>
|
|
597
|
+
<CarbonChart
|
|
598
|
+
testId={testId}
|
|
599
|
+
dataSet={dataSet}
|
|
600
|
+
options={chartOptions}
|
|
601
|
+
type={CarbonChartType.bar}
|
|
602
|
+
isInverse={false}
|
|
603
|
+
chartToolbar={{}}
|
|
604
|
+
/>
|
|
605
|
+
</ThemeContext.Provider>
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
const moreOptionsButton = getByTestId('chart-more-options-button');
|
|
609
|
+
userEvent.click(moreOptionsButton);
|
|
610
|
+
|
|
611
|
+
expect(getByText('Download as CSV')).toBeVisible();
|
|
612
|
+
expect(getByText('Download as PNG')).toBeVisible();
|
|
613
|
+
expect(getByText('Download as JPG')).toBeVisible();
|
|
614
|
+
|
|
615
|
+
userEvent.keyboard('{esc}');
|
|
616
|
+
expect(moreOptionsButton).toHaveFocus();
|
|
617
|
+
});
|
|
591
618
|
});
|
|
592
619
|
|
|
593
620
|
describe('Modal Focus Management', () => {
|
|
@@ -895,4 +922,141 @@ describe('CarbonChart', () => {
|
|
|
895
922
|
).toBeInTheDocument();
|
|
896
923
|
});
|
|
897
924
|
});
|
|
925
|
+
|
|
926
|
+
describe('dot keyboard accessibility', () => {
|
|
927
|
+
let rafCallbacks;
|
|
928
|
+
|
|
929
|
+
beforeEach(() => {
|
|
930
|
+
rafCallbacks = [];
|
|
931
|
+
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => {
|
|
932
|
+
rafCallbacks.push(cb);
|
|
933
|
+
return rafCallbacks.length - 1;
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
afterEach(() => {
|
|
938
|
+
window.requestAnimationFrame.mockRestore();
|
|
939
|
+
rafCallbacks = [];
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
function renderChart() {
|
|
943
|
+
const { getByTestId } = render(
|
|
944
|
+
<CarbonChart
|
|
945
|
+
testId="dot-tab-test"
|
|
946
|
+
dataSet={dataSet}
|
|
947
|
+
options={chartOptions}
|
|
948
|
+
type={CarbonChartType.scatter}
|
|
949
|
+
/>
|
|
950
|
+
);
|
|
951
|
+
return getByTestId('dot-tab-test');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function addDot(wrapper) {
|
|
955
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
956
|
+
const dot = document.createElementNS(
|
|
957
|
+
'http://www.w3.org/2000/svg',
|
|
958
|
+
'circle'
|
|
959
|
+
);
|
|
960
|
+
dot.classList.add('dot');
|
|
961
|
+
svg.appendChild(dot);
|
|
962
|
+
wrapper.appendChild(svg);
|
|
963
|
+
return dot;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
describe('tabbing', () => {
|
|
967
|
+
it('should stamp tabindex="0" on circle.dot elements so they are reachable by Tab', () => {
|
|
968
|
+
const wrapper = renderChart();
|
|
969
|
+
const dot = addDot(wrapper);
|
|
970
|
+
|
|
971
|
+
// The last captured RAF is the dots-stamping callback (no ariaLabel provided)
|
|
972
|
+
act(() => rafCallbacks[rafCallbacks.length - 1](0));
|
|
973
|
+
|
|
974
|
+
expect(dot).toHaveAttribute('tabindex', '0');
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it('should not overwrite an existing tabindex on circle.dot', () => {
|
|
978
|
+
const wrapper = renderChart();
|
|
979
|
+
const dot = addDot(wrapper);
|
|
980
|
+
dot.setAttribute('tabindex', '-1');
|
|
981
|
+
|
|
982
|
+
act(() => rafCallbacks[rafCallbacks.length - 1](0));
|
|
983
|
+
|
|
984
|
+
expect(dot).toHaveAttribute('tabindex', '-1');
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
describe('focus on dot content', () => {
|
|
989
|
+
it('should set opacity to 1 when a dot receives focus', () => {
|
|
990
|
+
const wrapper = renderChart();
|
|
991
|
+
const dot = addDot(wrapper);
|
|
992
|
+
|
|
993
|
+
fireEvent.focusIn(dot);
|
|
994
|
+
|
|
995
|
+
expect(dot.style.opacity).toBe('1');
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it('should dispatch mouseover on dot focusin to reveal tooltip data', () => {
|
|
999
|
+
const wrapper = renderChart();
|
|
1000
|
+
const dot = addDot(wrapper);
|
|
1001
|
+
|
|
1002
|
+
const mouseoverSpy = jest.fn();
|
|
1003
|
+
dot.addEventListener('mouseover', mouseoverSpy);
|
|
1004
|
+
|
|
1005
|
+
fireEvent.focusIn(dot);
|
|
1006
|
+
|
|
1007
|
+
expect(mouseoverSpy).toHaveBeenCalledTimes(1);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('should dispatch mousemove on dot focusin', () => {
|
|
1011
|
+
const wrapper = renderChart();
|
|
1012
|
+
const dot = addDot(wrapper);
|
|
1013
|
+
|
|
1014
|
+
const mousemoveSpy = jest.fn();
|
|
1015
|
+
dot.addEventListener('mousemove', mousemoveSpy);
|
|
1016
|
+
|
|
1017
|
+
fireEvent.focusIn(dot);
|
|
1018
|
+
|
|
1019
|
+
expect(mousemoveSpy).toHaveBeenCalledTimes(1);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
it('should not change opacity for non-dot circle elements on focusin', () => {
|
|
1023
|
+
const wrapper = renderChart();
|
|
1024
|
+
const nonDot = document.createElementNS(
|
|
1025
|
+
'http://www.w3.org/2000/svg',
|
|
1026
|
+
'circle'
|
|
1027
|
+
);
|
|
1028
|
+
wrapper.appendChild(nonDot);
|
|
1029
|
+
|
|
1030
|
+
fireEvent.focusIn(nonDot);
|
|
1031
|
+
|
|
1032
|
+
expect(nonDot.style.opacity).toBe('');
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
describe('data visibility', () => {
|
|
1037
|
+
it('should reset dot opacity when dot loses focus', () => {
|
|
1038
|
+
const wrapper = renderChart();
|
|
1039
|
+
const dot = addDot(wrapper);
|
|
1040
|
+
|
|
1041
|
+
fireEvent.focusIn(dot);
|
|
1042
|
+
expect(dot.style.opacity).toBe('1');
|
|
1043
|
+
|
|
1044
|
+
fireEvent.focusOut(dot);
|
|
1045
|
+
expect(dot.style.opacity).toBe('');
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it('should dispatch mouseout on dot focusout to hide tooltip', () => {
|
|
1049
|
+
const wrapper = renderChart();
|
|
1050
|
+
const dot = addDot(wrapper);
|
|
1051
|
+
|
|
1052
|
+
const mouseoutSpy = jest.fn();
|
|
1053
|
+
dot.addEventListener('mouseout', mouseoutSpy);
|
|
1054
|
+
|
|
1055
|
+
fireEvent.focusIn(dot);
|
|
1056
|
+
fireEvent.focusOut(dot);
|
|
1057
|
+
|
|
1058
|
+
expect(mouseoutSpy).toHaveBeenCalledTimes(1);
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
898
1062
|
});
|
|
@@ -490,6 +490,9 @@ const CarbonChartWrapper = styled.div<{
|
|
|
490
490
|
: props.theme.colors.focus} !important;
|
|
491
491
|
outline-offset: 0;
|
|
492
492
|
}
|
|
493
|
+
circle.dot:focus {
|
|
494
|
+
outline: none !important;
|
|
495
|
+
}
|
|
493
496
|
.cds--overflow-menu-options__btn:focus,
|
|
494
497
|
.cds--overflow-menu:focus,
|
|
495
498
|
.cds--overflow-menu__trigger:focus,
|
|
@@ -594,6 +597,17 @@ const CarbonChartWrapper = styled.div<{
|
|
|
594
597
|
overflow: visible;
|
|
595
598
|
}
|
|
596
599
|
|
|
600
|
+
circle.dot:focus {
|
|
601
|
+
outline: none;
|
|
602
|
+
stroke: ${props =>
|
|
603
|
+
props.isInverse
|
|
604
|
+
? props.theme.colors.focusInverse
|
|
605
|
+
: props.theme.colors.focus} !important;
|
|
606
|
+
stroke-width: 6px !important;
|
|
607
|
+
stroke-opacity: 1 !important;
|
|
608
|
+
paint-order: stroke fill;
|
|
609
|
+
}
|
|
610
|
+
|
|
597
611
|
.cds--cc--chart-wrapper text {
|
|
598
612
|
font-size: ${props => props.theme.typeScale.size02.fontSize};
|
|
599
613
|
}
|
|
@@ -637,6 +651,11 @@ interface ColorsObject {
|
|
|
637
651
|
[key: string]: string;
|
|
638
652
|
}
|
|
639
653
|
|
|
654
|
+
interface ExtendedChartOptions extends ChartOptions {
|
|
655
|
+
title?: string;
|
|
656
|
+
colors?: string[];
|
|
657
|
+
}
|
|
658
|
+
|
|
640
659
|
const ToolbarWrapper = styled.div<{
|
|
641
660
|
isFullscreen?: boolean;
|
|
642
661
|
isInverse?: boolean;
|
|
@@ -838,86 +857,124 @@ function downloadImage(
|
|
|
838
857
|
|
|
839
858
|
const svgRect = svg.getBoundingClientRect();
|
|
840
859
|
const legendItems = readLegendItems(wrapper);
|
|
841
|
-
const scale = 2;
|
|
842
860
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
let
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
861
|
+
const doWork = () => {
|
|
862
|
+
const scale = 2;
|
|
863
|
+
|
|
864
|
+
// Measure legend height
|
|
865
|
+
const tempCanvas = document.createElement('canvas');
|
|
866
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
867
|
+
const fontSize = 13 * scale;
|
|
868
|
+
const swatchSize = 12 * scale;
|
|
869
|
+
const gap = 8 * scale;
|
|
870
|
+
const itemGap = 16 * scale;
|
|
871
|
+
const paddingX = 16 * scale;
|
|
872
|
+
const canvasWidth = svgRect.width * scale;
|
|
873
|
+
|
|
874
|
+
let legendHeight = 0;
|
|
875
|
+
if (legendItems.length > 0 && tempCtx) {
|
|
876
|
+
tempCtx.font = `${fontSize}px sans-serif`;
|
|
877
|
+
let x = paddingX;
|
|
878
|
+
let rows = 1;
|
|
879
|
+
for (const item of legendItems) {
|
|
880
|
+
const textWidth = tempCtx.measureText(item.label).width;
|
|
881
|
+
const itemWidth = swatchSize + gap + textWidth + itemGap;
|
|
882
|
+
if (x + itemWidth > canvasWidth - paddingX && x > paddingX) {
|
|
883
|
+
x = paddingX;
|
|
884
|
+
rows++;
|
|
885
|
+
}
|
|
886
|
+
x += itemWidth;
|
|
864
887
|
}
|
|
865
|
-
|
|
888
|
+
legendHeight = rows * (fontSize + gap) + gap * 2;
|
|
866
889
|
}
|
|
867
|
-
legendHeight = rows * (fontSize + gap) + gap * 2;
|
|
868
|
-
}
|
|
869
890
|
|
|
870
|
-
|
|
871
|
-
|
|
891
|
+
const width = svgRect.width * scale;
|
|
892
|
+
const height = svgRect.height * scale + legendHeight;
|
|
872
893
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
894
|
+
const clone = svg.cloneNode(true) as SVGSVGElement;
|
|
895
|
+
clone.setAttribute('width', String(svgRect.width));
|
|
896
|
+
clone.setAttribute('height', String(svgRect.height));
|
|
897
|
+
clone.setAttribute('viewBox', `0 0 ${svgRect.width} ${svgRect.height}`);
|
|
898
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
878
899
|
|
|
879
|
-
|
|
900
|
+
inlineStyles(svg, clone);
|
|
880
901
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
902
|
+
const serializer = new XMLSerializer();
|
|
903
|
+
const svgString = serializer.serializeToString(clone);
|
|
904
|
+
const svgBlob = new Blob([svgString], {
|
|
905
|
+
type: 'image/svg+xml;charset=utf-8',
|
|
906
|
+
});
|
|
907
|
+
const url = URL.createObjectURL(svgBlob);
|
|
908
|
+
|
|
909
|
+
const mimeType = format === 'jpg' ? 'image/jpeg' : 'image/png';
|
|
910
|
+
const ext = format === 'jpg' ? 'jpg' : 'png';
|
|
911
|
+
|
|
912
|
+
const img = new Image();
|
|
913
|
+
img.onload = () => {
|
|
914
|
+
const canvas = document.createElement('canvas');
|
|
915
|
+
canvas.width = width;
|
|
916
|
+
canvas.height = height;
|
|
917
|
+
const ctx = canvas.getContext('2d');
|
|
918
|
+
if (!ctx) return;
|
|
919
|
+
|
|
920
|
+
ctx.fillStyle = '#ffffff';
|
|
921
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
922
|
+
ctx.drawImage(img, 0, 0, svgRect.width * scale, svgRect.height * scale);
|
|
923
|
+
URL.revokeObjectURL(url);
|
|
924
|
+
|
|
925
|
+
if (legendItems.length > 0) {
|
|
926
|
+
drawLegend(
|
|
927
|
+
ctx,
|
|
928
|
+
legendItems,
|
|
929
|
+
svgRect.height * scale + gap,
|
|
930
|
+
width,
|
|
931
|
+
scale
|
|
932
|
+
);
|
|
933
|
+
}
|
|
907
934
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
935
|
+
canvas.toBlob(blob => {
|
|
936
|
+
if (!blob) return;
|
|
937
|
+
const imgUrl = URL.createObjectURL(blob);
|
|
938
|
+
const a = document.createElement('a');
|
|
939
|
+
a.href = imgUrl;
|
|
940
|
+
a.download = `${title || 'chart'}.${ext}`;
|
|
941
|
+
a.click();
|
|
942
|
+
URL.revokeObjectURL(imgUrl);
|
|
943
|
+
}, mimeType);
|
|
944
|
+
};
|
|
945
|
+
img.onerror = () => URL.revokeObjectURL(url);
|
|
946
|
+
img.src = url;
|
|
917
947
|
};
|
|
918
|
-
|
|
948
|
+
|
|
949
|
+
// Defer to idle time with a 2-second deadline so it always runs.
|
|
950
|
+
if (typeof requestIdleCallback === 'function') {
|
|
951
|
+
requestIdleCallback(doWork, { timeout: 2000 });
|
|
952
|
+
} else {
|
|
953
|
+
setTimeout(doWork, 0);
|
|
954
|
+
}
|
|
919
955
|
}
|
|
920
956
|
|
|
957
|
+
const ALL_CHARTS: Record<CarbonChartType, React.ComponentType<any>> = {
|
|
958
|
+
area: AreaChart,
|
|
959
|
+
areaStacked: StackedAreaChart,
|
|
960
|
+
bar: SimpleBarChart,
|
|
961
|
+
barGrouped: GroupedBarChart,
|
|
962
|
+
barStacked: StackedBarChart,
|
|
963
|
+
donut: DonutChart,
|
|
964
|
+
line: LineChart,
|
|
965
|
+
lollipop: LollipopChart,
|
|
966
|
+
pie: PieChart,
|
|
967
|
+
radar: RadarChart,
|
|
968
|
+
boxplot: BoxplotChart,
|
|
969
|
+
bubble: BubbleChart,
|
|
970
|
+
bullet: BulletChart,
|
|
971
|
+
gauge: GaugeChart,
|
|
972
|
+
histogram: HistogramChart,
|
|
973
|
+
meter: MeterChart,
|
|
974
|
+
scatter: ScatterChart,
|
|
975
|
+
combo: ComboChart,
|
|
976
|
+
};
|
|
977
|
+
|
|
921
978
|
interface InternalToolbarProps {
|
|
922
979
|
config: ChartToolbarConfig;
|
|
923
980
|
dataSet: Array<Record<string, unknown>>;
|
|
@@ -1078,8 +1135,15 @@ export const CarbonChart = React.forwardRef<HTMLDivElement, CarbonChartProps>(
|
|
|
1078
1135
|
}
|
|
1079
1136
|
};
|
|
1080
1137
|
document.addEventListener('fullscreenchange', onFullscreenChange);
|
|
1081
|
-
return () =>
|
|
1138
|
+
return () => {
|
|
1082
1139
|
document.removeEventListener('fullscreenchange', onFullscreenChange);
|
|
1140
|
+
// Restore height if the component unmounts while in fullscreen.
|
|
1141
|
+
const chartHolder =
|
|
1142
|
+
internalRef.current?.querySelector<HTMLElement>('.cds--chart-holder');
|
|
1143
|
+
if (chartHolder && document.fullscreenElement) {
|
|
1144
|
+
chartHolder.style.height = savedHeightRef.current;
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1083
1147
|
}, [fullscreenEnabled]);
|
|
1084
1148
|
|
|
1085
1149
|
const openTableModal = React.useCallback(
|
|
@@ -1092,9 +1156,9 @@ export const CarbonChart = React.forwardRef<HTMLDivElement, CarbonChartProps>(
|
|
|
1092
1156
|
|
|
1093
1157
|
const closeTableModal = React.useCallback(() => {
|
|
1094
1158
|
setIsTableOpen(false);
|
|
1095
|
-
|
|
1159
|
+
requestAnimationFrame(() => {
|
|
1096
1160
|
lastTableTriggerRef.current?.focus();
|
|
1097
|
-
}
|
|
1161
|
+
});
|
|
1098
1162
|
}, []);
|
|
1099
1163
|
|
|
1100
1164
|
const toggleFullscreen = React.useCallback(() => {
|
|
@@ -1110,64 +1174,47 @@ export const CarbonChart = React.forwardRef<HTMLDivElement, CarbonChartProps>(
|
|
|
1110
1174
|
}, []);
|
|
1111
1175
|
|
|
1112
1176
|
const chartTitle: string =
|
|
1113
|
-
(options as
|
|
1177
|
+
(options as ExtendedChartOptions).title || toolbarI18n.defaultTitle;
|
|
1114
1178
|
|
|
1115
1179
|
const handleModalDownloadCsv = React.useCallback(() => {
|
|
1116
1180
|
downloadCsv(dataSet as Array<Record<string, unknown>>, chartTitle);
|
|
1117
1181
|
}, [dataSet, chartTitle]);
|
|
1118
1182
|
|
|
1119
1183
|
useCarbonModalFocusManagement(internalRef);
|
|
1120
|
-
const allCharts = {
|
|
1121
|
-
area: AreaChart,
|
|
1122
|
-
areaStacked: StackedAreaChart,
|
|
1123
|
-
bar: SimpleBarChart,
|
|
1124
|
-
barGrouped: GroupedBarChart,
|
|
1125
|
-
barStacked: StackedBarChart,
|
|
1126
|
-
donut: DonutChart,
|
|
1127
|
-
line: LineChart,
|
|
1128
|
-
lollipop: LollipopChart,
|
|
1129
|
-
pie: PieChart,
|
|
1130
|
-
radar: RadarChart,
|
|
1131
|
-
boxplot: BoxplotChart,
|
|
1132
|
-
bubble: BubbleChart,
|
|
1133
|
-
bullet: BulletChart,
|
|
1134
|
-
gauge: GaugeChart,
|
|
1135
|
-
histogram: HistogramChart,
|
|
1136
|
-
meter: MeterChart,
|
|
1137
|
-
scatter: ScatterChart,
|
|
1138
|
-
combo: ComboChart,
|
|
1139
|
-
};
|
|
1140
1184
|
|
|
1141
|
-
|
|
1185
|
+
const colorScale = React.useMemo(() => {
|
|
1142
1186
|
const scaleColorsObj: ColorsObject = {};
|
|
1143
|
-
|
|
1144
|
-
const allGroups = dataSet.map(item => {
|
|
1145
|
-
return 'group' in item ? item['group'] : null;
|
|
1146
|
-
});
|
|
1147
|
-
const uniqueGroups = allGroups.filter(
|
|
1148
|
-
(g, index) => allGroups.indexOf(g) === index
|
|
1149
|
-
);
|
|
1150
|
-
const customColors = ((options as any).colors as string[]) || [];
|
|
1187
|
+
const customColors = (options as ExtendedChartOptions).colors || [];
|
|
1151
1188
|
const allColors = [...customColors, ...theme.chartColors];
|
|
1152
1189
|
const allInverseColors = [...customColors, ...theme.chartColorsInverse];
|
|
1190
|
+
const allGroups = dataSet.map(item =>
|
|
1191
|
+
'group' in item ? (item as Record<string, unknown>)['group'] : null
|
|
1192
|
+
);
|
|
1193
|
+
const uniqueGroups = Array.from(new Set(allGroups));
|
|
1153
1194
|
|
|
1154
|
-
uniqueGroups.
|
|
1155
|
-
|
|
1156
|
-
|
|
1195
|
+
if (uniqueGroups.length <= allColors.length) {
|
|
1196
|
+
for (let i = 0; i < uniqueGroups.length; i++) {
|
|
1197
|
+
const group = uniqueGroups[i];
|
|
1198
|
+
scaleColorsObj[String(group ?? 'null')] = isInverse
|
|
1157
1199
|
? allInverseColors[i]
|
|
1158
|
-
: allColors[i]
|
|
1200
|
+
: allColors[i];
|
|
1159
1201
|
}
|
|
1160
|
-
|
|
1161
|
-
});
|
|
1202
|
+
}
|
|
1162
1203
|
|
|
1163
1204
|
return scaleColorsObj;
|
|
1164
|
-
}
|
|
1205
|
+
}, [
|
|
1206
|
+
dataSet,
|
|
1207
|
+
options,
|
|
1208
|
+
theme.chartColors,
|
|
1209
|
+
theme.chartColorsInverse,
|
|
1210
|
+
isInverse,
|
|
1211
|
+
]);
|
|
1165
1212
|
|
|
1166
1213
|
const newOptions = {
|
|
1167
1214
|
...options,
|
|
1168
1215
|
theme: isInverse ? ChartTheme.G100 : ChartTheme.WHITE,
|
|
1169
1216
|
color: {
|
|
1170
|
-
scale:
|
|
1217
|
+
scale: colorScale,
|
|
1171
1218
|
},
|
|
1172
1219
|
tooltip: {
|
|
1173
1220
|
...(options?.tooltip || {}),
|
|
@@ -1178,21 +1225,107 @@ export const CarbonChart = React.forwardRef<HTMLDivElement, CarbonChartProps>(
|
|
|
1178
1225
|
...(chartToolbar ? { toolbar: { enabled: false } } : {}),
|
|
1179
1226
|
};
|
|
1180
1227
|
|
|
1181
|
-
const ChartType =
|
|
1228
|
+
const ChartType = ALL_CHARTS[type];
|
|
1182
1229
|
|
|
1183
|
-
// Adding aria-label to main SVG container
|
|
1184
1230
|
React.useEffect(() => {
|
|
1185
|
-
if (ariaLabel)
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1231
|
+
if (!ariaLabel) return;
|
|
1232
|
+
const rafId = requestAnimationFrame(() => {
|
|
1233
|
+
const svgEl = internalRef.current?.querySelector('.graph-frame');
|
|
1234
|
+
if (!svgEl) return;
|
|
1235
|
+
svgEl.setAttribute('aria-label', ariaLabel);
|
|
1236
|
+
});
|
|
1237
|
+
return () => cancelAnimationFrame(rafId);
|
|
1238
|
+
}, [ariaLabel]);
|
|
1239
|
+
|
|
1240
|
+
// Make Carbon Charts data points keyboard-focusable.
|
|
1241
|
+
React.useEffect(() => {
|
|
1242
|
+
const wrapper = internalRef.current;
|
|
1243
|
+
if (!wrapper) return;
|
|
1244
|
+
|
|
1245
|
+
const isDot = (el: EventTarget | null): el is SVGCircleElement =>
|
|
1246
|
+
el instanceof Element &&
|
|
1247
|
+
el.nodeName.toLowerCase() === 'circle' &&
|
|
1248
|
+
el.classList.contains('dot');
|
|
1249
|
+
|
|
1250
|
+
const onFocusIn = (e: FocusEvent) => {
|
|
1251
|
+
if (!isDot(e.target)) return;
|
|
1252
|
+
const dot = e.target as SVGCircleElement;
|
|
1253
|
+
dot.style.opacity = '1';
|
|
1254
|
+
const { left, top, width, height } = dot.getBoundingClientRect();
|
|
1255
|
+
const cx = left + width / 2;
|
|
1256
|
+
const cy = top + height / 2;
|
|
1257
|
+
dot.dispatchEvent(
|
|
1258
|
+
new MouseEvent('mouseover', {
|
|
1259
|
+
bubbles: true,
|
|
1260
|
+
clientX: cx,
|
|
1261
|
+
clientY: cy,
|
|
1262
|
+
screenX: cx,
|
|
1263
|
+
screenY: cy,
|
|
1264
|
+
})
|
|
1265
|
+
);
|
|
1266
|
+
dot.dispatchEvent(
|
|
1267
|
+
new MouseEvent('mousemove', {
|
|
1268
|
+
bubbles: true,
|
|
1269
|
+
clientX: cx,
|
|
1270
|
+
clientY: cy,
|
|
1271
|
+
screenX: cx,
|
|
1272
|
+
screenY: cy,
|
|
1273
|
+
})
|
|
1274
|
+
);
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
const onFocusOut = (e: FocusEvent) => {
|
|
1278
|
+
if (!isDot(e.target)) return;
|
|
1279
|
+
const dot = e.target;
|
|
1280
|
+
dot.style.opacity = '';
|
|
1281
|
+
dot.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
1285
|
+
if (!isDot(e.target)) return;
|
|
1286
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
1287
|
+
e.preventDefault();
|
|
1288
|
+
const dot = e.target;
|
|
1289
|
+
const { left, top, width, height } = dot.getBoundingClientRect();
|
|
1290
|
+
const cx = left + width / 2;
|
|
1291
|
+
const cy = top + height / 2;
|
|
1292
|
+
dot.dispatchEvent(
|
|
1293
|
+
new MouseEvent('click', {
|
|
1294
|
+
bubbles: true,
|
|
1295
|
+
clientX: cx,
|
|
1296
|
+
clientY: cy,
|
|
1297
|
+
})
|
|
1298
|
+
);
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
const rafId = requestAnimationFrame(() => {
|
|
1302
|
+
wrapper
|
|
1303
|
+
.querySelectorAll<SVGCircleElement>('circle.dot')
|
|
1304
|
+
.forEach(dot => {
|
|
1305
|
+
if (!dot.hasAttribute('tabindex')) {
|
|
1306
|
+
dot.setAttribute('tabindex', '0');
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
});
|
|
1191
1310
|
|
|
1192
|
-
|
|
1311
|
+
wrapper.addEventListener('focusin', onFocusIn);
|
|
1312
|
+
wrapper.addEventListener('focusout', onFocusOut);
|
|
1313
|
+
wrapper.addEventListener('keydown', onKeyDown);
|
|
1314
|
+
|
|
1315
|
+
return () => {
|
|
1316
|
+
cancelAnimationFrame(rafId);
|
|
1317
|
+
wrapper.removeEventListener('focusin', onFocusIn);
|
|
1318
|
+
wrapper.removeEventListener('focusout', onFocusOut);
|
|
1319
|
+
wrapper.removeEventListener('keydown', onKeyDown);
|
|
1320
|
+
};
|
|
1321
|
+
}, [type]);
|
|
1322
|
+
|
|
1323
|
+
const groupsLength = Object.keys(colorScale).length;
|
|
1193
1324
|
|
|
1194
1325
|
const showTable = chartToolbar?.showAsTable !== false;
|
|
1195
1326
|
|
|
1327
|
+
if (!ChartType) return null;
|
|
1328
|
+
|
|
1196
1329
|
return (
|
|
1197
1330
|
<FullscreenRoot ref={mergedRef} isInverse={isInverse} theme={theme}>
|
|
1198
1331
|
<CarbonChartWrapper
|