Skip to content

Commit

Permalink
feat(react-components): add l4e datasource
Browse files Browse the repository at this point in the history
  • Loading branch information
jmbuss authored and corteggiano committed Apr 11, 2024
1 parent 415c9e0 commit 2947002
Show file tree
Hide file tree
Showing 36 changed files with 1,116 additions and 204 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export const barHeightEncoding = 'BAR_HEIGHT_ENCODING';
export const barHeight = 100;

// defines 3 x-axes:
const L4E_X_AXIS = {
const ANOMALY_X_AXIS = {
axisPointer: {
link: [
{
Expand All @@ -10,7 +13,7 @@ const L4E_X_AXIS = {
xAxis: [
// 1. x axis for large timeline, which shows a time value
{
name: 'l4e-timeline-axis',
name: 'anomaly-timeline-axis',
type: 'time',
boundaryGap: false,
show: true,
Expand All @@ -29,7 +32,7 @@ const L4E_X_AXIS = {
},
// 2. x axis for step chart
{
name: 'l4e-step',
name: 'anomaly-step',
type: 'time',
show: true,
axisLabel: {
Expand All @@ -47,7 +50,7 @@ const L4E_X_AXIS = {
},
// 3. hides the x-axis for slider zoom timeline
{
name: 'l4e-selection-axis',
name: 'anomaly-selection-axis',
type: 'time',
gridIndex: 2,
boundaryGap: false,
Expand All @@ -66,7 +69,7 @@ const L4E_X_AXIS = {
};

// defines 3 y-axes:
const L4E_Y_AXIS = {
const ANOMALY_Y_AXIS = {
yAxis: [
// 1. y axis for timeline
{
Expand All @@ -79,11 +82,11 @@ const L4E_Y_AXIS = {
{
type: 'value',
min: 0,
max: 100,
max: 1,
gridIndex: 1,
axisLabel: {
margin: 4,
formatter: (value: number) => `${value}%`,
formatter: (value: number) => `${value * 100}%`,
},
},
// 3. y axis for slider zoom timeline
Expand All @@ -98,7 +101,7 @@ const L4E_Y_AXIS = {
};

// defines 3 data zooms:
const L4E_DATA_ZOOM = {
const ANOMALY_DATA_ZOOM = {
// 1. inside zoom (for gesture handling) used by timeline and step chart
dataZoom: [
{
Expand Down Expand Up @@ -148,7 +151,7 @@ const L4E_DATA_ZOOM = {
};

// grid layout with 3 sections:
const L4E_GRID = {
const ANOMALY_GRID = {
grid: [
// 1. top section is the large timeline
{
Expand Down Expand Up @@ -179,46 +182,46 @@ const L4E_GRID = {
};

// defines 2 series:
export const L4E_SERIES = {
export const ANOMALY_SERIES = {
series: [
// 1. large timeline, which a user directly interacts with
{
id: 'l4e_timeline',
id: 'anomaly_timeline',
type: 'bar',
color: '#D13212',
barMinWidth: 5,
barMaxWidth: 10,
xAxisIndex: 0,
yAxisIndex: 0,
datasetIndex: 1,
encode: {
dataSetIndex: 0,
x: 'time',
y: 'value',
x: 'timestamp',
y: barHeightEncoding,
},
},
// 2. smaller data zoom slider timeline, which always shows 100% of the data
{
id: 'l4e_slider',
id: 'anomaly_slider',
type: 'bar',
color: '#D13212',
silent: true,
barMinWidth: 5,
barMaxWidth: 5,
xAxisIndex: 2,
yAxisIndex: 2,
datasetIndex: 1,
tooltip: {
show: false,
},
encode: {
datasetIndex: 0,
x: 'time',
y: 'value',
x: 'timestamp',
y: barHeightEncoding,
},
},
],
};

const L4E_TITLE = {
const ANOMALY_TITLE = {
title: [
{
id: 'widget-title',
Expand All @@ -237,10 +240,10 @@ const L4E_TITLE = {
],
};

export const DEFAULT_L4E_WIDGET_SETTINGS = {
...L4E_GRID,
...L4E_DATA_ZOOM,
...L4E_X_AXIS,
...L4E_Y_AXIS,
...L4E_TITLE,
export const DEFAULT_ANOMALY_WIDGET_SETTINGS = {
...ANOMALY_GRID,
...ANOMALY_DATA_ZOOM,
...ANOMALY_X_AXIS,
...ANOMALY_Y_AXIS,
...ANOMALY_TITLE,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useMemo } from 'react';
import { AnomalyData } from '../../../data';
import { barHeight, barHeightEncoding } from '../constants';

const datasetFromData = (data: AnomalyData) => {
if (Array.isArray(data)) {
return data.map((d) => ({
...d,
[barHeightEncoding]: barHeight,
}));
}
return {
...data,
[barHeightEncoding]: data.timestamp.map(() => barHeight),
};
};

export const useDataSet = ({ data }: { data: AnomalyData | undefined }) => {
return useMemo(() => {
if (!data) return;
return {
dataset: [
{
source: datasetFromData(data),
},
{
// Transform using echarts so we don't need to care
// about the format of the data
transform: [
{
type: 'sort',
config: { dimension: 'timestamp', order: 'asc' },
},
],
},
],
};
}, [data]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useMemo } from 'react';
import { ANOMALY_SERIES } from '../constants';
import { AnomalyDescription } from '../../../data';

export const useSeries = ({
description,
}: {
description: AnomalyDescription | undefined;
}) => {
return useMemo(() => {
if (!description) {
return {
series: [...ANOMALY_SERIES.series],
};
}
const diagnosticSeries = description.diagnostics.map((d) => {
return {
id: d.id,
name: d.name,
encode: {
x: 'timestamp',
y: d.id,
},
type: 'line',
step: 'start',
xAxisIndex: 1,
yAxisIndex: 1,
datasetIndex: 1,
areaStyle: {},
stack: 'Total',
};
});

return {
series: [...ANOMALY_SERIES.series, ...diagnosticSeries],
};
}, [description]);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { ChartRef } from '../../../hooks/useECharts';
import { DEFAULT_L4E_WIDGET_SETTINGS } from '../constants';
import { DEFAULT_ANOMALY_WIDGET_SETTINGS } from '../constants';

export const useSetOptions = ({
chartRef,
Expand All @@ -11,14 +11,14 @@ export const useSetOptions = ({
customOptions: any;
}) => {
useEffect(() => {
const l4e = chartRef.current;
const anomaly = chartRef.current;
// set default chart options
l4e?.setOption({ ...DEFAULT_L4E_WIDGET_SETTINGS });
anomaly?.setOption({ ...DEFAULT_ANOMALY_WIDGET_SETTINGS });
});

useEffect(() => {
const l4e = chartRef.current;
const anomaly = chartRef.current;
// set customOptions, which will merge with default options set above
l4e?.setOption({ ...customOptions });
anomaly?.setOption({ ...customOptions });
}, [chartRef, customOptions]);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { TooltipData, formatTooltip } from '../utils/formatTooltip';

const L4E_TOOLTIP = {
const ANOMALY_TOOLTIP = {
trigger: 'axis',
axisPointer: {
animation: false,
Expand All @@ -18,9 +18,10 @@ export const useTooltip = ({
return useMemo(() => {
return {
tooltip: {
...L4E_TOOLTIP,
formatter: (data: TooltipData) =>
formatTooltip(data, decimalPlaces, tooltipSort),
...ANOMALY_TOOLTIP,
formatter: (params: TooltipData) => {
return formatTooltip(params, decimalPlaces, tooltipSort);
},
},
};
}, [decimalPlaces, tooltipSort]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ export const useXAxis = ({
viewportStart && viewportEnd
? [
{
name: 'l4e-timeline-axis',
name: 'anomaly-timeline-axis',
min: viewportStart.getTime(),
max: viewportEnd.getTime(),
},
{
name: 'l4e-selection-axis',
name: 'anomaly-selection-axis',
min: viewportStart.getTime(),
max: viewportEnd.getTime(),
},
{
name: 'l4e-line',
name: 'anomaly-line',
min: viewportStart.getTime(),
max: viewportEnd.getTime(),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,44 @@
import React from 'react';
import { useECharts } from '../../hooks/useECharts';
import { AnomalyResult } from './types';
import { AnomalyWidgetOptions } from './types';
import { useSetOptions } from './hooks/useSetOptions';
import { useXAxis } from './hooks/useXAxis';
import { useTitle } from './hooks/useTitle';
import { useDataSet } from './hooks/useDataSet';
import { useSeries } from './hooks/useSeries';
import { useTooltip } from './hooks/useTooltip';
import {
AnomalyObjectDataSourceTransformer,
DataSourceLoader,
} from '../../data';

export interface L4EWidgetProps {
// data: [some generic data type]; //// this will need its own doc to create a flexible data type
data: AnomalyResult[]; // placeholder data type
mode?: 'light' | 'dark'; // sets the theme of the widget
decimalPlaces?: number; // sets the number of decimal places values will be rounded to
title?: string; // title for the widget, can be used to display the prediction model name
// if no start / end is provided, start / end will be determined from the data
viewportStart?: Date;
viewportEnd?: Date;
tooltipSort?: 'value' | 'alphabetical';
}
const AnomalyDataSourceLoader = new DataSourceLoader([
new AnomalyObjectDataSourceTransformer(),
]);

export const L4EWidget = ({
data,
export const AnomalyWidget = ({
datasources,
viewport,
title,
decimalPlaces,
viewportStart,
viewportEnd,
tooltipSort,
}: L4EWidgetProps) => {
}: AnomalyWidgetOptions) => {
const { ref, chartRef } = useECharts();

const customXAxis = useXAxis({ viewportStart, viewportEnd });
/**
* Datasources is a fixed length array of 1.
* The widget can only display 1 anomaly for now.
*/
const data = AnomalyDataSourceLoader.transform([...datasources]).at(0);
const description = AnomalyDataSourceLoader.describe([...datasources]).at(0);

const customXAxis = useXAxis({
viewportStart: viewport.start,
viewportEnd: viewport.end,
});
const customTitle = useTitle({ title });
const customDataSet = useDataSet({ data });
const customSeries = useSeries({ data });
const customSeries = useSeries({ description });
const customTooltip = useTooltip({ decimalPlaces, tooltipSort });

const customOptions = {
Expand Down
13 changes: 13 additions & 0 deletions packages/react-components/src/components/anomaly-widget/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { HistoricalViewport } from '@iot-app-kit/core';
import type { FixedLengthArray } from 'type-fest';
import { AnomalyObjectDataSource } from '../../data/transformers/anomaly/object/datasource';

type AnomalyWidgetDataSources = AnomalyObjectDataSource;
export type AnomalyWidgetOptions = {
title?: string;
mode?: 'light' | 'dark';
decimalPlaces?: number;
datasources: FixedLengthArray<AnomalyWidgetDataSources, 1>;
viewport: HistoricalViewport;
tooltipSort?: 'value' | 'alphabetical';
};
Loading

0 comments on commit 2947002

Please sign in to comment.