diff --git a/index.html b/index.html
index 2b95c58..c6bbe20 100644
--- a/index.html
+++ b/index.html
@@ -216,7 +216,7 @@
}]
};
- const DATA_INPUT_LINE_3 = {
+ const DATA_INPUT_BAR_3 = {
chart: 'bar',
title: 'Bar Chart with OGC Features',
query: {
@@ -251,6 +251,40 @@
}]
};
+ const DATA_INPUT_LINE_3 = {
+ chart: 'line',
+ title: 'Line Chart with OGC Features',
+ query: {
+ type: "ogcAPIFeatures",
+ url: "https://api.czs-dev.services.geo.ca/collections/sante_canada_tab",
+ queryOptions: {
+ whereClauses: [
+ {
+ field: "location_name",
+ prefix: "'",
+ valueIs: "8000_8_45.1134_-66.8242",
+ suffix: "'"
+ }]
+ }
+ },
+ geochart: {
+ xAxis: {
+ type: 'time',
+ property: 'start_time',
+ },
+ yAxis: {
+ property: 'value_nsvhr'
+ }
+ },
+ ui: {
+ resetStates: true,
+ download: true
+ },
+ datasources: [{
+ display: "Data",
+ }]
+ };
+
const DATA_INPUT_2 = {
chart: 'bar',
title: 'Bar Chart',
@@ -712,7 +746,8 @@
-
+
+
@@ -857,7 +892,7 @@
}
// Load data 1
- importDataInputs(DATA_INPUT_LINE_1);
+ importDataInputs(DATA_INPUT_LINE_1); // DATA_INPUT_LINE_3
importDataParsed(DATA_NATIVE_1);
importOptionsParsed(OPTIONS_NATIVE_1);
diff --git a/locales/en/translation.json b/locales/en/translation.json
index 8c6f03f..85437b6 100644
--- a/locales/en/translation.json
+++ b/locales/en/translation.json
@@ -6,6 +6,7 @@
"parsingError": "There was an error parsing the Chart inputs.",
"viewConsoleDetails": "View console for details.",
"downloadFiltered": "Download visible",
- "downloadAll": "Download all"
+ "downloadAll": "Download all",
+ "resetStates": "Reset states"
}
}
\ No newline at end of file
diff --git a/locales/fr/translation.json b/locales/fr/translation.json
index b19a7e0..cf98948 100644
--- a/locales/fr/translation.json
+++ b/locales/fr/translation.json
@@ -6,6 +6,7 @@
"parsingError": "Une erreur est survenue lors de la lecture des paramètres.",
"viewConsoleDetails": "Voir détails dans la console.",
"downloadFiltered": "Télécharger visuel",
- "downloadAll": "Télécharger tout"
+ "downloadAll": "Télécharger tout",
+ "resetStates": "Réinitialiser états"
}
}
\ No newline at end of file
diff --git a/schema-chartjs-options.json b/schema-chartjs-options.json
index e7295f0..5239243 100644
--- a/schema-chartjs-options.json
+++ b/schema-chartjs-options.json
@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "GeoChart Options Schema",
- "description": "This Schema validator validates the ChartJS options. ABSOLUTELY UNFINISHED.",
+ "description": "This Schema validator validates the ChartJS options.",
"type": "object",
"properties": {
"responsive": { "type": "boolean" },
@@ -15,6 +15,9 @@
}
}
}
+ },
+ "scales": {
+ "type": "object"
}
},
"required": ["responsive", "plugins"]
diff --git a/schema-inputs.json b/schema-inputs.json
index 0ecfedf..6226f44 100644
--- a/schema-inputs.json
+++ b/schema-inputs.json
@@ -1,17 +1,312 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "GeoChart Inputs Schema",
- "description": "This Schema validator validates the GeoChart Inputs. ABSOLUTELY UNFINISHED.",
+ "description": "This Schema validator validates the GeoChart Inputs.",
"type": "object",
"properties": {
"chart": {
"description": "Supported types of chart.",
"enum": ["line", "bar", "pie", "doughnut"],
- "default": "line"
+ "default": "line"
+ },
+ "title": {
+ "description": "Optionally provide the title of the chart",
+ "type": "string"
+ },
+ "query": {
+ "description": "Groups information on how the data should be queried in the table source",
+ "type": "object",
+ "properties": {
+ "type": {
+ "description": "Indicates the kind of query to perform - supported values are: 'esriRegular', 'ogcAPIFeatures' and 'json'",
+ "type": "string"
+ },
+ "url": {
+ "description": "Indicates the url where to fetch the data to build the chart with - supported urls are Esri services, OGC API Features services or urls pointing to a .json file built on the GeoJson format",
+ "type": "string"
+ },
+ "queryOptions": {
+ "description": "",
+ "type": "object",
+ "properties": {
+ "whereClauses": {
+ "description": "Indicates how to generate the where clause to fetch the correct data in the table source. This is an array to support filtering on more than 1 field. The and logic operator is implicit",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field": {
+ "description": "Indicates the field name, in the table source, on which to filter",
+ "type": "string"
+ },
+ "prefix": {
+ "description": "Indicates the prefix/suffix to use to build the query (useful to support single-quotes when the attribute to query is a string)",
+ "type": "string"
+ },
+ "valueIs": {
+ "description": "Indicates the value as a literal information (not read from a property name from the datasource)",
+ "type": "string"
+ },
+ "valueFrom": {
+ "description": "Indicates the property name, in the datasource.sourceItem(!), to use to query the table source (the url)",
+ "type": "string"
+ },
+ "suffix": {
+ "description": "Indicates the prefix/suffix to use to build the query (useful to support single-quotes when the attribute to query is a string)",
+ "type": "string"
+ }
+ },
+ "required": ["field"]
+ }
+ },
+ "orderByField": {
+ "description": "Indicates the property on which to order the results of the data coming from the table source",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "required": ["type", "url"]
},
"geochart": {
+ "description": "Groups information on how to build the chart",
+ "type": "object",
+ "properties": {
+ "borderWidth": {
+ "description": "Indicates the thickness of the borders (or lines in the line chart)",
+ "type": "number"
+ },
+ "useSteps": {
+ "description": "Indicates if the line chart should use steps - supported values are: 'before', 'middle', 'after', false",
+ "enum": ["before", "after", "middle", false],
+ "default": false
+ },
+ "tension": {
+ "description": "Indicates if the line chart should use tension when drawing the line between the values",
+ "type": "number"
+ },
+ "xAxis": {
+ "description": "Groups information on the x axis",
+ "type": "object",
+ "properties": {
+ "property": {
+ "description": "Indicates the property name on which to read the information from the table source",
+ "type": "string"
+ },
+ "type": {
+ "description": "Indicates the type of axis - supported values are: 'linear', 'time', 'timeseries', 'logarithmic', 'category'",
+ "type": "string",
+ "default": "linear"
+ },
+ "label": {
+ "description": "Indicates the text in the user interface that should be shown for the axis",
+ "type": "string"
+ },
+ "usePalette": {
+ "description": "Indicates if a pre-determined (GeoChart specific) color palette should be used",
+ "type": "boolean"
+ },
+ "paletteBackgrounds": {
+ "description": "Indicates the array of rgba color values to use as the palette for background coloring",
+ "type": "array",
+ "items": {
+ "description": "A rgba() color",
+ "type": "string"
+ }
+ },
+ "paletteBorders": {
+ "description": "Indicates the array of rgb color values to use as the palette for border coloring",
+ "type": "array",
+ "items": {
+ "description": "A rgb() color",
+ "type": "string"
+ }
+ },
+ "tooltipSuffix": {
+ "description": "Indicates the suffix to use on for the values when displayed in the tooltip",
+ "type": "string"
+ }
+ },
+ "required": ["property"]
+ },
+ "yAxis": {
+ "description": "Groups information on the y axis",
+ "type": "object",
+ "properties": {
+ "property": {
+ "description": "Indicates the property name on which to read the information from the table source",
+ "type": "string"
+ },
+ "type": {
+ "description": "Indicates the type of axis - supported values are: 'linear', 'time', 'timeseries', 'logarithmic', 'category'",
+ "type": "string"
+ },
+ "label": {
+ "description": "Indicates the text in the user interface that should be shown for the axis",
+ "type": "string"
+ },
+ "usePalette": {
+ "description": "Indicates if a pre-determined (GeoChart specific) color palette should be used",
+ "type": "boolean"
+ },
+ "paletteBackgrounds": {
+ "description": "Indicates the array of rgba color values to use as the palette for background coloring",
+ "type": "array",
+ "items": {
+ "description": "A rgba() color",
+ "type": "string"
+ }
+ },
+ "paletteBorders": {
+ "description": "Indicates the array of rgb color values to use as the palette for border coloring",
+ "type": "array",
+ "items": {
+ "description": "A rgb() color",
+ "type": "string"
+ }
+ },
+ "tooltipSuffix": {
+ "description": "Indicates the suffix to use on for the values when displayed in the tooltip",
+ "type": "string"
+ }
+ },
+ "required": ["property"]
+ }
+ },
+ "required": ["xAxis", "yAxis"]
+ },
+ "category": {
+ "description": "Indicates how the data from the table source should be categorized (this creates the datasets aka the legend)",
+ "type": "object",
+ "properties": {
+ "property": {
+ "description": "Indicates the property name to use to categorize records",
+ "type": "string"
+ },
+ "usePalette": {
+ "description": "Indicates if a pre-determined (GeoChart specific) color palette should be used",
+ "type": "boolean"
+ },
+ "paletteBackgrounds": {
+ "description": "Indicates the array of rgba color values to use as the palette for background coloring",
+ "type": "array",
+ "items": {
+ "description": "A rgba() color",
+ "type": "string"
+ }
+ },
+ "paletteBorders": {
+ "description": "Indicates the array of rgb color values to use as the palette for border coloring",
+ "type": "array",
+ "items": {
+ "description": "A rgb() color",
+ "type": "string"
+ }
+ }
+ },
+ "required": ["property"]
+ },
+ "datasources": {
+ "description": "Groups information on the datasources to build the datasource drop down and the chart",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "display": {
+ "description": "Indicates the string to be displayed in the drop down",
+ "type": "string"
+ },
+ "sourceItem": {
+ "description": "Indicates the source item(!) used as reference to query the data from. This property has an object with a property that should equal the property in query.queryOptions.whereClauses.valueFrom",
+ "type": "object"
+ },
+ "value": {
+ "description": "Indicates the inner value used for the 'sourceItem'",
+ "type": "string"
+ },
+ "items": {
+ "description": "Indicates the actual items (coming from the table source), associated with the datasources.sourceItem (coming from the origin source), used to build the chart with. When items is already specified/populated, the data isn't fetched via the query.url",
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ },
+ "required": ["display"]
+ }
+ },
+ "ui": {
+ "description": "Indicates what ui elements to show with the chart",
+ "type": "object",
+ "properties": {
+ "xSlider": {
+ "description": "Groups information on the x slider",
+ "type": "object",
+ "properties": {
+ "display": {
+ "description": "Indicates if the slider should be displayed",
+ "type": "boolean"
+ },
+ "step": {
+ "description": "Indicates the steps the slider should jump when sliding",
+ "type": "number"
+ },
+ "min": {
+ "description": "Indicates the minimum value for the slider",
+ "type": "number"
+ },
+ "max": {
+ "description": "Indicates the maximum value for the slider",
+ "type": "number"
+ }
+ },
+ "required": ["display"]
+ },
+ "ySlider": {
+ "description": "Groups information on the y slider",
+ "type": "object",
+ "properties": {
+ "display": {
+ "description": "Indicates if the slider should be displayed",
+ "type": "boolean"
+ },
+ "step": {
+ "description": "Indicates the steps the slider should jump when sliding",
+ "type": "number"
+ },
+ "min": {
+ "description": "Indicates the minimum value for the slider",
+ "type": "number"
+ },
+ "max": {
+ "description": "Indicates the maximum value for the slider",
+ "type": "number"
+ }
+ },
+ "required": ["display"]
+ },
+ "stepsSwitcher": {
+ "description": "Indicates if the select drop down to switch the steps on-the-fly is displayed",
+ "type": "boolean"
+ },
+ "resetStates": {
+ "description": "Indicates if the button to reset the states is displayed",
+ "type": "boolean"
+ },
+ "description": {
+ "description": "Indicates the description text to show at the bottom of the chart",
+ "type": "string"
+ },
+ "download": {
+ "description": "Indicates if a download button should be displayed",
+ "type": "boolean"
+ }
+ }
+ },
+ "chartjsOptions": {
+ "description": "Iindicates further ChartJS specific options to open the door to further customization when natively supported by ChartJS: https://www.chartjs.org/docs/latest/general/options.html",
"type": "object"
}
},
- "required": ["chart", "geochart"]
+ "required": ["chart", "geochart", "datasources"]
}
\ No newline at end of file
diff --git a/src/app.tsx b/src/app.tsx
index 1f1d6fa..8bc7156 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,6 +1,6 @@
import { GeoChart } from './chart';
import { GeoChartConfig, ChartType, ChartOptions, ChartData, GeoChartAction, DefaultDataPoint } from './chart-types';
-import { SchemaValidator, ValidatorResult } from './chart-schema-validator';
+import { SchemaValidator } from './chart-schema-validator';
/**
* Main props for the Application
@@ -25,9 +25,9 @@ export function App(props: TypeAppProps): JSX.Element {
const { schemaValidator } = props;
// Translation
- const { t, i18n } = useTranslation();
+ const { i18n } = useTranslation();
- /** ****************************************** USE STATE SECTION START ************************************************ */
+ // #region USE STATE SECTION ****************************************************************************************
const [inputs, setInputs] = useState() as [
GeoChartConfig | undefined,
@@ -43,8 +43,9 @@ export function App(props: TypeAppProps): JSX.Element {
const [isLoadingChart, setIsLoadingChart] = useState() as [boolean, React.Dispatch>];
const [isLoadingDatasource, setIsLoadingDatasource] = useState() as [boolean, React.Dispatch>];
- /** ****************************************** USE STATE SECTION END ************************************************** */
- /** *************************************** EVENT HANDLERS SECTION START ********************************************** */
+ // #endregion
+
+ // #region EVENT HANDLERS SECTION ***********************************************************************************
/**
* Handles when the Chart has to be loaded with data or options.
@@ -89,49 +90,45 @@ export function App(props: TypeAppProps): JSX.Element {
if (ev.detail.state === 2) setIsLoadingDatasource(true);
};
- /** **************************************** EVENT HANDLERS SECTION END *********************************************** */
- /** ******************************************* HOOKS SECTION START *************************************************** */
+ // #endregion
- /**
- * Handles when the Chart language is changed.
- */
- const handleChartLanguage = useCallback(
- (e: Event): void => {
- const ev = e as CustomEvent;
- i18n.changeLanguage(ev.detail.language);
- },
- [i18n]
- );
+ // #region HOOKS SECTION ********************************************************************************************
/**
* Handles when the Chart has parsed inputs.
- * We use a 'useCallback' so that any child component with a useEffect dependency on the callback
- * doesn't get triggered everytime this parent component re-renders and re-generates its stub.
+ * @param theChart ChartType The chart type
+ * @param theOptions ChartOptions The chart options
+ * @param theData ChartData The chart data
*/
const handleParsed = useCallback((theChart: ChartType, theOptions: ChartOptions, theData: ChartData): void => {
// Raise event higher
window.dispatchEvent(new CustomEvent('chart/parsed', { detail: { chart: theChart, options: theOptions, data: theData } }));
- }, []) as (theChart: ChartType, theOptions: ChartOptions, theData: ChartData) => void; // Crazy typing, because can't use the generic version of 'useCallback'
+ }, []);
/**
- * Handles an error that happened in the Chart component.
- * We use a 'useCallback' so that any child component with a useEffect dependency on the callback
- * doesn't get triggered everytime this parent component re-renders and re-generates its stub.
- * @param dataErrors The data errors that happened (if any)
- * @param optionsErrors The options errors that happened (if any)
+ * Handles a generic error that happened in the Chart component.
+ * @param error The error message
+ * @param exception The exception that happened (if any)
*/
- const handleError = useCallback(
- (validators: (ValidatorResult | undefined)[]): void => {
- // Gather all error messages
- const msgAll = SchemaValidator.parseValidatorResultsMessages(validators);
-
- // Show the error using an alert. We can't use the cgpv SnackBar as that component is attached to
- // a map and we're not even running a cgpv.init() at all here.
- // eslint-disable-next-line no-alert
- alert(`${t('geochart.parsingError')}\n\n${msgAll}\n\n${t('geochart.viewConsoleDetails')}`);
+ const handleError = useCallback((error: string, exception: unknown): void => {
+ // Show the error using an alert. We can't use the cgpv SnackBar as that component is attached to
+ // a map and we're not even running a cgpv.init() at all here.
+ // eslint-disable-next-line no-console
+ console.error(error, exception);
+ // eslint-disable-next-line no-alert
+ alert(error);
+ }, []);
+
+ /**
+ * Handles when the Chart language is changed.
+ */
+ const handleChartLanguage = useCallback(
+ (e: Event): void => {
+ const ev = e as CustomEvent;
+ i18n.changeLanguage(ev.detail.language);
},
- [t]
- ) as (validators: (ValidatorResult | undefined)[]) => void; // Crazy typing, because can't use the generic version of 'useCallback'
+ [i18n]
+ );
// Effect hook to add and remove event listeners.
// Using window.addEventListener is unconventional here, but this is strictly for the 'app' logic with the index.html.
@@ -149,8 +146,9 @@ export function App(props: TypeAppProps): JSX.Element {
};
}, [handleChartLanguage]);
- /** ********************************************* HOOKS SECTION END *************************************************** */
- /** ******************************************** RENDER SECTION START ************************************************* */
+ // #endregion
+
+ // #region RENDER SECTION START *************************************************************************************
// Render the Chart
return (
@@ -167,6 +165,8 @@ export function App(props: TypeAppProps): JSX.Element {
onError={handleError}
/>
);
+
+ // #endregion
}
export default App;
diff --git a/src/chart-parsing.ts b/src/chart-parsing.ts
index c5e3ede..2563440 100644
--- a/src/chart-parsing.ts
+++ b/src/chart-parsing.ts
@@ -1,4 +1,4 @@
-import { ChartType, ChartData, ChartDataset, ChartOptions, Tick } from 'chart.js';
+import { ChartType, ChartData, ChartDataset, ChartOptions, Tick, PluginChartOptions } from 'chart.js';
import {
GeoChartConfig,
GeoChartXYData,
@@ -7,6 +7,7 @@ import {
GeoChartCategoriesGroup,
GeoChartQuery,
GeoChartQueryOptionClause,
+ GeoChartSelectedDataset,
StepsPossibilities,
DEFAULT_COLOR_PALETTE_CUSTOM_TRANSPARENT,
DEFAULT_COLOR_PALETTE_CUSTOM_OPAQUE,
@@ -16,7 +17,7 @@ import {
DEFAULT_COLOR_PALETTE_CHARTJS_OPAQUE,
DATE_OPTIONS_AXIS,
} from './chart-types';
-import { isNumber, getColorFromPalette } from './chart-util';
+import { isNumber } from './chart-util';
/**
* Sorts all ChartDatasets based on the X values of their data.
@@ -63,66 +64,13 @@ function sortOnDatasetLabels The GeoChart configuration
- */
-function setColorsUsingPalette, TLabel = string>(
- chartConfig: GeoChartConfig,
- data: ChartData
-): void {
- // If pie or doughnut
- let colorPaletteForAll: string[];
- if (chartConfig.chart === 'pie' || chartConfig.chart === 'doughnut') {
- // Create a new color array of expected length
- colorPaletteForAll = Array.from({ length: data.labels!.length }, (_, paletteIndex: number) => {
- return getColorFromPalette(chartConfig.geochart.xAxis!.paletteBackgrounds, paletteIndex)!;
- });
- }
-
- // For each dataset
- data.datasets.forEach((ds: ChartDataset, idx: number) => {
- // If we categorize
- let backgroundColor: string | string[] | undefined;
- let borderColor: string | string[] | undefined;
- if (chartConfig.category?.property) {
- // If pie or doughnut
- if (chartConfig.chart === 'pie' || chartConfig.chart === 'doughnut') {
- backgroundColor = colorPaletteForAll;
- } else {
- // The colors to use
- backgroundColor = getColorFromPalette(chartConfig.category!.paletteBackgrounds, idx);
- }
- borderColor = getColorFromPalette(chartConfig.category!.paletteBorders, idx);
- } else {
- // Not categorizing
- // If pie or doughnut
- // eslint-disable-next-line no-lonely-if
- if (chartConfig.chart === 'pie' || chartConfig.chart === 'doughnut') {
- backgroundColor = colorPaletteForAll;
- borderColor = undefined;
- } else {
- // The colors to use
- backgroundColor = getColorFromPalette(chartConfig.geochart.xAxis!.paletteBackgrounds, idx);
- borderColor = getColorFromPalette(chartConfig.geochart.xAxis!.paletteBorders, idx);
- }
- }
-
- // Set the colors
- // eslint-disable-next-line no-param-reassign
- if (backgroundColor) ds.backgroundColor = backgroundColor;
- // eslint-disable-next-line no-param-reassign
- if (borderColor) ds.borderColor = borderColor;
- });
-}
-
/**
* Builds a where clause string, to be used in an url, given the array of GeoChartQueryOptionClause.
* @param whereClauses GeoChartQueryOptionClause[] The array of where clauses objects.
- * @param source TypeJsonObject The source to read the information from when building the clause.
+ * @param sourceItem TypeJsonObject The source to read the information from when building the clause in case 'valueFrom' is needed.
* @returns string Returns the where clause string
*/
-const buildQueryWhereClause = (whereClauses: GeoChartQueryOptionClause[], source: TypeJsonObject): string => {
+const buildQueryWhereClause = (whereClauses: GeoChartQueryOptionClause[], sourceItem: TypeJsonObject | undefined): string => {
// Loop on each url options
let theWhereClause = '';
if (whereClauses) {
@@ -132,9 +80,9 @@ const buildQueryWhereClause = (whereClauses: GeoChartQueryOptionClause[], source
if (urlOpt.valueIs) {
// As-is replace
val = urlOpt.valueIs;
- } else if (urlOpt.valueFrom) {
+ } else if (urlOpt.valueFrom && sourceItem) {
// Value comes from the record object
- val = source[urlOpt.valueFrom] as string;
+ val = sourceItem[urlOpt.valueFrom] as string;
}
// If value was read, concatenate to the where clause
if (val) {
@@ -232,8 +180,8 @@ export async function queryOGCFeaturesByUrl(url: string): Promise => {
// Depending on the type of query
let entries: TypeJsonObject[];
@@ -242,7 +190,7 @@ export const fetchItemsViaQueryForDatasource = async (
let { url } = queryConfig;
// Append the mandatory params
- url += `/items?f=json&lang=${language}&skipGeometry=true&offset=0`;
+ url += `/items?f=json&lang=${language}&skipGeometry=true&offset=0&filter-lang=cql-text`;
// If any query options
if (queryConfig.queryOptions) {
@@ -310,7 +258,8 @@ function createDataXYFormat(chartConfig: GeoChartConfig
if (chartConfig.geochart.xAxis?.type === 'time' || chartConfig.geochart.xAxis?.type === 'timeseries') {
// Make sure it's a date object
if (valRawX instanceof Date) xVal = valRawX as Date;
- if (isNumber(valRawX)) xVal = new Date(valRawX as number);
+ // Do our best to convert to date
+ xVal = new Date(valRawX as string);
}
// Read the value in y, hopefully it's a number, that's what GeoChartXYPair supports for now (there's a TODO there)
@@ -370,41 +319,38 @@ function createDataCompressedForPieDoughnut<
*/
function createDataset = GeoDefaultDataPoint>(
chartConfig: GeoChartConfig,
+ backgroundColor: string | string[] | undefined,
+ borderColor: string | string[] | undefined,
steps: StepsPossibilities | undefined,
label?: string
): ChartDataset {
// Transform the TypeFeatureJson data to ChartDataset
- let theDatasetGeneric: ChartDataset;
+ const theDataset: ChartDataset = {
+ label,
+ data: [],
+ } as unknown as ChartDataset;
// If building a line chart
if (chartConfig.chart === 'line') {
// Transform the TypeFeatureJson data to ChartDataset
- const theDatasetLine: ChartDataset<'line', GeoDefaultDataPoint<'line'>> = {
- label,
- data: [],
- };
+ const theDatasetLine = theDataset as ChartDataset<'line'>;
// If useSteps is defined, set it for each dataset
if (steps !== undefined) theDatasetLine.stepped = steps;
// If tension is defined, set it for each dataset
if (chartConfig.geochart.tension) theDatasetLine.tension = chartConfig.geochart.tension;
-
- // Switch to generic type
- theDatasetGeneric = theDatasetLine as ChartDataset;
- } else {
- // Switch to generic type, for all unspecific types, so typed to unknown first
- theDatasetGeneric = {
- label,
- data: [],
- } as unknown as ChartDataset;
}
+ // Set the colors
+ if (backgroundColor) theDataset.backgroundColor = backgroundColor;
+ if (borderColor) theDataset.borderColor = borderColor;
+
// If the border width is set (applies to all datasets the same)
if (chartConfig.geochart.borderWidth) {
- theDatasetGeneric.borderWidth = chartConfig.geochart.borderWidth;
+ theDataset.borderWidth = chartConfig.geochart.borderWidth;
}
- return theDatasetGeneric!;
+ return theDataset!;
}
/**
@@ -419,7 +365,12 @@ function createDatasetsLineBar<
TType extends ChartType = 'line' | 'bar',
TData extends GeoDefaultDataPoint = GeoDefaultDataPoint,
TLabel extends string = string
->(chartConfig: GeoChartConfig, steps: StepsPossibilities, records: TypeJsonObject[]): ChartData {
+>(
+ chartConfig: GeoChartConfig,
+ datasetsRegistry: GeoChartSelectedDataset,
+ steps: StepsPossibilities,
+ records: TypeJsonObject[]
+): ChartData {
// Transform the TypeFeatureJson data to ChartData
const returnedChartData: ChartData = {
labels: [],
@@ -435,24 +386,34 @@ function createDatasetsLineBar<
// Read the category as a string
const catName = rec[chartConfig.category!.property] as string;
- // If new category
- if (!Object.keys(categoriesRead).includes(catName)) {
- // Create dataset
- const newDataset = createDataset(chartConfig, steps, catName);
- categoriesRead[catName] = { index: idx++, data: newDataset.data };
- returnedChartData.datasets.push(newDataset);
+ // If it's a category we actually want
+ if (datasetsRegistry[catName].checked) {
+ // If new category
+ if (!Object.keys(categoriesRead).includes(catName)) {
+ // Get the color using the registry
+ // Create dataset
+ const newDataset = createDataset(
+ chartConfig,
+ datasetsRegistry[catName].backgroundColor,
+ datasetsRegistry[catName].borderColor,
+ steps,
+ catName
+ );
+ categoriesRead[catName] = { index: idx++, data: newDataset.data };
+ returnedChartData.datasets.push(newDataset);
+ }
+
+ // Parse data
+ const dataParsed = createDataXYFormat(chartConfig, rec);
+
+ // Find the data array and push in it.
+ categoriesRead[catName].data.push(dataParsed);
}
-
- // Parse data
- const dataParsed = createDataXYFormat(chartConfig, rec);
-
- // Find the data array and push in it.
- categoriesRead[catName].data.push(dataParsed);
});
} else {
// 1 feature = 1 dataset
// Create dataset
- const newDataset = createDataset(chartConfig, steps, undefined);
+ const newDataset = createDataset(chartConfig, undefined, undefined, steps, undefined);
returnedChartData.datasets.push(newDataset);
// For each record
@@ -479,7 +440,12 @@ function createDatasetsPieDoughnut<
TType extends ChartType = 'pie' | 'doughnut',
TData extends GeoDefaultDataPoint = GeoDefaultDataPoint,
TLabel extends string = string
->(chartConfig: GeoChartConfig, records: TypeJsonObject[]): ChartData {
+>(
+ chartConfig: GeoChartConfig,
+ datasetsRegistry: GeoChartSelectedDataset,
+ datasRegistry: GeoChartSelectedDataset,
+ records: TypeJsonObject[]
+): ChartData {
// Transform the TypeFeatureJson data to ChartData
const returnedChartData: ChartData = {
labels: [],
@@ -493,6 +459,14 @@ function createDatasetsPieDoughnut<
if (!returnedChartData.labels!.includes(valX)) returnedChartData.labels!.push(valX);
});
+ // Build the color palette using registry and label (the same palette is used for all datasets)
+ let paletteBackgroundAll: string[];
+ if (returnedChartData.labels) {
+ paletteBackgroundAll = returnedChartData.labels?.map((label: TLabel) => {
+ return datasRegistry[label].backgroundColor;
+ });
+ }
+
// If we categorize
if (chartConfig.category?.property) {
// 1 category = 1 dataset
@@ -502,12 +476,15 @@ function createDatasetsPieDoughnut<
// Read the category as a string
const catName = rec[chartConfig.category!.property] as string;
- // If new category
- if (!Object.keys(categoriesRead).includes(catName)) {
- // Create dataset
- const newDataset = createDataset(chartConfig, undefined, catName);
- categoriesRead[catName] = { index: idx++, data: newDataset.data };
- returnedChartData.datasets.push(newDataset);
+ // If it's a category we actually want
+ if (datasetsRegistry[catName].checked) {
+ // If new category
+ if (!Object.keys(categoriesRead).includes(catName)) {
+ // Create dataset
+ const newDataset = createDataset(chartConfig, paletteBackgroundAll, undefined, undefined, catName);
+ categoriesRead[catName] = { index: idx++, data: newDataset.data };
+ returnedChartData.datasets.push(newDataset);
+ }
}
});
@@ -524,7 +501,7 @@ function createDatasetsPieDoughnut<
} else {
// 1 feature = 1 dataset
// Create dataset
- const newDataset = createDataset(chartConfig, undefined, undefined);
+ const newDataset = createDataset(chartConfig, undefined, undefined, undefined, undefined);
returnedChartData.datasets.push(newDataset);
// Compress the data for the ChartDataset
@@ -550,13 +527,19 @@ function createDatasets<
TType extends ChartType = ChartType,
TData extends GeoDefaultDataPoint = GeoDefaultDataPoint,
TLabel extends string = string
->(chartConfig: GeoChartConfig, steps: StepsPossibilities, records: TypeJsonObject[]): ChartData {
+>(
+ chartConfig: GeoChartConfig,
+ datasetsRegistry: GeoChartSelectedDataset,
+ datasRegistry: GeoChartSelectedDataset,
+ steps: StepsPossibilities,
+ records: TypeJsonObject[]
+): ChartData {
// Depending on the ChartType
if (chartConfig.chart === 'line' || chartConfig.chart === 'bar') {
- return createDatasetsLineBar(chartConfig, steps, records);
+ return createDatasetsLineBar(chartConfig, datasetsRegistry, steps, records);
}
if (chartConfig.chart === 'pie' || chartConfig.chart === 'doughnut') {
- return createDatasetsPieDoughnut(chartConfig, records);
+ return createDatasetsPieDoughnut(chartConfig, datasetsRegistry, datasRegistry, records);
}
throw Error('Unsupported chart type');
}
@@ -573,9 +556,9 @@ function createDatasets<
* is explicitely used.
* @param chartConfig The GeoChart Inputs to use to build the ChartJS ingestable information.
*/
-function createChartJSOptionsColorPalette(chartConfig: GeoChartConfig): void {
+export function setColorPalettes(chartConfig: GeoChartConfig | undefined): void {
// If there's a category
- if (chartConfig.category) {
+ if (chartConfig?.category) {
// If there's no background palettes
if (!chartConfig.category.paletteBackgrounds) {
// For line or bar charts, set the ChartJS default color palette
@@ -599,7 +582,7 @@ function createChartJSOptionsColorPalette(chartConfig:
}
// If there's a X-Axis
- if (chartConfig.geochart.xAxis) {
+ if (chartConfig?.geochart.xAxis) {
// If there's no background palettes
if (!chartConfig.geochart.xAxis.paletteBackgrounds) {
// eslint-disable-next-line no-param-reassign
@@ -631,10 +614,11 @@ export function createChartJSOptions(
language: string
): ChartOptions {
// The Chart JS Options as entered or the default options
- const options = (chartConfig.chartjsOptions || { ...defaultOptions }) as ChartOptions;
-
- // Verify the color palette is alright
- createChartJSOptionsColorPalette(chartConfig);
+ const options = {
+ ...defaultOptions,
+ ...chartConfig.chartjsOptions,
+ plugins: { ...(defaultOptions as PluginChartOptions).plugins },
+ } as ChartOptions;
// If line and using a time series
if (chartConfig.chart === 'line' && (chartConfig.geochart.xAxis?.type === 'time' || chartConfig.geochart.xAxis?.type === 'timeseries')) {
@@ -672,27 +656,30 @@ export function createChartJSOptions(
};
}
- // If line and using a time series
- if ((chartConfig.chart === 'line' || chartConfig.chart === 'bar') && chartConfig.geochart.yAxis?.type) {
+ // If line or bar
+ if (chartConfig.chart === 'line' || chartConfig.chart === 'bar') {
const optionsLine = options as ChartOptions<'line' | 'bar'>;
- optionsLine.scales = {
- ...optionsLine.scales,
- y: {
- type: chartConfig.geochart.yAxis?.type,
- },
- };
- }
+ // If type is set
+ if (chartConfig.geochart.yAxis?.type) {
+ optionsLine.scales = {
+ ...optionsLine.scales,
+ y: {
+ type: chartConfig.geochart.yAxis?.type,
+ },
+ };
+ }
- // If there's a custom suffix for the tooltip for the values
- if (chartConfig.geochart.yAxis.tooltipSuffix) {
- const optionsLine = options as ChartOptions<'line' | 'bar'>;
// Drill
optionsLine.plugins = optionsLine.plugins || {};
optionsLine.plugins.tooltip = optionsLine.plugins.tooltip || {};
optionsLine.plugins.tooltip.callbacks = optionsLine.plugins.tooltip.callbacks || {};
- optionsLine.plugins.tooltip.callbacks.label = (context): string => {
- return `${context.formattedValue} ${chartConfig.geochart.yAxis.tooltipSuffix}`;
- };
+
+ // If tooltip
+ if (chartConfig.geochart.yAxis.tooltipSuffix) {
+ optionsLine.plugins.tooltip.callbacks.label = (context): string => {
+ return `${context.formattedValue} ${chartConfig.geochart.yAxis.tooltipSuffix}`;
+ };
+ }
}
// Return the ChartJS Options
@@ -701,7 +688,8 @@ export function createChartJSOptions(
/**
* Creates the ChartJS Data object necessary for ChartJS process.
- * When the xAxis reprensents time, the datasets are sorted by date.
+ * The datasets are being sorted by labels.
+ * When the xAxis reprensents time, the datasets are internally sorted by date.
* @param chartConfig GeoChartConfigThe GeoChart Inputs to use to build the ChartJS ingestable information.
* @param records TypeJsonObject[] | undefined The Records to build the data from.
* @param defaultData ChartDataThe default, basic, necessary Data for ChartJS.
@@ -713,6 +701,8 @@ export function createChartJSData<
TLabel extends string = string
>(
chartConfig: GeoChartConfig,
+ datasetsRegistry: GeoChartSelectedDataset,
+ datasRegistry: GeoChartSelectedDataset,
steps: StepsPossibilities,
records: TypeJsonObject[] | undefined,
defaultData: ChartData
@@ -720,15 +710,12 @@ export function createChartJSData<
// If there's a data source, parse it to a GeoChart data
let data: ChartData = { ...defaultData };
if (records && records.length > 0) {
- data = createDatasets(chartConfig, steps, records);
+ data = createDatasets(chartConfig, datasetsRegistry, datasRegistry, steps, records);
}
// Sort the dataset labels
sortOnDatasetLabels(data);
- // Now that the datasets are ordered, set the color palette for them
- setColorsUsingPalette(chartConfig, data);
-
// If the x axis type is time
if (chartConfig.geochart.xAxis?.type === 'time' || chartConfig.geochart.xAxis?.type === 'timeseries') {
// Make sure the datasets data are sorted on X
diff --git a/src/chart-types.ts b/src/chart-types.ts
index 242d233..67d435f 100644
--- a/src/chart-types.ts
+++ b/src/chart-types.ts
@@ -13,21 +13,15 @@ export type TypeJsonObject = TypeJsonValue & { [key: string]: TypeJsonObject };
/**
* The Main GeoChart Configuration used by the GeoChart Component
*/
-export type GeoChartConfig = GeoChartOptions & {
- chartjsOptions: ChartOptions;
-};
-
-/**
- * The Main GeoChart Options Configuration used by the GeoChart Component
- */
-export type GeoChartOptions = {
+export type GeoChartConfig = {
chart: TType;
- title: string;
- query?: GeoChartQuery;
geochart: GeoChartOptionsGeochart;
+ datasources: GeoChartDatasource[];
+ title?: string;
+ query?: GeoChartQuery;
category?: GeoChartCategory;
ui?: GeoChartOptionsUI;
- datasources: GeoChartDatasource[];
+ chartjsOptions?: ChartOptions;
};
/**
@@ -105,9 +99,9 @@ export type GeoChartOptionsUI = {
* The Datasource object to hold the data, as supported by GeoChart.
*/
export type GeoChartDatasource = {
- value?: string;
display: string;
- sourceItem: TypeJsonObject; // Associated source item linking back to the source of the data
+ sourceItem?: TypeJsonObject; // Associated source item linking back to the source of the data
+ value?: string;
items?: TypeJsonObject[];
};
@@ -183,23 +177,46 @@ export type GeoChartAction = {
};
/**
- * Helper type to work with the Selected Datasets state.
+ * Helper type to work with the Datasets and their states.
*/
-export type GeoChartSelectedDatasets = {
- [label: string]: boolean;
+export type GeoChartDatasetOption = {
+ visible: boolean;
+ checked: boolean;
+ borderColor: string;
+ backgroundColor: string;
+};
+
+/**
+ * Helper type to work with the Datasets.
+ */
+export type GeoChartSelectedDataset = {
+ [label: string]: GeoChartDatasetOption;
};
/**
* The default color palette that ChartJS uses (I couldn't easily find out where that const is stored within ChartJS)
*/
export const DEFAULT_COLOR_PALETTE_CHARTJS_TRANSPARENT: string[] = [
- 'rgba(54, 162, 235, 0.5)',
- 'rgba(255, 99, 132, 0.5)',
- 'rgba(75, 192, 192, 0.5)',
- 'rgba(255, 159, 64, 0.5)',
- 'rgba(153, 102, 255, 0.5)',
- 'rgba(255, 205, 86, 0.5)',
- 'rgba(201, 203, 207, 0.5)',
+ 'rgba(54, 162, 235, 0.5)', // light blue
+ 'rgba(255, 99, 132, 0.5)', // light red
+ 'rgba(75, 192, 192, 0.5)', // light green
+ 'rgba(255, 159, 64, 0.5)', // light orange
+ 'rgba(153, 102, 255, 0.5)', // light purple
+ 'rgba(255, 205, 86, 0.5)', // light yellow
+ 'rgba(201, 203, 207, 0.5)', // light gray
+ 'rgba(0, 0, 255, 0.5)', // blue
+ 'rgba(0, 255, 0, 0.5)', // green
+ 'rgba(255, 0, 0, 0.5)', // red
+ 'rgba(255, 150, 0, 0.5)', // orange
+ 'rgba(255, 0, 255, 0.5)', // pink
+ 'rgba(30, 219, 34, 0.5)', // lime green
+ 'rgba(190, 0, 190, 0.5)', // purple
+ 'rgba(132, 255, 255, 0.5)', // cyan
+ 'rgba(255, 250, 0, 0.5)', // yellow
+ 'rgba(128, 0, 128, 0.5)', // maroon
+ 'rgba(0, 128, 128, 0.5)', // teal
+ 'rgba(128, 128, 0, 0.5)', // olive
+ 'rgba(128, 128, 128, 0.5)', // gray
];
/**
diff --git a/src/chart-util.ts b/src/chart-util.ts
index 30b8e67..534c49c 100644
--- a/src/chart-util.ts
+++ b/src/chart-util.ts
@@ -14,9 +14,9 @@ export const isNumber = (val: unknown): boolean => {
* @param index number The index we should find a color for
* @returns string The color at the specified index location in the palette
*/
-export const getColorFromPalette = (colorPalette: string[] | undefined, index: number): string | undefined => {
+export const getColorFromPalette = (colorPalette: string[] | undefined, index: number, defaultColor: string): string => {
if (colorPalette) return colorPalette[index % colorPalette.length];
- return undefined;
+ return defaultColor;
};
/**
@@ -58,20 +58,6 @@ export const extractColor = (color: string): string => {
return `rgb(${r}, ${g}, ${b})`;
}
- // TODO: Get rid of this code when determined that we really won't support color names written like 'red', 'green', etc.
- // TO.DO.CONT: If we do want to support such naming convention, this code works.
- // // Handle named colors (like "red")
- // const opt = new Option();
- // opt.style.color = color;
- // const namedColor = opt.style.color;
- // if (namedColor) {
- // document.body.appendChild(opt);
- // debugger;
- // const computedColor = getComputedStyle(opt).color;
- // document.body.removeChild(opt);
- // return extractColor(computedColor);
- // }
-
// As-is
return color;
};
diff --git a/src/chart.tsx b/src/chart.tsx
index 8b01ff0..299223c 100644
--- a/src/chart.tsx
+++ b/src/chart.tsx
@@ -1,6 +1,9 @@
-import { Chart as ChartJS, ChartType, ChartOptions, ChartData, ChartDataset, registerables } from 'chart.js';
+import { Chart as ChartJS, ChartType, ChartOptions, ChartData, ChartDataset, registerables, ChartConfiguration, Plugin } from 'chart.js';
import { Chart as ChartReact } from 'react-chartjs-2';
import 'chartjs-adapter-moment';
+// Keeping the import line commented, until we decide to use it or code an equivalent.
+// So that a dev can locally install it to work. This package is pretty useful to track this GeoChart Component.
+// import { useWhatChanged, setUseWhatChange } from '@simbathesailor/use-what-changed';
import {
GeoChartConfig,
GeoChartAction,
@@ -10,47 +13,101 @@ import {
GeoChartOptionsUI,
GeoChartQuery,
GeoChartDatasource,
- GeoChartSelectedDatasets,
+ GeoChartSelectedDataset,
TypeJsonObject,
StepsPossibilitiesConst,
StepsPossibilities,
DATE_OPTIONS_LONG,
+ GeoChartDatasetOption,
} from './chart-types';
import { SchemaValidator, ValidatorResult } from './chart-schema-validator';
-import { createChartJSOptions, createChartJSData, fetchItemsViaQueryForDatasource } from './chart-parsing';
-import { isNumber, extractColor, downloadJson } from './chart-util';
+import { createChartJSOptions, createChartJSData, fetchItemsViaQueryForDatasource, setColorPalettes } from './chart-parsing';
+import { isNumber, downloadJson, getColorFromPalette } from './chart-util';
import { sxClasses } from './chart-style';
-import { log, LOG_LEVEL_MAXIMUM, LOG_LEVEL_HIGH, LOG_LEVEL_MEDIUM, LOG_LEVEL_LOW } from './logger';
+import { logHigh, logUseEffectMount, logUseEffectUnmount } from './logger';
import T_EN from '../locales/en/translation.json';
import T_FR from '../locales/fr/translation.json';
+// Activate useWhatChanged in development (leaving the code commented, see header of file for reason)
+// setUseWhatChange(process.env.NODE_ENV === 'development');
+
/**
- * Main props for the Chart
+ * Main props for the Chart.
+ * There are 2 main ways to create a chart:
+ * (1) Using the 'inputs' parameter which configures an elaborated GeoChart or;
+ * (2) Using the 'chart'+'options'+'data' parameters which creates a basic GeoChart with essential ChartJS parameters.
*/
export interface TypeChartChartProps<
TType extends ChartType = ChartType,
TData extends GeoDefaultDataPoint = GeoDefaultDataPoint,
TLabel = string
> {
- sx?: unknown; // Will be casted as CSSProperties later via the imported cgpv react
- inputs?: GeoChartConfig; // The official way to work with all GeoChart features
- datasource?: GeoChartDatasource; // The selected datasource
- schemaValidator: SchemaValidator; // The schemas validator object
- chart?: TType; // When no inputs is specified, the GeoChart will use this chart props to work directly with ChartJS
- options?: ChartOptions; // When no inputs is specified, the GeoChart will use these options props to work directly with ChartJS
- data?: ChartData; // When no inputs is specified, the GeoChart will use this data props to work directly with ChartJS
- action?: GeoChartAction; // Indicate an action, user interface related, to be performed by the component
+ // The schemas validator object
+ schemaValidator: SchemaValidator;
+
+ // Will be casted as CSSProperties later via the imported cgpv react
+ sx?: unknown;
+
+ // The official way to work with all GeoChart features
+ inputs?: GeoChartConfig;
+
+ // The selected datasource (the selected value in the dropdown on top left corner of the ui)
+ datasource?: GeoChartDatasource;
+
+ // When no inputs is specified, the GeoChart will use this chart props to work directly with ChartJS
+ chart?: TType;
+ // When no inputs is specified, the GeoChart will use this options props to work directly with ChartJS
+ options?: ChartOptions;
+ // When no inputs is specified, the GeoChart will use this data props to work directly with ChartJS
+ data?: ChartData;
+
+ // Indicate an action, user interface related, to be performed by the component
+ action?: GeoChartAction;
+
+ // The default colors to apply to the Chart look (essentially redirected to ChartJS)
defaultColors?: GeoChartDefaultColors;
+
+ // State indicating that the GeoChart is in 'loading' state
isLoadingChart?: boolean;
+
+ // State indicating that the GeoChart is in 'loading datasource' state
isLoadingDatasource?: boolean;
- onParsed?: (chart: TType, options: ChartOptions, data: ChartData) => void;
+
+ // Callback executed when the data source changes (the selected value in the dropdown on top left corner of the ui)
onDatasourceChanged?: (value: GeoChartDatasource | undefined, language: string) => void;
- onDataChanged?: (dataIndex: number, dataLabel: string, checked: boolean) => void;
+
+ // Callback executed when the checked dataset (legend) changes
onDatasetChanged?: (datasetIndex: number, datasetLabel: string | undefined, checked: boolean) => void;
+
+ // Callback executed, for the pie/doughnut chart only, when the checked data changes
+ onDataChanged?: (dataIndex: number, dataLabel: string, checked: boolean) => void;
+
+ // Callback executed when user has changed the value on the slider on X axis
onSliderXChanged?: (value: number | number[]) => void;
+
+ // Callback executed when the value display for the X axis wants to show up
+ onSliderXValueDisplaying?: (value: number) => string;
+
+ // Callback executed when user has changed the value on the slider on Y axis
onSliderYChanged?: (value: number | number[]) => void;
+
+ // Callback executed when the value display for the Y axis wants to show up
+ onSliderYValueDisplaying?: (value: number) => string;
+
+ // Callback executed when the use has clicked the download button
+ onDownloadClicked?: (value: GeoChartDatasource, index: number) => string;
+
+ // Callback executed when user has selected another steps value from the ui (top right corner in the ui at the time of writing)
onStepSwitcherChanged?: (value: string) => void;
- onError?: (validators: (ValidatorResult | undefined)[]) => void;
+
+ // Callback executed when user has clicked the reset states button (top right corner in the ui at the time of writing)
+ onResetStates?: () => void;
+
+ // Callback executed when the data coming from the inputs parameters have been parsed and is ready to redirect to ChartJS for rendering
+ onParsed?: (chart: TType, options: ChartOptions, data: ChartData) => void;
+
+ // Callback executed when an error has happened
+ onError?: (error: string, exception: unknown | undefined) => void;
}
/**
@@ -99,13 +156,17 @@ export function GeoChart<
defaultColors,
isLoadingChart,
isLoadingDatasource: parentLoadingDatasource,
- onParsed,
onDatasourceChanged,
onDataChanged,
onDatasetChanged,
onSliderXChanged,
+ onSliderXValueDisplaying,
onSliderYChanged,
+ onSliderYValueDisplaying,
+ onDownloadClicked,
onStepSwitcherChanged,
+ onResetStates,
+ onParsed,
onError,
} = props;
@@ -115,35 +176,37 @@ export function GeoChart<
// Cast the style
const sx = elStyle as typeof CSSProperties;
- // Attribute the ChartJS default colors
- if (defaultColors?.backgroundColor) ChartJS.defaults.backgroundColor = defaultColors?.backgroundColor;
- if (defaultColors?.borderColor) ChartJS.defaults.borderColor = defaultColors?.borderColor;
- if (defaultColors?.color) ChartJS.defaults.color = defaultColors?.color;
-
// #region USE STATE SECTION ****************************************************************************************
// TODO: Refactor - Check why the useState and useCallback coming from cgpv lose their generic capabilities.
// TO.DO.CONT: This is rather problematic. It forces the devs to explicitely use some "not so pretty" type assertions
// so that things remain typed instead of becoming 'any' when using functions such as 'useState', 'useCallback', 'useRef', etc.
+ // Inner component states attached to the parent component
const [inputs, setInputs] = useState(parentInputs) as [
GeoChartConfig | undefined,
React.Dispatch | undefined>
];
const [chartType, setChartType] = useState(parentChart!) as [TType, React.Dispatch];
- const [selectedDatasource, setSelectedDatasource] = useState(parentDatasource) as [
- GeoChartDatasource | undefined,
- React.Dispatch
- ];
- const [chartOptions, setChartOptions] = useState(parentOptions!) as [ChartOptions, React.Dispatch>];
const [chartData, setChartData] = useState(parentData!) as [
ChartData,
React.Dispatch>
];
- const [action, setAction] = useState() as [GeoChartAction, React.Dispatch];
- const [selectedDatasets, setSelectedDatasets] = useState({}) as [GeoChartSelectedDatasets, React.Dispatch];
- const [selectedDatas, setSelectedDatas] = useState({}) as [GeoChartSelectedDatasets, React.Dispatch];
+ const [chartOptions, setChartOptions] = useState(parentOptions!) as [
+ ChartOptions | undefined,
+ React.Dispatch | undefined>
+ ];
+ const [selectedDatasource, setSelectedDatasource] = useState(parentDatasource) as [
+ GeoChartDatasource | undefined,
+ React.Dispatch
+ ];
const [redraw, setRedraw] = useState(parentAction?.shouldRedraw) as [boolean | undefined, React.Dispatch];
+ const [isLoadingDatasource, setIsLoadingDatasource] = useState(parentLoadingDatasource) as [boolean, React.Dispatch];
+
+ // Inner component states unrelated to the parent component
+ const [action, setAction] = useState() as [GeoChartAction, React.Dispatch];
+ const [datasetRegistry, setDatasetRegistry] = useState({}) as [GeoChartSelectedDataset, React.Dispatch];
+ const [datasRegistry, setDatasRegistry] = useState({}) as [GeoChartSelectedDataset, React.Dispatch];
const [filteredRecords, setFilteredRecords] = useState() as [TypeJsonObject[] | undefined, React.Dispatch];
const [xSliderMin, setXSliderMin] = useState(0) as [number, React.Dispatch];
const [xSliderMax, setXSliderMax] = useState(0) as [number, React.Dispatch];
@@ -156,17 +219,29 @@ export function GeoChart<
const [validatorInputs, setValidatorInputs] = useState() as [ValidatorResult | undefined, React.Dispatch];
const [validatorOptions, setValidatorOptions] = useState() as [ValidatorResult | undefined, React.Dispatch];
const [validatorData, setValidatorData] = useState() as [ValidatorResult | undefined, React.Dispatch];
-
- let stepsSwitcher: StepsPossibilities = false; // False by default
- if (inputs && inputs!.geochart.useSteps) stepsSwitcher = inputs!.geochart.useSteps;
- const [selectedSteps, setSelectedSteps] = useState(stepsSwitcher) as [
- StepsPossibilities | undefined,
- React.Dispatch
+ const [selectedSteps, setSelectedSteps] = useState(inputs?.geochart.useSteps || false) as [
+ StepsPossibilities,
+ React.Dispatch
];
+ const [plugins, setPlugins] = useState() as [Plugin[] | undefined, React.Dispatch[] | undefined>];
+ const [colorPaletteCategoryBackgroundIndex, setColorPaletteCategoryBackgroundIndex] = useState(0) as [number, React.Dispatch];
+ const [colorPaletteCategoryBorderIndex, setColorPaletteCategoryBorderIndex] = useState(0) as [number, React.Dispatch];
+ const [colorPaletteAxisBackgroundIndex, setColorPaletteAxisBackgroundIndex] = useState(0) as [number, React.Dispatch];
+ const [colorPaletteAxisBorderIndex, setColorPaletteAxisBorderIndex] = useState(0) as [number, React.Dispatch];
- const [isLoadingDatasource, setIsLoadingDatasource] = useState(parentLoadingDatasource) as [boolean, React.Dispatch];
+ const chartRef = useRef() as React.MutableRefObject>;
+
+ // #endregion
- const chartRef = useRef() as React.MutableRefObject>;
+ // #region DEFAULTS SECTION *****************************************************************************************
+
+ // Attribute the ChartJS default colors
+ if (defaultColors?.backgroundColor) ChartJS.defaults.backgroundColor = defaultColors?.backgroundColor;
+ if (defaultColors?.borderColor) ChartJS.defaults.borderColor = defaultColors?.borderColor;
+ if (defaultColors?.color) ChartJS.defaults.color = defaultColors?.color;
+
+ // Attribute the color palettes
+ setColorPalettes(inputs);
// #endregion
@@ -180,18 +255,22 @@ export function GeoChart<
const processAxes = (
geochart: GeoChartOptionsGeochart,
uiOptions: GeoChartOptionsUI | undefined,
- datasourceItems: TypeJsonObject[]
+ datasourceItems: TypeJsonObject[] | undefined
): (number | undefined)[] => {
// If has a xSlider and property and numbers as property
let xMinVal = uiOptions?.xSlider?.min;
let xMaxVal = uiOptions?.xSlider?.max;
if (uiOptions?.xSlider?.display) {
// If using numbers as data value
- if (datasourceItems && datasourceItems.length > 0 && isNumber(datasourceItems![0][geochart.xAxis.property])) {
+ if (datasourceItems && datasourceItems.length > 0) {
// If either min or max isn't preset
if (xMinVal === undefined || xMaxVal === undefined) {
// Dynamically calculate them
const values = datasourceItems!.map((x: TypeJsonObject) => {
+ // If date
+ if (geochart.xAxis.type === 'time' || geochart.xAxis.type === 'timeseries') {
+ return new Date(x[geochart.xAxis.property] as string).getTime();
+ }
return x[geochart.xAxis.property] as number;
});
xMinVal = xMinVal !== undefined ? xMinVal : Math.floor(Math.min(...values));
@@ -241,35 +320,40 @@ export function GeoChart<
* @param theYSliderValues number[] | undefined
*/
const processAxesValues = (
+ uiOptions: GeoChartOptionsUI | undefined,
xMinVal: number | undefined,
xMaxVal: number | undefined,
yMinVal: number | undefined,
yMaxVal: number | undefined,
theXSliderValues: number[] | undefined,
theYSliderValues: number[] | undefined
- ): [boolean, number[]] => {
+ ): [boolean, (number | undefined)[]] => {
// If still not set
let valuesComeFromState: boolean = false;
- if (xMaxVal && !theXSliderValues) {
- // Set the values for x axis to min/max
- setXSliderValues([xMinVal!, xMaxVal!]);
- } else if (theXSliderValues) {
- // eslint-disable-next-line no-param-reassign
- [xMinVal, xMaxVal] = theXSliderValues;
- valuesComeFromState = true;
+ if (uiOptions?.xSlider?.display) {
+ if (xMaxVal && !theXSliderValues) {
+ // Set the values for x axis to min/max
+ setXSliderValues([xMinVal!, xMaxVal!]);
+ } else if (theXSliderValues) {
+ // eslint-disable-next-line no-param-reassign
+ [xMinVal, xMaxVal] = theXSliderValues;
+ valuesComeFromState = true;
+ }
}
// If still not set
- if (yMaxVal && !theYSliderValues) {
- // Set the state
- setYSliderValues([yMinVal!, yMaxVal!]);
- } else if (theYSliderValues) {
- // eslint-disable-next-line no-param-reassign
- [yMinVal, yMaxVal] = theYSliderValues;
- valuesComeFromState = true;
+ if (uiOptions?.ySlider?.display) {
+ if (yMaxVal && !theYSliderValues) {
+ // Set the state
+ setYSliderValues([yMinVal!, yMaxVal!]);
+ } else if (theYSliderValues) {
+ // eslint-disable-next-line no-param-reassign
+ [yMinVal, yMaxVal] = theYSliderValues;
+ valuesComeFromState = true;
+ }
}
// Return if the values were set
- return [valuesComeFromState, [xMinVal!, xMaxVal!, yMinVal!, yMaxVal!]];
+ return [valuesComeFromState, [xMinVal, xMaxVal, yMinVal, yMaxVal]];
};
/**
@@ -279,15 +363,20 @@ export function GeoChart<
*/
const fetchDatasourceItems = async (
chartQuery: GeoChartQuery,
- sourceItem: TypeJsonObject,
- language: string
+ language: string,
+ sourceItem: TypeJsonObject | undefined,
+ errorCallback: ((error: string, exception: unknown | undefined) => void) | undefined
): Promise => {
try {
// Loading
setIsLoadingDatasource(true);
// Fetch the items for the data source in question
- return await fetchItemsViaQueryForDatasource(chartQuery, sourceItem, language);
+ return await fetchItemsViaQueryForDatasource(chartQuery, language, sourceItem);
+ } catch (ex) {
+ // Error
+ errorCallback?.('Failed to fetch the data', ex);
+ return Promise.resolve([]);
} finally {
// Done
setIsLoadingDatasource(false);
@@ -321,193 +410,262 @@ export function GeoChart<
// #endregion
- // #region EVENT HANDLERS SECTION ***********************************************************************************
+ // #region HOOKS USE CALLBACK GEOCHART SECTION **********************************************************************
/**
- * Handles when the Datasource changes
- * @param e Event The Select change event
- * @param item MenuItem The selected MenuItem
- */
- const handleDatasourceChanged = async (e: Event, item: typeof MenuItem): Promise => {
- // Find the selected datasource reference based on the MenuItem
- const ds: GeoChartDatasource | undefined = inputs!.datasources.find((x) => {
- return (x.value || x.display) === item.props.value;
- });
- if (!ds) return;
-
- // If the data source has no items
- if (!ds.items) {
- ds.items = await fetchDatasourceItems(inputs!.query!, ds.sourceItem, i18n.language);
- }
-
- // Set the selected datasource
- setSelectedDatasource(ds);
-
- // Callback
- onDatasourceChanged?.(ds, i18n.language);
- };
-
- /**
- * Handles when a dataset was checked/unchecked (via the legend)
- * @param datasetIndex number Indicates the dataset index that was checked/unchecked
- * @param datasetLabel string | undefined Indicates the dataset label that was checked/unchecked
- * @param checked boolean Indicates the checked state
+ * Updates the selected datasets object in synch with the actual datasets read from the data.
+ * @param theChartData ChartData
*/
- const handleDatasetChecked = (datasetIndex: number, datasetLabel: string | undefined, checked: boolean): void => {
- // Keep track
- selectedDatasets[datasetLabel!] = checked;
+ const processDatasets = useCallback(
+ (
+ items: TypeJsonObject[] | undefined,
+ catPropertyName: string | undefined,
+ paletteBackgrounds: string[] | undefined,
+ paletteBorders: string[] | undefined
+ ): void => {
+ // Check
+ if (!items || !catPropertyName) return;
+
+ // Loop on the items
+ let oneSelectedDatasetUpdated = false;
+ const catNames: string[] = [];
+ let backgroundIndex = colorPaletteCategoryBackgroundIndex;
+ let borderIndex = colorPaletteCategoryBorderIndex;
+ items?.forEach((item: TypeJsonObject) => {
+ // Read the category as a string
+ const catName = item[catPropertyName] as string;
+
+ // Build list
+ if (!catNames.includes(catName)) catNames.push(catName);
- // Update
- setSelectedDatasets({ ...selectedDatasets });
-
- // Callback
- onDatasetChanged?.(datasetIndex, datasetLabel, checked);
- };
-
- /**
- * Handles when a data was checked/unchecked (via the legend). This is only used by Pie and Doughnut Charts.
- * @param dataIndex number Indicates the data index that was checked/unchecked
- * @param dataLabel string Indicates the data label that was checked/unchecked
- * @param checked boolean Indicates the checked state
- */
- const handleDataChecked = (dataIndex: number, dataLabel: string, checked: boolean): void => {
- // Keep track
- selectedDatas[dataLabel] = checked;
+ // If not set
+ if (datasetRegistry[catName] === undefined) {
+ // eslint-disable-next-line no-param-reassign
+ datasetRegistry[catName] = {
+ visible: true,
+ checked: true,
+ backgroundColor: getColorFromPalette(paletteBackgrounds, backgroundIndex, ChartJS.defaults.color as string),
+ borderColor: getColorFromPalette(paletteBorders, borderIndex, ChartJS.defaults.color as string),
+ };
+ backgroundIndex++;
+ borderIndex++;
+ oneSelectedDatasetUpdated = true;
+ }
- // Update
- setSelectedDatas({ ...selectedDatas });
+ // If not visible, make sure it's visible
+ if (!datasetRegistry[catName].visible) {
+ // eslint-disable-next-line no-param-reassign
+ datasetRegistry[catName].visible = true;
+ oneSelectedDatasetUpdated = true;
+ }
+ });
+ setColorPaletteCategoryBackgroundIndex(backgroundIndex);
+ setColorPaletteCategoryBorderIndex(borderIndex);
+
+ // For any categories that weren't found, make sure they're invisible
+ Object.keys(datasetRegistry).forEach((catName: string) => {
+ if (!catNames.includes(catName) && datasetRegistry[catName].visible) {
+ // eslint-disable-next-line no-param-reassign
+ datasetRegistry[catName].visible = false;
+ oneSelectedDatasetUpdated = true;
+ }
+ });
- // Callback
- onDataChanged?.(dataIndex, dataLabel, checked);
- };
+ // If at least one dataset was updated (prevents loopback)
+ if (oneSelectedDatasetUpdated) {
+ setDatasetRegistry({ ...datasetRegistry });
+ }
+ },
+ [datasetRegistry, colorPaletteCategoryBackgroundIndex, colorPaletteCategoryBorderIndex]
+ ) as (
+ items: TypeJsonObject[] | undefined,
+ catPropertyName: string | undefined,
+ paletteBackgrounds: string[] | undefined,
+ paletteBorders: string[] | undefined
+ ) => void;
/**
- * Handles when the X Slider changes
- * @param value number | number[] Indicates the slider value
+ * Updates the selected data object in synch with the actual labels read from the data.
+ * @param theChartData ChartData
*/
- const handleSliderXChange = (newValue: number | number[]): void => {
- // Set the X State
- setXSliderValues(newValue);
+ const processLabels = useCallback(
+ (
+ theChartType: string,
+ items: TypeJsonObject[] | undefined,
+ labelPropertyName: string | undefined,
+ paletteBackgrounds: string[] | undefined,
+ paletteBorders: string[] | undefined
+ ): void => {
+ // Check
+ if (!items || !labelPropertyName) return;
+
+ // Only working on pie or doughnut
+ if (theChartType === 'pie' || theChartType === 'doughnut') {
+ // Loop on the items
+ let oneSelectedDataUpdated = false;
+ const labelNames: string[] = [];
+ let backgroundIndex = colorPaletteAxisBackgroundIndex;
+ let borderIndex = colorPaletteAxisBorderIndex;
+ items?.forEach((item: TypeJsonObject) => {
+ // Read the label as a string
+ const labelName = item[labelPropertyName] as string;
+
+ // Build list
+ if (!labelNames.includes(labelName)) labelNames.push(labelName);
+
+ // If not set
+ if (datasRegistry[labelName] === undefined) {
+ // eslint-disable-next-line no-param-reassign
+ datasRegistry[labelName] = {
+ visible: true,
+ checked: true,
+ backgroundColor: getColorFromPalette(paletteBackgrounds, backgroundIndex, ChartJS.defaults.color as string),
+ borderColor: getColorFromPalette(paletteBorders, borderIndex, ChartJS.defaults.color as string),
+ };
+ backgroundIndex++;
+ borderIndex++;
+ oneSelectedDataUpdated = true;
+ }
- // Callback
- onSliderXChanged?.(newValue);
- };
+ // If not visible, make sure it's visible
+ if (!datasRegistry[labelName].visible) {
+ // eslint-disable-next-line no-param-reassign
+ datasRegistry[labelName].visible = true;
+ oneSelectedDataUpdated = true;
+ }
+ });
- /**
- * Handles when the Y Slider changes
- * @param value number | number[] Indicates the slider value
- */
- const handleSliderYChange = (newValue: number | number[]): void => {
- // Set the Y State
- setYSliderValues(newValue);
+ // For any categories that weren't found, make sure they're invisible
+ Object.keys(datasRegistry).forEach((labelName: string) => {
+ if (!labelNames.includes(labelName) && datasRegistry[labelName].visible) {
+ // eslint-disable-next-line no-param-reassign
+ datasRegistry[labelName].visible = false;
+ oneSelectedDataUpdated = true;
+ }
+ });
- // Callback
- onSliderYChanged?.(newValue);
- };
+ // If at least one dataset was updated (prevents loopback)
+ if (oneSelectedDataUpdated) {
+ setDatasRegistry({ ...datasRegistry });
+ }
+ setColorPaletteAxisBackgroundIndex(backgroundIndex);
+ setColorPaletteAxisBorderIndex(borderIndex);
+ }
+ },
+ [datasRegistry, colorPaletteAxisBackgroundIndex, colorPaletteAxisBorderIndex]
+ ) as (
+ theChartType: string,
+ items: TypeJsonObject[] | undefined,
+ labelPropertyName: string | undefined,
+ paletteBackgrounds: string[] | undefined,
+ paletteBorders: string[] | undefined
+ ) => void;
/**
- * Handles when the Steps Switcher changes
- * @param value string Indicates the steps value
+ * Updates the chart dataset visibility based on the currently selected datasets.
+ * @param theChartData ChartData
+ * @param theSelectedDatasets GeoChartSelectedDatasets
*/
- const handleStepsSwitcherChanged = (e: unknown, item: typeof MenuItem): void => {
- // Set the step switcher
- setSelectedSteps(item.props.value);
-
- // Callback
- onStepSwitcherChanged?.(item.props.value);
- };
+ const updateDatasetVisibilityUsingState = useCallback(
+ (theChartRef: ChartJS, theDatasetRegistry: GeoChartSelectedDataset): void => {
+ if (!theChartRef) return;
- /**
- * Handles when the States must be cleared
- */
- const handleResetStates = (): void => {
- // Reset all selected datasets to true
- Object.keys(selectedDatasets).forEach((dataset: string) => {
- selectedDatasets[dataset] = true;
- });
+ // Get the current dataset labels
+ const dsLabels = theChartRef.data.datasets.map((x: ChartDataset) => {
+ return x.label!;
+ });
- // Reset all selected datasets to true
- Object.keys(selectedDatas).forEach((data: string) => {
- selectedDatas[data] = true;
- });
+ // Make sure the datasets visibility follow the state
+ Object.keys(theDatasetRegistry).forEach((value: string) => {
+ const idx = dsLabels.indexOf(value);
+ if (idx >= 0) theChartRef.setDatasetVisibility(idx, theDatasetRegistry[value]?.checked);
+ });
- // Clear all states
- setSelectedDatasets({ ...selectedDatasets });
- setSelectedDatas({ ...selectedDatas });
- setSelectedSteps(false);
- setXSliderValues([xSliderMin, xSliderMax]);
- setYSliderValues([ySliderMin, ySliderMax]);
- };
+ // Update visibility
+ theChartRef.update();
+ },
+ []
+ ) as (theChartRef: ChartJS, theDatasetRegistry: GeoChartSelectedDataset) => void;
/**
- * Handles the display of the label on the X Slider
- * @param value number | number[] Indicates the slider value
+ * Updates the chart data visibility based on the currently selected data.
+ * @param theChartData ChartData
+ * @param theSelectedData GeoChartSelectedDatasets
*/
- const handleSliderXValueDisplay = (value: number): string => {
- // If current chart has time as xAxis
- if (inputs?.geochart?.xAxis.type === 'time' || inputs?.geochart?.xAxis.type === 'timeseries') {
- return new Date(value).toLocaleDateString(i18n.language, DATE_OPTIONS_LONG);
- }
-
- // Default
- return value.toString();
- };
+ const updateDataVisibilityUsingState = useCallback(
+ (theChartRef: ChartJS, theDatasRegistry: GeoChartSelectedDataset): void => {
+ // Check
+ if (!theChartRef) return;
+
+ // The Config
+ const chartConf = theChartRef.config as ChartConfiguration;
+
+ // Only working on pie or doughnut
+ if (chartConf.type === 'pie' || chartConf.type === 'doughnut') {
+ // Make sure the datas visibility follow the state
+ theChartRef.data.labels?.forEach((value: TLabel) => {
+ const idx = theChartRef.data.labels!.indexOf(value);
+ const currVis = theChartRef.getDataVisibility(idx);
+ if (theDatasRegistry[value]?.checked !== currVis) {
+ theChartRef.toggleDataVisibility(idx);
+ }
+ });
- /**
- * Handles the display of the label on the Y Slider
- * @param value number | number[] Indicates the slider value
- */
- const handleSliderYValueDisplay = (value: number): string => {
- // Default
- return value.toString();
- };
+ // Update visibility
+ theChartRef.update();
+ }
+ },
+ []
+ ) as (theChartRef: ChartJS, theDatasRegistry: GeoChartSelectedDataset) => void;
/**
- * Handles when the download button is clicked
- * @param value number Indicates the button drop down selection when it was clicked
+ * Essential function to load the records in the Chart.
+ * @param datasource GeoChartDatasource The Datasource on which the records were grabbed
+ * @param records TypeJsonObject[] The actual records to load in the Chart.
*/
- const handleDownloadClick = (index: number): void => {
- // Get the data
- const data = { ...selectedDatasource! };
-
- // If only the filtered information
- if (index === 0) {
- data.items = filteredRecords;
-
- // If using categories
- if (inputs?.category) {
- // The selected datasets strings
- const selDatasetsStrings = Object.keys(selectedDatasets).filter((ds) => {
- return selectedDatasets[ds];
- });
-
- // Also filter on the selected datasets
- data.items = data.items?.filter((value: TypeJsonObject) => {
- return selDatasetsStrings.includes(value[inputs!.category!.property] as string);
- });
-
- // In case of pie/doughnut
- if (chartType === 'pie' || chartType === 'doughnut') {
- // The selected datas strings
- const selDatasStrings = Object.keys(selectedDatas).filter((ds) => {
- return selectedDatas[ds];
- });
-
- // Also filter on selected datas
- data.items = data.items?.filter((value: TypeJsonObject) => {
- return selDatasStrings.includes(value[inputs!.geochart.xAxis.property] as string);
- });
- }
- }
- }
+ const processLoadingRecords = useCallback(
+ (
+ theInputs: GeoChartConfig,
+ theDatasetRegistry: GeoChartSelectedDataset,
+ theDatasRegistry: GeoChartSelectedDataset,
+ theLanguage: string,
+ theSteps: StepsPossibilities,
+ records: TypeJsonObject[] | undefined
+ ): void => {
+ // Parse the data
+ const parsedOptions = createChartJSOptions(theInputs, parentOptions!, theLanguage);
+ const parsedData = createChartJSData(
+ theInputs,
+ theDatasetRegistry,
+ theDatasRegistry,
+ theSteps,
+ records,
+ parentData!
+ );
- // Download the data as json
- downloadJson(data, 'chart-data.json');
- };
+ // Callback
+ onParsed?.(theInputs!.chart, parsedOptions, parsedData);
- // #endregion
+ // Override
+ setChartType(theInputs!.chart);
+ setChartOptions(parsedOptions);
+ setChartData(parsedData);
- // #region HOOKS SECTION ********************************************************************************************
+ // If the resulting datasets array is empty, force a redraw action, otherwise ChartJS hangs on the last shown graphic
+ if (parsedData.datasets?.length === 0) setAction({ shouldRedraw: true });
+ },
+ // NO REACT for 'onParsed' (explicitely excluding it here instead of relying on
+ // the parent component to have used useCallback as they should have)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [parentOptions, parentData]
+ ) as (
+ theInputs: GeoChartConfig,
+ theDatasetRegistry: GeoChartSelectedDataset,
+ theDatasRegistry: GeoChartSelectedDataset,
+ theLanguage: string,
+ theSteps: StepsPossibilities,
+ records: TypeJsonObject[] | undefined
+ ) => void;
/**
* Helper function to filter datasource items based on 2 possible and independent axis.
@@ -516,10 +674,19 @@ export function GeoChart<
* @param xValues number | number[] The values in X to filter on
* @param yValues number | number[] The values in Y to filter on
*/
- const processFiltering = useCallback(
- (theInputs: GeoChartConfig, datasourceItems: TypeJsonObject[], xValues: number | number[], yValues: number | number[]): void => {
+ const processLoadingRecordsFilteringFirst = useCallback(
+ (
+ theInputs: GeoChartConfig,
+ theDatasetRegistry: GeoChartSelectedDataset,
+ theDatasRegistry: GeoChartSelectedDataset,
+ theLanguage: string,
+ theSteps: StepsPossibilities,
+ records: TypeJsonObject[] | undefined,
+ xValues: number | number[],
+ yValues: number | number[]
+ ): void => {
// If chart type is line
- let resItemsFinal: TypeJsonObject[] = [...datasourceItems!];
+ let resItemsFinal: TypeJsonObject[] = records ? [...records] : [];
if (theInputs?.chart === 'line') {
// If filterings on x supported
if (Array.isArray(xValues) && xValues.length === 2) {
@@ -530,7 +697,7 @@ export function GeoChart<
const theDateTo = new Date(xValues[1]);
// Filter the datasourceItems
- resItemsFinal = datasourceItems!.filter((item: TypeJsonObject) => {
+ resItemsFinal = records!.filter((item: TypeJsonObject) => {
const d = new Date(item[theInputs.geochart.xAxis.property] as string);
return theDateFrom <= d && d <= theDateTo;
});
@@ -540,7 +707,7 @@ export function GeoChart<
const to = xValues[1];
// Filter the datasourceItems
- resItemsFinal = datasourceItems!.filter((item: TypeJsonObject) => {
+ resItemsFinal = records!.filter((item: TypeJsonObject) => {
return (
from <= (item[theInputs.geochart.xAxis.property] as number) && (item[theInputs.geochart.xAxis.property] as number) <= to
);
@@ -560,172 +727,62 @@ export function GeoChart<
}
}
+ // Filter
+ processLoadingRecords(theInputs, theDatasetRegistry, theDatasRegistry, theLanguage, theSteps, resItemsFinal);
+
// Set new filtered inputs
setFilteredRecords(resItemsFinal);
},
- []
+ [processLoadingRecords]
) as (
theInputs: GeoChartConfig,
- datasourceItems: TypeJsonObject[],
+ theDatasetRegistry: GeoChartSelectedDataset,
+ theDatasRegistry: GeoChartSelectedDataset,
+ theLanguage: string,
+ theSteps: StepsPossibilities,
+ records: TypeJsonObject[] | undefined,
xValues: number | number[],
yValues: number | number[]
) => void;
- /**
- * Updates the selected datasets object in synch with the actual datasets read from the data.
- * @param theChartData ChartData
- */
- const processLoadingRecordsUpdateDatasets = useCallback((theChartData: ChartData): void => {
- // Get the dataset labels for the new data
- let oneSelectedDatasetNew = false;
- theChartData.datasets
- .map((x: ChartDataset) => {
- return x.label!;
- })
- .forEach((label: string) => {
- // If not set
- if (selectedDatasets[label] === undefined) {
- selectedDatasets[label] = true;
- oneSelectedDatasetNew = true;
- }
- });
-
- // If at least one dataset was new
- if (oneSelectedDatasetNew) setSelectedDatasets({ ...selectedDatasets });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []) as (theChartData: ChartData) => void; // No need for the selectedDatasets dependency (for performance)
-
- /**
- * Updates the selected data object in synch with the actual labels read from the data.
- * @param theChartData ChartData
- */
- const processLoadingRecordsUpdateLabels = useCallback((theChartData: ChartData): void => {
- // Get the labels for the new data
- let oneSelectedDataNew = false;
- theChartData.labels?.forEach((label: string) => {
- // If not set
- if (selectedDatas[label] === undefined) {
- selectedDatas[label] = true;
- oneSelectedDataNew = true;
- }
- });
-
- // If at least one dataset was new
- if (oneSelectedDataNew) setSelectedDatas({ ...selectedDatas });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []) as (theChartData: ChartData) => void; // No need for the selectedDatas dependency (for performance)
-
- /**
- * Updates the chart dataset visibility based on the currently selected datasets.
- * @param theChartData ChartData
- * @param theSelectedDatasets GeoChartSelectedDatasets
- */
- const updateDatasetVisibilityUsingState = useCallback(
- (theChartData: ChartData, theSelectedDatasets: GeoChartSelectedDatasets): void => {
- // Log
- if (!chartRef.current) return;
-
- // Get the current dataset labels
- const dsLabels = theChartData.datasets.map((x: ChartDataset) => {
- return x.label!;
- });
-
- // Make sure the datasets visibility follow the state
- Object.keys(theSelectedDatasets).forEach((value: string) => {
- chartRef.current.setDatasetVisibility(dsLabels.indexOf(value), theSelectedDatasets[value]);
- });
+ // #endregion
- // Update visibility
- chartRef.current.update();
- },
- []
- ) as (theChartData: ChartData, theSelectedDatasets: GeoChartSelectedDatasets) => void;
+ // #region HOOKS USE CALLBACK CHARTJS SECTION ***********************************************************************
/**
- * Updates the chart data visibility based on the currently selected data.
- * @param theChartData ChartData
- * @param theSelectedData GeoChartSelectedDatasets
+ * Handles when the ChartJS has finished initializing and before it started drawing data.
+ * @param chart ChartJS The ChartJS reference.
*/
- const updateDataVisibilityUsingState = useCallback(
- (theChartType: TType, theChartData: ChartData, theSelectedDatas: GeoChartSelectedDatasets): void => {
+ const handleChartJSAfterInit = useCallback(
+ (chart: ChartJS): void => {
// Log
- if (!chartRef.current) return;
- if (theChartType !== 'pie' && theChartType !== 'doughnut') return;
-
- // Make sure the datas visibility follow the state
- theChartData.labels?.forEach((value: TLabel) => {
- const idx = theChartData.labels!.indexOf(value);
- const currVis = chartRef.current.getDataVisibility(idx);
- if (theSelectedDatas[value] !== currVis) {
- chartRef.current.toggleDataVisibility(idx);
- }
- });
+ logHigh('CHARTJS AFTER INIT', chart, datasetRegistry);
- // Update visibility
- chartRef.current.update();
+ // Make sure the UI fits with the registry state before the first render is made. Mostly useful for pie/doughnut charts.
+ updateDatasetVisibilityUsingState(chart, datasetRegistry);
+ updateDataVisibilityUsingState(chart, datasRegistry);
},
- []
- ) as (theChartType: TType, theChartData: ChartData, theSelectedDatas: GeoChartSelectedDatasets) => void;
+ [datasRegistry, datasetRegistry, updateDataVisibilityUsingState, updateDatasetVisibilityUsingState]
+ );
- /**
- * Essential function to load the records in the Chart.
- * @param datasource GeoChartDatasource The Datasource on which the records were grabbed
- * @param records TypeJsonObject[] The actual records to load in the Chart.
- */
- const processLoadingRecords = useCallback(
- (theInputs: GeoChartConfig, theLanguage: string, theSteps: StepsPossibilities, records: TypeJsonObject[] | undefined): void => {
- // Parse the data
- const parsedOptions = createChartJSOptions(theInputs, parentOptions!, theLanguage);
- const parsedData = createChartJSData(theInputs, theSteps, records, parentData!);
-
- // Callback
- onParsed?.(theInputs!.chart, parsedOptions, parsedData);
-
- // Override
- setChartType(theInputs!.chart);
- setChartOptions(parsedOptions);
- setChartData(parsedData);
-
- // Update the selected datasets
- processLoadingRecordsUpdateDatasets(parsedData);
- processLoadingRecordsUpdateLabels(parsedData);
-
- // If the resulting datasets array is empty, force a redraw action, otherwise ChartJS hands on the last shown graphic
- if (parsedData.datasets?.length === 0) setAction({ shouldRedraw: true });
- },
- [parentData, parentOptions, onParsed, processLoadingRecordsUpdateDatasets, processLoadingRecordsUpdateLabels]
- ) as (theInputs: GeoChartConfig, theLanguage: string, theSteps: StepsPossibilities, records: TypeJsonObject[] | undefined) => void;
-
- /**
- * Makes sure the datasource items are initialized correctly for the first load of the Chart.
- * @param datasource GeoChartDatasource The Datasource on which the records were grabbed
- * @param records TypeJsonObject[] The actual records to load in the Chart.
- */
- const initDatasourceItems = useCallback(
- async (chartQuery: GeoChartQuery | undefined, ds: GeoChartDatasource, language: string): Promise => {
- // If no items
- const datasourceToLoad = ds;
- if (!ds.items) {
- // Must fetch straight away
- datasourceToLoad.items = await fetchDatasourceItems(chartQuery!, ds.sourceItem, language);
- }
+ // #endregion
- // Set the datasource
- setSelectedDatasource(datasourceToLoad);
- },
- []
- ) as (chartQuery: GeoChartQuery | undefined, ds: GeoChartDatasource, language: string) => Promise;
+ // #region HOOKS USE EFFECT PARENT COMP SECTION *********************************************************************************
// Effect hook when the inputs change - coming from parent component.
useEffect(() => {
// Log
- log(LOG_LEVEL_LOW, 'USE EFFECT PARENT INPUTS', parentInputs);
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - PARENT - INPUTS';
+ logUseEffectMount(USE_EFFECT_FUNC, parentInputs);
// Refresh the inputs in this component
setInputs(parentInputs);
- // Clear the selected datasource, because we're cleaning house
+ // Clear dependency states because we're cleaning house and until the selected datasource is
+ // property reset, inputs might be unrelated to the selected datasource in the other useEffects.
setSelectedDatasource(undefined);
+ setChartData(GeoChart.defaultProps.data as ChartData);
+ setChartOptions(GeoChart.defaultProps.options as ChartOptions);
// If parentInputs is specified
if (parentInputs) {
@@ -735,60 +792,174 @@ export function GeoChart<
return () => {
// Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT PARENT INPUTS - UNMOUNT', parentInputs);
+ logUseEffectUnmount(USE_EFFECT_FUNC, parentInputs);
};
}, [parentInputs, schemaValidator]);
+ // Effect hook when the main props about charttype, options and data change - coming from parent component.
+ useEffect(() => {
+ // Log
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - PARENT - CHARTJS INPUTS';
+ logUseEffectMount(USE_EFFECT_FUNC);
+
+ // Override
+ setChartType(parentChart!);
+ setChartOptions(parentOptions!);
+ setChartData(parentData!);
+
+ return () => {
+ // Log
+ logUseEffectUnmount(USE_EFFECT_FUNC);
+ };
+ }, [parentChart, parentOptions, parentData]);
+
+ // Effect hook when the selected datasource changes - coming from parent component.
+ useEffect(() => {
+ // Log
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - PARENT - DATASOURCE';
+ logUseEffectMount(USE_EFFECT_FUNC, parentDatasource);
+
+ // Set the datasource as provided
+ setSelectedDatasource(parentDatasource);
+
+ return () => {
+ // Log
+ logUseEffectUnmount(USE_EFFECT_FUNC, parentDatasource);
+ };
+ }, [parentDatasource]);
+
+ // Effect hook to be executed with loading datasource - coming from parent component.
+ useEffect(() => {
+ // Log
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - PARENT - LOADING DATASOURCE';
+ logUseEffectMount(USE_EFFECT_FUNC, parentLoadingDatasource);
+
+ // If defined, update the state
+ if (parentLoadingDatasource !== undefined) setIsLoadingDatasource(parentLoadingDatasource);
+
+ return () => {
+ // Log
+ logUseEffectUnmount(USE_EFFECT_FUNC);
+ };
+ }, [parentLoadingDatasource]);
+
+ // Effect hook when an action needs to happen - coming from parent component.
+ useEffect(() => {
+ // Log
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - PARENT - ACTION';
+ logUseEffectMount(USE_EFFECT_FUNC, parentAction);
+
+ // Set action for the component
+ if (parentAction) setAction(parentAction);
+
+ return () => {
+ // Log
+ logUseEffectUnmount(USE_EFFECT_FUNC);
+ };
+ }, [parentAction]);
+
+ // #endregion
+
+ // #region HOOKS USE EFFECT CURRENT COMP SECTION *********************************************************************************
+
+ // Effect hook ran once when initializing
+ useEffect(() => {
+ const plugin = {
+ id: 'geochart-chartjs-plugin',
+ afterInit: (chartEvent: unknown): void => handleChartJSAfterInit(chartEvent as ChartJS),
+ };
+
+ // Register
+ setPlugins([plugin]);
+ }, [handleChartJSAfterInit]);
+
// Effect hook when the inputs change - coming from this component.
useEffect(() => {
// Log
- log(LOG_LEVEL_MEDIUM, 'USE EFFECT INPUTS', inputs);
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - CURRENT - INPUTS';
+ logUseEffectMount(USE_EFFECT_FUNC, inputs);
+
+ // Async function to fetch data from within a sync useEffect :|
+ const fetchAndSetSelectedDatasource = async (query: GeoChartQuery, language: string, datasource: GeoChartDatasource): Promise => {
+ // Perform the fetch
+ // eslint-disable-next-line no-param-reassign
+ datasource.items = await fetchDatasourceItems(query, language, datasource.sourceItem, onError);
+
+ // Set the datasource
+ setSelectedDatasource(datasource);
+ };
- // Reset selected datasets (leave the code there, tentatively for now)
- // setSelectedDatasets({});
+ // If no steps switcher, reset the state of the useSteps to the config, we don't want to be stuck on a setting set by a ui which may not exist anymore
+ if (!inputs?.ui?.stepsSwitcher) setSelectedSteps(inputs?.geochart.useSteps || false);
// If no datasources on the inputs, create a default one
- if (inputs) {
+ if (inputs && inputs.datasources && inputs.datasources.length > 0) {
+ // The datasource to load on start
+ const ds = inputs.datasources[0];
+
// Init the datasource items for the first record and sets it
- initDatasourceItems(inputs!.query, inputs!.datasources[0], i18n.language);
+ if (!ds.items && inputs.query) {
+ // Must fetch straight away
+ fetchAndSetSelectedDatasource(inputs.query, i18n.language, ds);
+ } else setSelectedDatasource(ds);
} else setSelectedDatasource(undefined);
return () => {
// Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT INPUTS - UNMOUNT', inputs);
+ logUseEffectUnmount(USE_EFFECT_FUNC, inputs);
};
- }, [inputs, i18n.language, initDatasourceItems]);
+ // NO REACT for 'onError' (explicitely excluding it here instead of relying on
+ // the parent component to have used useCallback as they should have)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [inputs, i18n.language]);
- // Effect hook when the selected datasource changes - coming from parent component.
+ // Effect hook when the selected datasource changes - coming from this component.
useEffect(() => {
// Log
- log(LOG_LEVEL_LOW, 'USE EFFECT PARENT DATASOURCE', parentDatasource);
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - CURRENT - PROCESS DATA - REGISTRY';
+ logUseEffectMount(USE_EFFECT_FUNC, inputs, selectedDatasource);
- // Set the datasource as provided
- setSelectedDatasource(parentDatasource);
+ // If selectedDatasource is specified
+ if (inputs && selectedDatasource) {
+ // Update the Datasets Registry based on the chart information
+ processDatasets(
+ selectedDatasource.items,
+ inputs.category?.property,
+ inputs.category?.paletteBackgrounds,
+ inputs.category?.paletteBorders
+ );
+
+ // Update the Datas/Labels Registry based on the chart information
+ processLabels(
+ inputs.chart,
+ selectedDatasource.items,
+ inputs.geochart.xAxis.property,
+ inputs.geochart.xAxis.paletteBackgrounds,
+ inputs.geochart.xAxis.paletteBorders
+ );
+ }
return () => {
// Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT PARENT DATASOURCE - UNMOUNT', parentDatasource);
+ logUseEffectUnmount(USE_EFFECT_FUNC, selectedDatasource);
};
- }, [parentDatasource]);
+ }, [inputs, selectedDatasource, processDatasets, processLabels]);
// Effect hook when the selected datasource changes - coming from this component.
useEffect(() => {
// Log
- log(LOG_LEVEL_MEDIUM, 'USE EFFECT PROCESS DATA', inputs, selectedDatasource);
-
- // Clear any record filters
- setFilteredRecords(undefined);
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - CURRENT - PROCESS DATA - DATA';
+ logUseEffectMount(USE_EFFECT_FUNC, inputs, selectedDatasource);
// If selectedDatasource is specified
- if (selectedDatasource) {
+ if (inputs && selectedDatasource) {
// Process the axes
- let [xMinVal, xMaxVal, yMinVal, yMaxVal] = processAxes(inputs!.geochart, inputs!.ui, selectedDatasource!.items!);
+ let [xMinVal, xMaxVal, yMinVal, yMaxVal] = processAxes(inputs.geochart, inputs.ui, selectedDatasource.items);
- // If any sliders
+ // Process the axes values
let valuesComeFromState = false;
[valuesComeFromState, [xMinVal, xMaxVal, yMinVal, yMaxVal]] = processAxesValues(
+ inputs!.ui,
xMinVal,
xMaxVal,
yMinVal,
@@ -799,54 +970,45 @@ export function GeoChart<
// If using the state, filter right away
if (valuesComeFromState) {
- // Load data with filters
- processFiltering(inputs!, selectedDatasource!.items!, [xMinVal!, xMaxVal!], [yMinVal!, yMaxVal!]);
+ // Load records with filtering
+ processLoadingRecordsFilteringFirst(
+ inputs,
+ datasetRegistry,
+ datasRegistry,
+ i18n.language,
+ selectedSteps,
+ selectedDatasource.items,
+ [xMinVal!, xMaxVal!],
+ [yMinVal!, yMaxVal!]
+ );
} else {
// Load records without filtering for nothing
- processLoadingRecords(inputs!, i18n.language, selectedSteps!, selectedDatasource!.items);
+ processLoadingRecords(inputs, datasetRegistry, datasRegistry, i18n.language, selectedSteps, selectedDatasource.items);
}
}
return () => {
// Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT PROCESS DATA - UNMOUNT', selectedDatasource);
- };
- }, [inputs, i18n.language, selectedDatasource, selectedSteps, xSliderValues, ySliderValues, processLoadingRecords, processFiltering]);
-
- // Effect hook when the filtered records change - coming from this component.
- useEffect(() => {
- // Log
- log(LOG_LEVEL_MEDIUM, 'USE EFFECT PROCESS DATA FILTERED RECORDS', selectedDatasource, filteredRecords, selectedSteps);
-
- // Process loading records
- if (selectedDatasource) processLoadingRecords(inputs!, i18n.language, selectedSteps!, filteredRecords || selectedDatasource!.items);
-
- return () => {
- // Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT PROCESS DATA FILTERED RECORDS - UNMOUNT', selectedDatasource);
+ logUseEffectUnmount(USE_EFFECT_FUNC, selectedDatasource);
};
- }, [inputs, i18n.language, selectedDatasource, filteredRecords, selectedSteps, processLoadingRecords]);
-
- // Effect hook when the main props about charttype, options and data change - coming from parent component.
- useEffect(() => {
- // Log
- log(LOG_LEVEL_LOW, 'USE EFFECT PARENT CHARTJS INPUTS');
-
- // Override
- setChartType(parentChart!);
- setChartOptions(parentOptions!);
- setChartData(parentData!);
-
- return () => {
- // Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT PARENT CHARTJS INPUTS - UNMOUNT');
- };
- }, [parentChart, parentOptions, parentData]);
+ }, [
+ inputs,
+ selectedDatasource,
+ datasRegistry,
+ datasetRegistry,
+ i18n.language,
+ selectedSteps,
+ xSliderValues,
+ ySliderValues,
+ processLoadingRecordsFilteringFirst,
+ processLoadingRecords,
+ ]);
// Effect hook when the chartOptions, chartData change - coming from this component.
useEffect(() => {
// Log
- log(LOG_LEVEL_MEDIUM, 'USE EFFECT CHARTJS OPTIONS+DATA', chartOptions, chartData);
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - CURRENT - CHARTJS OPTIONS+DATA';
+ logUseEffectMount(USE_EFFECT_FUNC, chartOptions, chartData);
// If chart options. Validate the parsing we did do follow ChartJS options schema validating
if (chartOptions) setValidatorOptions(schemaValidator.validateOptions(chartOptions));
@@ -856,117 +1018,111 @@ export function GeoChart<
return () => {
// Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT CHARTJS OPTIONS+DATA - UNMOUNT');
+ logUseEffectUnmount(USE_EFFECT_FUNC);
};
}, [chartOptions, chartData, schemaValidator]);
- // Effect hook when the selectedDatasets change - coming from this component.
+ // Effect hook when the datasetRegistry change - coming from this component.
useEffect(() => {
// Log
- log(LOG_LEVEL_MEDIUM, 'USE EFFECT SELECTED DATASETS', selectedDatasets);
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - CURRENT - SELECTED DATASETS';
+ logUseEffectMount(USE_EFFECT_FUNC);
// Make sure the visibility of the chart aligns with the selected datasets
- updateDatasetVisibilityUsingState(chartData, selectedDatasets);
+ updateDatasetVisibilityUsingState(chartRef.current, datasetRegistry);
return () => {
// Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT SELECTED DATASETS - UNMOUNT');
+ logUseEffectUnmount(USE_EFFECT_FUNC);
};
- }, [chartData, selectedDatasets, updateDatasetVisibilityUsingState]);
+ }, [datasetRegistry, updateDatasetVisibilityUsingState]);
- // Effect hook when the selectedDatas change - coming from this component.
+ // Effect hook when the datasRegistry change - coming from this component.
useEffect(() => {
// Log
- log(LOG_LEVEL_MEDIUM, 'USE EFFECT SELECTED DATAS', selectedDatas);
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - CURRENT - SELECTED DATAS';
+ logUseEffectMount(USE_EFFECT_FUNC);
// Make sure the visibility of the chart aligns with the selected datas
- updateDataVisibilityUsingState(chartType, chartData, selectedDatas);
+ updateDataVisibilityUsingState(chartRef.current, datasRegistry);
return () => {
// Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT SELECTED DATAS - UNMOUNT');
+ logUseEffectUnmount(USE_EFFECT_FUNC);
};
- }, [chartType, chartData, selectedDatas, updateDataVisibilityUsingState]);
+ }, [datasRegistry, updateDataVisibilityUsingState]);
// Effect hook to validate the schemas of inputs - coming from this component.
useEffect(() => {
// Log
- log(LOG_LEVEL_HIGH, 'USE EFFECT VALIDATORS INPUTS', hasValidSchemas([validatorInputs]));
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - CURRENT - VALIDATORS - INPUTS';
+ logUseEffectMount(USE_EFFECT_FUNC, hasValidSchemas([validatorInputs]));
// If any error
if (!hasValidSchemas([validatorInputs])) {
+ // Gather error messages
+ const error = SchemaValidator.parseValidatorResultsMessages([validatorInputs]);
// If a callback is defined
- onError?.([validatorInputs]);
- // eslint-disable-next-line no-console
- console.error([validatorInputs]);
+ onError?.(`${t('geochart.parsingError')}\n\n${error}`, undefined);
}
return () => {
// Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT VALIDATORS INPUTS - UNMOUNT');
+ logUseEffectUnmount(USE_EFFECT_FUNC);
};
- }, [validatorInputs, onError]);
+ // NO REACT for 'onError' (explicitely excluding it here instead of relying on
+ // the parent component to have used useCallback as they should have)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [validatorInputs, t]);
// Effect hook to validate the schemas of inputs - coming from this component.
useEffect(() => {
// Log
- log(LOG_LEVEL_HIGH, 'USE EFFECT VALIDATORS OPTIONS+DATA', hasValidSchemas([validatorOptions, validatorData]));
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - CURRENT - VALIDATORS - OPTIONS+DATA';
+ logUseEffectMount(USE_EFFECT_FUNC, hasValidSchemas([validatorOptions, validatorData]));
// If any error
if (!hasValidSchemas([validatorOptions, validatorData])) {
+ // Gather error messages
+ const error = SchemaValidator.parseValidatorResultsMessages([validatorOptions, validatorData]);
// If a callback is defined
- onError?.([validatorOptions, validatorData]);
- // eslint-disable-next-line no-console
- console.error([validatorOptions, validatorData]);
+ onError?.(`${t('geochart.parsingError')}\n\n${error}`, undefined);
}
return () => {
// Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT VALIDATORS OPTIONS+DATA - UNMOUNT');
- };
- }, [validatorOptions, validatorData, onError]);
-
- // Effect hook when an action needs to happen - coming from parent component.
- useEffect(() => {
- // Log
- log(LOG_LEVEL_MEDIUM, 'USE EFFECT PARENT ACTION', parentAction);
-
- // Set action for the component
- if (parentAction) setAction(parentAction);
-
- return () => {
- // Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT PARENT ACTION - UNMOUNT');
+ logUseEffectUnmount(USE_EFFECT_FUNC);
};
- }, [parentAction]);
+ // NO REACT for 'onError' (explicitely excluding it here instead of relying on
+ // the parent component to have used useCallback as they should have)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [validatorOptions, validatorData, t]);
// Effect hook when an action needs to happen - coming from this component.
useEffect(() => {
// Log
- log(LOG_LEVEL_MEDIUM, 'USE EFFECT ACTION', action);
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - CURRENT - ACTION';
+ logUseEffectMount(USE_EFFECT_FUNC, action);
// If redraw is true, reset the property in the action, 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 only way I could find without having code in the Parent Component.
+ // A bit funky, but only way I could find without having code the logic within the Parent Component.
if (action?.shouldRedraw) {
action!.shouldRedraw = false;
// Redraw
- performRedraw().then(() => {
- // Readjust visibility, because redraw resets all datasets visibility to true
- updateDatasetVisibilityUsingState(chartData, selectedDatasets);
- updateDataVisibilityUsingState(chartType, chartData, selectedDatas);
- });
+ performRedraw();
}
return () => {
// Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT ACTION - UNMOUNT');
+ logUseEffectUnmount(USE_EFFECT_FUNC);
};
- }, [chartType, chartData, action, selectedDatasets, selectedDatas, updateDatasetVisibilityUsingState, updateDataVisibilityUsingState]);
+ }, [action]);
// Effect hook to be executed with i18n
useEffect(() => {
// Log
- log(LOG_LEVEL_MEDIUM, 'USE EFFECT ADD_RESOURCE_BUNDLE');
+ const USE_EFFECT_FUNC = 'GEOCHART - USE EFFECT - CURRENT - i18n';
+ logUseEffectMount(USE_EFFECT_FUNC);
// Add GeoChart translations file
i18n.addResourceBundle('en', 'translation', T_EN);
@@ -974,23 +1130,215 @@ export function GeoChart<
return () => {
// Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT ADD_RESOURCE_BUNDLE - UNMOUNT');
+ logUseEffectUnmount(USE_EFFECT_FUNC);
};
}, [i18n]);
- // Effect hook to be executed with loading datasource - coming from parent component.
- useEffect(() => {
- // Log
- log(LOG_LEVEL_MEDIUM, 'USE EFFECT PARENT LOADING DATASOURCE', parentLoadingDatasource);
+ // #endregion
- // If defined, update the state
- if (parentLoadingDatasource !== undefined) setIsLoadingDatasource(parentLoadingDatasource);
+ // #region EVENT HANDLERS SECTION ***********************************************************************************
- return () => {
- // Log
- log(LOG_LEVEL_MAXIMUM, 'USE EFFECT PARENT LOADING DATASOURCE - UNMOUNT');
- };
- }, [parentLoadingDatasource]);
+ /**
+ * Handles when the Datasource changes
+ * @param e Event The Select change event
+ * @param item MenuItem The selected MenuItem
+ */
+ const handleDatasourceChanged = async (e: Event, item: typeof MenuItem): Promise => {
+ // Find the selected datasource reference based on the MenuItem
+ const ds: GeoChartDatasource | undefined = inputs!.datasources.find((datasource: GeoChartDatasource) => {
+ return (datasource.value || datasource.display) === item.props.value;
+ });
+ if (!ds) return;
+
+ // If the data source has no items
+ if (!ds.items) {
+ ds.items = await fetchDatasourceItems(inputs!.query!, i18n.language, ds.sourceItem, onError);
+ }
+
+ // Set the selected datasource
+ setSelectedDatasource(ds);
+
+ // Callback
+ onDatasourceChanged?.(ds, i18n.language);
+ };
+
+ /**
+ * Handles when a dataset was checked/unchecked (via the legend)
+ * @param datasetIndex number Indicates the dataset index that was checked/unchecked
+ * @param datasetLabel string | undefined Indicates the dataset label that was checked/unchecked
+ * @param checked boolean Indicates the checked state
+ */
+ const handleDatasetChecked = (datasetIndex: number, datasetLabel: string | undefined, checked: boolean): void => {
+ // Keep track
+ datasetRegistry[datasetLabel!].checked = checked;
+
+ // Update the registry
+ setDatasetRegistry({ ...datasetRegistry });
+
+ // Callback
+ onDatasetChanged?.(datasetIndex, datasetLabel, checked);
+ };
+
+ /**
+ * Handles when a data was checked/unchecked (via the legend). This is only used by Pie and Doughnut Charts.
+ * @param dataIndex number Indicates the data index that was checked/unchecked
+ * @param dataLabel string Indicates the data label that was checked/unchecked
+ * @param checked boolean Indicates the checked state
+ */
+ const handleDataChecked = (dataIndex: number, dataLabel: string, checked: boolean): void => {
+ // Keep track
+ datasRegistry[dataLabel].checked = checked;
+
+ // Update the registry
+ setDatasRegistry({ ...datasRegistry });
+
+ // Callback
+ onDataChanged?.(dataIndex, dataLabel, checked);
+ };
+
+ /**
+ * Handles when the X Slider changes
+ * @param value number | number[] Indicates the slider value
+ */
+ const handleSliderXChange = (newValue: number | number[]): void => {
+ // Set the X State
+ setXSliderValues(newValue);
+
+ // Callback
+ onSliderXChanged?.(newValue);
+ };
+
+ /**
+ * Handles when the Y Slider changes
+ * @param value number | number[] Indicates the slider value
+ */
+ const handleSliderYChange = (newValue: number | number[]): void => {
+ // Set the Y State
+ setYSliderValues(newValue);
+
+ // Callback
+ onSliderYChanged?.(newValue);
+ };
+
+ /**
+ * Handles when the Steps Switcher changes
+ * @param value string Indicates the steps value
+ */
+ const handleStepsSwitcherChanged = (e: unknown, item: typeof MenuItem): void => {
+ // Set the step switcher
+ setSelectedSteps(item.props.value);
+
+ // Callback
+ onStepSwitcherChanged?.(item.props.value);
+ };
+
+ /**
+ * Handles when the States must be cleared
+ */
+ const handleResetStates = (): void => {
+ // Reset all selected datasets to true
+ Object.keys(datasetRegistry).forEach((dataset: string) => {
+ datasetRegistry[dataset].checked = true;
+ });
+
+ // Reset all selected datasets to true
+ Object.keys(datasRegistry).forEach((data: string) => {
+ datasRegistry[data].checked = true;
+ });
+
+ // Clear all states
+ setDatasetRegistry({ ...datasetRegistry });
+ setDatasRegistry({ ...datasRegistry });
+ setSelectedSteps(inputs?.geochart.useSteps || false);
+ setXSliderValues([xSliderMin, xSliderMax]);
+ setYSliderValues([ySliderMin, ySliderMax]);
+
+ // Callback
+ onResetStates?.();
+ };
+
+ /**
+ * Handles the display of the label on the X Slider
+ * @param value number | number[] Indicates the slider value
+ */
+ const handleSliderXValueDisplay = (value: number): string => {
+ // Callback in case we're overriding this behavior
+ const val = onSliderXValueDisplaying?.(value);
+ if (val) return val;
+
+ // Default behavior
+ // If current chart has time as xAxis
+ if (inputs?.geochart?.xAxis.type === 'time' || inputs?.geochart?.xAxis.type === 'timeseries') {
+ return new Date(value).toLocaleDateString(i18n.language, DATE_OPTIONS_LONG);
+ }
+
+ // Default value as is
+ return value.toString();
+ };
+
+ /**
+ * Handles the display of the label on the Y Slider
+ * @param value number | number[] Indicates the slider value
+ */
+ const handleSliderYValueDisplay = (value: number): string => {
+ // Callback in case we're overriding this behavior
+ const val = onSliderYValueDisplaying?.(value);
+ if (val) return val;
+
+ // Default value as is
+ return value.toString();
+ };
+
+ /**
+ * Handles when the download button is clicked
+ * @param index number Indicates the button drop down selection index when it was clicked.
+ * For our button usage:
+ * - 0: Means 'download view' was selected when button was clicked
+ * - 1: Means 'download all' was selected when button was clicked
+ */
+ const handleDownloadClick = (index: number): void => {
+ // Get the data
+ const data = { ...selectedDatasource! } as GeoChartDatasource;
+
+ // If only the filtered information
+ if (index === 0) {
+ // Get either the actually filtered records (via the sliders) or the data.items
+ data.items = filteredRecords || data.items;
+
+ // If using categories
+ if (inputs?.category) {
+ // The checked datasets strings
+ const checkedDatasetsStrings = Object.keys(datasetRegistry).filter((ds) => {
+ return datasetRegistry[ds].checked;
+ });
+
+ // Also filter on the selected datasets
+ data.items = data.items?.filter((value: TypeJsonObject) => {
+ return checkedDatasetsStrings.includes(value[inputs.category!.property] as string);
+ });
+
+ // In case of pie/doughnut
+ if (chartType === 'pie' || chartType === 'doughnut') {
+ // The checked datas strings
+ const checkedDatasStrings = Object.keys(datasRegistry).filter((ds) => {
+ return datasRegistry[ds].checked;
+ });
+
+ // Also filter on selected datas
+ data.items = data.items?.filter((value: TypeJsonObject) => {
+ return checkedDatasStrings.includes(value[inputs.geochart.xAxis.property] as string);
+ });
+ }
+ }
+ }
+
+ // Callback
+ let fileName = onDownloadClicked?.(data, index);
+ if (!fileName) fileName = 'chart-data.json';
+
+ // Download the data as json
+ downloadJson(data, fileName);
+ };
// #endregion
@@ -1001,7 +1349,7 @@ export function GeoChart<
* @returns The Chart JSX.Element itself using Line as default
*/
const renderChart = (): JSX.Element => {
- return ;
+ return ;
};
/**
@@ -1113,7 +1461,7 @@ export function GeoChart<
if (inputs) {
// Create the menu items
const menuItems: (typeof TypeMenuItemProps)[] = [];
- inputs!.datasources.forEach((s: GeoChartDatasource) => {
+ inputs.datasources.forEach((s: GeoChartDatasource) => {
menuItems.push({ key: s.value || s.display, item: { value: s.value || s.display, children: s.display || s.value } });
});
return (
@@ -1138,7 +1486,7 @@ export function GeoChart<
* @returns The Ttile Element
*/
const renderTitle = (): JSX.Element => {
- if (inputs) {
+ if (inputs && inputs.title) {
return {inputs.title};
}
@@ -1171,7 +1519,7 @@ export function GeoChart<
if (inputs?.ui?.resetStates) {
return (
);
}
@@ -1196,32 +1544,34 @@ export function GeoChart<
* @returns The Dataset selector Element
*/
const renderDatasetSelector = (): JSX.Element => {
- if (chartData) {
- const { datasets } = chartData;
- if (datasets.length > 1) {
+ if (inputs && chartData && inputs.category) {
+ if (Object.keys(datasetRegistry).length > 1) {
const label = chartType === 'pie' || chartType === 'doughnut' ? `${t('geochart.category')}:` : '';
return (
{label}
- {datasets.map((ds: ChartDataset, idx: number) => {
- let { color } = ChartJS.defaults;
- if (ds.borderColor) color = ds.borderColor! as string;
- else if ((chartType === 'line' || chartType === 'bar') && ds.backgroundColor) color = ds.backgroundColor! as string;
-
- return (
-
- ): void => {
- handleDatasetChecked(idx, ds.label, e.target?.checked);
- }}
- checked={selectedDatasets[ds.label!] !== undefined ? selectedDatasets[ds.label!] : true}
- />
-
- {ds.label}
-
-
- );
- })}
+ {Object.entries(datasetRegistry)
+ .filter(([, dsOption]: [string, GeoChartDatasetOption]) => {
+ return dsOption.visible;
+ })
+ .map(([dsLabel, dsOption]: [string, GeoChartDatasetOption], idx: number) => {
+ let color;
+ if (chartType === 'line' || chartType === 'bar') color = dsOption.borderColor as string;
+
+ return (
+
+ ): void => {
+ handleDatasetChecked(idx, dsLabel, e.target?.checked);
+ }}
+ checked={datasetRegistry[dsLabel].checked !== undefined ? datasetRegistry[dsLabel].checked : true}
+ />
+
+ {dsLabel}
+
+
+ );
+ })}
);
}
@@ -1236,31 +1586,35 @@ export function GeoChart<
* @returns The Data selector Element
*/
const renderDataSelector = (): JSX.Element => {
- if (chartData) {
- const { labels, datasets } = chartData;
- if ((chartType === 'pie' || chartType === 'doughnut') && labels && datasets && labels.length > 1 && datasets.length >= 1) {
- return (
-
- {labels.map((lbl: TLabel, idx: number) => {
- let { color } = ChartJS.defaults;
- if (Array.isArray(datasets[0].backgroundColor)) color = extractColor(datasets[0].backgroundColor[idx])!;
-
- return (
-
- ): void => {
- handleDataChecked(idx, lbl, e.target?.checked);
- }}
- checked={selectedDatas[lbl] !== undefined ? selectedDatas[lbl] : true}
- />
-
- {lbl}
-
-
- );
- })}
-
- );
+ if (inputs && chartData) {
+ if (chartType === 'pie' || chartType === 'doughnut') {
+ if (Object.keys(datasRegistry).length > 1) {
+ return (
+
+ {Object.entries(datasRegistry)
+ .filter(([, dsOption]: [string, GeoChartDatasetOption]) => {
+ return dsOption.visible;
+ })
+ .map(([dsLabel, dsOption]: [string, GeoChartDatasetOption], idx: number) => {
+ const color = dsOption.borderColor;
+
+ return (
+
+ ): void => {
+ handleDataChecked(idx, dsLabel, e.target?.checked);
+ }}
+ checked={datasRegistry[dsLabel].checked !== undefined ? datasRegistry[dsLabel].checked : true}
+ />
+
+ {dsLabel}
+
+
+ );
+ })}
+
+ );
+ }
}
}
@@ -1273,47 +1627,41 @@ export function GeoChart<
* @returns The Chart container JSX.Element or an empty box
*/
const renderChartContainer = (): JSX.Element => {
- // If not loading
- if (chartOptions) {
- // The xs: 1, 11 and 12 used here are as documented online
- return (
-
-
-
-
- {renderDatasourceSelector()}
- {renderTitle()}
- {renderUIOptions()}
-
- {renderDataSelector()}
- {renderDatasetSelector()}
-
-
- {renderYAxisLabel()}
-
-
- {isLoadingDatasource && }
- {renderChart()}
-
-
- {renderYSlider()}
-
-
-
- {renderXAxisLabel()}
- {renderXSlider()}
-
-
- {renderDescription()}
- {renderDownload()}
-
+ // The xs: 1, 11 and 12 used here are as documented online
+ return (
+
+
+
+
+ {renderDatasourceSelector()}
+ {renderTitle()}
+ {renderUIOptions()}
+
+ {renderDataSelector()}
+ {renderDatasetSelector()}
-
- );
- }
-
- // Empty
- return ;
+
+ {renderYAxisLabel()}
+
+
+ {isLoadingDatasource && }
+ {renderChart()}
+
+
+ {renderYSlider()}
+
+
+
+ {renderXAxisLabel()}
+ {renderXSlider()}
+
+
+ {renderDescription()}
+ {renderDownload()}
+
+
+
+ );
};
/**
@@ -1343,6 +1691,7 @@ export function GeoChart<
// #endregion
+ // TODO: Add a check if there's a 'current error', not just a 'valid schemas' error
// If no errors
if (hasValidSchemas([validatorInputs, validatorOptions, validatorData])) {
// Render the chart
@@ -1369,12 +1718,4 @@ GeoChart.defaultProps = {
},
} as ChartOptions,
data: { datasets: [], labels: [] },
- defaultColors: null,
- action: null,
- onParsed: null,
- onDatasourceChanged: null,
- onDatasetChanged: null,
- onSliderXChanged: null,
- onSliderYChanged: null,
- onError: null,
};
diff --git a/src/logger.ts b/src/logger.ts
index dfc3a9a..62b1dd9 100644
--- a/src/logger.ts
+++ b/src/logger.ts
@@ -1,11 +1,11 @@
/* eslint-disable no-console */
// Some arbitrary log levels.
-export const LOG_LEVEL_MAXIMUM = 10;
-export const LOG_LEVEL_HIGH = 8;
-export const LOG_LEVEL_MEDIUM = 5;
-export const LOG_LEVEL_LOW = 3;
-export const LOG_LEVEL_MINIMAL = 1;
+export const LOG_LEVEL_MAXIMUM = 10; // Max detail steps (like useEffects unmounting)
+export const LOG_LEVEL_HIGH = 8; // Fine detail steps (like useEffects)
+export const LOG_LEVEL_MEDIUM = 5; // Medium level steps
+export const LOG_LEVEL_LOW = 3; // Essential steps
+export const LOG_LEVEL_MINIMAL = 1; // Core steps
// Indicates logging level. The higher the number, the more detailed the log.
const LOGGING_LEVEL: number = LOG_LEVEL_LOW;
@@ -14,8 +14,8 @@ const LOGGING_LEVEL: number = LOG_LEVEL_LOW;
* Checks if the web application is running localhost
* @returns boolean true if running localhost
*/
-const runningLocalhost = (): boolean => {
- return window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
+const runningDev = (): boolean => {
+ return process.env.NODE_ENV === 'development';
};
/**
@@ -24,8 +24,43 @@ const runningLocalhost = (): boolean => {
* @param level number the level associated with the message to be logged.
* @param message string[] the messages to log
*/
-export const log = (level: number, ...message: unknown[]): void => {
+const log = (level: number, ...message: unknown[]): void => {
// If not running localhost, skip
- if (!runningLocalhost) return;
+ if (!runningDev()) return;
if (level <= LOGGING_LEVEL) console.log(`${'-'.repeat(level)}>`, ...message);
};
+
+export const logMaximum = (...message: unknown[]): void => {
+ // Redirect
+ log(LOG_LEVEL_MAXIMUM, ...message);
+};
+
+export const logHigh = (...message: unknown[]): void => {
+ // Redirect
+ log(LOG_LEVEL_HIGH, ...message);
+};
+
+export const logMedium = (...message: unknown[]): void => {
+ // Redirect
+ log(LOG_LEVEL_MEDIUM, ...message);
+};
+
+export const logLow = (...message: unknown[]): void => {
+ // Redirect
+ log(LOG_LEVEL_LOW, ...message);
+};
+
+export const logMinimal = (...message: unknown[]): void => {
+ // Redirect
+ log(LOG_LEVEL_MINIMAL, ...message);
+};
+
+export const logUseEffectMount = (useEffectFunction: string, ...message: unknown[]): void => {
+ // Redirect
+ logHigh(`${useEffectFunction} - MOUNT`, ...message);
+};
+
+export const logUseEffectUnmount = (useEffectFunction: string, ...message: unknown[]): void => {
+ // Redirect
+ logMaximum(`${useEffectFunction} - UNMOUNT`, ...message);
+};