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 289d95c7d09ab82c89f78ba5364a8b16ca855bf5
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Fri Oct 25 15:03:53 2024 -0700

    check in
---
 .../src/components/rule-editor/rule-editor.tsx     |   4 +-
 .../src/components/segment-timeline/bar-group.tsx  |  70 ---
 .../src/components/segment-timeline/common.ts      |  34 +-
 ...ar-chart.scss => segment-bar-chart-render.scss} |   8 +-
 .../segment-timeline/segment-bar-chart-render.tsx  | 155 +++++
 .../segment-timeline/segment-bar-chart.scss}       |  10 +-
 .../segment-timeline/segment-bar-chart.tsx         | 194 ++++++
 .../segment-timeline/segment-timeline.scss         |  12 +-
 .../segment-timeline/segment-timeline.spec.tsx     |  25 -
 .../segment-timeline/segment-timeline.tsx          | 685 ++++-----------------
 .../segment-timeline/stacked-bar-chart.tsx         | 148 -----
 .../dialogs/retention-dialog/retention-dialog.tsx  |   2 +-
 web-console/src/druid-models/index.ts              |   1 +
 .../{utils => druid-models/load-rule}/load-rule.ts |   2 +-
 .../{views/explore-view/models => utils}/stage.ts  |  14 +
 .../views/datasources-view/datasources-view.tsx    |   5 +-
 .../components/module-pane/module-pane.tsx         |   3 +-
 web-console/src/views/explore-view/models/index.ts |   1 -
 .../module-repository/module-repository.ts         |   3 +-
 19 files changed, 520 insertions(+), 856 deletions(-)

