diff --git a/index.html b/index.html index 8815971..e487a57 100644 --- a/index.html +++ b/index.html @@ -18,7 +18,6 @@ labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [ { - label: '# of Votes', data: [12, 19, 3, 5, 2, 3], backgroundColor: [ 'rgba(255, 99, 132, 0.2)', @@ -59,6 +58,23 @@ ], }; + const DATA_NATIVE_3 = { + datasets: [{ + data: { + January: 10, + February: 20, + March: 30 + } + }] + }; + + const DATA_NATIVE_4 = { + labels: ['Red', 'Blue', 'Yellow'], + datasets: [{ + data: [{"x": 10, "y": 15}, {"x": 15, "y": 25}, {"x": 20, "y": 10}] + }] + }; + const OPTIONS_NATIVE_1_A = { geochart: { chart: "line" @@ -89,15 +105,15 @@ xSlider: { display: true, min: 0, - max: 30, - value: 15, + max: 100, + value: 50, track: 'normal', }, ySlider: { display: true, min: 0, - max: 30, - value: 30, + max: 100, + value: 100, track: 'normal', } } @@ -118,15 +134,15 @@ xSlider: { display: true, min: 0, - max: 30, - value: 15, + max: 100, + value: 50, track: 'normal', }, ySlider: { display: true, min: 0, - max: 30, - value: 30, + max: 100, + value: 100, track: 'normal', }, xAxis: { @@ -144,13 +160,15 @@
- CHART INPUTS + GEOCHART INPUTS
- - + + + +
diff --git a/src/Globals.d.ts b/src/Globals.d.ts deleted file mode 100644 index a38a891..0000000 --- a/src/Globals.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare module '*.module.css'; -declare module '*.module.scss'; diff --git a/src/chart-types.ts b/src/chart-types.ts index b0155f4..71b7648 100644 --- a/src/chart-types.ts +++ b/src/chart-types.ts @@ -13,6 +13,15 @@ export interface GeoChartData> = ChartDataset; +/** + * Indicates an action to be performed by the Chart. + * Special type that allows the child component a accept a 'todo action' via props and reset the prop value without the parent being notified. + * This is essentially to simplify the setTimeout handling to be managed inside the Chart component instead of higher in the application. + */ +export type GeoChartAction = { + shouldRedraw?: boolean; +}; + /** * Extends the ChartOptions used by Chart.js with more 'GeoChart' options */ diff --git a/src/chart-validator.ts b/src/chart-validator.ts index cb7d0d2..45247e1 100644 --- a/src/chart-validator.ts +++ b/src/chart-validator.ts @@ -16,13 +16,86 @@ export class ChartValidator { private ajv: Ajv.Ajv; public SCHEMA_DATA = { + $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { - labels: { type: 'array' }, - datasets: { type: 'array' }, + labels: { + type: 'array', + items: { + type: 'string', + }, + }, + datasets: { + type: 'array', + items: { + type: 'object', + properties: { + label: { + type: 'string', + }, + data: { + oneOf: [ + { + type: 'array', + items: { + type: 'number', + }, + }, + { + type: 'array', + items: { + type: 'object', + properties: { + x: { + type: 'number', + }, + y: { + type: 'number', + }, + }, + required: ['x', 'y'], + }, + }, + { + type: 'object', + }, + ], + }, + backgroundColor: { + oneOf: [ + { + type: 'string', + }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], + }, + borderColor: { + oneOf: [ + { + type: 'string', + }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], + }, + borderWidth: { + type: 'integer', + }, + }, + required: ['data'], + }, + }, }, - required: ['labels', 'datasets'], - // additionalProperties: false + required: ['datasets'], }; public SCHEMA_OPTIONS = { @@ -43,12 +116,15 @@ export class ChartValidator { geochart: { type: 'object', properties: { - chart: { type: 'string' }, + chart: { + enum: ['line', 'bar', 'pie', 'doughnut'], + default: 'line', + description: 'Supported types of chart.', + }, }, }, }, required: ['geochart'], - // additionalProperties: false }; /** diff --git a/src/chart.module.css b/src/chart.module.css deleted file mode 100644 index 17dfd9d..0000000 --- a/src/chart.module.css +++ /dev/null @@ -1,25 +0,0 @@ -.chartContainer { - display: grid; - width: 100%; -} - -.chartContainerGrid1 { - grid-column: 1; - grid-row: 1; - height: 100%; -} - -.chartContainerGrid2 { - grid-column: 2; - grid-row: 1; -} - -.chartContainerGrid3 { - grid-column: 1; - grid-row: 2; -} - -.chartContainerGrid4 { - grid-column: 2; - grid-row: 2; -} diff --git a/src/chart.tsx b/src/chart.tsx index 0e25c2b..6a074ab 100644 --- a/src/chart.tsx +++ b/src/chart.tsx @@ -1,27 +1,50 @@ +/* eslint-disable no-console */ +// TODO: Remove the disable above import { Box } from '@mui/material'; -import { Chart as ChartJS, ChartData, ChartOptions, DefaultDataPoint } from 'chart.js'; -import { GeoChartOptions, GeoChartType, GeoChartData, GeoChartDefaultColors } from './chart-types'; +import { + Chart as ChartJS, + ChartDataset, + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Title, + Tooltip, + ArcElement, +} from 'chart.js'; +import { Chart as ChartReact } from 'react-chartjs-2'; +import { GeoChartOptions, GeoChartType, GeoChartData, GeoChartAction, GeoChartDefaultColors } from './chart-types'; import { ChartValidator, ValidatorResult } from './chart-validator'; -import { ChartDoughnut } from './charts/chart-doughnut'; -import { ChartBarsVertical } from './charts/chart-bars-vertical'; -import { ChartPie } from './charts/chart-pie'; -import { ChartLine } from './charts/chart-line'; -import styles from './chart.module.css'; /** * Main props for the Chart */ export interface TypeChartChartProps { - style?: unknown; + style?: unknown; // Will be casted as CSSProperties later via the imported cgpv react defaultColors?: GeoChartDefaultColors; data?: GeoChartData; options?: GeoChartOptions; - redraw?: boolean; + action?: GeoChartAction; handleSliderXChanged?: (value: number | number[]) => void; handleSliderYChanged?: (value: number | number[]) => void; handleError?: (dataErrors: ValidatorResult, optionsErrors: ValidatorResult) => void; } +/** + * SX Classes for the Chart + */ +const sxClasses = { + checkDatasetWrapper: { + display: 'inline-block', + }, + checkDataset: { + display: 'inline-flex', + verticalAlign: 'middle', + marginRight: '20px !important', + }, +}; + /** * Create a customized Chart UI * @@ -33,14 +56,15 @@ export function Chart(props: TypeChartChartProps): JSX.Element { // eslint-disable-next-line @typescript-eslint/no-explicit-any const w = window as any; const { cgpv } = w; - const { CSSProperties } = cgpv.react; - const { Slider } = cgpv.ui.elements; - const { style: elStyle, data, options: elOptions, redraw } = props; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { useEffect, useState, useRef, CSSProperties } = cgpv.react; + const { Grid, Checkbox, Slider, Typography } = cgpv.ui.elements; + const { style: elStyle, data, options: elOptions, action: elAction } = props; // Cast the style const style = elStyle as typeof CSSProperties; - // Attribute the default colors + // Attribute the ChartJS default colors if (props.defaultColors?.backgroundColor) ChartJS.defaults.backgroundColor = props.defaultColors?.backgroundColor; if (props.defaultColors?.borderColor) ChartJS.defaults.borderColor = props.defaultColors?.borderColor; if (props.defaultColors?.color) ChartJS.defaults.color = props.defaultColors?.color; @@ -63,6 +87,25 @@ export function Chart(props: TypeChartChartProps): JSX.Element { } } + // STATE / REF SECTION ******* + const [redraw, setRedraw] = useState(elAction?.shouldRedraw); + const chartRef = useRef(null); + // const [selectedDatasets, setSelectedDatasets] = useState(); + + // If redraw is true, reset the property, set the redraw property to true for the chart, then prep a timer to reset it to false after the redraw has happened. + // A bit funky, but as documented online. + if (elAction?.shouldRedraw) { + elAction!.shouldRedraw = false; + setRedraw(true); + setTimeout(() => { + setRedraw(false); + }, 200); + } + + /** + * Handles when the X Slider changes + * @param value number | number[] Indicates the slider value + */ const handleSliderXChange = (value: number | number[]) => { // If callback set if (props.handleSliderXChanged) { @@ -70,6 +113,10 @@ export function Chart(props: TypeChartChartProps): JSX.Element { } }; + /** + * Handles when the Y Slider changes + * @param value number | number[] Indicates the slider value + */ const handleSliderYChange = (value: number | number[]) => { // If callback set if (props.handleSliderYChanged) { @@ -77,56 +124,37 @@ export function Chart(props: TypeChartChartProps): JSX.Element { } }; + /** + * Handles when a dataset was checked/unchecked (via the legend) + * @param datasetIndex number Indicates the dataset index that was checked/unchecked + * @param checked boolean Indicates the checked state + */ + const handleDatasetChecked = (datasetIndex: number, checked: boolean) => { + // Toggle visibility of the dataset + chartRef.current.setDatasetVisibility(datasetIndex, checked); + chartRef.current.update(); + }; + /** * Renders the Chart JSX.Element itself using Line as default * @returns The Chart JSX.Element itself using Line as default */ const renderChart = (): JSX.Element => { // Depending on the type of chart - switch (options!.geochart.chart) { + switch (options.geochart.chart) { case 'bar': - // Vertical Bars Chart - return ( - , string>} - options={options as ChartOptions<'bar'>} - redraw={redraw} - /> - ); + return ; case 'pie': - // Pie Chart - return ( - , string>} - options={options as ChartOptions<'pie'>} - redraw={redraw} - /> - ); + return ; case 'doughnut': // Doughnut Chart - return ( - } - options={options as ChartOptions<'doughnut'>} - redraw={redraw} - /> - ); + return ; default: // Line Chart is default - return ( - , string>} - options={options as ChartOptions<'line'>} - redraw={redraw} - /> - ); + return ; } }; @@ -135,7 +163,7 @@ export function Chart(props: TypeChartChartProps): JSX.Element { * @returns The X Chart Slider JSX.Element or an empty div */ const renderXSlider = (): JSX.Element => { - const { xSlider } = options!.geochart; + const { xSlider } = options.geochart; if (xSlider?.display) { return ( @@ -159,7 +187,7 @@ export function Chart(props: TypeChartChartProps): JSX.Element { * @returns The Y Chart Slider JSX.Element or an empty div */ const renderYSlider = (): JSX.Element => { - const { ySlider } = options!.geochart; + const { ySlider } = options.geochart; if (ySlider?.display) { return ( @@ -175,6 +203,46 @@ export function Chart(props: TypeChartChartProps): JSX.Element { ); } + // None + return
; + }; + + /** + * Renders the Dataset selector, aka the legend + * @returns The Dataset selector Element + */ + const renderDatasetSelector = (): JSX.Element => { + const { datasets } = data!; + if (datasets.length > 1) { + return ( +
+ {datasets.map((ds: ChartDataset, idx: number) => { + // Find a color for the legend based on the dataset info + let { color } = ChartJS.defaults; + if (ds.borderColor) color = ds.borderColor! as string; + else if (ds.backgroundColor) color = ds.backgroundColor! as string; + + // Return the Legend item + return ( + // eslint-disable-next-line react/no-array-index-key + + ) => { + handleDatasetChecked(idx, e.target?.checked); + }} + defaultChecked + /> + + {ds.label} + + + ); + })} +
+ ); + } + // None return
; }; @@ -183,20 +251,34 @@ export function Chart(props: TypeChartChartProps): JSX.Element { * @returns The whole Chart container JSX.Element or an empty div */ const renderChartContainer = (): JSX.Element => { - if (data && options && options.geochart) { + if (options.geochart && data?.datasets) { return ( -
-
{renderChart()}
-
{renderYSlider()}
-
{renderXSlider()}
-
-
+ + + {renderDatasetSelector()} + + + {renderChart()} + + + {renderYSlider()} + + + {renderXSlider()} + + ); } return
; }; + // Effect hook to add and remove event listeners + useEffect(() => { + // Prep ChartJS + ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, BarElement, Title, Tooltip, ArcElement); + }, []); + return renderChartContainer(); } diff --git a/src/charts/chart-bars-vertical.tsx b/src/charts/chart-bars-vertical.tsx deleted file mode 100644 index e862c21..0000000 --- a/src/charts/chart-bars-vertical.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Chart as ChartJS, DefaultDataPoint, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; -import { Bar, ChartProps } from 'react-chartjs-2'; - -/** - * Create a customized Chart Vertical Bars UI - * - * @param {TypeChartVerticalProps} props the properties passed to the Chart element - * @returns {JSX.Element} the created Chart element - */ -export function ChartBarsVertical(props: ChartProps<'bar', DefaultDataPoint<'bar'>>): JSX.Element { - const { data, options, redraw, style } = props; - - ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); - - return ; -} diff --git a/src/charts/chart-doughnut.tsx b/src/charts/chart-doughnut.tsx deleted file mode 100644 index 4c122a7..0000000 --- a/src/charts/chart-doughnut.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Chart as ChartJS, DefaultDataPoint, ArcElement, Tooltip, Legend } from 'chart.js'; -import { Doughnut, ChartProps } from 'react-chartjs-2'; - -/** - * Create a customized Chart Doughnut UI - * - * @param {TypeChartDoughnutProps} props the properties passed to the Chart element - * @returns {JSX.Element} the created Chart element - */ -export function ChartDoughnut(props: ChartProps<'doughnut', DefaultDataPoint<'doughnut'>>): JSX.Element { - const { data, options, redraw, style } = props; - - ChartJS.register(ArcElement, Tooltip, Legend); - - return ; -} diff --git a/src/charts/chart-line.tsx b/src/charts/chart-line.tsx deleted file mode 100644 index f9734b5..0000000 --- a/src/charts/chart-line.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { - Chart as ChartJS, - DefaultDataPoint, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, -} from 'chart.js'; -import { Line, ChartProps } from 'react-chartjs-2'; - -/** - * Create a customized Chart Line UI - * - * @param {TypeChartLineProps} props the properties passed to the Chart element - * @returns {JSX.Element} the created Chart element - */ -export function ChartLine(props: ChartProps<'line', DefaultDataPoint<'line'>>): JSX.Element { - const { data, options, redraw, style } = props; - - ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); - - return ; -} diff --git a/src/charts/chart-pie.tsx b/src/charts/chart-pie.tsx deleted file mode 100644 index b6e333b..0000000 --- a/src/charts/chart-pie.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Chart as ChartJS, DefaultDataPoint, ArcElement, Tooltip, Legend } from 'chart.js'; -import { Pie, ChartProps } from 'react-chartjs-2'; - -/** - * Create a customized Chart Pie UI - * - * @param {TypeChartPieProps} props the properties passed to the Chart element - * @returns {JSX.Element} the created Chart element - */ -export function ChartPie(props: ChartProps<'pie', DefaultDataPoint<'pie'>>): JSX.Element { - const { data, options, redraw, style } = props; - - ChartJS.register(ArcElement, Tooltip, Legend); - - return ; -}