Browse Source

feat(ddm): Multi axis support (#66568)

Create an axis for each unit family in the chart.
Two axis are visible at once.
ArthurKnaus 1 year ago
parent
commit
175ed513af

+ 77 - 31
static/app/views/ddm/chart/chart.tsx

@@ -26,8 +26,7 @@ import type {CombinedMetricChartProps, Series} from 'sentry/views/ddm/chart/type
 import type {UseFocusAreaResult} from 'sentry/views/ddm/chart/useFocusArea';
 import type {UseMetricSamplesResult} from 'sentry/views/ddm/chart/useMetricChartSamples';
 
-export const MAIN_X_AXIS_ID = 'xAxis';
-export const MAIN_Y_AXIS_ID = 'yAxis';
+const MAIN_X_AXIS_ID = 'xAxis';
 
 type ChartProps = {
   displayType: MetricDisplayType;
@@ -72,7 +71,14 @@ function addSeriesPadding(data: Series['data']) {
 export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
   ({series, displayType, height, group, samples, focusArea}, forwardedRef) => {
     const chartRef = useRef<ReactEchartsRef>(null);
-    const firstUnit = series.find(s => !s.hidden)?.unit || 'none';
+
+    const filteredSeries = useMemo(() => series.filter(s => !s.hidden), [series]);
+
+    const firstUnit = filteredSeries[0]?.unit || 'none';
+    const uniqueUnits = useMemo(
+      () => [...new Set(filteredSeries.map(s => s.unit || 'none'))],
+      [filteredSeries]
+    );
 
     useEffect(() => {
       if (!group) {
@@ -95,19 +101,26 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
 
     const seriesToShow = useMemo(
       () =>
-        series
-          .filter(s => !s.hidden)
-          .map(s => ({
-            ...s,
-            silent: true,
-            ...(displayType !== MetricDisplayType.BAR
-              ? addSeriesPadding(s.data)
-              : {data: s.data}),
-          }))
+        filteredSeries
+          .map(s => {
+            const mappedSeries = {
+              ...s,
+              silent: true,
+              yAxisIndex: uniqueUnits.indexOf(s.unit),
+              xAxisIndex: 0,
+              ...(displayType !== MetricDisplayType.BAR
+                ? addSeriesPadding(s.data)
+                : {data: s.data}),
+            };
+            if (displayType === MetricDisplayType.BAR) {
+              mappedSeries.stack = s.unit;
+            }
+            return mappedSeries;
+          })
           // Split series in two parts, one for the main chart and one for the fog of war
           // The order is important as the tooltip will show the first series first (for overlaps)
           .flatMap(s => createIngestionSeries(s, ingestionBuckets, displayType)),
-      [series, ingestionBuckets, displayType]
+      [filteredSeries, uniqueUnits, displayType, ingestionBuckets]
     );
 
     const {selection} = usePageFilters();
@@ -117,7 +130,6 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
     }, [selection.datetime]);
 
     const chartProps = useMemo(() => {
-      const hasMultipleUnits = new Set(seriesToShow.map(s => s.unit)).size > 1;
       const seriesUnits = seriesToShow.reduce(
         (acc, s) => {
           acc[s.seriesName] = s.unit;
@@ -137,7 +149,7 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
         addSecondsToTimeFormat: isSubMinuteBucket,
         limit: 10,
         filter: (_, seriesParam) => {
-          return seriesParam?.axisId === 'xAxis';
+          return seriesParam?.axisId === MAIN_X_AXIS_ID;
         },
       };
 
@@ -153,7 +165,12 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
         renderer: 'canvas' as const,
         isGroupedByDate: true,
         colors: seriesToShow.map(s => s.color),
-        grid: {top: 5, bottom: 0, left: 0, right: 0},
+        grid: {
+          top: 5,
+          bottom: 0,
+          left: 0,
+          right: 0,
+        },
         tooltip: {
           formatter: (params, asyncTicket) => {
             // Only show the tooltip if the current chart is hovered
@@ -219,23 +236,51 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
             return getFormatter(timeseriesFormatters)(params, asyncTicket);
           },
         },
-        yAxes: [
-          {
-            // used to find and convert datapoint to pixel position
-            id: MAIN_Y_AXIS_ID,
-            axisLabel: {
-              formatter: (value: number) => {
-                return formatMetricUsingUnit(
-                  value,
-                  hasMultipleUnits ? 'none' : firstUnit
-                );
-              },
-            },
-          },
-        ],
+        yAxes:
+          uniqueUnits.length === 0
+            ? // fallback axis for when there are no series as echarts requires at least one axis
+              [
+                {
+                  id: 'none',
+                  axisLabel: {
+                    formatter: (value: number) => {
+                      return formatMetricUsingUnit(value, 'none');
+                    },
+                  },
+                },
+              ]
+            : [
+                ...uniqueUnits.map((unit, index) =>
+                  unit === firstUnit
+                    ? {
+                        id: unit,
+                        axisLabel: {
+                          formatter: (value: number) => {
+                            return formatMetricUsingUnit(value, unit);
+                          },
+                        },
+                      }
+                    : {
+                        id: unit,
+                        show: index === 1,
+                        axisLabel: {
+                          show: index === 1,
+                          formatter: (value: number) => {
+                            return formatMetricUsingUnit(value, unit);
+                          },
+                        },
+                        splitLine: {
+                          show: false,
+                        },
+                        position: 'right' as const,
+                        axisPointer: {
+                          type: 'none' as const,
+                        },
+                      }
+                ),
+              ],
         xAxes: [
           {
-            // used to find and convert datapoint to pixel position
             id: MAIN_X_AXIS_ID,
             axisPointer: {
               snap: true,
@@ -261,6 +306,7 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
       height,
       displayType,
       forwardedRef,
+      uniqueUnits,
       samples,
       focusArea,
       firstUnit,

+ 5 - 1
static/app/views/ddm/chart/chartUtils.tsx

@@ -2,6 +2,10 @@ import type {RefObject} from 'react';
 import moment from 'moment';
 
 import type {ReactEchartsRef} from 'sentry/types/echarts';
+import {
+  SAMPLES_X_AXIS_ID,
+  SAMPLES_Y_AXIS_ID,
+} from 'sentry/views/ddm/chart/useMetricChartSamples';
 
 export type ValueRect = {
   xMax: number;
@@ -35,7 +39,7 @@ export function getValueRect(chartRef?: RefObject<ReactEchartsRef>): ValueRect {
     return DEFAULT_VALUE_RECT;
   }
 
-  const finder = {xAxisId: 'xAxis', yAxisId: 'yAxis'};
+  const finder = {xAxisId: SAMPLES_X_AXIS_ID, yAxisId: SAMPLES_Y_AXIS_ID};
 
   const topLeft = chartInstance.convertFromPixel(finder, [0, 0]);
   const bottomRight = chartInstance.convertFromPixel(finder, [

+ 1 - 0
static/app/views/ddm/chart/types.tsx

@@ -13,6 +13,7 @@ export type Series = {
   paddingIndices?: Set<number>;
   release?: string;
   scalingFactor?: number;
+  stack?: string;
   transaction?: string;
 };
 

+ 20 - 31
static/app/views/ddm/chart/useFocusArea.tsx

@@ -14,7 +14,6 @@ import {space} from 'sentry/styles/space';
 import type {DateString} from 'sentry/types';
 import type {EChartBrushEndHandler, ReactEchartsRef} from 'sentry/types/echarts';
 import mergeRefs from 'sentry/utils/mergeRefs';
-import {MAIN_X_AXIS_ID, MAIN_Y_AXIS_ID} from 'sentry/views/ddm/chart/chart';
 import type {ValueRect} from 'sentry/views/ddm/chart/chartUtils';
 import {getValueRect} from 'sentry/views/ddm/chart/chartUtils';
 import type {
@@ -22,6 +21,10 @@ import type {
   FocusAreaSelection,
   SelectionRange,
 } from 'sentry/views/ddm/chart/types';
+import {
+  SAMPLES_X_AXIS_ID,
+  SAMPLES_Y_AXIS_ID,
+} from 'sentry/views/ddm/chart/useMetricChartSamples';
 import {CHART_HEIGHT} from 'sentry/views/ddm/constants';
 import type {FocusAreaProps} from 'sentry/views/ddm/context';
 
@@ -52,7 +55,6 @@ type BrushEndResult = Parameters<EChartBrushEndHandler>[0];
 export function useFocusArea({
   selection: selection,
   opts: {widgetIndex, isDisabled, useFullYAxis},
-  scalingFactor,
   onAdd,
   onDraw,
   onRemove,
@@ -109,13 +111,7 @@ export function useFocusArea({
       if (!rect) {
         return;
       }
-
-      const range = getSelectionRange(
-        brushEnd,
-        !!useFullYAxis,
-        getValueRect(chartRef),
-        scalingFactor
-      );
+      const range = getSelectionRange(brushEnd, !!useFullYAxis, getValueRect(chartRef));
       onAdd?.({
         widgetIndex,
         range,
@@ -131,7 +127,7 @@ export function useFocusArea({
       });
       isDrawingRef.current = false;
     },
-    [isDisabled, useFullYAxis, scalingFactor, onAdd, widgetIndex]
+    [isDisabled, useFullYAxis, onAdd, widgetIndex]
   );
 
   const handleRemove = useCallback(() => {
@@ -178,7 +174,12 @@ export function useFocusArea({
         },
         brush: {
           toolbox: ['rect'],
-          xAxisIndex: 0,
+          xAxisIndex: Array.isArray(baseProps.xAxes)
+            ? baseProps.xAxes.findIndex(a => a?.id === SAMPLES_X_AXIS_ID)
+            : 0,
+          yAxisIndex: Array.isArray(baseProps.yAxes)
+            ? baseProps.yAxes.findIndex(a => a?.id === SAMPLES_Y_AXIS_ID)
+            : 0,
           brushStyle: {
             borderWidth: 2,
             borderColor: theme.gray500,
@@ -207,19 +208,10 @@ export function useFocusArea({
           onZoom={handleZoomIn}
           chartRef={chartRef}
           useFullYAxis={!!useFullYAxis}
-          scalingFactor={scalingFactor}
         />
       ) : null,
     }),
-    [
-      applyChartProps,
-      handleRemove,
-      handleZoomIn,
-      hasFocusArea,
-      scalingFactor,
-      selection,
-      useFullYAxis,
-    ]
+    [applyChartProps, handleRemove, handleZoomIn, hasFocusArea, selection, useFullYAxis]
   );
 }
 
@@ -230,7 +222,6 @@ type FocusAreaOverlayProps = {
   onRemove: () => void;
   onZoom: () => void;
   rect: FocusAreaSelection | null;
-  scalingFactor: number;
   useFullYAxis: boolean;
 };
 
@@ -240,7 +231,6 @@ function FocusAreaOverlay({
   onRemove,
   useFullYAxis,
   chartRef,
-  scalingFactor,
 }: FocusAreaOverlayProps) {
   const [position, setPosition] = useState<AbsolutePosition | null>(null);
   const wrapperRef = useRef<HTMLDivElement>(null);
@@ -266,10 +256,10 @@ function FocusAreaOverlay({
     ) {
       return;
     }
-    const finder = {xAxisId: MAIN_X_AXIS_ID, yAxisId: MAIN_Y_AXIS_ID};
+    const finder = {xAxisId: SAMPLES_X_AXIS_ID, yAxisId: SAMPLES_Y_AXIS_ID};
 
-    const max = rect.range.max * scalingFactor;
-    const min = rect.range.min * scalingFactor;
+    const max = rect.range.max;
+    const min = rect.range.min;
 
     const topLeft = chartInstance.convertToPixel(finder, [
       getTimestamp(rect.range.start),
@@ -306,7 +296,7 @@ function FocusAreaOverlay({
     if (!isEqual(newPosition, position)) {
       setPosition(newPosition);
     }
-  }, [chartRef, rect, scalingFactor, useFullYAxis, position]);
+  }, [chartRef, rect, useFullYAxis, position]);
 
   useEffect(() => {
     updatePosition();
@@ -343,8 +333,7 @@ const getTimestamp = (date: DateString) => moment.utc(date).valueOf();
 const getSelectionRange = (
   params: BrushEndResult,
   useFullYAxis: boolean,
-  boundingRect: ValueRect,
-  scalingFactor: number
+  boundingRect: ValueRect
 ): SelectionRange => {
   const rect = params.areas[0];
 
@@ -354,8 +343,8 @@ const getSelectionRange = (
   const startDate = getDateString(Math.max(startTimestamp, boundingRect.xMin));
   const endDate = getDateString(Math.min(endTimestamp, boundingRect.xMax));
 
-  const min = useFullYAxis ? NaN : Math.min(...rect.coordRange[1]) / scalingFactor;
-  const max = useFullYAxis ? NaN : Math.max(...rect.coordRange[1]) / scalingFactor;
+  const min = useFullYAxis ? NaN : Math.min(...rect.coordRange[1]);
+  const max = useFullYAxis ? NaN : Math.max(...rect.coordRange[1]);
 
   return {
     start: startDate,

+ 174 - 173
static/app/views/ddm/chart/useMetricChartSamples.tsx

@@ -18,10 +18,39 @@ import {
   getSummaryValueForOp,
   type MetricsSamplesResults,
 } from 'sentry/utils/metrics/useMetricsSamples';
-import {fitToValueRect, getValueRect} from 'sentry/views/ddm/chart/chartUtils';
-import type {CombinedMetricChartProps, Series} from 'sentry/views/ddm/chart/types';
+import {fitToValueRect} from 'sentry/views/ddm/chart/chartUtils';
+import type {
+  CombinedMetricChartProps,
+  ScatterSeries,
+  Series,
+} from 'sentry/views/ddm/chart/types';
 import type {Sample} from 'sentry/views/ddm/widget';
 
+export const SAMPLES_X_AXIS_ID = 'xAxisSamples';
+export const SAMPLES_Y_AXIS_ID = 'yAxisSamples';
+
+function getValueRectFromSeries(series: Series[]) {
+  const referenceSeries = series[0];
+  if (!referenceSeries) {
+    return {xMin: -Infinity, xMax: Infinity, yMin: -Infinity, yMax: Infinity};
+  }
+  const seriesWithSameUnit = series.filter(
+    s => s.unit === referenceSeries.unit && !s.hidden
+  );
+  const scalingFactor = referenceSeries.scalingFactor ?? 1;
+  const xValues = referenceSeries.data.map(entry => entry.name);
+  const yValues = [referenceSeries, ...seriesWithSameUnit].flatMap(s =>
+    s.data.map(entry => entry.value)
+  );
+
+  return {
+    xMin: Math.min(...xValues),
+    xMax: Math.max(...xValues),
+    yMin: Math.min(0, ...yValues) / scalingFactor,
+    yMax: Math.max(0, ...yValues) / scalingFactor,
+  };
+}
+
 type UseChartSamplesProps = {
   timeseries: Series[];
   chartRef?: RefObject<ReactEchartsRef>;
@@ -36,17 +65,6 @@ type UseChartSamplesProps = {
 
 // TODO: remove this once we have a stabilized type for this
 type ChartSample = MetricCorrelation & MetricSummary;
-
-function getDateRange(timeseries: Series[]) {
-  if (!timeseries?.length) {
-    return {min: -Infinity, max: Infinity};
-  }
-  const min = timeseries[0].data[0].name as number;
-  const max = timeseries[0].data[timeseries[0].data.length - 1].name as number;
-
-  return {min, max};
-}
-
 type EChartMouseEventParam = Parameters<EChartClickHandler>[0];
 
 export function useMetricChartSamples({
@@ -59,9 +77,8 @@ export function useMetricChartSamples({
 }: UseChartSamplesProps) {
   const theme = useTheme();
   const chartRef = useRef<ReactEchartsRef>(null);
-  const scalingFactor = timeseries?.[0]?.scalingFactor ?? 1;
 
-  const [valueRect, setValueRect] = useState(getValueRect(chartRef));
+  const [valueRect, setValueRect] = useState(() => getValueRectFromSeries(timeseries));
 
   const samples: Record<string, ChartSample> = useMemo(() => {
     return (correlations ?? [])
@@ -77,40 +94,31 @@ export function useMetricChartSamples({
   useEffect(() => {
     // Changes in timeseries change the valueRect since the timeseries yAxis auto scales
     // and scatter yAxis needs to match the scale
-    setValueRect(getValueRect(chartRef));
-  }, [chartRef, timeseries]);
+    setValueRect(getValueRectFromSeries(timeseries));
+  }, [timeseries]);
 
   const xAxis: XAXisOption = useMemo(() => {
-    const {min, max} = getDateRange(timeseries);
-
     return {
-      id: 'xAxisScatter',
-      scale: false,
+      id: SAMPLES_X_AXIS_ID,
       show: false,
       axisLabel: {
-        formatter: () => {
-          return '';
-        },
+        show: false,
       },
       axisPointer: {
         type: 'none',
       },
-      min: Math.max(valueRect.xMin, min),
-      max: Math.min(valueRect.xMax, max),
+      min: valueRect.xMin,
+      max: valueRect.xMax,
     };
-  }, [valueRect.xMin, valueRect.xMax, timeseries]);
+  }, [valueRect.xMin, valueRect.xMax]);
 
   const yAxis: YAXisOption = useMemo(() => {
     return {
-      id: 'yAxisScatter',
-      scale: false,
+      id: SAMPLES_Y_AXIS_ID,
       show: false,
       axisLabel: {
-        formatter: () => {
-          return '';
-        },
+        show: false,
       },
-
       min: valueRect.yMin,
       max: valueRect.yMax,
     };
@@ -137,67 +145,6 @@ export function useMetricChartSamples({
     [getSample, onClick]
   );
 
-  const series = useMemo(() => {
-    if (isCumulativeOp(operation)) {
-      // TODO: for now we do not show samples for cumulative operations,
-      // we will implement them as marklines
-      return [];
-    }
-
-    return Object.values(samples).map(sample => {
-      const isHighlighted = highlightedSampleId === sample.transactionId;
-
-      const xValue = moment(sample.timestamp).valueOf();
-      const yValue = (((sample.min ?? 0) + (sample.max ?? 0)) / 2) * scalingFactor;
-
-      const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect);
-
-      const symbol = yPosition === yValue ? 'circle' : 'arrow';
-      const symbolRotate = yPosition > yValue ? 180 : 0;
-
-      return {
-        seriesName: sample.transactionId,
-        id: sample.spanId,
-        operation: '',
-        unit: '',
-        symbolSize: isHighlighted ? 20 : 10,
-        animation: false,
-        symbol,
-        symbolRotate,
-        color: theme.purple400,
-        // TODO: for now we just pass these ids through, but we should probably index
-        // samples by an id and then just pass that reference
-        itemStyle: {
-          color: theme.purple400,
-          opacity: 1,
-        },
-        yAxisIndex: 1,
-        xAxisIndex: 1,
-        xValue,
-        yValue,
-        tooltip: {
-          axisPointer: {
-            type: 'none',
-          },
-        },
-        data: [
-          {
-            name: xPosition,
-            value: yPosition,
-          },
-        ],
-        z: 10,
-      };
-    });
-  }, [
-    operation,
-    samples,
-    highlightedSampleId,
-    scalingFactor,
-    valueRect,
-    theme.purple400,
-  ]);
-
   const formatterOptions = useMemo(() => {
     return {
       isGroupedByDate: true,
@@ -218,6 +165,58 @@ export function useMetricChartSamples({
 
   const applyChartProps = useCallback(
     (baseProps: CombinedMetricChartProps): CombinedMetricChartProps => {
+      let series: ScatterSeries[] = [];
+      // TODO: for now we do not show samples for cumulative operations,
+      // we will implement them as marklines
+      if (!isCumulativeOp(operation)) {
+        const newYAxisIndex = Array.isArray(baseProps.yAxes) ? baseProps.yAxes.length : 1;
+        const newXAxisIndex = Array.isArray(baseProps.xAxes) ? baseProps.xAxes.length : 1;
+
+        series = Object.values(samples).map(sample => {
+          const isHighlighted = highlightedSampleId === sample.transactionId;
+
+          const xValue = moment(sample.timestamp).valueOf();
+          const yValue = ((sample.min ?? 0) + (sample.max ?? 0)) / 2;
+
+          const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect);
+
+          const symbol = yPosition === yValue ? 'circle' : 'arrow';
+          const symbolRotate = yPosition > yValue ? 180 : 0;
+
+          return {
+            seriesName: sample.transactionId,
+            id: sample.spanId,
+            operation: '',
+            unit: '',
+            symbolSize: isHighlighted ? 20 : 10,
+            animation: false,
+            symbol,
+            symbolRotate,
+            color: theme.purple400,
+            itemStyle: {
+              color: theme.purple400,
+              opacity: 1,
+            },
+            yAxisIndex: newYAxisIndex,
+            xAxisIndex: newXAxisIndex,
+            xValue,
+            yValue,
+            tooltip: {
+              axisPointer: {
+                type: 'none',
+              },
+            },
+            data: [
+              {
+                name: xPosition,
+                value: yPosition,
+              },
+            ],
+            z: 10,
+          };
+        });
+      }
+
       return {
         ...baseProps,
         forwardedRef: mergeRefs([baseProps.forwardedRef, chartRef]),
@@ -257,7 +256,17 @@ export function useMetricChartSamples({
         },
       };
     },
-    [formatterOptions, handleClick, series, xAxis, yAxis]
+    [
+      formatterOptions,
+      handleClick,
+      highlightedSampleId,
+      operation,
+      samples,
+      theme.purple400,
+      valueRect,
+      xAxis,
+      yAxis,
+    ]
   );
 
   // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -288,9 +297,8 @@ export function useMetricChartSamplesV2({
 }: UseMetricChartSamplesV2Options) {
   const theme = useTheme();
   const chartRef = useRef<ReactEchartsRef>(null);
-  const timeseriesScalingFactor = timeseries?.[0]?.scalingFactor ?? 1;
 
-  const [valueRect, setValueRect] = useState(getValueRect(chartRef));
+  const [valueRect, setValueRect] = useState(() => getValueRectFromSeries(timeseries));
 
   const samplesById = useMemo(() => {
     return (samples ?? []).reduce((acc, sample) => {
@@ -302,102 +310,36 @@ export function useMetricChartSamplesV2({
   useEffect(() => {
     // Changes in timeseries change the valueRect since the timeseries yAxis auto scales
     // and scatter yAxis needs to match the scale
-    setValueRect(getValueRect(chartRef));
-  }, [chartRef, timeseries]);
+    setValueRect(getValueRectFromSeries(timeseries));
+  }, [timeseries]);
 
   const xAxis: XAXisOption = useMemo(() => {
-    const {min, max} = getDateRange(timeseries);
-
     return {
-      id: 'xAxisScatter',
-      scale: false,
+      id: SAMPLES_X_AXIS_ID,
       show: false,
       axisLabel: {
-        formatter: () => {
-          return '';
-        },
+        show: false,
       },
       axisPointer: {
         type: 'none',
       },
-      min: Math.max(valueRect.xMin, min),
-      max: Math.min(valueRect.xMax, max),
+      min: valueRect.xMin,
+      max: valueRect.xMax,
     };
-  }, [valueRect.xMin, valueRect.xMax, timeseries]);
+  }, [valueRect.xMin, valueRect.xMax]);
 
   const yAxis: YAXisOption = useMemo(() => {
     return {
-      id: 'yAxisScatter',
-      scale: false,
+      id: SAMPLES_Y_AXIS_ID,
       show: false,
       axisLabel: {
-        formatter: () => {
-          return '';
-        },
+        show: false,
       },
-
       min: valueRect.yMin,
       max: valueRect.yMax,
     };
   }, [valueRect.yMin, valueRect.yMax]);
 
-  const series = useMemo(() => {
-    if (isCumulativeOp(operation)) {
-      // TODO: for now we do not show samples for cumulative operations
-      // figure out how should this be shown
-      return [];
-    }
-
-    return (samples ?? []).map(sample => {
-      const isHighlighted = highlightedSampleId === sample.id;
-
-      const xValue = moment(sample.timestamp).valueOf();
-      const value = getSummaryValueForOp(sample.summary, operation);
-      const yValue = value * timeseriesScalingFactor;
-
-      const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect);
-
-      return {
-        seriesName: sample.id,
-        id: sample.id,
-        operation: '',
-        unit: '',
-        symbolSize: isHighlighted ? 20 : 10,
-        animation: false,
-        symbol: yPosition === yValue ? 'circle' : 'arrow',
-        symbolRotate: yPosition > yValue ? 180 : 0,
-        color: theme.purple400,
-        itemStyle: {
-          color: theme.purple400,
-          opacity: 1,
-        },
-        yAxisIndex: 1,
-        xAxisIndex: 1,
-        xValue,
-        yValue,
-        tooltip: {
-          axisPointer: {
-            type: 'none',
-          },
-        },
-        data: [
-          {
-            name: xPosition,
-            value: yPosition,
-          },
-        ],
-        z: 10,
-      };
-    });
-  }, [
-    highlightedSampleId,
-    operation,
-    samples,
-    theme.purple400,
-    timeseriesScalingFactor,
-    valueRect,
-  ]);
-
   const formatterOptions = useMemo(() => {
     return {
       isGroupedByDate: true,
@@ -428,6 +370,55 @@ export function useMetricChartSamplesV2({
 
   const applyChartProps = useCallback(
     (baseProps: CombinedMetricChartProps): CombinedMetricChartProps => {
+      let series: ScatterSeries[] = [];
+
+      const newYAxisIndex = Array.isArray(baseProps.yAxes) ? baseProps.yAxes.length : 1;
+      const newXAxisIndex = Array.isArray(baseProps.xAxes) ? baseProps.xAxes.length : 1;
+
+      if (!isCumulativeOp(operation)) {
+        series = (samples ?? []).map(sample => {
+          const isHighlighted = highlightedSampleId === sample.id;
+
+          const xValue = moment(sample.timestamp).valueOf();
+          const value = getSummaryValueForOp(sample.summary, operation);
+          const yValue = value;
+
+          const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect);
+
+          return {
+            seriesName: sample.id,
+            id: sample.id,
+            operation: '',
+            unit: '',
+            symbolSize: isHighlighted ? 20 : 10,
+            animation: false,
+            symbol: yPosition === yValue ? 'circle' : 'arrow',
+            symbolRotate: yPosition > yValue ? 180 : 0,
+            color: theme.purple400,
+            itemStyle: {
+              color: theme.purple400,
+              opacity: 1,
+            },
+            yAxisIndex: newYAxisIndex,
+            xAxisIndex: newXAxisIndex,
+            xValue,
+            yValue,
+            tooltip: {
+              axisPointer: {
+                type: 'none',
+              },
+            },
+            data: [
+              {
+                name: xPosition,
+                value: yPosition,
+              },
+            ],
+            z: 10,
+          };
+        });
+      }
+
       return {
         ...baseProps,
         forwardedRef: mergeRefs([baseProps.forwardedRef, chartRef]),
@@ -467,7 +458,17 @@ export function useMetricChartSamplesV2({
         },
       };
     },
-    [formatterOptions, handleClick, series, xAxis, yAxis]
+    [
+      formatterOptions,
+      handleClick,
+      highlightedSampleId,
+      operation,
+      samples,
+      theme.purple400,
+      valueRect,
+      xAxis,
+      yAxis,
+    ]
   );
 
   return useMemo(() => {

+ 3 - 4
static/app/views/ddm/widget.tsx

@@ -393,9 +393,8 @@ const MetricWidgetBody = memo(
       [router]
     );
 
-    const hasCumulativeOp = queries.some(
-      q => !isMetricFormula(q) && isCumulativeOp(q.op)
-    );
+    const isCumulativeSamplesOp =
+      queries[0] && !isMetricFormula(queries[0]) && isCumulativeOp(queries[0].op);
     const firstScalingFactor = chartSeries.find(s => !s.hidden)?.scalingFactor || 1;
 
     const focusArea = useFocusArea({
@@ -405,7 +404,7 @@ const MetricWidgetBody = memo(
       opts: {
         widgetIndex,
         isDisabled: !focusAreaProps.onAdd,
-        useFullYAxis: hasCumulativeOp,
+        useFullYAxis: isCumulativeSamplesOp,
       },
       onZoom: handleZoom,
     });