diff --git a/web/src/components/charts/TimeSeriesChart/ChartTooltip.tsx b/web/src/components/charts/TimeSeriesChart/ChartTooltip.tsx
new file mode 100644
index 0000000000..98abdc0070
--- /dev/null
+++ b/web/src/components/charts/TimeSeriesChart/ChartTooltip.tsx
@@ -0,0 +1,55 @@
+/**
+ * Panther is a Cloud-Native SIEM for the Modern Security Team.
+ * Copyright (C) 2020 Panther Labs Inc
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import React from 'react';
+import { Box, Flex, Text } from 'pouncejs';
+import { formatDatetime } from 'Helpers/utils';
+
+export interface ChartTooltipProps {
+ params: any[];
+ units: string;
+}
+
+const ChartTooltip: React.FC = ({ params, units }) => {
+ return (
+
+
+ {formatDatetime(params[0].value[0], true)}
+
+
+ {params.map((seriesInfo, i) => {
+ return (
+
+
+
+
+ {seriesInfo.seriesName}
+
+
+ {seriesInfo.value[1].toLocaleString('en')}
+ {units ? ` ${units}` : ''}
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default ChartTooltip;
diff --git a/web/src/components/charts/TimeSeriesChart/TimeSeriesChart.tsx b/web/src/components/charts/TimeSeriesChart/TimeSeriesChart.tsx
index aa8562c91e..a8bcf5b840 100644
--- a/web/src/components/charts/TimeSeriesChart/TimeSeriesChart.tsx
+++ b/web/src/components/charts/TimeSeriesChart/TimeSeriesChart.tsx
@@ -18,19 +18,29 @@
import React from 'react';
import ReactDOM from 'react-dom';
-import { Box, Flex, Text, useTheme } from 'pouncejs';
-import { formatTime, formatDatetime, remToPx, capitalize } from 'Helpers/utils';
+import { Box, Flex, theme as Theme, useTheme } from 'pouncejs';
+import { formatTime, remToPx, capitalize } from 'Helpers/utils';
import { FloatSeriesData, LongSeriesData, FloatSeries, LongSeries } from 'Generated/schema';
import { EChartOption, ECharts } from 'echarts';
import mapKeys from 'lodash/mapKeys';
import { SEVERITY_COLOR_MAP } from 'Source/constants';
import { stringToPaleColor } from 'Helpers/colors';
+import ChartTooltip, { ChartTooltipProps } from './ChartTooltip';
+import useChartOptions from './useChartOptions';
import ResetButton from '../ResetButton';
import ScaleControls from '../ScaleControls';
-interface TimeSeriesLinesProps {
+type SeriesMetadata = {
+ color?: keyof typeof Theme['colors'];
+};
+
+export type SeriesDataMetadata = {
+ metadata?: any[];
+};
+
+interface TimeSeriesChartProps {
/** The data for the time series */
- data: LongSeriesData | FloatSeriesData;
+ data: (LongSeriesData | FloatSeriesData) & SeriesDataMetadata;
/**
* The number of segments that the X-axis is split into
@@ -59,6 +69,24 @@ interface TimeSeriesLinesProps {
*/
maxZoomPeriod?: number;
+ /**
+ * Whether to render chart as lines or bars
+ * @default line
+ */
+ chartType?: 'line' | 'bar';
+
+ /**
+ * Whether to show label for series
+ * @default true
+ */
+ hideSeriesLabels?: boolean;
+
+ /**
+ * Whether to hide legend
+ * @default false
+ */
+ hideLegend?: boolean;
+
/**
* This is parameter determines if we need to display the values with an appropriate suffix
*/
@@ -68,6 +96,12 @@ interface TimeSeriesLinesProps {
* This is an optional parameter that will render the text provided above legend if defined
*/
title?: string;
+
+ /**
+ *
+ * @default ChartTooltip
+ */
+ tooltipComponent?: React.FC;
}
const severityColors = mapKeys(SEVERITY_COLOR_MAP, (val, key) => capitalize(key.toLowerCase()));
@@ -79,21 +113,25 @@ function formatDateString(timestamp) {
return `${hourFormat(timestamp)}\n${dateFormat(timestamp).toUpperCase()}`;
}
-const TimeSeriesChart: React.FC = ({
+const TimeSeriesChart: React.FC = ({
data,
zoomable = false,
scaleControls = true,
segments = 12,
maxZoomPeriod = 3600 * 1000 * 24,
+ chartType = 'line',
+ hideLegend = false,
+ hideSeriesLabels = true,
units,
title,
+ tooltipComponent = ChartTooltip,
}) => {
const [scaleType, setScaleType] = React.useState('value');
const theme = useTheme();
+ const { getLegend } = useChartOptions();
const timeSeriesChart = React.useRef(null);
const container = React.useRef(null);
const tooltip = React.useRef(document.createElement('div'));
-
/*
* Defining ChartOptions
*/
@@ -102,13 +140,7 @@ const TimeSeriesChart: React.FC = ({
* Timestamps & Series are common for all series since everything has the same interval
* and the same time frame
*/
- const series = data.series as (LongSeries | FloatSeries)[];
- /*
- * 'legendData' must be an array of values that matches 'series.name'in order
- * to display them in correct order and color
- * e.g. [AWS.ALB, AWS.S3, ...etc]
- */
- const legendData = series.map(({ label }) => label);
+ const series = data.series as ((LongSeries | FloatSeries) & SeriesMetadata)[];
/*
* 'series' must be an array of objects that includes some graph options
@@ -116,20 +148,26 @@ const TimeSeriesChart: React.FC = ({
* is an array of values for all datapoints
* Must be ordered
*/
- const seriesData = series.map(({ label, values }) => {
+ const seriesData = series.map(({ label, values, color }) => {
return {
name: label,
- type: 'line',
+ type: chartType,
symbol: 'none',
smooth: true,
+ barMaxWidth: 24,
itemStyle: {
- color: theme.colors[severityColors[label]] || stringToPaleColor(label),
+ color: theme.colors[color || severityColors[label]] || stringToPaleColor(label),
+ },
+ label: {
+ show: !hideSeriesLabels,
+ position: 'top',
+ color: '#fff',
},
data: values
.map((v, i) => {
return {
name: label,
- value: [data.timestamps[i], v],
+ value: [data.timestamps[i], v, data.metadata ? data.metadata[i] : null],
};
})
/* This reverse is needed cause data provided by API are coming by descending timestamp.
@@ -143,7 +181,7 @@ const TimeSeriesChart: React.FC = ({
const options: EChartOption = {
grid: {
- left: 180,
+ left: hideLegend ? 0 : 180,
right: 50,
bottom: 50,
containLabel: true,
@@ -178,67 +216,20 @@ const TimeSeriesChart: React.FC = ({
}),
tooltip: {
trigger: 'axis' as const,
+ axisPointer: {
+ type: chartType === 'line' ? 'line' : 'none',
+ },
backgroundColor: theme.colors['navyblue-300'],
formatter: (params: EChartOption.Tooltip.Format[]) => {
if (!params || !params.length) {
return '';
}
- const component = (
-
-
- {formatDatetime(params[0].value[0], true)}
-
-
- {params.map(seriesTooltip => (
-
-
-
- {seriesTooltip.seriesName}
-
-
- {seriesTooltip.value[1].toLocaleString('en')}
- {units ? ` ${units}` : ''}
-
-
- ))}
-
-
- );
-
- ReactDOM.render(component, tooltip.current);
+ ReactDOM.render(tooltipComponent({ params, units }), tooltip.current);
return tooltip.current.innerHTML;
},
},
- legend: {
- type: 'scroll' as const,
- orient: 'vertical' as const,
- left: 'auto',
- right: 'auto',
- // Pushing down legend to fit title
- top: title ? 30 : 'auto',
- icon: 'circle',
- data: legendData,
- inactiveColor: theme.colors['gray-400'],
- textStyle: {
- color: theme.colors['gray-50'],
- fontFamily: theme.fonts.primary,
- fontSize: remToPx(theme.fontSizes['x-small']),
- },
- pageIcons: {
- vertical: ['M7 10L12 15L17 10H7Z', 'M7 14L12 9L17 14H7Z'],
- },
- pageIconColor: theme.colors['gray-50'],
- pageIconInactiveColor: theme.colors['navyblue-300'],
- pageIconSize: 12,
- pageTextStyle: {
- fontFamily: theme.fonts.primary,
- color: theme.colors['gray-50'],
- fontWeight: theme.fontWeights.bold as any,
- fontSize: remToPx(theme.fontSizes['x-small']),
- },
- pageButtonGap: theme.space[3] as number,
- },
+ ...(!hideLegend && { legend: getLegend({ series, title }) }),
xAxis: {
type: 'time' as const,
splitNumber: segments,
@@ -296,7 +287,7 @@ const TimeSeriesChart: React.FC = ({
const [echarts] = await Promise.all(
[
import(/* webpackChunkName: "echarts" */ 'echarts/lib/echarts'),
- import(/* webpackChunkName: "echarts" */ 'echarts/lib/chart/line'),
+ import(/* webpackChunkName: "echarts" */ `echarts/lib/chart/${chartType}`),
import(/* webpackChunkName: "echarts" */ 'echarts/lib/component/tooltip'),
zoomable && import(/* webpackChunkName: "echarts" */ 'echarts/lib/component/dataZoom'),
// This is needed for reset functionality
@@ -343,7 +334,7 @@ const TimeSeriesChart: React.FC = ({
// eslint-disable-next-line func-names
newChart.on('restore', function () {
const options = chartOptions;
- if (options.legend.selected) {
+ if (options.legend?.selected) {
options.legend.selected = Object.keys(options.legend.selected).reduce((acc, cur) => {
acc[cur] = true;
return acc;
@@ -370,8 +361,7 @@ const TimeSeriesChart: React.FC = ({
{title}
-
-
+
{scaleControls && }
diff --git a/web/src/components/charts/TimeSeriesChart/useChartOptions.tsx b/web/src/components/charts/TimeSeriesChart/useChartOptions.tsx
new file mode 100644
index 0000000000..e939a6058d
--- /dev/null
+++ b/web/src/components/charts/TimeSeriesChart/useChartOptions.tsx
@@ -0,0 +1,77 @@
+/**
+ * Panther is a Cloud-Native SIEM for the Modern Security Team.
+ * Copyright (C) 2020 Panther Labs Inc
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+import { remToPx } from 'Helpers/utils';
+import { FloatSeries, LongSeries } from 'Generated/schema';
+import { useTheme } from 'pouncejs';
+
+type GetLegendProps = {
+ series: (LongSeries | FloatSeries)[];
+ title?: string;
+};
+
+type GetLegendFunc = (props: GetLegendProps) => any;
+
+const useChartOptions = () => {
+ const theme = useTheme();
+ const getLegend: GetLegendFunc = React.useCallback(
+ ({ series, title }) => {
+ /*
+ * 'legendData' must be an array of values that matches 'series.name' in order
+ * to display them in correct order and color
+ * e.g. [AWS.ALB, AWS.S3, ...etc]
+ */
+ const legendData = series.map(({ label }) => label);
+ return {
+ type: 'scroll' as const,
+ orient: 'vertical' as const,
+ left: 'auto',
+ right: 'auto',
+ // Pushing down legend to fit title
+ top: title ? 30 : 'auto',
+ icon: 'circle',
+ data: legendData,
+ inactiveColor: theme.colors['gray-400'],
+ textStyle: {
+ color: theme.colors['gray-50'],
+ fontFamily: theme.fonts.primary,
+ fontSize: remToPx(theme.fontSizes['x-small']),
+ },
+ pageIcons: {
+ vertical: ['M7 10L12 15L17 10H7Z', 'M7 14L12 9L17 14H7Z'],
+ },
+ pageIconColor: theme.colors['gray-50'],
+ pageIconInactiveColor: theme.colors['navyblue-300'],
+ pageIconSize: 12,
+ pageTextStyle: {
+ fontFamily: theme.fonts.primary,
+ color: theme.colors['gray-50'],
+ fontWeight: theme.fontWeights.bold as any,
+ fontSize: remToPx(theme.fontSizes['x-small']),
+ },
+ pageButtonGap: theme.space[3] as number,
+ };
+ },
+ [theme]
+ );
+
+ return React.useMemo(() => ({ getLegend }), [getLegend]);
+};
+
+export default useChartOptions;