diff --git a/src/components/Chart/__test__/chart.spec.js b/src/components/Chart/__test__/chart.spec.js index 9d1388fcc..48dec5595 100644 --- a/src/components/Chart/__test__/chart.spec.js +++ b/src/components/Chart/__test__/chart.spec.js @@ -8,6 +8,10 @@ jest.mock('../helpers/unregisterGlobalPlugins', () => jest.fn()); jest.mock('../chart', () => jest.fn(() => ({ + data: { + labels: [], + datasets: [], + }, update: jest.fn(), config: {}, })), @@ -25,18 +29,13 @@ describe('', () => { type: 'line', data: { labels: ['A', 'B', 'C', 'D'], - datasets: [expect.any(Object)], }, plugins: null, options: expect.any(Object), }); }); it('should call ChartJS update method to update the chart when the component changes', () => { - const component = mount( - - - , - ); + const component = mount(); const { chartInstance } = component.instance(); component.setProps({ labels: ['A', 'B', 'C', 'D', 'E'], @@ -46,16 +45,12 @@ describe('', () => { }); it('should call ChartJS update method to update the chart when the children changes', () => { - const component = mount( - - - , - ); + const component = mount(); const { chartInstance } = component.instance(); component.setProps({ children: [], }); - expect(chartInstance.update).toHaveBeenCalledTimes(1); + expect(chartInstance.update).toHaveBeenCalledTimes(2); }); it('should update the chart type when changed dynamically', () => { diff --git a/src/components/Chart/context.js b/src/components/Chart/context.js new file mode 100644 index 000000000..213cf3634 --- /dev/null +++ b/src/components/Chart/context.js @@ -0,0 +1,4 @@ +import React from 'react'; + +const ChartContext = React.createContext(); +export default ChartContext; diff --git a/src/components/Chart/index.js b/src/components/Chart/index.js index a73fec700..dfdb347d6 100644 --- a/src/components/Chart/index.js +++ b/src/components/Chart/index.js @@ -3,9 +3,10 @@ import PropTypes from 'prop-types'; import { withTheme } from 'styled-components'; import ChartJS from './chart'; import resolveOptions from './resolveOptions'; -import resolveDatasets from './resolveDatasets'; import StyledContainer from './styled/container'; +import DatasetContainer from './styled/datasetContainer'; import unregisterGlobalPlugins from './helpers/unregisterGlobalPlugins'; +import ChartContext from './context'; /** * A chart is a graphical representation of data. Charts allow users to better understand @@ -18,36 +19,48 @@ export class Chart extends Component { constructor(props) { super(props); this.chartRef = React.createRef(); - this.datasets = []; + this.datasets = {}; + this.registerDataset = this.registerDataset.bind(this); + this.unregisterDataset = this.unregisterDataset.bind(this); + this.updateDataset = this.updateDataset.bind(this); } componentDidMount() { - const { children } = this.props; - this.datasets = resolveDatasets(children); this.renderChart(); } componentDidUpdate() { - const { children } = this.props; - this.datasets = resolveDatasets(children); this.updateChart(); } updateChart() { const { labels, type, ...conditions } = this.props; - this.chartInstance.data = { - labels, - datasets: this.datasets, - }; + this.chartInstance.config.type = type; + this.chartInstance.data.labels = labels; + this.chartInstance.data.datasets = Object.values(this.datasets); + this.chartInstance.options = resolveOptions({ type, ...conditions }); + this.chartInstance.update(); + } - if (type && this.chartInstance.config && type !== this.chartInstance.config.type) { - this.chartInstance.config.type = type; - } + registerDataset(id, dataset) { + this.datasets[id] = dataset; + this.updateChart(); + } - this.chartInstance.options = resolveOptions({ type, ...conditions }); + unregisterDataset(id) { + const { [id]: remove, ...rest } = this.datasets; + this.datasets = rest; this.chartInstance.update(); } + updateDataset(id, dataset) { + const keys = Object.keys(dataset); + keys.forEach(key => { + this.datasets[id][key] = dataset[key]; + }); + this.updateChart(); + } + renderChart() { unregisterGlobalPlugins(ChartJS); const { type, labels, plugins, ...conditions } = this.props; @@ -57,7 +70,6 @@ export class Chart extends Component { type, data: { labels, - datasets: this.datasets, }, plugins: plugins || null, options: resolveOptions({ type, plugins, ...conditions }), @@ -65,12 +77,20 @@ export class Chart extends Component { } render() { - const { style, className } = this.props; + const { style, className, children } = this.props; + const context = { + registerDataset: this.registerDataset, + unregisterDataset: this.unregisterDataset, + updateDataset: this.updateDataset, + }; return ( - - - + + + + {children} + + ); } } diff --git a/src/components/Chart/styled/datasetContainer.js b/src/components/Chart/styled/datasetContainer.js new file mode 100644 index 000000000..5aed7e7d6 --- /dev/null +++ b/src/components/Chart/styled/datasetContainer.js @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +const StyledDatasetContainer = styled.div` + display: none; +`; + +export default StyledDatasetContainer; diff --git a/src/components/Dataset/__test__/dataset.spec.js b/src/components/Dataset/__test__/dataset.spec.js new file mode 100644 index 000000000..88b09d153 --- /dev/null +++ b/src/components/Dataset/__test__/dataset.spec.js @@ -0,0 +1,78 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import Dataset from '..'; + +const context = { + registerDataset: jest.fn(), + unregisterDataset: jest.fn(), + updateDataset: jest.fn(), +}; + +describe('', () => { + beforeEach(() => { + jest.resetAllMocks(); + React.useContext = jest.fn().mockReturnValue(context); + }); + + it('should call registerDataset with dataset props', () => { + mount( + , + ); + expect(context.registerDataset).toHaveBeenCalledWith(expect.any(String), { + label: 'Dataset 1', + data: [1, 2, 3], + backgroundColor: '#ccc', + borderColor: '#000', + stack: 'stack-1', + type: 'line', + fill: false, + }); + }); + + it('should call unregisterDataset when unmounted', () => { + const wrapper = mount( + , + ); + wrapper.unmount(); + expect(context.unregisterDataset).toHaveBeenCalledTimes(1); + }); + + it('should call updateDataset when props changes', () => { + const wrapper = mount( + , + ); + wrapper.setProps({ + values: [4, 5, 6], + }); + expect(context.updateDataset).toHaveBeenCalledWith(expect.any(String), { + label: 'Dataset 1', + data: [4, 5, 6], + backgroundColor: '#ccc', + borderColor: '#000', + stack: 'stack-1', + type: 'line', + fill: false, + }); + }); +}); diff --git a/src/components/Dataset/index.js b/src/components/Dataset/index.js index ac9aeb8fb..9f0de1e49 100644 --- a/src/components/Dataset/index.js +++ b/src/components/Dataset/index.js @@ -1,10 +1,44 @@ /* eslint-disable react/no-unused-prop-types */ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; +import ChartContext from '../Chart/context'; +import { useUniqueIdentifier } from '../../libs/hooks'; /** @category DataView */ -export default function Dataset() { - return
; +export default function Dataset(props) { + const datasetId = useUniqueIdentifier('dataset'); + const { title: label, values: data, ...rest } = props; + const { registerDataset, unregisterDataset, updateDataset } = React.useContext(ChartContext); + const isRegistered = useRef(); + + useEffect(() => { + if (isRegistered.current) { + updateDataset(datasetId, { + label, + data, + ...rest, + }); + } + }); + + useEffect(() => { + registerDataset(datasetId, { + label, + data, + ...rest, + }); + isRegistered.current = true; + + return () => { + if (isRegistered.current) { + unregisterDataset(datasetId); + isRegistered.current = false; + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return <>; } Dataset.propTypes = {