From f2fa322fd39b3ead17a27a34c1aac477fd55165a Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 17 Oct 2023 11:20:05 -0400 Subject: [PATCH] Starting my origin. Simplified the Chart type usage. Started work on the WCAG legend instead of using the canvas. Adjusted the chart validator schema Using Grid and Box now instead of html div Removed chart.module.css and removed Globals.d.ts Added comment Removed patterns Added default value for the enum for the validator Merged the style and the sx --- index.html | 42 +++++-- src/Globals.d.ts | 2 - src/chart-types.ts | 9 ++ src/chart-validator.ts | 88 ++++++++++++- src/chart.module.css | 25 ---- src/chart.tsx | 196 ++++++++++++++++++++--------- src/charts/chart-bars-vertical.tsx | 16 --- src/charts/chart-doughnut.tsx | 16 --- src/charts/chart-line.tsx | 26 ---- src/charts/chart-pie.tsx | 16 --- 10 files changed, 260 insertions(+), 176 deletions(-) delete mode 100644 src/Globals.d.ts delete mode 100644 src/chart.module.css delete mode 100644 src/charts/chart-bars-vertical.tsx delete mode 100644 src/charts/chart-doughnut.tsx delete mode 100644 src/charts/chart-line.tsx delete mode 100644 src/charts/chart-pie.tsx 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 ; -}