diff --git a/web-console/src/components/rule-editor/rule-editor.tsx 
b/web-console/src/components/rule-editor/rule-editor.tsx
index c5dfb24ddf0..0181e4341ba 100644
--- a/web-console/src/components/rule-editor/rule-editor.tsx
+++ b/web-console/src/components/rule-editor/rule-editor.tsx
@@ -30,9 +30,9 @@ import {
 import { IconNames } from '@blueprintjs/icons';
 import React, { useState } from 'react';
 
+import type { Rule } from '../../druid-models';
+import { RuleUtil } from '../../druid-models';
 import { durationSanitizer } from '../../utils';
-import type { Rule } from '../../utils/load-rule';
-import { RuleUtil } from '../../utils/load-rule';
 import { SuggestibleInput } from '../suggestible-input/suggestible-input';
 
 import './rule-editor.scss';
diff --git a/web-console/src/components/segment-timeline/bar-group.tsx 
b/web-console/src/components/segment-timeline/bar-group.tsx
deleted file mode 100644
index 3c61859115a..00000000000
--- a/web-console/src/components/segment-timeline/bar-group.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import type { AxisScale } from 'd3-axis';
-import React from 'react';
-
-import type { BarUnitData, HoveredBarInfo } from './common';
-
-interface BarGroupProps {
-  dataToRender: BarUnitData[];
-  changeActiveDatasource: (dataSource: string) => void;
-  xScale: AxisScale<Date>;
-  yScale: AxisScale<number>;
-  barWidth: number;
-  onHoverBar: (e: HoveredBarInfo) => void;
-}
-
-export class BarGroup extends React.Component<BarGroupProps> {
-  render() {
-    const { dataToRender, changeActiveDatasource, xScale, yScale, onHoverBar, 
barWidth } =
-      this.props;
-    if (dataToRender === undefined) return null;
-
-    return dataToRender.map((entry: BarUnitData, i: number) => {
-      const y0 = yScale(entry.y0 || 0) || 0;
-      const x = xScale(new Date(entry.x + 'T00:00:00Z'));
-      if (typeof x === 'undefined') return;
-
-      const y = yScale((entry.y0 || 0) + entry.y) || 0;
-      const height = Math.max(y0 - y, 0);
-      const barInfo: HoveredBarInfo = {
-        xCoordinate: x,
-        yCoordinate: y,
-        height,
-        datasource: entry.datasource,
-        xValue: entry.x,
-        yValue: entry.y,
-        dailySize: entry.dailySize,
-      };
-      return (
-        <rect
-          key={i}
-          className="bar-unit"
-          x={x}
-          y={y}
-          width={barWidth}
-          height={height}
-          style={{ fill: entry.color }}
-          onClick={() => changeActiveDatasource(entry.datasource)}
-          onMouseOver={() => onHoverBar(barInfo)}
-        />
-      );
-    });
-  }
-}
diff --git a/web-console/src/components/segment-timeline/common.ts 
b/web-console/src/components/segment-timeline/common.ts
index 9add957e2c1..10a29751721 100644
--- a/web-console/src/components/segment-timeline/common.ts
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -16,31 +16,19 @@
  * limitations under the License.
  */
 
-export interface BarUnitData {
-  x: number;
-  y: number;
-  y0?: number;
-  width: number;
-  datasource: string;
-  color: string;
-  dailySize: number;
-}
+export type SegmentStat = 'count' | 'size' | 'rows';
 
-export interface Margin {
-  top: number;
-  right: number;
-  bottom: number;
-  left: number;
+export interface SegmentRow extends Record<SegmentStat, number> {
+  start: string;
+  end: string;
+  datasource?: string;
 }
 
-export interface HoveredBarInfo {
-  xCoordinate: number;
-  yCoordinate: number;
-  height: number;
-  datasource: string;
-  xValue: number;
-  yValue: number;
-  dailySize: number;
+export interface SegmentBar extends SegmentRow {
+  startDate: Date;
+  endDate: Date;
 }
 
-export type SegmentStat = 'sizeData' | 'countData';
+export interface StackedSegmentBar extends SegmentBar {
+  offset: Record<SegmentStat, number>;
+}
diff --git a/web-console/src/components/segment-timeline/stacked-bar-chart.scss 
b/web-console/src/components/segment-timeline/segment-bar-chart-render.scss
similarity index 92%
rename from web-console/src/components/segment-timeline/stacked-bar-chart.scss
rename to 
web-console/src/components/segment-timeline/segment-bar-chart-render.scss
index 26e5f5186b5..629499e13db 100644
--- a/web-console/src/components/segment-timeline/stacked-bar-chart.scss
+++ b/web-console/src/components/segment-timeline/segment-bar-chart-render.scss
@@ -16,13 +16,13 @@
  * limitations under the License.
  */
 
-.stacked-bar-chart {
+.segment-bar-chart-render {
   position: relative;
   overflow: hidden;
 
   .bar-chart-tooltip {
     position: absolute;
-    left: 100px;
+    left: 20px;
     right: 0;
 
     div {
@@ -34,6 +34,10 @@
   svg {
     position: absolute;
 
+    .chart-axis text {
+      user-select: none;
+    }
+
     .hovered-bar {
       fill: transparent;
       stroke: #ffffff;
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
new file mode 100644
index 00000000000..41823b50b0a
--- /dev/null
+++ b/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { NonNullDateRange } from '@blueprintjs/datetime';
+import { max } from 'd3-array';
+import type { AxisScale } from 'd3-axis';
+import { axisBottom, axisLeft } from 'd3-axis';
+import { scaleLinear, scaleUtc } from 'd3-scale';
+import { useState } from 'react';
+
+import { formatBytes, formatInteger } from '../../utils';
+import type { Margin, Stage } from '../../utils/stage';
+
+import { ChartAxis } from './chart-axis';
+import type { SegmentBar, SegmentStat } from './common';
+
+import './segment-bar-chart-render.scss';
+
+const CHART_MARGIN: Margin = { top: 40, right: 5, bottom: 20, left: 60 };
+
+interface SegmentBarChartRenderProps {
+  stage: Stage;
+  dateRange: NonNullDateRange;
+  shownSegmentStat: SegmentStat;
+  segmentBars: SegmentBar[];
+  changeActiveDatasource(datasource: string | undefined): void;
+}
+
+export const SegmentBarChartRender = function SegmentBarChartRender(
+  props: SegmentBarChartRenderProps,
+) {
+  const { stage, shownSegmentStat, dateRange, segmentBars, 
changeActiveDatasource } = props;
+  const [hoverOn, setHoverOn] = useState<SegmentBar>();
+
+  const innerStage = stage.applyMargin(CHART_MARGIN);
+
+  const timeScale: AxisScale<Date> = scaleUtc().domain(dateRange).range([0, 
innerStage.width]);
+
+  const maxStat = max(segmentBars, d => d[shownSegmentStat]);
+  const statScale: AxisScale<number> = scaleLinear()
+    .rangeRound([innerStage.height, 0])
+    .domain([0, maxStat ?? 1]);
+
+  const formatTick = (n: number) => {
+    if (isNaN(n)) return '';
+    if (shownSegmentStat === 'count') {
+      return formatInteger(n);
+    } else {
+      return formatBytes(n);
+    }
+  };
+
+  function segmentBarToRect(segmentBar: SegmentBar) {
+    const y0 = statScale(0)!; // segmentBar.y0 ||
+    const xStart = timeScale(segmentBar.startDate)!;
+    const xEnd = timeScale(segmentBar.endDate)!;
+    const y = statScale(segmentBar[shownSegmentStat]) || 0;
+
+    return {
+      x: xStart,
+      y: y,
+      width: Math.max(xEnd - xStart, 1),
+      height: Math.abs(y0 - y),
+    };
+  }
+
+  return (
+    <div className="segment-bar-chart-render">
+      {hoverOn && (
+        <div className="bar-chart-tooltip">
+          <div>Datasource: {hoverOn.datasource}</div>
+          <div>Time: {hoverOn.start}</div>
+          <div>
+            {`${shownSegmentStat === 'count' ? 'Count' : 'Size'}: ${formatTick(
+              hoverOn[shownSegmentStat],
+            )}`}
+          </div>
+        </div>
+      )}
+      <svg
+        width={stage.width}
+        height={stage.height}
+        viewBox={`0 0 ${stage.width} ${stage.height}`}
+        preserveAspectRatio="xMinYMin meet"
+      >
+        <g
+          transform={`translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`}
+          onMouseLeave={() => setHoverOn(undefined)}
+        >
+          <ChartAxis
+            className="gridline-x"
+            transform="translate(0,0)"
+            axis={axisLeft(statScale)
+              .ticks(5)
+              .tickSize(-innerStage.width)
+              .tickFormat(() => '')
+              .tickSizeOuter(0)}
+          />
+          <ChartAxis
+            className="axis-x"
+            transform={`translate(0,${innerStage.height})`}
+            axis={axisBottom(timeScale)}
+          />
+          <ChartAxis
+            className="axis-y"
+            axis={axisLeft(statScale)
+              .ticks(5)
+              .tickFormat(e => formatTick(e))}
+          />
+          {segmentBars.map((segmentBar, i) => {
+            return (
+              <rect
+                key={i}
+                className="bar-unit"
+                {...segmentBarToRect(segmentBar)}
+                style={{ fill: i % 2 ? 'red' : 'blue' }}
+                onClick={
+                  segmentBar.datasource
+                    ? () => changeActiveDatasource(segmentBar.datasource)
+                    : undefined
+                }
+                onMouseOver={() => setHoverOn(segmentBar)}
+              />
+            );
+          })}
+          {hoverOn && (
+            <rect
+              className="hovered-bar"
+              {...segmentBarToRect(hoverOn)}
+              onClick={() => {
+                setHoverOn(undefined);
+                changeActiveDatasource(hoverOn.datasource);
+              }}
+            />
+          )}
+        </g>
+      </svg>
+    </div>
+  );
+};
diff --git a/web-console/src/views/explore-view/models/index.ts 
b/web-console/src/components/segment-timeline/segment-bar-chart.scss
similarity index 79%
copy from web-console/src/views/explore-view/models/index.ts
copy to web-console/src/components/segment-timeline/segment-bar-chart.scss
index 1e3c58ea09f..130997119e3 100644
--- a/web-console/src/views/explore-view/models/index.ts
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.scss
@@ -16,10 +16,6 @@
  * limitations under the License.
  */
 
-export * from './expression-meta';
-export * from './highlight';
-export * from './measure';
-export * from './measure-pattern';
-export * from './parameter';
-export * from './query-source';
-export * from './stage';
+.segment-bar-chart {
+  position: relative;
+}
diff --git a/web-console/src/components/segment-timeline/segment-bar-chart.tsx 
b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
new file mode 100644
index 00000000000..a6468ac3d4c
--- /dev/null
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { NonNullDateRange } from '@blueprintjs/datetime';
+import { C, F, N, sql, SqlQuery } from '@druid-toolkit/query';
+import { sum } from 'd3-array';
+import { useMemo } from 'react';
+
+import type { Capabilities } from '../../helpers';
+import { useQueryManager } from '../../hooks';
+import { Api } from '../../singletons';
+import { filterMap, groupBy, queryDruidSql } from '../../utils';
+import type { Stage } from '../../utils/stage';
+import { Loader } from '../loader/loader';
+
+import type { SegmentBar, SegmentRow, SegmentStat } from './common';
+import { SegmentBarChartRender } from './segment-bar-chart-render';
+
+import './segment-bar-chart.scss';
+
+type TrimDuration = 'PT1H' | 'P1D' | 'P1M' | 'P1Y';
+
+function trimUtcDate(date: string, duration: TrimDuration): string {
+  // date like 2024-09-26T00:00:00.000Z
+  switch (duration) {
+    case 'PT1H':
+      return date.substring(0, 13) + ':00:00Z';
+
+    case 'P1D':
+      return date.substring(0, 10) + 'T00:00:00Z';
+
+    case 'P1M':
+      return date.substring(0, 7) + '-01T00:00:00Z';
+
+    case 'P1Y':
+      return date.substring(0, 4) + '-01-01T00:00:00Z';
+
+    default:
+      throw new Error(`Unexpected duration: ${duration}`);
+  }
+}
+
+interface SegmentBarChartProps {
+  capabilities: Capabilities;
+  stage: Stage;
+  dateRange: NonNullDateRange;
+  breakByDataSource: boolean;
+  shownSegmentStat: SegmentStat;
+  changeActiveDatasource: (datasource: string | undefined) => void;
+}
+
+export const SegmentBarChart = function SegmentBarChart(props: 
SegmentBarChartProps) {
+  const {
+    capabilities,
+    dateRange,
+    breakByDataSource,
+    stage,
+    shownSegmentStat,
+    changeActiveDatasource,
+  } = props;
+
+  const intervalsQuery = useMemo(
+    () => ({ capabilities, dateRange, breakByDataSource }),
+    [capabilities, dateRange, breakByDataSource],
+  );
+
+  const [segmentRowsState] = useQueryManager({
+    query: intervalsQuery,
+    processQuery: async ({ capabilities, dateRange, breakByDataSource }, 
cancelToken) => {
+      let segmentRows: SegmentRow[];
+      if (capabilities.hasSql()) {
+        const query = SqlQuery.from(N('sys').table('segments'))
+          .changeWhereExpression(
+            sql`'${dateRange[0].toISOString()}' <= "start" AND "end" <= 
'${dateRange[1].toISOString()}' AND is_published = 1 AND is_overshadowed = 0`,
+          )
+          .addSelect(C('start'), { addToGroupBy: 'end' })
+          .addSelect(C('end'), { addToGroupBy: 'end' })
+          .applyIf(breakByDataSource, q => q.addSelect(C('datasource'), { 
addToGroupBy: 'end' }))
+          .addSelect(F.count().as('count'))
+          .addSelect(F.sum(C('size')).as('size'))
+          .addSelect(F.sum(C('num_rows')).as('rows'));
+
+        segmentRows = await queryDruidSql({ query: query.toString() });
+      } else {
+        const datasources: string[] = (
+          await Api.instance.get(`/druid/coordinator/v1/datasources`, { 
cancelToken })
+        ).data;
+        segmentRows = (
+          await Promise.all(
+            datasources.map(async datasource => {
+              const intervalMap = (
+                await Api.instance.get(
+                  `/druid/coordinator/v1/datasources/${Api.encodePath(
+                    datasource,
+                  )}/intervals?simple`,
+                  { cancelToken },
+                )
+              ).data;
+
+              return filterMap(Object.entries(intervalMap), ([interval, v]) => 
{
+                // ToDo: Filter on start end
+                const [start, end] = interval.split('/');
+                const { count, size, rows } = v as any;
+                return {
+                  start,
+                  end,
+                  datasource: breakByDataSource ? datasource : undefined,
+                  count,
+                  size,
+                  rows,
+                };
+              });
+            }),
+          )
+        ).flat();
+      }
+
+      const trimDuration: TrimDuration = 'P1D';
+      return groupBy(
+        segmentRows,
+        segmentRow =>
+          // This trimming should ideally be pushed into the SQL query but at 
the time of this writing queries on the sys.* tables do not allow substring
+          `${trimUtcDate(segmentRow.start, trimDuration)}/${trimUtcDate(
+            segmentRow.end,
+            trimDuration,
+          )}/${segmentRow.datasource || ''}`,
+        (segmentRows): SegmentBar => {
+          const firstRow = segmentRows[0];
+          const start = trimUtcDate(firstRow.start, trimDuration);
+          const end = trimUtcDate(firstRow.end, trimDuration);
+          return {
+            ...firstRow,
+            start,
+            startDate: new Date(start),
+            end,
+            endDate: new Date(end),
+            count: sum(segmentRows, s => s.count),
+            size: sum(segmentRows, s => s.size),
+            rows: sum(segmentRows, s => s.rows),
+          };
+        },
+      );
+    },
+  });
+
+  if (segmentRowsState.loading) {
+    return <Loader />;
+  }
+
+  if (segmentRowsState.error) {
+    return (
+      <div className="empty-placeholder">
+        <span className="no-data-text">{`Error when loading data: 
${segmentRowsState.getErrorMessage()}`}</span>
+      </div>
+    );
+  }
+
+  const segmentRows = segmentRowsState.data;
+  if (!segmentRows) return null;
+
+  if (!segmentRows.length) {
+    return (
+      <div className="empty-placeholder">
+        <span className="no-data-text">There are no segments for the selected 
interval</span>
+      </div>
+    );
+  }
+
+  console.log(segmentRows);
+  return (
+    <SegmentBarChartRender
+      stage={stage}
+      dateRange={dateRange}
+      shownSegmentStat={shownSegmentStat}
+      segmentBars={segmentRows}
+      changeActiveDatasource={changeActiveDatasource}
+    />
+  );
+};
diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss 
b/web-console/src/components/segment-timeline/segment-timeline.scss
index 6239e717ae8..583ce290ac9 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.scss
+++ b/web-console/src/components/segment-timeline/segment-timeline.scss
@@ -32,7 +32,17 @@
     padding: 10px;
   }
 
-  .stacked-bar-chart {
+  .chart-container {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+  }
+
+  .segment-bar-chart,
+  .segment-bar-chart-render {
+    position: absolute;
+    width: 100%;
     height: 100%;
   }
 
diff --git 
a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx 
b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
index 4f95842801a..f247f98794a 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
@@ -16,38 +16,13 @@
  * limitations under the License.
  */
 
-import { sane } from '@druid-toolkit/query';
 import { render } from '@testing-library/react';
 
 import { Capabilities } from '../../helpers';
 
 import { SegmentTimeline } from './segment-timeline';
 
-jest.useFakeTimers('modern').setSystemTime(Date.parse('2021-06-08T12:34:56Z'));
-
 describe('SegmentTimeline', () => {
-  it('.getSqlQuery', () => {
-    expect(
-      SegmentTimeline.getSqlQuery([
-        new Date('2020-01-01T00:00:00Z'),
-        new Date('2021-02-01T00:00:00Z'),
-      ]),
-    ).toEqual(sane`
-      SELECT
-        "start", "end", "datasource",
-        COUNT(*) AS "count",
-        SUM("size") AS "size"
-      FROM sys.segments
-      WHERE
-        '2020-01-01T00:00:00.000Z' <= "start" AND
-        "end" <= '2021-02-01T00:00:00.000Z' AND
-        is_published = 1 AND
-        is_overshadowed = 0
-      GROUP BY 1, 2, 3
-      ORDER BY "start" DESC
-    `);
-  });
-
   it('matches snapshot', () => {
     const segmentTimeline = <SegmentTimeline capabilities={Capabilities.FULL} 
/>;
     const { container } = render(segmentTimeline);
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx 
b/web-console/src/components/segment-timeline/segment-timeline.tsx
index edb7dfcf60f..df4e1c12960 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -16,40 +16,27 @@
  * limitations under the License.
  */
 
-import {
-  Button,
-  FormGroup,
-  MenuItem,
-  ResizeSensor,
-  SegmentedControl,
-  Tag,
-} from '@blueprintjs/core';
-import type { DateRange, NonNullDateRange } from '@blueprintjs/datetime';
+import { Button, FormGroup, MenuItem, ResizeSensor, SegmentedControl } from 
'@blueprintjs/core';
+import type { NonNullDateRange } from '@blueprintjs/datetime';
 import { DateRangeInput3 } from '@blueprintjs/datetime2';
 import { IconNames } from '@blueprintjs/icons';
-import type { ItemPredicate, ItemRenderer } from '@blueprintjs/select';
 import { Select } from '@blueprintjs/select';
-import type { AxisScale } from 'd3-axis';
-import { scaleLinear, scaleUtc } from 'd3-scale';
 import enUS from 'date-fns/locale/en-US';
-import React from 'react';
+import type React from 'react';
+import { useState } from 'react';
 
 import type { Capabilities } from '../../helpers';
-import { Api } from '../../singletons';
 import {
   ceilToUtcDay,
   isNonNullRange,
   localToUtcDateRange,
-  queryDruidSql,
-  QueryManager,
-  uniq,
   utcToLocalDateRange,
 } from '../../utils';
-import { Loader } from '../loader/loader';
+import { Stage } from '../../utils/stage';
 import { SplitterLayout } from '../splitter-layout/splitter-layout';
 
-import type { BarUnitData, SegmentStat } from './common';
-import { StackedBarChart } from './stacked-bar-chart';
+import type { SegmentStat } from './common';
+import { SegmentBarChart } from './segment-bar-chart';
 
 import './segment-timeline.scss';
 
@@ -57,37 +44,6 @@ interface SegmentTimelineProps {
   capabilities: Capabilities;
 }
 
-interface SegmentTimelineState {
-  chartHeight: number;
-  chartWidth: number;
-  data?: Record<string, any>;
-  datasources: string[];
-  stackedData?: Record<string, BarUnitData[]>;
-  singleDatasourceData?: Record<string, Record<string, BarUnitData[]>>;
-  activeDatasource?: string;
-  activeSegmentStat: SegmentStat;
-  dataToRender: BarUnitData[];
-  loading: boolean;
-  error?: Error;
-  xScale: AxisScale<Date> | null;
-  yScale: AxisScale<number> | null;
-  dateRange: NonNullDateRange;
-  selectedDateRange?: DateRange;
-}
-
-interface BarChartScales {
-  xScale: AxisScale<Date>;
-  yScale: AxisScale<number>;
-}
-
-interface IntervalRow {
-  start: string;
-  end: string;
-  datasource: string;
-  count: number;
-  size: number;
-}
-
 const DEFAULT_TIME_SPAN_MONTHS = 3;
 
 function getDefaultDateRange(): NonNullDateRange {
@@ -97,533 +53,122 @@ function getDefaultDateRange(): NonNullDateRange {
   return [start, end];
 }
 
-export class SegmentTimeline extends React.PureComponent<
-  SegmentTimelineProps,
-  SegmentTimelineState
-> {
-  static COLORS = [
-    '#b33040',
-    '#d25c4d',
-    '#f2b447',
-    '#d9d574',
-    '#4FAA7E',
-    '#57ceff',
-    '#789113',
-    '#098777',
-    '#b33040',
-    '#d2757b',
-    '#f29063',
-    '#d9a241',
-    '#80aa61',
-    '#c4ff9e',
-    '#915412',
-    '#87606c',
-  ];
-
-  static getColor(index: number): string {
-    return SegmentTimeline.COLORS[index % SegmentTimeline.COLORS.length];
-  }
-
-  static getSqlQuery(dateRange: NonNullDateRange): string {
-    return `SELECT
-  "start", "end", "datasource",
-  COUNT(*) AS "count",
-  SUM("size") AS "size"
-FROM sys.segments
-WHERE
-  '${dateRange[0].toISOString()}' <= "start" AND
-  "end" <= '${dateRange[1].toISOString()}' AND
-  is_published = 1 AND
-  is_overshadowed = 0
-GROUP BY 1, 2, 3
-ORDER BY "start" DESC`;
-  }
-
-  static processRawData(data: IntervalRow[]) {
-    if (data === null) return [];
-
-    const countData: Record<string, any> = {};
-    const sizeData: Record<string, any> = {};
-    data.forEach(entry => {
-      const start = entry.start;
-      const day = start.split('T')[0];
-      const datasource = entry.datasource;
-      const count = entry.count;
-      const segmentSize = entry.size;
-      if (countData[day] === undefined) {
-        countData[day] = {
-          day,
-          [datasource]: count,
-          total: count,
-        };
-        sizeData[day] = {
-          day,
-          [datasource]: segmentSize,
-          total: segmentSize,
-        };
-      } else {
-        const countDataEntry: number | undefined = countData[day][datasource];
-        countData[day][datasource] = count + (countDataEntry === undefined ? 0 
: countDataEntry);
-        const sizeDataEntry: number | undefined = sizeData[day][datasource];
-        sizeData[day][datasource] = segmentSize + (sizeDataEntry === undefined 
? 0 : sizeDataEntry);
-        countData[day].total += count;
-        sizeData[day].total += segmentSize;
-      }
-    });
-
-    const countDataArray = Object.keys(countData)
-      .reverse()
-      .map((time: any) => {
-        return countData[time];
-      });
-
-    const sizeDataArray = Object.keys(sizeData)
-      .reverse()
-      .map((time: any) => {
-        return sizeData[time];
-      });
-
-    return { countData: countDataArray, sizeData: sizeDataArray };
-  }
-
-  static calculateStackedData(
-    data: Record<string, any>,
-    datasources: string[],
-  ): Record<string, BarUnitData[]> {
-    const newStackedData: Record<string, BarUnitData[]> = {};
-    Object.keys(data).forEach((type: any) => {
-      const stackedData: any = data[type].map((d: any) => {
-        let y0 = 0;
-        return datasources.map((datasource: string, i) => {
-          const barUnitData = {
-            x: d.day,
-            y: d[datasource] === undefined ? 0 : d[datasource],
-            y0,
-            datasource,
-            color: SegmentTimeline.getColor(i),
-            dailySize: d.total,
-          };
-          y0 += d[datasource] === undefined ? 0 : d[datasource];
-          return barUnitData;
-        });
-      });
-      newStackedData[type] = stackedData.flat();
-    });
-
-    return newStackedData;
-  }
-
-  static calculateSingleDatasourceData(
-    data: Record<string, any>,
-    datasources: string[],
-  ): Record<string, Record<string, BarUnitData[]>> {
-    const singleDatasourceData: Record<string, Record<string, BarUnitData[]>> 
= {};
-    Object.keys(data).forEach(dataType => {
-      singleDatasourceData[dataType] = {};
-      datasources.forEach((datasource, i) => {
-        const currentData = data[dataType];
-        if (currentData.length === 0) return;
-        const dataResult = currentData.map((d: any) => {
-          let y = 0;
-          if (d[datasource] !== undefined) {
-            y = d[datasource];
-          }
-          return {
-            x: d.day,
-            y,
-            datasource,
-            color: SegmentTimeline.getColor(i),
-            dailySize: d.total,
-          };
-        });
-        if (!dataResult.every((d: any) => d.y === 0)) {
-          singleDatasourceData[dataType][datasource] = dataResult;
-        }
-      });
-    });
-
-    return singleDatasourceData;
-  }
-
-  private readonly dataQueryManager: QueryManager<
-    { capabilities: Capabilities; dateRange: NonNullDateRange },
-    any
-  >;
-
-  private readonly chartMargin = { top: 40, right: 0, bottom: 20, left: 60 };
-
-  constructor(props: SegmentTimelineProps) {
-    super(props);
-    const dateRange = getDefaultDateRange();
-
-    this.state = {
-      chartWidth: 1, // Dummy init values to be replaced
-      chartHeight: 1, // after first render
-      data: {},
-      datasources: [],
-      stackedData: {},
-      singleDatasourceData: {},
-      dataToRender: [],
-      activeSegmentStat: 'sizeData',
-      loading: true,
-      xScale: null,
-      yScale: null,
-      dateRange,
-    };
-
-    this.dataQueryManager = new QueryManager({
-      processQuery: async ({ capabilities, dateRange }, cancelToken) => {
-        let intervals: IntervalRow[];
-        let datasources: string[];
-        if (capabilities.hasSql()) {
-          intervals = await queryDruidSql(
-            {
-              query: SegmentTimeline.getSqlQuery(dateRange),
-            },
-            cancelToken,
-          );
-          datasources = uniq(intervals.map(r => r.datasource).sort());
-        } else if (capabilities.hasCoordinatorAccess()) {
-          const startIso = dateRange[0].toISOString();
-
-          datasources = (
-            await Api.instance.get(`/druid/coordinator/v1/datasources`, { 
cancelToken })
-          ).data;
-          intervals = (
-            await Promise.all(
-              datasources.map(async datasource => {
-                const intervalMap = (
-                  await Api.instance.get(
-                    `/druid/coordinator/v1/datasources/${Api.encodePath(
-                      datasource,
-                    )}/intervals?simple`,
-                    { cancelToken },
-                  )
-                ).data;
-
-                return Object.keys(intervalMap)
-                  .map(interval => {
-                    const [start, end] = interval.split('/');
-                    const { count, size } = intervalMap[interval];
-                    return {
-                      start,
-                      end,
-                      datasource,
-                      count,
-                      size,
-                    };
-                  })
-                  .filter(a => startIso < a.start);
-              }),
-            )
-          )
-            .flat()
-            .sort((a, b) => b.start.localeCompare(a.start));
-        } else {
-          throw new Error(`must have SQL or coordinator access`);
-        }
-
-        const data = SegmentTimeline.processRawData(intervals);
-        const stackedData = SegmentTimeline.calculateStackedData(data, 
datasources);
-        const singleDatasourceData = 
SegmentTimeline.calculateSingleDatasourceData(
-          data,
-          datasources,
-        );
-        return { data, datasources, stackedData, singleDatasourceData };
-      },
-      onStateChange: ({ data, loading, error }) => {
-        this.setState({
-          data: data ? data.data : undefined,
-          datasources: data ? data.datasources : [],
-          stackedData: data ? data.stackedData : undefined,
-          singleDatasourceData: data ? data.singleDatasourceData : undefined,
-          loading,
-          error,
-        });
-      },
-    });
-  }
-
-  componentDidMount(): void {
-    const { capabilities } = this.props;
-    const { dateRange } = this.state;
-
-    if (isNonNullRange(dateRange)) {
-      this.dataQueryManager.runQuery({ capabilities, dateRange });
-    }
-  }
-
-  componentWillUnmount(): void {
-    this.dataQueryManager.terminate();
-  }
-
-  componentDidUpdate(_prevProps: SegmentTimelineProps, prevState: 
SegmentTimelineState): void {
-    const { activeDatasource, activeSegmentStat, singleDatasourceData, 
stackedData } = this.state;
-    if (
-      prevState.data !== this.state.data ||
-      prevState.activeSegmentStat !== this.state.activeSegmentStat ||
-      prevState.activeDatasource !== this.state.activeDatasource ||
-      prevState.chartWidth !== this.state.chartWidth ||
-      prevState.chartHeight !== this.state.chartHeight
-    ) {
-      const scales: BarChartScales | undefined = this.calculateScales();
-      const dataToRender: BarUnitData[] | undefined = activeDatasource
-        ? singleDatasourceData
-          ? singleDatasourceData[activeSegmentStat][activeDatasource]
-          : undefined
-        : stackedData
-        ? stackedData[activeSegmentStat]
-        : undefined;
-
-      if (scales && dataToRender) {
-        this.setState({
-          dataToRender,
-          xScale: scales.xScale,
-          yScale: scales.yScale,
-        });
-      }
-    }
-  }
-
-  private calculateScales(): BarChartScales | undefined {
-    const {
-      chartWidth,
-      chartHeight,
-      data,
-      activeSegmentStat,
-      activeDatasource,
-      singleDatasourceData,
-      dateRange,
-    } = this.state;
-    if (!data || !Object.keys(data).length || !isNonNullRange(dateRange)) 
return;
-    const activeData = data[activeSegmentStat];
-
-    let yMax =
-      activeData.length === 0
-        ? 100
-        : activeData.reduce((max: any, d: any) => (max.total > d.total ? max : 
d)).total;
-
-    if (
-      activeDatasource &&
-      singleDatasourceData![activeSegmentStat][activeDatasource] !== undefined
-    ) {
-      yMax = 
singleDatasourceData![activeSegmentStat][activeDatasource].reduce((max: any, d: 
any) =>
-        max.y > d.y ? max : d,
-      ).y;
-    }
-
-    const xScale: AxisScale<Date> = scaleUtc()
-      .domain(dateRange)
-      .range([0, chartWidth - this.chartMargin.left - this.chartMargin.right]);
-
-    const yScale: AxisScale<number> = scaleLinear()
-      .rangeRound([chartHeight - this.chartMargin.top - 
this.chartMargin.bottom, 0])
-      .domain([0, yMax]);
+export const SegmentTimeline = function SegmentTimeline(props: 
SegmentTimelineProps) {
+  const { capabilities } = props;
+  const [stage, setStage] = useState<Stage | undefined>();
+  const [activeSegmentStat, setActiveSegmentStat] = 
useState<SegmentStat>('size');
+  const [activeDatasource, setActiveDatasource] = useState<string | 
undefined>();
+  const [dateRange, setDateRange] = 
useState<NonNullDateRange>(getDefaultDateRange);
 
-    return {
-      xScale,
-      yScale,
-    };
-  }
+  const datasources: string[] = ['wiki', 'kttm'];
 
-  private readonly handleResize = (entries: ResizeObserverEntry[]) => {
-    const chartRect = entries[0].contentRect;
-    this.setState({
-      chartWidth: chartRect.width,
-      chartHeight: chartRect.height,
-    });
-  };
-
-  renderStackedBarChart() {
-    const {
-      chartWidth,
-      chartHeight,
-      loading,
-      dataToRender,
-      activeSegmentStat,
-      error,
-      xScale,
-      yScale,
-      data,
-      activeDatasource,
-      dateRange,
-    } = this.state;
-
-    if (loading) {
-      return <Loader />;
-    }
-
-    if (error) {
-      return (
-        <div className="empty-placeholder">
-          <span className="no-data-text">Error when loading data: 
{error.message}</span>
-        </div>
-      );
-    }
-
-    if (xScale === null || yScale === null) {
-      return (
-        <div className="empty-placeholder">
-          <span className="no-data-text">Error when calculating scales</span>
-        </div>
-      );
-    }
-
-    if (data![activeSegmentStat].length === 0) {
-      return (
-        <div className="empty-placeholder">
-          <span className="no-data-text">There are no segments for the 
selected interval</span>
-        </div>
-      );
-    }
-
-    if (
-      activeDatasource &&
-      data![activeSegmentStat].every((d: any) => d[activeDatasource] === 
undefined)
-    ) {
-      return (
-        <div className="empty-placeholder">
-          <span className="no-data-text">
-            No data available for <Tag minimal>{activeDatasource}</Tag>
-          </span>
-        </div>
-      );
-    }
-
-    const millisecondsPerDay = 24 * 60 * 60 * 1000;
-    const barCounts = (dateRange[1].getTime() - dateRange[0].getTime()) / 
millisecondsPerDay;
-    const barWidth = Math.max(
-      0,
-      (chartWidth - this.chartMargin.left - this.chartMargin.right) / 
barCounts,
-    );
+  const DatasourceSelect: React.FC = () => {
+    const showAll = 'Show all';
+    const datasourcesWzAll = [showAll].concat(datasources);
     return (
-      <ResizeSensor onResize={this.handleResize}>
-        <StackedBarChart
-          dataToRender={dataToRender}
-          svgHeight={chartHeight}
-          svgWidth={chartWidth}
-          margin={this.chartMargin}
-          changeActiveDatasource={(datasource: string | undefined) =>
-            this.setState(prevState => ({
-              activeDatasource: prevState.activeDatasource ? undefined : 
datasource,
-            }))
+      <Select<string>
+        items={datasourcesWzAll}
+        onItemSelect={(selectedItem: string) => {
+          setActiveDatasource(selectedItem === showAll ? undefined : 
selectedItem);
+        }}
+        itemRenderer={(val, { handleClick, handleFocus, modifiers }) => {
+          if (!modifiers.matchesPredicate) return null;
+          return (
+            <MenuItem
+              key={val}
+              disabled={modifiers.disabled}
+              active={modifiers.active}
+              onClick={handleClick}
+              onFocus={handleFocus}
+              roleStructure="listoption"
+              text={val}
+            />
+          );
+        }}
+        noResults={<MenuItem disabled text="No results" 
roleStructure="listoption" />}
+        itemPredicate={(query, val, _index, exactMatch) => {
+          const normalizedTitle = val.toLowerCase();
+          const normalizedQuery = query.toLowerCase();
+
+          if (exactMatch) {
+            return normalizedTitle === normalizedQuery;
+          } else {
+            return normalizedTitle.includes(normalizedQuery);
           }
-          shownSegmentStat={activeSegmentStat}
-          xScale={xScale}
-          yScale={yScale}
-          barWidth={barWidth}
+        }}
+        fill
+      >
+        <Button
+          text={activeDatasource === null ? showAll : activeDatasource}
+          fill
+          rightIcon={IconNames.CARET_DOWN}
         />
-      </ResizeSensor>
+      </Select>
     );
-  }
-
-  render() {
-    const { capabilities } = this.props;
-    const { datasources, activeSegmentStat, activeDatasource, dateRange, 
selectedDateRange } =
-      this.state;
-
-    const filterDatasource: ItemPredicate<string> = (query, val, _index, 
exactMatch) => {
-      const normalizedTitle = val.toLowerCase();
-      const normalizedQuery = query.toLowerCase();
-
-      if (exactMatch) {
-        return normalizedTitle === normalizedQuery;
-      } else {
-        return normalizedTitle.includes(normalizedQuery);
-      }
-    };
-
-    const datasourceRenderer: ItemRenderer<string> = (
-      val,
-      { handleClick, handleFocus, modifiers },
-    ) => {
-      if (!modifiers.matchesPredicate) return null;
-      return (
-        <MenuItem
-          key={val}
-          disabled={modifiers.disabled}
-          active={modifiers.active}
-          onClick={handleClick}
-          onFocus={handleFocus}
-          roleStructure="listoption"
-          text={val}
-        />
-      );
-    };
-
-    const DatasourceSelect: React.FC = () => {
-      const showAll = 'Show all';
-      const handleItemSelected = (selectedItem: string) => {
-        this.setState({
-          activeDatasource: selectedItem === showAll ? undefined : 
selectedItem,
-        });
-      };
-      const datasourcesWzAll = [showAll].concat(datasources);
-      return (
-        <Select<string>
-          items={datasourcesWzAll}
-          onItemSelect={handleItemSelected}
-          itemRenderer={datasourceRenderer}
-          noResults={<MenuItem disabled text="No results" 
roleStructure="listoption" />}
-          itemPredicate={filterDatasource}
-          fill
-        >
-          <Button
-            text={activeDatasource === null ? showAll : activeDatasource}
-            fill
-            rightIcon={IconNames.CARET_DOWN}
-          />
-        </Select>
-      );
-    };
+  };
 
-    return (
-      <SplitterLayout
-        className="segment-timeline"
-        primaryMinSize={400}
-        secondaryInitialSize={220}
-        secondaryMaxSize={400}
+  return (
+    <SplitterLayout
+      className="segment-timeline"
+      primaryMinSize={400}
+      secondaryInitialSize={220}
+      secondaryMaxSize={400}
+    >
+      <ResizeSensor
+        onResize={(entries: ResizeObserverEntry[]) => {
+          const rect = entries[0].contentRect;
+          setStage(new Stage(rect.width, rect.height));
+        }}
       >
-        {this.renderStackedBarChart()}
-        <div className="side-control">
-          <FormGroup label="Show">
-            <SegmentedControl
-              value={activeSegmentStat}
-              onValueChange={activeDataType =>
-                this.setState({ activeSegmentStat: activeDataType as 
SegmentStat })
+        <div className="chart-container">
+          {stage && (
+            <SegmentBarChart
+              capabilities={capabilities}
+              stage={stage}
+              dateRange={dateRange}
+              shownSegmentStat={activeSegmentStat}
+              breakByDataSource={false}
+              changeActiveDatasource={(datasource: string | undefined) =>
+                setActiveDatasource(activeDatasource ? undefined : datasource)
               }
-              options={[
-                {
-                  label: 'Total size',
-                  value: 'sizeData',
-                },
-                {
-                  label: 'Segment count',
-                  value: 'countData',
-                },
-              ]}
-              fill
-            />
-          </FormGroup>
-          <FormGroup label="Interval">
-            <DateRangeInput3
-              value={utcToLocalDateRange(selectedDateRange || dateRange)}
-              onChange={newDateRange => {
-                const newUtcDateRange = localToUtcDateRange(newDateRange);
-                if (!isNonNullRange(newUtcDateRange)) return;
-                this.setState({ dateRange: newUtcDateRange, selectedDateRange: 
undefined }, () => {
-                  this.dataQueryManager.runQuery({ capabilities, dateRange: 
newUtcDateRange });
-                });
-              }}
-              fill
-              locale={enUS}
             />
-          </FormGroup>
-          <FormGroup label="Datasource">
-            <DatasourceSelect />
-          </FormGroup>
+          )}
         </div>
-      </SplitterLayout>
-    );
-  }
-}
+      </ResizeSensor>
+      <div className="side-control">
+        <FormGroup label="Show">
+          <SegmentedControl
+            value={activeSegmentStat}
+            onValueChange={s => setActiveSegmentStat(s as SegmentStat)}
+            fill
+            options={[
+              {
+                label: 'Size',
+                value: 'size',
+              },
+              {
+                label: 'Count',
+                value: 'count',
+              },
+            ]}
+          />
+        </FormGroup>
+        <FormGroup label="Interval">
+          <DateRangeInput3
+            value={utcToLocalDateRange(dateRange)}
+            onChange={newDateRange => {
+              const newUtcDateRange = localToUtcDateRange(newDateRange);
+              if (!isNonNullRange(newUtcDateRange)) return;
+              setDateRange(newUtcDateRange);
+            }}
+            fill
+            locale={enUS}
+          />
+        </FormGroup>
+        <FormGroup label="Datasource">
+          <DatasourceSelect />
+        </FormGroup>
+      </div>
+    </SplitterLayout>
+  );
+};
diff --git a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx 
b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
deleted file mode 100644
index 571c50f65a5..00000000000
--- a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import type { AxisScale } from 'd3-axis';
-import { axisBottom, axisLeft } from 'd3-axis';
-import React, { useState } from 'react';
-
-import { formatBytes, formatInteger } from '../../utils';
-
-import { BarGroup } from './bar-group';
-import { ChartAxis } from './chart-axis';
-import type { BarUnitData, HoveredBarInfo, Margin, SegmentStat } from 
'./common';
-
-import './stacked-bar-chart.scss';
-
-interface StackedBarChartProps {
-  svgWidth: number;
-  svgHeight: number;
-  margin: Margin;
-  shownSegmentStat: SegmentStat;
-  dataToRender: BarUnitData[];
-  changeActiveDatasource: (e: string | undefined) => void;
-  xScale: AxisScale<Date>;
-  yScale: AxisScale<number>;
-  barWidth: number;
-}
-
-export const StackedBarChart = React.forwardRef(function StackedBarChart(
-  props: StackedBarChartProps,
-  ref,
-) {
-  const {
-    shownSegmentStat,
-    svgWidth,
-    svgHeight,
-    margin,
-    xScale,
-    yScale,
-    dataToRender,
-    changeActiveDatasource,
-    barWidth,
-  } = props;
-  const [hoverOn, setHoverOn] = useState<HoveredBarInfo>();
-
-  const formatTick = (n: number) => {
-    if (isNaN(n)) return '';
-    if (shownSegmentStat === 'countData') {
-      return formatInteger(n);
-    } else {
-      return formatBytes(n);
-    }
-  };
-
-  const width = svgWidth - margin.left - margin.right;
-  const height = svgHeight - margin.top - margin.bottom;
-
-  return (
-    <div className="stacked-bar-chart" ref={ref as any}>
-      {hoverOn && (
-        <div className="bar-chart-tooltip">
-          <div>Datasource: {hoverOn.datasource}</div>
-          <div>Time: {hoverOn.xValue}</div>
-          <div>
-            {`${
-              shownSegmentStat === 'countData' ? 'Daily total count:' : 'Daily 
total size:'
-            } ${formatTick(hoverOn.dailySize)}`}
-          </div>
-          <div>
-            {`${shownSegmentStat === 'countData' ? 'Count:' : 'Size:'} 
${formatTick(
-              hoverOn.yValue,
-            )}`}
-          </div>
-        </div>
-      )}
-      <svg
-        width={svgWidth}
-        height={svgHeight}
-        viewBox={`0 0 ${svgWidth} ${svgHeight}`}
-        preserveAspectRatio="xMinYMin meet"
-      >
-        <g
-          transform={`translate(${margin.left}, ${margin.top})`}
-          onMouseLeave={() => setHoverOn(undefined)}
-        >
-          <ChartAxis
-            className="gridline-x"
-            transform="translate(0, 0)"
-            axis={axisLeft(yScale)
-              .ticks(5)
-              .tickSize(-width)
-              .tickFormat(() => '')
-              .tickSizeOuter(0)}
-          />
-          <BarGroup
-            dataToRender={dataToRender}
-            changeActiveDatasource={changeActiveDatasource}
-            xScale={xScale}
-            yScale={yScale}
-            onHoverBar={(e: HoveredBarInfo) => setHoverOn(e)}
-            barWidth={barWidth}
-          />
-          <ChartAxis
-            className="axis-x"
-            transform={`translate(0, ${height})`}
-            axis={axisBottom(xScale)}
-          />
-          <ChartAxis
-            className="axis-y"
-            axis={axisLeft(yScale)
-              .ticks(5)
-              .tickFormat(e => formatTick(e))}
-          />
-          {hoverOn && (
-            <g
-              className="hovered-bar"
-              onClick={() => {
-                setHoverOn(undefined);
-                changeActiveDatasource(hoverOn.datasource);
-              }}
-            >
-              <rect
-                x={hoverOn.xCoordinate}
-                y={hoverOn.yCoordinate}
-                width={barWidth}
-                height={hoverOn.height}
-              />
-            </g>
-          )}
-        </g>
-      </svg>
-    </div>
-  );
-});
diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx 
b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
index 9b657622d89..5ee4d51a3a5 100644
--- a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
+++ b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
@@ -22,12 +22,12 @@ import React, { useState } from 'react';
 
 import type { FormJsonTabs } from '../../components';
 import { ExternalLink, FormJsonSelector, JsonInput, RuleEditor } from 
'../../components';
+import type { Rule } from '../../druid-models';
 import type { Capabilities } from '../../helpers';
 import { useQueryManager } from '../../hooks';
 import { getLink } from '../../links';
 import { Api } from '../../singletons';
 import { filterMap, queryDruidSql, swapElements } from '../../utils';
-import type { Rule } from '../../utils/load-rule';
 import { SnitchDialog } from '..';
 
 import './retention-dialog.scss';
diff --git a/web-console/src/druid-models/index.ts 
b/web-console/src/druid-models/index.ts
index dfeeeeaac83..82f14aab4e6 100644
--- a/web-console/src/druid-models/index.ts
+++ b/web-console/src/druid-models/index.ts
@@ -32,6 +32,7 @@ export * from './ingest-query-pattern/ingest-query-pattern';
 export * from './ingestion-spec/ingestion-spec';
 export * from './input-format/input-format';
 export * from './input-source/input-source';
+export * from './load-rule/load-rule';
 export * from './lookup-spec/lookup-spec';
 export * from './metric-spec/metric-spec';
 export * from './overlord-dynamic-config/overlord-dynamic-config';
diff --git a/web-console/src/utils/load-rule.ts 
b/web-console/src/druid-models/load-rule/load-rule.ts
similarity index 98%
rename from web-console/src/utils/load-rule.ts
rename to web-console/src/druid-models/load-rule/load-rule.ts
index a32422bbb6a..63bd162c272 100644
--- a/web-console/src/utils/load-rule.ts
+++ b/web-console/src/druid-models/load-rule/load-rule.ts
@@ -18,7 +18,7 @@
 
 import { sum } from 'd3-array';
 
-import { deepMove, deepSet } from './object-change';
+import { deepMove, deepSet } from '../../utils';
 
 export type RuleType =
   | 'loadForever'
diff --git a/web-console/src/views/explore-view/models/stage.ts 
b/web-console/src/utils/stage.ts
similarity index 80%
rename from web-console/src/views/explore-view/models/stage.ts
rename to web-console/src/utils/stage.ts
index a1d01258f0a..7b65cfebd9c 100644
--- a/web-console/src/views/explore-view/models/stage.ts
+++ b/web-console/src/utils/stage.ts
@@ -16,6 +16,13 @@
  * limitations under the License.
  */
 
+export interface Margin {
+  top: number;
+  right: number;
+  bottom: number;
+  left: number;
+}
+
 export class Stage {
   public readonly width: number;
   public readonly height: number;
@@ -28,4 +35,11 @@ export class Stage {
   public equals(other: Stage | undefined): boolean {
     return Boolean(other && this.width === other.width && this.height === 
other.height);
   }
+
+  public applyMargin(margin: Margin): Stage {
+    return new Stage(
+      this.width - margin.left - margin.right,
+      this.height - margin.top - margin.bottom,
+    );
+  }
 }
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx 
b/web-console/src/views/datasources-view/datasources-view.tsx
index 8f89a0f1c38..a2bd3692346 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -51,8 +51,9 @@ import type {
   CompactionInfo,
   CompactionStatus,
   QueryWithContext,
+  Rule,
 } from '../../druid-models';
-import { formatCompactionInfo, zeroCompactionStatus } from 
'../../druid-models';
+import { formatCompactionInfo, RuleUtil, zeroCompactionStatus } from 
'../../druid-models';
 import type { Capabilities, CapabilitiesMode } from '../../helpers';
 import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from 
'../../react-table';
 import { Api, AppToaster } from '../../singletons';
@@ -82,8 +83,6 @@ import {
   twoLines,
 } from '../../utils';
 import type { BasicAction } from '../../utils/basic-action';
-import type { Rule } from '../../utils/load-rule';
-import { RuleUtil } from '../../utils/load-rule';
 
 import './datasources-view.scss';
 
diff --git 
a/web-console/src/views/explore-view/components/module-pane/module-pane.tsx 
b/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
index b360effc87c..2a1f55c3d4a 100644
--- a/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
+++ b/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
@@ -20,8 +20,9 @@ import { ResizeSensor } from '@blueprintjs/core';
 import type { QueryResult, SqlExpression, SqlQuery } from 
'@druid-toolkit/query';
 import React, { useMemo, useState } from 'react';
 
+import { Stage } from '../../../../utils/stage';
 import type { ParameterDefinition, ParameterValues, QuerySource } from 
'../../models';
-import { effectiveParameterDefault, Stage } from '../../models';
+import { effectiveParameterDefault } from '../../models';
 import { ModuleRepository } from '../../module-repository/module-repository';
 import { Issue } from '../issue/issue';
 
diff --git a/web-console/src/views/explore-view/models/index.ts 
b/web-console/src/views/explore-view/models/index.ts
index 1e3c58ea09f..8938818acc2 100644
--- a/web-console/src/views/explore-view/models/index.ts
+++ b/web-console/src/views/explore-view/models/index.ts
@@ -22,4 +22,3 @@ export * from './measure';
 export * from './measure-pattern';
 export * from './parameter';
 export * from './query-source';
-export * from './stage';
diff --git 
a/web-console/src/views/explore-view/module-repository/module-repository.ts 
b/web-console/src/views/explore-view/module-repository/module-repository.ts
index d050a2779c5..fd9dd19f1b3 100644
--- a/web-console/src/views/explore-view/module-repository/module-repository.ts
+++ b/web-console/src/views/explore-view/module-repository/module-repository.ts
@@ -20,7 +20,8 @@ import type { IconName } from '@blueprintjs/icons';
 import type { QueryResult, SqlExpression, SqlQuery } from 
'@druid-toolkit/query';
 import type { CancelToken } from 'axios';
 
-import type { ParameterDefinition, QuerySource, Stage } from '../models';
+import type { Stage } from '../../../utils/stage';
+import type { ParameterDefinition, QuerySource } from '../models';
 
 interface ModuleDefinition<P> {
   id: string;


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

Reply via email to