Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(D3 plugin): add basic data validation #366

Merged
merged 2 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading