This is an automated email from the ASF dual-hosted git repository.

vogievetsky pushed a commit to branch segment_timeline2
in repository https://gitbox.apache.org/repos/asf/druid.git

commit 7fe805aeb0ba94a6274ddc18f8d3a2619e52d05e
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Fri Nov 15 15:53:22 2024 -0800

    goodies
---
 .../segment-timeline/segment-bar-chart-render.tsx  | 224 +++++++++++++--------
 .../segment-timeline/segment-timeline.tsx          |   4 +-
 2 files changed, 142 insertions(+), 86 deletions(-)

diff --git 
a/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx 
b/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx
index dfbb50fc628..bc73d3db7a4 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx
@@ -38,7 +38,9 @@ import {
   groupByAsMap,
   minute,
   month,
+  pluralIfNeeded,
   TZ_UTC,
+  uniq,
 } from '../../utils';
 import type { Margin, Stage } from '../../utils/stage';
 
@@ -139,7 +141,10 @@ function offsetDateRange(dateRange: NonNullDateRange, 
offset: number): NonNullDa
   return [new Date(dateRange[0].valueOf() + offset), new 
Date(dateRange[1].valueOf() + offset)];
 }
 
-function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): 
IntervalBar[] {
+function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): {
+  intervalBars: IntervalBar[];
+  intervalTree: IntervalTree;
+} {
   // Total size of the datasource will be user as an ordering tiebreaker
   const datasourceToTotalSize = groupByAsMap(
     trimmedIntervalRows,
@@ -162,19 +167,29 @@ function stackIntervalRows(trimmedIntervalRows: 
TrimmedIntervalRow[]): IntervalB
   });
 
   const intervalTree = new IntervalTree();
-  return sortedIntervalRows.map(intervalRow => {
+  const intervalBars = sortedIntervalRows.map(intervalRow => {
     const startMs = intervalRow.start.valueOf();
     const endMs = intervalRow.end.valueOf();
-    const intervalRowsBelow = intervalTree.search([
-      startMs + 1,
-      startMs + 2,
-    ]) as TrimmedIntervalRow[];
-    intervalTree.insert([startMs, endMs], intervalRow);
-    return {
+    const intervalRowsBelow = intervalTree.search([startMs + 1, startMs + 2]) 
as IntervalBar[];
+    const intervalBar: IntervalBar = {
       ...intervalRow,
       offset: aggregateSegmentStats(intervalRowsBelow.map(i => i.normalized)),
     };
+    intervalTree.insert([startMs, endMs], intervalBar);
+    return intervalBar;
   });
+
+  return {
+    intervalBars,
+    intervalTree,
+  };
+}
+
+interface BubbleInfo {
+  start: Date;
+  end: Date;
+  timeLabel: string;
+  intervalBars: IntervalBar[];
 }
 
 export interface DatasourceRules {
@@ -206,15 +221,12 @@ export const SegmentBarChartRender = function 
SegmentBarChartRender(
     shownDatasource,
     changeShownDatasource,
   } = props;
-  const [hoveredIntervalBar, setHoveredIntervalBar] = useState<IntervalBar>();
   const [mouseDownAt, setMouseDownAt] = useState<
     { time: Date; action: 'select' | 'shift' } | undefined
   >();
   const [dragging, setDragging] = useState<NonNullDateRange | undefined>();
   const [shiftOffset, setShiftOffset] = useState<number | undefined>();
-  const [bubbleOpenOn, setBubbleOpenOn] = useState<
-    { start: Date; end: Date; x: number; y: number; text: string } | undefined
-  >();
+  const [bubbleInfo, setBubbleInfo] = useState<BubbleInfo | undefined>();
   const now = useClock(minute.canonicalLength);
   const svgRef = useRef<SVGSVGElement | null>(null);
 
@@ -226,7 +238,7 @@ export const SegmentBarChartRender = function 
SegmentBarChartRender(
     ).toString();
   }, [dateRange, stage.width]);
 
-  const intervalBars = useMemo(() => {
+  const { intervalBars, intervalTree } = useMemo(() => {
     const shownIntervalRows = shownDatasource
       ? intervalRows.filter(({ datasource }) => datasource === shownDatasource)
       : intervalRows;
@@ -320,21 +332,18 @@ export const SegmentBarChartRender = function 
SegmentBarChartRender(
 
   function handleMouseDown(e: ReactMouseEvent) {
     const svg = svgRef.current;
-    if (!svg || hoveredIntervalBar) return;
+    if (!svg) return;
     e.preventDefault();
     const rect = svg.getBoundingClientRect();
     const x = e.clientX - rect.x - CHART_MARGIN.left;
     const y = e.clientY - rect.y - CHART_MARGIN.top;
     const time = baseTimeScale.invert(x);
     const action = y > innerStage.height || e.shiftKey ? 'shift' : 'select';
-    setBubbleOpenOn(undefined);
+    setBubbleInfo(undefined);
     setMouseDownAt({
       time,
       action,
     });
-    if (action === 'select') {
-      setDragging([day.floor(time, TZ_UTC), day.ceil(time, TZ_UTC)]);
-    }
   }
 
   useGlobalEventListener('mousemove', (e: MouseEvent) => {
@@ -344,37 +353,59 @@ export const SegmentBarChartRender = function 
SegmentBarChartRender(
     const x = e.clientX - rect.x - CHART_MARGIN.left;
     const y = e.clientY - rect.y - CHART_MARGIN.top;
 
-    if (!mouseDownAt) {
-      if (0 <= x && x <= innerStage.width && 0 <= y && y <= innerStage.height) 
{
+    if (mouseDownAt) {
+      e.preventDefault();
+
+      const b = baseTimeScale.invert(x);
+      if (mouseDownAt.action === 'shift' || e.shiftKey) {
+        setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf());
+      } else {
+        if (mouseDownAt.time < b) {
+          setDragging([day.floor(mouseDownAt.time, TZ_UTC), day.ceil(b, 
TZ_UTC)]);
+        } else {
+          setDragging([day.floor(b, TZ_UTC), day.ceil(mouseDownAt.time, 
TZ_UTC)]);
+        }
+      }
+    } else {
+      if (
+        0 <= x &&
+        x <= innerStage.width &&
+        0 <= y &&
+        y <= innerStage.height + CHART_MARGIN.bottom
+      ) {
+        const time = baseTimeScale.invert(x);
         const shifter =
           new Duration(trimGranularity).getCanonicalLength() > 
day.canonicalLength * 25
             ? month
             : day;
-        const time = baseTimeScale.invert(x);
         const start = shifter.floor(time, TZ_UTC);
         const end = shifter.ceil(time, TZ_UTC);
-        setBubbleOpenOn({
+
+        let intervalBars: IntervalBar[] = [];
+        if (y <= innerStage.height) {
+          const bars = intervalTree.search([
+            time.valueOf() + 1,
+            time.valueOf() + 2,
+          ]) as IntervalBar[];
+
+          if (bars.length) {
+            const stat = statScale.invert(y);
+            const hoverBar = bars.find(
+              bar =>
+                bar.offset[shownIntervalStat] <= stat &&
+                stat < bar.offset[shownIntervalStat] + 
bar.normalized[shownIntervalStat],
+            );
+            intervalBars = hoverBar ? [hoverBar] : bars;
+          }
+        }
+        setBubbleInfo({
           start,
           end,
-          x: rect.x + CHART_MARGIN.left + baseTimeScale((start.valueOf() + 
end.valueOf()) / 2),
-          y: rect.y + CHART_MARGIN.top + innerStage.height + 10,
-          text: start.toISOString().slice(0, shifter === day ? 10 : 7),
+          timeLabel: start.toISOString().slice(0, shifter === day ? 10 : 7),
+          intervalBars,
         });
       } else {
-        setBubbleOpenOn(undefined);
-      }
-      return;
-    }
-    e.preventDefault();
-
-    const b = baseTimeScale.invert(x);
-    if (mouseDownAt.action === 'shift' || e.shiftKey) {
-      setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf());
-    } else {
-      if (mouseDownAt.time < b) {
-        setDragging([day.floor(mouseDownAt.time, TZ_UTC), day.ceil(b, 
TZ_UTC)]);
-      } else {
-        setDragging([day.floor(b, TZ_UTC), day.ceil(mouseDownAt.time, 
TZ_UTC)]);
+        setBubbleInfo(undefined);
       }
     }
   });
@@ -382,19 +413,41 @@ export const SegmentBarChartRender = function 
SegmentBarChartRender(
   useGlobalEventListener('mouseup', (e: MouseEvent) => {
     if (!mouseDownAt) return;
     e.preventDefault();
-
     setMouseDownAt(undefined);
 
-    if (!shiftOffset && !dragging) return;
-    setDragging(undefined);
-    setShiftOffset(undefined);
-    if (mouseDownAt.action === 'shift' || e.shiftKey) {
-      if (shiftOffset) {
-        changeDateRange(offsetDateRange(dateRange, shiftOffset));
+    const svg = svgRef.current;
+    if (!svg) return;
+    const rect = svg.getBoundingClientRect();
+    const x = e.clientX - rect.x - CHART_MARGIN.left;
+    const y = e.clientY - rect.y - CHART_MARGIN.top;
+
+    if (shiftOffset || dragging) {
+      setDragging(undefined);
+      setShiftOffset(undefined);
+      if (mouseDownAt.action === 'shift' || e.shiftKey) {
+        if (shiftOffset) {
+          changeDateRange(offsetDateRange(dateRange, shiftOffset));
+        }
+      } else {
+        if (dragging) {
+          changeDateRange(dragging);
+        }
       }
-    } else {
-      if (dragging) {
-        changeDateRange(dragging);
+    } else if (0 <= x && x <= innerStage.width && 0 <= y && y <= 
innerStage.height) {
+      const time = baseTimeScale.invert(x);
+
+      const bars = intervalTree.search([time.valueOf() + 1, time.valueOf() + 
2]) as IntervalBar[];
+
+      if (bars.length) {
+        const stat = statScale.invert(y);
+        const hoverBar = bars.find(
+          bar =>
+            bar.offset[shownIntervalStat] <= stat &&
+            stat < bar.offset[shownIntervalStat] + 
bar.normalized[shownIntervalStat],
+        );
+        if (hoverBar) {
+          changeShownDatasource(shownDatasource ? undefined : 
hoverBar.datasource);
+        }
       }
     }
   });
@@ -432,23 +485,22 @@ export const SegmentBarChartRender = function 
SegmentBarChartRender(
   console.log('Bar chart render');
 
   let hoveredOpenOn: { x: number; y: number; text: ReactNode } | undefined;
-  if (hoveredIntervalBar && svgRef.current) {
+  if (bubbleInfo && svgRef.current) {
+    const hoveredIntervalBars = bubbleInfo.intervalBars;
+
     const rect = svgRef.current.getBoundingClientRect();
-    hoveredOpenOn = {
-      x:
-        rect.x +
-        CHART_MARGIN.left +
-        timeScale(
-          new Date((hoveredIntervalBar.start.valueOf() + 
hoveredIntervalBar.end.valueOf()) / 2),
-        ),
-      y: rect.y + CHART_MARGIN.top - 10,
-      text: (
+    let text: ReactNode;
+    if (hoveredIntervalBars.length === 0) {
+      text = bubbleInfo.timeLabel;
+    } else if (hoveredIntervalBars.length === 1) {
+      const hoveredIntervalBar = hoveredIntervalBars[0];
+      text = (
         <>
           <div>{`${formatStartDuration(
             hoveredIntervalBar.start,
             hoveredIntervalBar.originalTimeSpan,
           )}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div>
-          <div>Datasource: {hoveredIntervalBar.datasource}</div>
+          <div>{`Datasource: ${hoveredIntervalBar.datasource}`}</div>
           <div>{`Size: ${
             hoveredIntervalBar.realtime
               ? 'estimated for realtime'
@@ -457,7 +509,28 @@ export const SegmentBarChartRender = function 
SegmentBarChartRender(
           <div>{`Rows: ${formatIntervalStat('rows', 
hoveredIntervalBar.rows)}`}</div>
           <div>{`Segments: ${formatIntervalStat('segments', 
hoveredIntervalBar.segments)}`}</div>
         </>
-      ),
+      );
+    } else {
+      const datasources = uniq(hoveredIntervalBars.map(b => b.datasource));
+      const agg = aggregateSegmentStats(hoveredIntervalBars);
+      text = (
+        <>
+          <div>{bubbleInfo.timeLabel}</div>
+          <div>{`Totals for ${pluralIfNeeded(datasources.length, 
'datasource')}:`}</div>
+          <div>{`Size: ${formatIntervalStat('size', agg.size)}`}</div>
+          <div>{`Rows: ${formatIntervalStat('rows', agg.rows)}`}</div>
+          <div>{`Segments: ${formatIntervalStat('segments', 
agg.segments)}`}</div>
+        </>
+      );
+    }
+
+    hoveredOpenOn = {
+      x:
+        rect.x +
+        CHART_MARGIN.left +
+        timeScale(new Date((bubbleInfo.start.valueOf() + 
bubbleInfo.end.valueOf()) / 2)),
+      y: rect.y + CHART_MARGIN.top - 10,
+      text,
     };
   }
 
@@ -491,10 +564,7 @@ export const SegmentBarChartRender = function 
SegmentBarChartRender(
         preserveAspectRatio="xMinYMin meet"
         onMouseDown={handleMouseDown}
       >
-        <g
-          transform={`translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`}
-          onMouseLeave={() => setHoveredIntervalBar(undefined)}
-        >
+        <g transform={`translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`}>
           <ChartAxis
             className="gridline-x"
             transform="translate(0,0)"
@@ -525,10 +595,10 @@ export const SegmentBarChartRender = function 
SegmentBarChartRender(
               .tickFormat(e => formatTickRate(e.valueOf()))}
           />
           <g className="bar-group">
-            {bubbleOpenOn && (
+            {bubbleInfo && (
               <rect
                 className="hover-highlight"
-                {...startEndToXWidth(bubbleOpenOn)}
+                {...startEndToXWidth(bubbleInfo)}
                 y={0}
                 height={innerStage.height}
               />
@@ -543,24 +613,13 @@ export const SegmentBarChartRender = function 
SegmentBarChartRender(
                   className={classNames('bar-unit', { realtime: 
intervalBar.realtime })}
                   {...segmentBarToRect(intervalBar)}
                   fill={getDatasourceColor(intervalBar.datasource)}
-                  onClick={() => changeShownDatasource(intervalBar.datasource)}
-                  onMouseOver={() => {
-                    if (mouseDownAt) return;
-                    setHoveredIntervalBar(intervalBar);
-                  }}
                 />
               );
             })}
-            {hoveredIntervalBar && (
-              <rect
-                className="hovered-bar"
-                {...segmentBarToRect(hoveredIntervalBar)}
-                onClick={() => {
-                  setHoveredIntervalBar(undefined);
-                  changeShownDatasource(hoveredIntervalBar.datasource);
-                }}
-              />
-            )}
+            {bubbleInfo &&
+              bubbleInfo.intervalBars.map((intervalBar, i) => (
+                <rect key={i} className="hovered-bar" 
{...segmentBarToRect(intervalBar)} />
+              ))}
             {dragging && (
               <rect
                 className="selection"
@@ -598,7 +657,6 @@ export const SegmentBarChartRender = function 
SegmentBarChartRender(
         </div>
       )}
       <PortalBubble openOn={hoveredOpenOn} mute direction="up" />
-      <PortalBubble openOn={bubbleOpenOn} mute direction="down" />
     </div>
   );
 };
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx 
b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 44d84cdc7d6..ae8cde2a9a7 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -350,9 +350,7 @@ export const SegmentTimeline = function 
SegmentTimeline(props: SegmentTimelinePr
               changeDateRange={setDateRange}
               shownIntervalStat={activeSegmentStat}
               shownDatasource={shownDatasource}
-              changeShownDatasource={(datasource: string | undefined) =>
-                setShownDatasource(shownDatasource ? undefined : datasource)
-              }
+              changeShownDatasource={setShownDatasource}
             />
           )}
           {initDatasourceDateRangeState.isLoading() && <Loader />}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to