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]
