From 5dee0e932680151dd5821da01e67ccc4964ed7b6 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 19 Oct 2023 12:07:00 -0400 Subject: [PATCH] Added the steps native examples Made chart-validator more generic with schema-validator Enforcing return types Moved the schema validators into json files Added comments and cleanup Now with 2 handles per slider --- .eslintrc | 11 +- index.html | 39 ++++-- src/app.tsx | 11 +- src/chart-validator.ts | 195 ------------------------------ src/chart.tsx | 50 ++++---- src/index.tsx | 3 +- src/schema-validator-data.json | 86 +++++++++++++ src/schema-validator-options.json | 31 +++++ src/schema-validator.ts | 94 ++++++++++++++ 9 files changed, 283 insertions(+), 237 deletions(-) delete mode 100644 src/chart-validator.ts create mode 100644 src/schema-validator-data.json create mode 100644 src/schema-validator-options.json create mode 100644 src/schema-validator.ts diff --git a/.eslintrc b/.eslintrc index bd8bbc7..aba3e1f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -74,5 +74,14 @@ "endOfLine": "auto" } ] - } + }, + "overrides": [ + { + // enable the rule specifically for TypeScript files + "files": ["*.ts", "*.mts", "*.cts", "*.tsx"], + "rules": { + "@typescript-eslint/explicit-function-return-type": "error" + } + } + ] } diff --git a/index.html b/index.html index e487a57..be11895 100644 --- a/index.html +++ b/index.html @@ -59,6 +59,26 @@ }; const DATA_NATIVE_3 = { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [ + { + label: 'Dataset 1', + data: [12, 19, 3, 5, 2, 3], + borderColor: "rgba(255, 99, 132, 0.5)", + backgroundColor: 'rgba(255, 99, 132, 0.5)', + stepped: 'middle', + }, + { + label: 'Dataset 2', + data: [22, 29, 13, 15, 12, 13], + borderColor: "rgba(53, 162, 235, 0.5)", + backgroundColor: 'rgba(53, 162, 235, 0.5)', + stepped: 'middle', + }, + ], + }; + + const DATA_NATIVE_4 = { datasets: [{ data: { January: 10, @@ -68,7 +88,7 @@ }] }; - const DATA_NATIVE_4 = { + const DATA_NATIVE_5 = { labels: ['Red', 'Blue', 'Yellow'], datasets: [{ data: [{"x": 10, "y": 15}, {"x": 15, "y": 25}, {"x": 20, "y": 10}] @@ -106,15 +126,13 @@ display: true, min: 0, max: 100, - value: 50, - track: 'normal', + value: [0, 100], }, ySlider: { display: true, min: 0, max: 100, - value: 100, - track: 'normal', + value: [0, 100], } } }; @@ -135,15 +153,13 @@ display: true, min: 0, max: 100, - value: 50, - track: 'normal', + value: [0, 100], }, ySlider: { display: true, min: 0, max: 100, - value: 100, - track: 'normal', + value: [0, 100], }, xAxis: { type: 'time' @@ -167,8 +183,9 @@
- - + + +
diff --git a/src/app.tsx b/src/app.tsx index 28e9861..dafe712 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,5 +1,5 @@ import { Chart } from './chart'; -import { ChartValidator, ValidatorResult } from './chart-validator'; +import { SchemaValidator, ValidatorResult } from './schema-validator'; /** * Create a container to visualize a GeoChart in a standalone manner. @@ -21,7 +21,7 @@ export function App(): JSX.Element { /** * Handles when the Chart has to be loaded with data or options. */ - const handleChartLoad = (e: Event) => { + const handleChartLoad = (e: Event): void => { const ev = e as CustomEvent; if (ev.detail.data) { setData(ev.detail.data); @@ -36,11 +36,12 @@ export function App(): JSX.Element { * @param dataErrors The data errors that happened (if any) * @param optionsErrors The options errors that happened (if any) */ - const handleError = (dataErrors: ValidatorResult, optionsErrors: ValidatorResult) => { + const handleError = (dataErrors: ValidatorResult, optionsErrors: ValidatorResult): void => { // Gather all error messages - const msgAll = ChartValidator.parseValidatorResultsMessages([dataErrors, optionsErrors]); + const msgAll = SchemaValidator.parseValidatorResultsMessages([dataErrors, optionsErrors]); - // Show the error (actually, can't because the snackbar is linked to a map at the moment and geochart is standalone) + // Show the error (actually, can't because the snackbar is linked to a map at the moment + // and geochart is standalone without a cgpv.init() at all) // TODO: Decide if we want the snackbar outside of a map or not and use showError or not cgpv.api.utilities.showError('', msgAll); alert(`There was an error parsing the Chart inputs.\n\n${msgAll}\n\nView console for details.`); diff --git a/src/chart-validator.ts b/src/chart-validator.ts deleted file mode 100644 index a548b12..0000000 --- a/src/chart-validator.ts +++ /dev/null @@ -1,195 +0,0 @@ -import Ajv from 'ajv'; - -/** - * Represents the result of a Chart data or options inputs validations. - */ -export type ValidatorResult = { - param: string; - valid: boolean; - errors?: string[]; -}; - -/** - * The Char Validator class to validate data and options inputs. - */ -export class ChartValidator { - // The JSON validator used by ChartValidate - private ajv: Ajv.Ajv; - - public SCHEMA_DATA = { - $schema: 'http://json-schema.org/draft-07/schema#', - type: 'object', - properties: { - 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: ['datasets'], - }; - - public SCHEMA_OPTIONS = { - type: 'object', - properties: { - responsive: { type: 'boolean' }, - plugins: { - type: 'object', - properties: { - legend: { - type: 'object', - properties: { - display: { type: 'boolean' }, - }, - }, - }, - }, - geochart: { - type: 'object', - properties: { - chart: { - enum: ['line', 'bar', 'pie', 'doughnut'], - default: 'line', - description: 'Supported types of chart.', - }, - }, - }, - }, - required: ['geochart'], - }; - - /** - * Constructs a Chart Validate object to validate schemas. - */ - constructor() { - // The embedded JSON validator - this.ajv = new Ajv(); - } - - /** - * Validates the data input parameters. - */ - validateData = (data: unknown): ValidatorResult => { - // Compile - const validate = this.ajv.compile(this.SCHEMA_DATA); - - // Validate - const valid = validate(data) as boolean; - return { - param: 'data', - valid, - errors: validate.errors?.map((e: Ajv.ErrorObject) => { - const m = e.message || 'generic schema error'; - return `${e.schemaPath} | ${e.keyword} | ${m}`; - }), - }; - }; - - /** - * Validates the options input parameters. - */ - validateOptions = (options: unknown): ValidatorResult => { - // Compile - const validate = this.ajv.compile(this.SCHEMA_OPTIONS); - - // Validate - const valid = validate(options) as boolean; - return { - param: 'options', - valid, - errors: validate.errors?.map((e: Ajv.ErrorObject) => { - const m = e.message || 'generic schema error'; - return `${e.schemaPath} | ${e.keyword} | ${m}`; - }), - }; - }; - - public static parseValidatorResultsMessages(valRes: ValidatorResult[]) { - // Gather all error messages for data input - let msg = ''; - valRes.forEach((v) => { - // Redirect - msg += ChartValidator.parseValidatorResultMessage(v); - }); - return msg.replace(/^\n+|\n+$/gm, ''); - } - - public static parseValidatorResultMessage(valRes: ValidatorResult) { - // Gather all error messages for data input - let msg = ''; - valRes.errors?.forEach((m: string) => { - msg += `${m}\n`; - }); - return msg.replace(/^\n+|\n+$/gm, ''); - } -} diff --git a/src/chart.tsx b/src/chart.tsx index f5047f4..08b2d65 100644 --- a/src/chart.tsx +++ b/src/chart.tsx @@ -4,7 +4,7 @@ import { Box } from '@mui/material'; import { Chart as ChartJS, ChartDataset, registerables } 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 { SchemaValidator, ValidatorResult } from './schema-validator'; /** * Main props for the Chart @@ -24,6 +24,10 @@ export interface TypeChartChartProps { * SX Classes for the Chart */ const sxClasses = { + chartError: { + fontStyle: 'italic', + color: 'red', + }, checkDatasetWrapper: { display: 'inline-block', }, @@ -84,7 +88,7 @@ export function Chart(props: TypeChartChartProps): JSX.Element { * Handles when the X Slider changes * @param value number | number[] Indicates the slider value */ - const handleSliderXChange = (value: number | number[]) => { + const handleSliderXChange = (value: number | number[]): void => { // Callback handleSliderXChanged?.(value); }; @@ -93,7 +97,7 @@ 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[]) => { + const handleSliderYChange = (value: number | number[]): void => { // Callback handleSliderYChanged?.(value); }; @@ -103,7 +107,7 @@ export function Chart(props: TypeChartChartProps): JSX.Element { * @param datasetIndex number Indicates the dataset index that was checked/unchecked * @param checked boolean Indicates the checked state */ - const handleDatasetChecked = (datasetIndex: number, checked: boolean) => { + const handleDatasetChecked = (datasetIndex: number, checked: boolean): void => { // Toggle visibility of the dataset chartRef.current.setDatasetVisibility(datasetIndex, checked); chartRef.current.update(); @@ -119,8 +123,8 @@ export function Chart(props: TypeChartChartProps): JSX.Element { }; /** - * Renders the X Chart Slider JSX.Element or an empty div - * @returns The X Chart Slider JSX.Element or an empty div + * Renders the X Chart Slider JSX.Element or an empty box + * @returns The X Chart Slider JSX.Element or an empty box */ const renderXSlider = (): JSX.Element => { const { xSlider } = options.geochart; @@ -132,19 +136,18 @@ export function Chart(props: TypeChartChartProps): JSX.Element { min={xSlider.min || 0} max={xSlider.max || 100} value={xSlider.value || 0} - track={xSlider.track || false} customOnChange={handleSliderXChange} /> ); } // None - return
; + return ; }; /** - * Renders the Y Chart Slider JSX.Element or an empty div - * @returns The Y Chart Slider JSX.Element or an empty div + * Renders the Y Chart Slider JSX.Element or an empty box + * @returns The Y Chart Slider JSX.Element or an empty box */ const renderYSlider = (): JSX.Element => { const { ySlider } = options.geochart; @@ -156,7 +159,6 @@ export function Chart(props: TypeChartChartProps): JSX.Element { min={ySlider.min || 0} max={ySlider.max || 100} value={ySlider.value || 0} - track={ySlider.track || false} orientation="vertical" customOnChange={handleSliderYChange} /> @@ -164,7 +166,7 @@ export function Chart(props: TypeChartChartProps): JSX.Element { ); } // None - return
; + return ; }; /** @@ -175,7 +177,7 @@ export function Chart(props: TypeChartChartProps): 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; @@ -188,7 +190,7 @@ export function Chart(props: TypeChartChartProps): JSX.Element { ) => { + onChange={(e: React.ChangeEvent): void => { handleDatasetChecked(idx, e.target?.checked); }} defaultChecked @@ -199,16 +201,16 @@ export function Chart(props: TypeChartChartProps): JSX.Element { ); })} -
+
); } // None - return
; + return ; }; /** - * Renders the whole Chart container JSX.Element or an empty div - * @returns The whole Chart container JSX.Element or an empty div + * Renders the whole Chart container JSX.Element or an empty box + * @returns The whole Chart container JSX.Element or an empty box */ const renderChartContainer = (): JSX.Element => { if (options.geochart && data?.datasets) { @@ -230,15 +232,15 @@ export function Chart(props: TypeChartChartProps): JSX.Element { ); } - return
; + return ; }; /** - * Renders the whole Chart container JSX.Element or an empty div - * @returns The whole Chart container JSX.Element or an empty div + * Renders the whole Chart container JSX.Element or an empty box + * @returns The whole Chart container JSX.Element or an empty box */ const renderChartContainerFailed = (): JSX.Element => { - return
Error rendering the Chart. Check console for details.
; + return Error rendering the Chart. Check console for details.; }; // @@ -250,7 +252,7 @@ export function Chart(props: TypeChartChartProps): JSX.Element { let resData: ValidatorResult | undefined; if (options && data) { // Validate the data and options as received - const validator = new ChartValidator(); + const validator = new SchemaValidator(); resOptions = validator.validateOptions(options) || undefined; resData = validator.validateData(data); } @@ -290,7 +292,7 @@ export function Chart(props: TypeChartChartProps): JSX.Element { } // Nothing to render, no errors either - return
; + return ; } /** diff --git a/src/index.tsx b/src/index.tsx index 71a1af0..cd9e549 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,8 @@ import App from './app'; +// Export the types from the package export * from './chart-types'; -export * from './chart-validator'; +export * from './schema-validator'; export * from './chart'; // Search for a special root in case we are loading the geochart standalone diff --git a/src/schema-validator-data.json b/src/schema-validator-data.json new file mode 100644 index 0000000..8f2176d --- /dev/null +++ b/src/schema-validator-data.json @@ -0,0 +1,86 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GeoChart Data Schema", + "description": "This Schema validator validates the GeoChart data.", + "type": "object", + "properties": { + "labels": { + "description": "The labels to use for the X axis.", + "type": "array", + "items": { + "type": "string" + } + }, + "datasets": { + "description": "The mandatory datasets information to use to build the chart.", + "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": ["datasets"] +} \ No newline at end of file diff --git a/src/schema-validator-options.json b/src/schema-validator-options.json new file mode 100644 index 0000000..c48036c --- /dev/null +++ b/src/schema-validator-options.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GeoChart Options Schema", + "description": "This Schema validator validates the GeoChart options.", + "type": "object", + "properties": { + "responsive": { "type": "boolean" }, + "plugins": { + "type": "object", + "properties": { + "legend": { + "type": "object", + "properties": { + "display": { "type": "boolean" } + } + } + } + }, + "geochart": { + "type": "object", + "properties": { + "chart": { + "enum": ["line", "bar", "pie", "doughnut"], + "default": "line", + "description": "Supported types of chart." + } + } + } + }, + "required": ["geochart"] +} \ No newline at end of file diff --git a/src/schema-validator.ts b/src/schema-validator.ts new file mode 100644 index 0000000..aeaae7e --- /dev/null +++ b/src/schema-validator.ts @@ -0,0 +1,94 @@ +import Ajv from 'ajv'; +import SCHEMA_DATA from './schema-validator-data.json'; +import SCHEMA_OPTIONS from './schema-validator-options.json'; + +/** + * Represents the result of a Chart data or options inputs validations. + */ +export type ValidatorResult = { + valid: boolean; + errors?: string[]; +}; + +/** + * The Schema Validator class to validate json objects. + */ +export class SchemaValidator { + // The embedded JSON validator + private ajv: Ajv.Ajv; + + /** + * Constructs a Chart Validate object to validate schemas. + */ + constructor() { + // The embedded JSON validator + this.ajv = new Ajv(); + } + + /** + * Validates the data input parameters. + * @param data object the data json object to validate + */ + validateData = (data: object): ValidatorResult => { + // Redirect + return this.validateJsonSchema(SCHEMA_DATA, data); + }; + + /** + * Validates the options input parameters. + * @param options object the options json object to validate + */ + validateOptions = (options: object): ValidatorResult => { + // Redirect + return this.validateJsonSchema(SCHEMA_OPTIONS, options); + }; + + /** + * Validates the a jsonObj using a schema validator. + * @param schema object the schema validator json to validate the jsonObj with + * @param jsonObj object the json object to validate + */ + validateJsonSchema = (schema: object, jsonObj: object): ValidatorResult => { + // Compile + const validate = this.ajv.compile(schema); + + // Validate + const valid = validate(jsonObj) as boolean; + + // Return a ValidatorResult + return { + valid, + errors: validate.errors?.map((e: Ajv.ErrorObject) => { + const m = e.message || 'generic schema error'; + return `${e.schemaPath} | ${e.keyword} | ${m}`; + }), + }; + }; + + /** + * Returns a string representation of the errors of all ValidatorResult objects. + * @param valRes ValidatorResult[] the list of validation results to read and put to string + */ + public static parseValidatorResultsMessages = (valRes: ValidatorResult[]): string => { + // Gather all error messages for data input + let msg = ''; + valRes.forEach((v) => { + // Redirect + msg += SchemaValidator.parseValidatorResultMessage(v); + }); + return msg.replace(/^\n+|\n+$/gm, ''); + }; + + /** + * Returns a string representation of the error in the ValidatorResult object. + * @param valRes ValidatorResult the validation result to read and put to string + */ + public static parseValidatorResultMessage = (valRes: ValidatorResult): string => { + // Gather all error messages for data input + let msg = ''; + valRes.errors?.forEach((m: string) => { + msg += `${m}\n`; + }); + return msg.replace(/^\n+|\n+$/gm, ''); + }; +}