Browse Source

fix(ddm): responsive focus area (#62464)

Ogi 1 year ago
parent
commit
2c0d2ca598
2 changed files with 91 additions and 39 deletions
  1. 4 0
      static/app/views/ddm/chart.tsx
  2. 87 39
      static/app/views/ddm/chartBrush.tsx

+ 4 - 0
static/app/views/ddm/chart.tsx

@@ -136,6 +136,8 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
           },
         },
         yAxis: {
+          // used to find and convert datapoint to pixel position
+          id: 'yAxis',
           axisLabel: {
             formatter: (value: number) => {
               return formatMetricsUsingUnitAndOp(value, unit, operation);
@@ -143,6 +145,8 @@ export const MetricChart = forwardRef<ReactEchartsRef, ChartProps>(
           },
         },
         xAxis: {
+          // used to find and convert datapoint to pixel position
+          id: 'xAxis',
           axisPointer: {
             snap: true,
           },

+ 87 - 39
static/app/views/ddm/chartBrush.tsx

@@ -1,5 +1,6 @@
-import {RefObject, useCallback, useMemo, useRef} from 'react';
+import {RefObject, useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import styled from '@emotion/styled';
+import {useResizeObserver} from '@react-aria/utils';
 import {EChartsOption} from 'echarts';
 import moment from 'moment';
 
@@ -20,7 +21,6 @@ interface AbsolutePosition {
 }
 
 export interface FocusArea {
-  position: AbsolutePosition;
   range: MetricRange;
   widgetIndex: number;
 }
@@ -58,11 +58,8 @@ export function useFocusAreaBrush(
         return;
       }
 
-      const chartWidth = chartRef.current?.getEchartsInstance().getWidth() ?? 100;
-
       onAdd({
         widgetIndex,
-        position: getPosition(brushEnd, chartWidth),
         range: getMetricRange(brushEnd),
       });
 
@@ -138,6 +135,7 @@ export function useFocusAreaBrush(
           rect={focusArea}
           onRemove={handleRemove}
           onZoom={handleZoomIn}
+          chartRef={chartRef}
         />
       ),
       isDrawingRef,
@@ -149,56 +147,98 @@ export function useFocusAreaBrush(
   return {
     overlay: null,
     isDrawingRef,
-
     startBrush,
     options: brushOptions,
   };
 }
 
-function BrushRectOverlay({rect, onZoom, onRemove}) {
-  if (!rect) {
+type BrushRectOverlayProps = {
+  chartRef: RefObject<ReactEchartsRef>;
+  onRemove: () => void;
+  onZoom: () => void;
+  rect: FocusArea | null;
+};
+
+function BrushRectOverlay({rect, onZoom, onRemove, chartRef}: BrushRectOverlayProps) {
+  const chartInstance = chartRef.current?.getEchartsInstance();
+  const [position, setPosition] = useState<AbsolutePosition | null>(null);
+  const wrapperRef = useRef<HTMLDivElement>(null);
+
+  useResizeObserver({
+    ref: wrapperRef,
+    onResize: () => {
+      chartInstance?.resize();
+      updatePosition();
+    },
+  });
+
+  const updatePosition = useCallback(() => {
+    if (!rect || !chartInstance) {
+      return;
+    }
+
+    const finder = {xAxisId: 'xAxis', yAxisId: 'yAxis'};
+
+    const topLeft = chartInstance.convertToPixel(finder, [
+      getTimestamp(rect.range.start),
+      rect.range.max,
+    ] as number[]);
+    const bottomRight = chartInstance.convertToPixel(finder, [
+      getTimestamp(rect.range.end),
+      rect.range.min,
+    ] as number[]);
+
+    if (!topLeft || !bottomRight) {
+      return;
+    }
+
+    const widthPx = bottomRight[0] - topLeft[0];
+    const heightPx = bottomRight[1] - topLeft[1];
+
+    setPosition({
+      left: `${topLeft[0].toPrecision(5)}px`,
+      top: `${topLeft[1].toPrecision(5)}px`,
+      width: `${widthPx.toPrecision(5)}px`,
+      height: `${heightPx.toPrecision(5)}px`,
+    });
+  }, [rect, chartInstance]);
+
+  useEffect(() => {
+    updatePosition();
+  }, [rect, updatePosition]);
+
+  if (!position) {
     return null;
   }
 
-  const {top, left, width, height} = rect.position;
+  const {left, top, width, height} = position;
 
   return (
-    <FocusAreaRect top={top} left={left} width={width} height={height}>
-      <FocusAreaRectActions top={height}>
-        <Button
-          size="xs"
-          onClick={onZoom}
-          icon={<IconZoom isZoomIn />}
-          aria-label="zoom"
-        />
-        <Button size="xs" onClick={onRemove} icon={<IconDelete />} aria-label="remove" />
-      </FocusAreaRectActions>
-    </FocusAreaRect>
+    <FocusAreaWrapper ref={wrapperRef}>
+      <FocusAreaRect top={top} left={left} width={width} height={height}>
+        <FocusAreaRectActions top={height}>
+          <Button
+            size="xs"
+            onClick={onZoom}
+            icon={<IconZoom isZoomIn />}
+            aria-label="zoom"
+          />
+          <Button
+            size="xs"
+            onClick={onRemove}
+            icon={<IconDelete />}
+            aria-label="remove"
+          />
+        </FocusAreaRectActions>
+      </FocusAreaRect>
+    </FocusAreaWrapper>
   );
 }
 
 const getDate = date =>
   date ? moment.utc(date).format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS) : null;
 
-const getPosition = (params: BrushEndResult, chartWidth: number): AbsolutePosition => {
-  const rect = params.areas[0];
-
-  const left = rect.range[0][0];
-  const width = rect.range[0][1] - left;
-
-  const leftPercentage = (left / chartWidth) * 100;
-  const widthPercentage = (width / chartWidth) * 100;
-
-  const topPx = Math.min(...rect.range[1]);
-  const heightPx = Math.max(...rect.range[1]) - topPx;
-
-  return {
-    left: `${leftPercentage.toPrecision(3)}%`,
-    top: `${topPx}px`,
-    width: `${widthPercentage.toPrecision(3)}%`,
-    height: `${heightPx}px`,
-  };
-};
+const getTimestamp = date => (date ? moment.utc(date).valueOf() : null);
 
 const getMetricRange = (params: BrushEndResult): MetricRange => {
   const rect = params.areas[0];
@@ -220,6 +260,14 @@ const getMetricRange = (params: BrushEndResult): MetricRange => {
   };
 };
 
+const FocusAreaWrapper = styled('div')`
+  position: absolute;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
+`;
+
 const FocusAreaRectActions = styled('div')<{
   top: string;
 }>`