Skip to content

Commit

Permalink
feat(D3 plugin): add basic data validation (#366)
Browse files Browse the repository at this point in the history
* feat(D3 plugin): add basic data validation

* fix: review fixes
  • Loading branch information
korvin89 authored Dec 20, 2023
1 parent 9762964 commit 75186b5
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 35 deletions.
7 changes: 6 additions & 1 deletion src/i18n/keysets/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@
"error": {
"label_no-data": "No data",
"label_unknown-plugin": "Unknown plugin type \"{{type}}\"",
"label_unknown-error": "Unknown error"
"label_unknown-error": "Unknown error",
"label_invalid-axis-category-data-point": "It seems you are trying to use inappropriate data type for \"{{key}}\" value in series \"{{seriesName}}\" for axis with type \"category\". Strings and numbers are allowed.",
"label_invalid-axis-datetime-data-point": "It seems you are trying to use inappropriate data type for \"{{key}}\" value in series \"{{seriesName}}\" for axis with type \"datetime\". Only numbers are allowed.",
"label_invalid-axis-linear-data-point": "It seems you are trying to use inappropriate data type for \"{{key}}\" value in series \"{{seriesName}}\" for axis with type \"linear\". Numbers and nulls are allowed.",
"label_invalid-pie-data-value": "It seems you are trying to use inappropriate data type for \"value\" value. Only numbers are allowed.",
"label_invalid-series-type": "It seems you haven't defined \"series.type\" property, or defined it incorrectly. Available values: [{{types}}]."
},
"highcharts": {
"reset-zoom-title": "Reset zoom",
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/keysets/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@
"error": {
"label_no-data": "Нет данных",
"label_unknown-plugin": "Неизвестный тип плагина \"{{type}}\"",
"label_unknown-error": "Неизвестная ошибка"
"label_unknown-error": "Неизвестная ошибка",
"label_invalid-axis-category-data-point": "Похоже, что вы пытаетесь использовать недопустимый тип данных для значения \"{{key}}\" в серии \"{{seriesName}}\" для оси с типом \"category\". Допускается использование строк и чисел.",
"label_invalid-axis-datetime-data-point": "Похоже, что вы пытаетесь использовать недопустимый тип данных для значения \"{{key}}\" в серии \"{{seriesName}}\" для оси с типом \"datetime\". Допускается только использование чисел.",
"label_invalid-axis-linear-data-point": "Похоже, что вы пытаетесь использовать недопустимый тип данных для значения \"{{key}}\" в серии \"{{seriesName}}\" для оси с типом \"linear\". Допускается использование чисел и значений null.",
"label_invalid-pie-data-value": "Похоже, что вы пытаетесь использовать недопустимый тип данных для значения \"value\". Допускается только использование чисел.",
"label_invalid-series-type": "Похоже, что вы не указали значение \"series.type\" или указали его неверно. Доступные значения: [{{types}}]."
},
"highcharts": {
"reset-zoom-title": "Сбросить увеличение",
Expand Down
1 change: 1 addition & 0 deletions src/libs/chartkit-error/chartkit-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type ChartKitErrorArgs = {

export const CHARTKIT_ERROR_CODE = {
NO_DATA: 'ERR.CK.NO_DATA',
INVALID_DATA: 'ERR.CK.INVALID_DATA',
UNKNOWN: 'ERR.CK.UNKNOWN_ERROR',
UNKNOWN_PLUGIN: 'ERR.CK.UNKNOWN_PLUGIN',
TOO_MANY_LINES: 'ERR.CK.TOO_MANY_LINES',
Expand Down
21 changes: 15 additions & 6 deletions src/plugins/d3/examples/bar-x/Basic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,25 @@ import type {ChartKitWidgetData, BarXSeries, BarXSeriesData} from '../../../../t
import nintendoGames from '../nintendoGames';
import {groups} from 'd3';

function prepareData(field: 'platform' | 'meta_score' | 'date' = 'platform') {
function prepareData(
{field, filterNulls}: {field: 'platform' | 'meta_score' | 'date'; filterNulls?: boolean} = {
field: 'platform',
},
) {
const gamesByPlatform = groups(nintendoGames, (item) => item[field]);
const data = gamesByPlatform.map(([value, games]) => ({
let resultData = gamesByPlatform;

if (filterNulls) {
resultData = gamesByPlatform.filter(([value]) => typeof value === 'number');
}

const data = resultData.map(([value, games]) => ({
x: value,
y: games.length,
}));

return {
categories: gamesByPlatform.map(([key]) => key),
categories: resultData.map(([key]) => key),
series: [
{
data,
Expand All @@ -24,7 +34,6 @@ function prepareData(field: 'platform' | 'meta_score' | 'date' = 'platform') {

export const BasicBarXChart = () => {
const {categories, series} = prepareData();

const widgetData: ChartKitWidgetData = {
series: {
data: series.map<BarXSeries>((s) => ({
Expand All @@ -47,7 +56,7 @@ export const BasicBarXChart = () => {
};

export const BasicLinearBarXChart = () => {
const {series} = prepareData('meta_score');
const {series} = prepareData({field: 'meta_score'});

const widgetData: ChartKitWidgetData = {
series: {
Expand All @@ -68,7 +77,7 @@ export const BasicLinearBarXChart = () => {
};

export const BasicDateTimeBarXChart = () => {
const {series} = prepareData('date');
const {series} = prepareData({field: 'date', filterNulls: true});

const widgetData: ChartKitWidgetData = {
series: {
Expand Down
55 changes: 30 additions & 25 deletions src/plugins/d3/renderer/D3Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import afterFrame from 'afterframe';
import type {ChartKitProps, ChartKitWidgetRef} from '../../../types';
import {getRandomCKId, measurePerformance} from '../../../utils';
import {Chart} from './components';
import {validateData} from './validation';

type ChartDimensions = {
width: number;
Expand All @@ -23,31 +24,6 @@ const D3Widget = React.forwardRef<ChartKitWidgetRef | undefined, ChartKitProps<'
measurePerformance(),
);

React.useLayoutEffect(() => {
if (onChartLoad) {
onChartLoad({});
}
}, [onChartLoad]);

React.useLayoutEffect(() => {
if (dimensions?.width) {
if (!performanceMeasure.current) {
performanceMeasure.current = measurePerformance();
}

afterFrame(() => {
const renderTime = performanceMeasure.current?.end();
onRender?.({
renderTime,
});
onLoad?.({
widgetRendering: renderTime,
});
performanceMeasure.current = null;
});
}
}, [data, onRender, onLoad, dimensions]);

const handleResize = React.useCallback(() => {
const parentElement = ref.current?.parentElement;

Expand Down Expand Up @@ -90,6 +66,35 @@ const D3Widget = React.forwardRef<ChartKitWidgetRef | undefined, ChartKitProps<'
handleResize();
}, [handleResize]);

React.useEffect(() => {
validateData(data);
}, [data]);

React.useLayoutEffect(() => {
if (onChartLoad) {
onChartLoad({});
}
}, [onChartLoad]);

React.useLayoutEffect(() => {
if (dimensions?.width) {
if (!performanceMeasure.current) {
performanceMeasure.current = measurePerformance();
}

afterFrame(() => {
const renderTime = performanceMeasure.current?.end();
onRender?.({
renderTime,
});
onLoad?.({
widgetRendering: renderTime,
});
performanceMeasure.current = null;
});
}
}, [data, onRender, onLoad, dimensions]);

return (
<div
ref={ref}
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/d3/renderer/constants/defaults/axis.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {ChartKitWidgetAxisType} from '../../../../../types';

export const axisLabelsDefaults = {
margin: 10,
padding: 10,
Expand All @@ -18,3 +20,5 @@ export const yAxisTitleDefaults = {
...axisTitleDefaults,
margin: 8,
};

export const DEFAULT_AXIS_TYPE: ChartKitWidgetAxisType = 'linear';
5 changes: 3 additions & 2 deletions src/plugins/d3/renderer/hooks/useAxisScales/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import type {AxisDirection} from '../../utils';
import {PreparedSeries} from '../useSeries/types';
import {ChartKitWidgetAxis, ChartKitWidgetSeries} from '../../../../../types';
import {DEFAULT_AXIS_TYPE} from '../../constants';

export type ChartScale =
| ScaleLinear<number, number>
Expand Down Expand Up @@ -58,7 +59,7 @@ const filterCategoriesByVisibleSeries = (args: {
};

export function createYScale(axis: PreparedAxis, series: PreparedSeries[], boundsHeight: number) {
const yType = get(axis, 'type', 'linear');
const yType = get(axis, 'type', DEFAULT_AXIS_TYPE);
const yMin = get(axis, 'min');
const yCategories = get(axis, 'categories');
const yTimestamps = get(axis, 'timestamps');
Expand Down Expand Up @@ -133,7 +134,7 @@ export function createXScale(
boundsWidth: number,
) {
const xMin = get(axis, 'min');
const xType = get(axis, 'type', 'linear');
const xType = get(axis, 'type', DEFAULT_AXIS_TYPE);
const xCategories = get(axis, 'categories');
const xTimestamps = get(axis, 'timestamps');
const maxPadding = get(axis, 'maxPadding', 0);
Expand Down
47 changes: 47 additions & 0 deletions src/plugins/d3/renderer/validation/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {ChartKitWidgetData} from '../../../../../types';

export const XY_SERIES: Record<string, ChartKitWidgetData> = {
INVALID_CATEGORY_X: {
series: {
data: [{type: 'scatter', data: [{x: undefined, y: 1}], name: 'Series'}],
},
xAxis: {type: 'category'},
},
INVALID_CATEGORY_Y: {
series: {
data: [{type: 'scatter', data: [{x: 1, y: undefined}], name: 'Series'}],
},
yAxis: [{type: 'category'}],
},
INVALID_DATETIME_X: {
series: {
data: [{type: 'scatter', data: [{x: undefined, y: 1}], name: 'Series'}],
},
xAxis: {type: 'datetime'},
},
INVALID_DATETIME_Y: {
series: {
data: [{type: 'scatter', data: [{x: undefined, y: 1}], name: 'Series'}],
},
yAxis: [{type: 'datetime'}],
},
INVALID_LINEAR_X: {
series: {
data: [{type: 'scatter', data: [{x: 'str', y: 1}], name: 'Series'}],
},
},
INVALID_LINEAR_Y: {
series: {
data: [{type: 'scatter', data: [{x: 1, y: 'str'}], name: 'Series'}],
},
},
};

export const PIE_SERIES: Record<string, ChartKitWidgetData> = {
INVALID_VALUE: {
series: {
// @ts-expect-error
data: [{type: 'pie', data: [{value: undefined, name: 'Series'}]}],
},
},
};
73 changes: 73 additions & 0 deletions src/plugins/d3/renderer/validation/__tests__/validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {ChartKitError, CHARTKIT_ERROR_CODE} from '../../../../../libs';
import {ChartKitWidgetData} from '../../../../../types';
import {validateData} from '../';
import {PIE_SERIES, XY_SERIES} from '../__mocks__';

describe('plugins/d3/validation', () => {
test.each<any>([undefined, null, {}, {series: {}}, {series: {data: []}}])(
'validateData should throw an error in case of empty data (data: %j)',
(data) => {
let error: ChartKitError | null = null;

try {
validateData(data);
} catch (e) {
error = e as ChartKitError;
}

expect(error?.code).toEqual(CHARTKIT_ERROR_CODE.NO_DATA);
},
);

test.each<any>([
{series: {data: [{data: [{x: 1, y: 1}]}]}},
{series: {data: [{type: 'invalid-type', data: [{x: 1, y: 1}]}]}},
])('validateData should throw an error in case of incorrect series type (data: %j)', (data) => {
let error: ChartKitError | null = null;

try {
validateData(data);
} catch (e) {
error = e as ChartKitError;
}

expect(error?.code).toEqual(CHARTKIT_ERROR_CODE.INVALID_DATA);
});

test.each<ChartKitWidgetData>([
XY_SERIES.INVALID_CATEGORY_X,
XY_SERIES.INVALID_CATEGORY_Y,
XY_SERIES.INVALID_DATETIME_X,
XY_SERIES.INVALID_DATETIME_Y,
XY_SERIES.INVALID_LINEAR_X,
XY_SERIES.INVALID_LINEAR_Y,
])(
'[XY Series] validateData should throw an error in case of invalid data (data: %j)',
(data) => {
let error: ChartKitError | null = null;

try {
validateData(data);
} catch (e) {
error = e as ChartKitError;
}

expect(error?.code).toEqual(CHARTKIT_ERROR_CODE.INVALID_DATA);
},
);

test.each<ChartKitWidgetData>([PIE_SERIES.INVALID_VALUE])(
'[Pie Series] validateData should throw an error in case of invalid data (data: %j)',
(data) => {
let error: ChartKitError | null = null;

try {
validateData(data);
} catch (e) {
error = e as ChartKitError;
}

expect(error?.code).toEqual(CHARTKIT_ERROR_CODE.INVALID_DATA);
},
);
});
Loading

0 comments on commit 75186b5

Please sign in to comment.