|
@@ -5,15 +5,18 @@ import * as echarts from 'echarts/core';
|
|
|
import {CanvasRenderer} from 'echarts/renderers';
|
|
|
|
|
|
import {updateDateTime} from 'sentry/actionCreators/pageFilters';
|
|
|
-import {AreaChart} from 'sentry/components/charts/areaChart';
|
|
|
-import {BarChart} from 'sentry/components/charts/barChart';
|
|
|
-import {LineChart} from 'sentry/components/charts/lineChart';
|
|
|
+import {transformToAreaSeries} from 'sentry/components/charts/areaChart';
|
|
|
+import {transformToBarSeries} from 'sentry/components/charts/barChart';
|
|
|
+import BaseChart, {BaseChartProps} from 'sentry/components/charts/baseChart';
|
|
|
+import {transformToLineSeries} from 'sentry/components/charts/lineChart';
|
|
|
+import ScatterSeries from 'sentry/components/charts/series/scatterSeries';
|
|
|
import {DateTimeObject} from 'sentry/components/charts/utils';
|
|
|
import {ReactEchartsRef} from 'sentry/types/echarts';
|
|
|
import mergeRefs from 'sentry/utils/mergeRefs';
|
|
|
import {
|
|
|
formatMetricsUsingUnitAndOp,
|
|
|
isCumulativeOp,
|
|
|
+ MetricCorrelation,
|
|
|
MetricDisplayType,
|
|
|
} from 'sentry/utils/metrics';
|
|
|
import useRouter from 'sentry/utils/useRouter';
|
|
@@ -22,7 +25,8 @@ import {FocusArea, useFocusArea} from 'sentry/views/ddm/focusArea';
|
|
|
|
|
|
import {getFormatter} from '../../components/charts/components/tooltip';
|
|
|
|
|
|
-import {Series} from './widget';
|
|
|
+import {useMetricSamples} from './useMetricSamples';
|
|
|
+import {Sample, ScatterSeries as ScatterSeriesType, Series} from './widget';
|
|
|
|
|
|
type ChartProps = {
|
|
|
displayType: MetricDisplayType;
|
|
@@ -30,8 +34,11 @@ type ChartProps = {
|
|
|
series: Series[];
|
|
|
widgetIndex: number;
|
|
|
addFocusArea?: (area: FocusArea) => void;
|
|
|
+ correlations?: MetricCorrelation[];
|
|
|
drawFocusArea?: () => void;
|
|
|
height?: number;
|
|
|
+ highlightedSampleId?: string;
|
|
|
+ onSampleClick?: (sample: Sample) => void;
|
|
|
operation?: string;
|
|
|
removeFocusArea?: () => void;
|
|
|
};
|
|
@@ -53,6 +60,9 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
|
|
|
focusArea,
|
|
|
removeFocusArea,
|
|
|
height,
|
|
|
+ correlations,
|
|
|
+ onSampleClick,
|
|
|
+ highlightedSampleId,
|
|
|
},
|
|
|
forwardedRef
|
|
|
) => {
|
|
@@ -88,6 +98,7 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
|
|
|
});
|
|
|
|
|
|
const unit = series[0]?.unit;
|
|
|
+
|
|
|
const seriesToShow = useMemo(
|
|
|
() =>
|
|
|
series
|
|
@@ -99,6 +110,22 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
|
|
|
[series]
|
|
|
);
|
|
|
|
|
|
+ const valueFormatter = useCallback(
|
|
|
+ (value: number) => {
|
|
|
+ return formatMetricsUsingUnitAndOp(value, unit, operation);
|
|
|
+ },
|
|
|
+ [unit, operation]
|
|
|
+ );
|
|
|
+
|
|
|
+ const samples = useMetricSamples({
|
|
|
+ chartRef,
|
|
|
+ correlations,
|
|
|
+ onClick: onSampleClick,
|
|
|
+ highlightedSampleId,
|
|
|
+ operation,
|
|
|
+ timeseries: series,
|
|
|
+ });
|
|
|
+
|
|
|
// TODO(ddm): This assumes that all series have the same bucket size
|
|
|
const bucketSize = seriesToShow[0]?.data[1]?.name - seriesToShow[0]?.data[0]?.name;
|
|
|
const isSubMinuteBucket = bucketSize < 60_000;
|
|
@@ -106,26 +133,31 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
|
|
|
const displayFogOfWar = isCumulativeOp(operation);
|
|
|
|
|
|
const chartProps = useMemo(() => {
|
|
|
- const formatters = {
|
|
|
- valueFormatter: (value: number) =>
|
|
|
- formatMetricsUsingUnitAndOp(value, unit, operation),
|
|
|
+ const timeseriesFormatters = {
|
|
|
+ valueFormatter,
|
|
|
isGroupedByDate: true,
|
|
|
bucketSize,
|
|
|
showTimeInTooltip: true,
|
|
|
addSecondsToTimeFormat: isSubMinuteBucket,
|
|
|
limit: 10,
|
|
|
+ filter: (_, seriesParam) => {
|
|
|
+ return seriesParam?.axisId === 'xAxis';
|
|
|
+ },
|
|
|
};
|
|
|
+
|
|
|
const heightOptions = height ? {height} : {autoHeightResize: true};
|
|
|
|
|
|
return {
|
|
|
...heightOptions,
|
|
|
...focusAreaBrush.options,
|
|
|
+
|
|
|
forwardedRef: mergeRefs([forwardedRef, chartRef]),
|
|
|
series: seriesToShow,
|
|
|
renderer: seriesToShow.length > 20 ? ('canvas' as const) : ('svg' as const),
|
|
|
isGroupedByDate: true,
|
|
|
colors: seriesToShow.map(s => s.color),
|
|
|
grid: {top: 5, bottom: 0, left: 0, right: 0},
|
|
|
+ onClick: samples.handleClick,
|
|
|
tooltip: {
|
|
|
formatter: (params, asyncTicket) => {
|
|
|
if (focusAreaBrush.isDrawingRef.current) {
|
|
@@ -136,29 +168,37 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
|
|
|
).find(element => {
|
|
|
return element.classList.contains('echarts-for-react');
|
|
|
});
|
|
|
-
|
|
|
+ if (params.seriesType === 'scatter') {
|
|
|
+ return getFormatter(samples.formatters)(params, asyncTicket);
|
|
|
+ }
|
|
|
if (hoveredEchartElement === chartRef?.current?.ele) {
|
|
|
- return getFormatter(formatters)(params, asyncTicket);
|
|
|
+ return getFormatter(timeseriesFormatters)(params, asyncTicket);
|
|
|
}
|
|
|
return '';
|
|
|
},
|
|
|
},
|
|
|
- yAxis: {
|
|
|
- // used to find and convert datapoint to pixel position
|
|
|
- id: 'yAxis',
|
|
|
- axisLabel: {
|
|
|
- formatter: (value: number) => {
|
|
|
- return formatMetricsUsingUnitAndOp(value, unit, operation);
|
|
|
+ yAxes: [
|
|
|
+ {
|
|
|
+ // used to find and convert datapoint to pixel position
|
|
|
+ id: 'yAxis',
|
|
|
+ axisLabel: {
|
|
|
+ formatter: (value: number) => {
|
|
|
+ return valueFormatter(value);
|
|
|
+ },
|
|
|
},
|
|
|
},
|
|
|
- },
|
|
|
- xAxis: {
|
|
|
- // used to find and convert datapoint to pixel position
|
|
|
- id: 'xAxis',
|
|
|
- axisPointer: {
|
|
|
- snap: true,
|
|
|
+ samples.yAxis,
|
|
|
+ ],
|
|
|
+ xAxes: [
|
|
|
+ {
|
|
|
+ // used to find and convert datapoint to pixel position
|
|
|
+ id: 'xAxis',
|
|
|
+ axisPointer: {
|
|
|
+ snap: true,
|
|
|
+ },
|
|
|
},
|
|
|
- },
|
|
|
+ samples.xAxis,
|
|
|
+ ],
|
|
|
};
|
|
|
}, [
|
|
|
bucketSize,
|
|
@@ -166,22 +206,23 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
|
|
|
focusAreaBrush.isDrawingRef,
|
|
|
forwardedRef,
|
|
|
isSubMinuteBucket,
|
|
|
- operation,
|
|
|
seriesToShow,
|
|
|
- unit,
|
|
|
height,
|
|
|
+ samples.handleClick,
|
|
|
+ samples.xAxis,
|
|
|
+ samples.yAxis,
|
|
|
+ samples.formatters,
|
|
|
+ valueFormatter,
|
|
|
]);
|
|
|
|
|
|
return (
|
|
|
<ChartWrapper>
|
|
|
{focusAreaBrush.overlay}
|
|
|
- {displayType === MetricDisplayType.LINE ? (
|
|
|
- <LineChart {...chartProps} />
|
|
|
- ) : displayType === MetricDisplayType.AREA ? (
|
|
|
- <AreaChart stacked {...chartProps} />
|
|
|
- ) : (
|
|
|
- <BarChart stacked animation={false} {...chartProps} />
|
|
|
- )}
|
|
|
+ <CombinedChart
|
|
|
+ {...chartProps}
|
|
|
+ displayType={displayType}
|
|
|
+ scatterSeries={samples.series}
|
|
|
+ />
|
|
|
{displayFogOfWar && (
|
|
|
<FogOfWar bucketSize={bucketSize} seriesLength={seriesLength} />
|
|
|
)}
|
|
@@ -190,6 +231,71 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
|
|
|
}
|
|
|
);
|
|
|
|
|
|
+type CombinedChartProps = BaseChartProps & {
|
|
|
+ displayType: MetricDisplayType;
|
|
|
+ series: Series[];
|
|
|
+ scatterSeries?: ScatterSeriesType[];
|
|
|
+};
|
|
|
+
|
|
|
+function CombinedChart({
|
|
|
+ displayType,
|
|
|
+ series,
|
|
|
+ scatterSeries = [],
|
|
|
+ ...chartProps
|
|
|
+}: CombinedChartProps) {
|
|
|
+ const combinedSeries = useMemo(() => {
|
|
|
+ if (displayType === MetricDisplayType.LINE) {
|
|
|
+ return [
|
|
|
+ ...transformToLineSeries({series}),
|
|
|
+ ...transformToScatterSeries({series: scatterSeries, displayType}),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (displayType === MetricDisplayType.BAR) {
|
|
|
+ return [
|
|
|
+ ...transformToBarSeries({series, stacked: true, animation: false}),
|
|
|
+ ...transformToScatterSeries({series: scatterSeries, displayType}),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (displayType === MetricDisplayType.AREA) {
|
|
|
+ return [
|
|
|
+ ...transformToAreaSeries({series, stacked: true, colors: chartProps.colors}),
|
|
|
+ ...transformToScatterSeries({series: scatterSeries, displayType}),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return [];
|
|
|
+ }, [displayType, scatterSeries, series, chartProps.colors]);
|
|
|
+
|
|
|
+ return <BaseChart {...chartProps} series={combinedSeries} />;
|
|
|
+}
|
|
|
+
|
|
|
+function transformToScatterSeries({
|
|
|
+ series,
|
|
|
+ displayType,
|
|
|
+}: {
|
|
|
+ displayType: MetricDisplayType;
|
|
|
+ series: Series[];
|
|
|
+}) {
|
|
|
+ return series.map(({seriesName, data: seriesData, ...options}) => {
|
|
|
+ if (displayType === MetricDisplayType.BAR) {
|
|
|
+ return ScatterSeries({
|
|
|
+ ...options,
|
|
|
+ name: seriesName,
|
|
|
+ data: seriesData?.map(({value, name}) => ({value: [name, value]})),
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return ScatterSeries({
|
|
|
+ ...options,
|
|
|
+ name: seriesName,
|
|
|
+ data: seriesData?.map(({value, name}) => [name, value]),
|
|
|
+ animation: false,
|
|
|
+ });
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
function FogOfWar({
|
|
|
bucketSize,
|
|
|
seriesLength,
|