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

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

commit 9d3e728961fd292f439cae1ad9f62132e4b29aba
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Wed Aug 14 20:34:16 2024 -0700

    time chart
---
 .../_save/modules/time-chart-echarts-module.ts     | 368 ------------------
 web-console/src/modules/bar-chart-module.tsx       |   1 -
 web-console/src/modules/grouping-table-module.tsx  |   1 -
 web-console/src/modules/index.ts                   |   1 +
 .../modules/module-repository/module-repository.ts |   1 -
 web-console/src/modules/overall-module.tsx         |   1 -
 web-console/src/modules/pie-chart-module.tsx       |   1 -
 web-console/src/modules/record-table-module.tsx    |   1 -
 web-console/src/modules/time-chart-module.tsx      | 410 +++++++++++++++++++++
 web-console/src/modules/timeline-module.tsx        |   1 -
 .../src/views/explore-view/explore-view.tsx        |   1 +
 11 files changed, 412 insertions(+), 375 deletions(-)

diff --git a/web-console/_save/modules/time-chart-echarts-module.ts 
b/web-console/_save/modules/time-chart-echarts-module.ts
deleted file mode 100644
index 2e2badcfa0f..00000000000
--- a/web-console/_save/modules/time-chart-echarts-module.ts
+++ /dev/null
@@ -1,368 +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 { C, F, L, SqlCase, SqlExpression } from '@druid-toolkit/query';
-import { typedVisualModule } from '@druid-toolkit/visuals-core';
-import * as echarts from 'echarts';
-
-import { highlightStore } from '../highlight-store/highlight-store';
-import { DATE_FORMAT, getAutoGranularity, getInitQuery, snapToGranularity } 
from '../utils';
-
-const TIME_NAME = '__t__';
-const METRIC_NAME = '__met__';
-const STACK_NAME = '__stack__';
-const OTHERS_VALUE = 'Others';
-
-function transformData(data: any[], vs: string[]): Record<string, number>[] {
-  const zeroDatum = Object.fromEntries(vs.map(v => [v, 0]));
-
-  let lastTime = -1;
-  let lastDatum: Record<string, number> | undefined;
-  const ret = [];
-  for (const d of data) {
-    const t = d[TIME_NAME];
-    if (t.valueOf() !== lastTime) {
-      if (lastDatum) ret.push(lastDatum);
-      lastTime = t.valueOf();
-      lastDatum = { ...zeroDatum, [TIME_NAME]: t };
-    }
-    lastDatum![d[STACK_NAME]] = d[METRIC_NAME];
-  }
-  if (lastDatum) ret.push(lastDatum);
-  return ret;
-}
-
-export default typedVisualModule({
-  parameters: {
-    timeGranularity: {
-      type: 'option',
-      options: ['auto', 'PT1M', 'PT5M', 'PT30M', 'PT1H', 'P1D'],
-      default: 'auto',
-      control: {
-        optionLabels: {
-          auto: 'Auto',
-          PT1M: 'Minute',
-          PT5M: '5 minutes',
-          PT30M: '30 minutes',
-          PT1H: 'Hour',
-          PT6H: '6 hours',
-          P1D: 'Day',
-        },
-      },
-    },
-    splitColumn: {
-      type: 'column',
-      control: {
-        label: 'Stack by',
-        // transferGroup: 'show',
-      },
-    },
-    numberToStack: {
-      type: 'number',
-      default: 7,
-      min: 2,
-      control: {
-        label: 'Max stacks',
-        required: true,
-        visible: ({ params }) => Boolean(params.splitColumn),
-      },
-    },
-    showOthers: {
-      type: 'boolean',
-      default: true,
-      control: {
-        visible: ({ params }) => Boolean(params.splitColumn),
-      },
-    },
-    metric: {
-      type: 'aggregate',
-      default: { expression: SqlExpression.parse('COUNT(*)'), name: 'Count', 
sqlType: 'BIGINT' },
-      control: {
-        label: 'Metric to show',
-        required: true,
-        // transferGroup: 'show-agg',
-      },
-    },
-    snappyHighlight: {
-      type: 'boolean',
-      default: true,
-      control: {
-        label: 'Snap highlight to nearest dates',
-      },
-    },
-  },
-  module: ({ container, host, updateWhere }) => {
-    const myChart = echarts.init(container, 'dark');
-
-    myChart.setOption({
-      dataset: {
-        dimensions: [],
-        source: [],
-      },
-      tooltip: {
-        trigger: 'axis',
-        transitionDuration: 0,
-        axisPointer: {
-          type: 'cross',
-          label: {
-            backgroundColor: '#6a7985',
-          },
-        },
-      },
-      legend: {
-        data: [],
-      },
-      toolbox: {
-        feature: {
-          saveAsImage: {},
-        },
-      },
-      brush: {
-        toolbox: ['lineX'],
-        xAxisIndex: 0,
-      },
-      grid: {
-        left: '3%',
-        right: '4%',
-        bottom: '3%',
-        containLabel: true,
-      },
-      xAxis: [
-        {
-          type: 'time',
-          boundaryGap: false,
-        },
-      ],
-      yAxis: [
-        {
-          type: 'value',
-        },
-      ],
-      series: [],
-    });
-
-    // auto-enables the brush tool on load
-    myChart.dispatchAction({
-      type: 'takeGlobalCursor',
-      key: 'brush',
-      brushOption: {
-        brushType: 'lineX',
-      },
-    });
-
-    return {
-      async update({ table, where, parameterValues, context }) {
-        const { splitColumn, metric, numberToStack, showOthers, 
snappyHighlight } = parameterValues;
-
-        if (String(table).includes('select source')) return;
-
-        // this should probably be a parameter
-        const timeColumnName = '__time';
-
-        const timeGranularity =
-          parameterValues.timeGranularity === 'auto'
-            ? getAutoGranularity(where, timeColumnName)
-            : parameterValues.timeGranularity;
-
-        myChart.off('brush');
-        myChart.off('brushend');
-
-        const vs = splitColumn
-          ? (
-              await host.sqlQuery(
-                getInitQuery(table, where)
-                  .addSelect(splitColumn.expression.as('v'), { addToGroupBy: 
'end' })
-                  
.changeOrderByExpression(metric.expression.toOrderByExpression('DESC'))
-                  .changeLimitValue(numberToStack),
-              )
-            ).getColumnByIndex(0)!
-          : undefined;
-
-        const dataset = (
-          await host.sqlQuery(
-            getInitQuery(
-              table,
-              splitColumn && vs && !showOthers ? 
where.and(splitColumn.expression.in(vs)) : where,
-            )
-              .addSelect(F.timeFloor(C(timeColumnName), 
L(timeGranularity)).as(TIME_NAME), {
-                addToGroupBy: 'end',
-                addToOrderBy: 'end',
-                direction: 'ASC',
-              })
-              .applyIf(splitColumn, q => {
-                if (!splitColumn || !vs) return q; // Should never get here, 
doing this to make peace between eslint and TS
-                const splitEx = splitColumn.expression;
-                return q.addSelect(
-                  (showOthers
-                    ? SqlCase.ifThenElse(splitEx.in(vs), splitEx, 
L(OTHERS_VALUE))
-                    : splitEx
-                  ).as(STACK_NAME),
-                  { addToGroupBy: 'end' },
-                );
-              })
-              .addSelect(metric.expression.as(METRIC_NAME)),
-          )
-        ).toObjectArray();
-
-        const effectiveVs = vs && showOthers ? vs.concat(OTHERS_VALUE) : vs;
-        const sourceData = effectiveVs ? transformData(dataset, effectiveVs) : 
dataset;
-
-        myChart.on('brush', (params: any) => {
-          if (!params.areas.length) return;
-
-          // this is only used for the label and the data saved in the 
highlight
-          // the positioning is done with the true coordinates until the user
-          // releases the mouse button (in the `brushend` event)
-          const { start, end } = snappyHighlight
-            ? snapToGranularity(
-                params.areas[0].coordRange[0],
-                params.areas[0].coordRange[1],
-                timeGranularity,
-                context.timezone,
-              )
-            : { start: params.areas[0].coordRange[0], end: 
params.areas[0].coordRange[1] };
-
-          const x0 = myChart.convertToPixel({ xAxisIndex: 0 }, 
params.areas[0].coordRange[0]);
-          const x1 = myChart.convertToPixel({ xAxisIndex: 0 }, 
params.areas[0].coordRange[1]);
-
-          highlightStore.getState().setHighlight({
-            label: DATE_FORMAT.formatRange(start, end),
-            x: x0 + (x1 - x0) / 2,
-            y: 40,
-            data: { start, end },
-            onDrop: () => {
-              highlightStore.getState().dropHighlight();
-              myChart.dispatchAction({
-                type: 'brush',
-                command: 'clear',
-                areas: [],
-              });
-            },
-            onSave: () => {
-              updateWhere(
-                where.changeClauseInWhere(
-                  SqlExpression.parse(
-                    `TIME_IN_INTERVAL(${C(
-                      timeColumnName,
-                    )}, '${start.toISOString()}/${end.toISOString()}')`,
-                  ),
-                ) as SqlExpression,
-              );
-              highlightStore.getState().dropHighlight();
-              myChart.dispatchAction({
-                type: 'brush',
-                command: 'clear',
-                areas: [],
-              });
-            },
-          });
-        });
-
-        // once the user is done selecting a range, this will snap the start 
and end
-        myChart.on('brushend', () => {
-          const highlight = highlightStore.getState().highlight;
-          if (!highlight) return;
-
-          // this is already snapped
-          const { start, end } = highlight.data;
-
-          const x0 = myChart.convertToPixel({ xAxisIndex: 0 }, start);
-          const x1 = myChart.convertToPixel({ xAxisIndex: 0 }, end);
-
-          // positions the bubble on the snapped start and end
-          highlightStore.getState().updateHighlight({
-            x: x0 + (x1 - x0) / 2,
-          });
-
-          // gives the chart the snapped range to highlight
-          // (will replace the area the user just selected)
-          myChart.dispatchAction({
-            type: 'brush',
-            areas: [
-              {
-                brushType: 'lineX',
-                coordRange: [start, end],
-                xAxisIndex: 0,
-              },
-            ],
-          });
-        });
-
-        const showSymbol = sourceData.length < 2;
-        myChart.setOption(
-          {
-            dataset: {
-              dimensions: [TIME_NAME].concat(effectiveVs || [METRIC_NAME]),
-              source: sourceData,
-            },
-            animation: false,
-            legend: effectiveVs
-              ? {
-                  data: effectiveVs,
-                }
-              : undefined,
-            series: (effectiveVs || [METRIC_NAME]).map(v => {
-              return {
-                id: v,
-                name: effectiveVs ? v : metric.name,
-                type: 'line',
-                stack: 'Total',
-                showSymbol,
-                lineStyle: v === OTHERS_VALUE ? { color: '#ccc' } : {},
-                areaStyle: v === OTHERS_VALUE ? { color: '#ccc' } : {},
-                emphasis: {
-                  focus: 'series',
-                },
-                encode: {
-                  x: TIME_NAME,
-                  y: v,
-                  itemId: v,
-                },
-              };
-            }),
-          },
-          {
-            replaceMerge: ['legend', 'series'],
-          },
-        );
-      },
-
-      resize() {
-        myChart.resize();
-
-        // if there is a highlight, update its x position
-        // by calculating new pixel position from the highlight's data
-        const highlight = highlightStore.getState().highlight;
-        if (highlight) {
-          const { start, end } = highlight.data;
-
-          const x0 = myChart.convertToPixel({ xAxisIndex: 0 }, start);
-          const x1 = myChart.convertToPixel({ xAxisIndex: 0 }, end);
-
-          highlightStore.getState().updateHighlight({
-            x: x0 + (x1 - x0) / 2,
-          });
-        }
-      },
-
-      destroy() {
-        myChart.dispose();
-      },
-    };
-  },
-});
diff --git a/web-console/src/modules/bar-chart-module.tsx 
b/web-console/src/modules/bar-chart-module.tsx
index 585f85a172c..4f9d6253484 100644
--- a/web-console/src/modules/bar-chart-module.tsx
+++ b/web-console/src/modules/bar-chart-module.tsx
@@ -41,7 +41,6 @@ interface BarChartParameterValues {
 ModuleRepository.registerModule<BarChartParameterValues>({
   id: 'bar-chart',
   title: 'Bar chart',
-  description: 'An echarts bar chart',
   parameters: {
     splitColumn: {
       type: 'expression',
diff --git a/web-console/src/modules/grouping-table-module.tsx 
b/web-console/src/modules/grouping-table-module.tsx
index 9254361ddc9..aba2b77cb5a 100644
--- a/web-console/src/modules/grouping-table-module.tsx
+++ b/web-console/src/modules/grouping-table-module.tsx
@@ -62,7 +62,6 @@ interface GroupingTableParameterValues {
 ModuleRepository.registerModule<GroupingTableParameterValues>({
   id: 'grouping-table',
   title: 'Grouping table',
-  description: 'A table with extensive compare support',
   parameters: {
     splitColumns: {
       type: 'expressions',
diff --git a/web-console/src/modules/index.ts b/web-console/src/modules/index.ts
index 1ce08c91970..644d12b82a0 100644
--- a/web-console/src/modules/index.ts
+++ b/web-console/src/modules/index.ts
@@ -22,6 +22,7 @@ import './bar-chart-module';
 import './pie-chart-module';
 import './overall-module';
 import './timeline-module';
+import './time-chart-module';
 
 export * from './models';
 export * from './utils/query-with-measures';
diff --git a/web-console/src/modules/module-repository/module-repository.ts 
b/web-console/src/modules/module-repository/module-repository.ts
index e134c25eaa3..91310572a66 100644
--- a/web-console/src/modules/module-repository/module-repository.ts
+++ b/web-console/src/modules/module-repository/module-repository.ts
@@ -23,7 +23,6 @@ import type { ParameterDefinition, QuerySource, Stage } from 
'../models';
 interface ModuleDefinition<P> {
   id: string;
   title: string;
-  description: string;
   parameters: Record<keyof P, ParameterDefinition>;
   component: (props: ModuleComponentProps<P>) => any;
 }
diff --git a/web-console/src/modules/overall-module.tsx 
b/web-console/src/modules/overall-module.tsx
index d8145f08510..70566e3027e 100644
--- a/web-console/src/modules/overall-module.tsx
+++ b/web-console/src/modules/overall-module.tsx
@@ -36,7 +36,6 @@ interface OverallParameterValues {
 ModuleRepository.registerModule<OverallParameterValues>({
   id: 'overall',
   title: 'Overall',
-  description: 'Shows the count',
   parameters: {
     metrics: {
       type: 'aggregates',
diff --git a/web-console/src/modules/pie-chart-module.tsx 
b/web-console/src/modules/pie-chart-module.tsx
index fdd32f2ee99..5932562d759 100644
--- a/web-console/src/modules/pie-chart-module.tsx
+++ b/web-console/src/modules/pie-chart-module.tsx
@@ -62,7 +62,6 @@ interface PieChartParameterValues {
 ModuleRepository.registerModule<PieChartParameterValues>({
   id: 'pie-chart',
   title: 'Pie chart',
-  description: 'An echarts pie chart',
   parameters: {
     splitColumn: {
       type: 'expression',
diff --git a/web-console/src/modules/record-table-module.tsx 
b/web-console/src/modules/record-table-module.tsx
index 1eeda2ae7f1..05e4bd98c54 100644
--- a/web-console/src/modules/record-table-module.tsx
+++ b/web-console/src/modules/record-table-module.tsx
@@ -36,7 +36,6 @@ interface RecordTableParameterValues {
 ModuleRepository.registerModule<RecordTableParameterValues>({
   id: 'record-table',
   title: 'Record table',
-  description: 'A table with extensive compare support',
   parameters: {
     maxRows: {
       type: 'number',
diff --git a/web-console/src/modules/time-chart-module.tsx 
b/web-console/src/modules/time-chart-module.tsx
new file mode 100644
index 00000000000..9527eb4a306
--- /dev/null
+++ b/web-console/src/modules/time-chart-module.tsx
@@ -0,0 +1,410 @@
+/*
+ * 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 { C, F, L, SqlCase, SqlExpression, SqlQuery } from 
'@druid-toolkit/query';
+import type { ECharts } from 'echarts';
+import * as echarts from 'echarts';
+import React, { useEffect, useMemo, useRef } from 'react';
+
+import { useQueryManager } from '../hooks';
+import { highlightStore } from 
'../views/explore-view/highlight-store/highlight-store';
+import { DATE_FORMAT, getAutoGranularity, snapToGranularity } from 
'../views/explore-view/utils';
+
+import type { ExpressionMeta } from './models';
+import { ModuleRepository } from './module-repository/module-repository';
+
+import './record-table-module.scss';
+
+const TIME_NAME = '__t__';
+const METRIC_NAME = '__met__';
+const STACK_NAME = '__stack__';
+const OTHERS_VALUE = 'Others';
+
+function transformData(data: any[], vs: string[]): Record<string, number>[] {
+  const zeroDatum = Object.fromEntries(vs.map(v => [v, 0]));
+
+  let lastTime = -1;
+  let lastDatum: Record<string, number> | undefined;
+  const ret = [];
+  for (const d of data) {
+    const t = d[TIME_NAME];
+    if (t.valueOf() !== lastTime) {
+      if (lastDatum) ret.push(lastDatum);
+      lastTime = t.valueOf();
+      lastDatum = { ...zeroDatum, [TIME_NAME]: t };
+    }
+    lastDatum![d[STACK_NAME]] = d[METRIC_NAME];
+  }
+  if (lastDatum) ret.push(lastDatum);
+  return ret;
+}
+
+interface TimeChartParameterValues {
+  timeGranularity: string;
+  splitColumn?: ExpressionMeta;
+  numberToStack: number;
+  showOthers: boolean;
+  measure: ExpressionMeta;
+  snappyHighlight: boolean;
+}
+
+ModuleRepository.registerModule<TimeChartParameterValues>({
+  id: 'time-chart',
+  title: 'Time chart',
+  parameters: {
+    timeGranularity: {
+      type: 'option',
+      options: ['auto', 'PT1M', 'PT5M', 'PT30M', 'PT1H', 'P1D'],
+      default: 'auto',
+      control: {
+        optionLabels: {
+          auto: 'Auto',
+          PT1M: 'Minute',
+          PT5M: '5 minutes',
+          PT30M: '30 minutes',
+          PT1H: 'Hour',
+          PT6H: '6 hours',
+          P1D: 'Day',
+        },
+      },
+    },
+    splitColumn: {
+      type: 'expression',
+      control: {
+        label: 'Stack by',
+        // transferGroup: 'show',
+      },
+    },
+    numberToStack: {
+      type: 'number',
+      default: 7,
+      min: 2,
+      control: {
+        label: 'Max stacks',
+        required: true,
+        visible: ({ parameterValues }) => Boolean(parameterValues.splitColumn),
+      },
+    },
+    showOthers: {
+      type: 'boolean',
+      default: true,
+      control: {
+        visible: ({ parameterValues }) => Boolean(parameterValues.splitColumn),
+      },
+    },
+    measure: {
+      type: 'aggregate',
+      default: { expression: SqlExpression.parse('COUNT(*)'), name: 'Count', 
sqlType: 'BIGINT' },
+      control: {
+        label: 'Metric to show',
+        required: true,
+        // transferGroup: 'show-agg',
+      },
+    },
+    snappyHighlight: {
+      type: 'boolean',
+      default: true,
+      control: {
+        label: 'Snap highlight to nearest dates',
+      },
+    },
+  },
+  component: function TimeChartModule(props) {
+    const { querySource, where, setWhere, parameterValues, stage, runSqlQuery 
} = props;
+    const chartRef = useRef<ECharts>();
+
+    const timeColumnName = '__time'; // this should probably be a parameter
+    const timeGranularity =
+      parameterValues.timeGranularity === 'auto'
+        ? getAutoGranularity(where, timeColumnName)
+        : parameterValues.timeGranularity;
+
+    const { splitColumn, numberToStack, showOthers, measure, snappyHighlight } 
= parameterValues;
+
+    const dataQuery = useMemo(() => {
+      return {
+        baseQuery: SqlQuery.from(querySource.query).addWhere(where),
+        measure,
+        splitExpression: splitColumn?.expression,
+      };
+    }, [querySource, where, measure, splitColumn]);
+
+    const [sourceDataState] = useQueryManager({
+      query: dataQuery,
+      processQuery: async ({ baseQuery, measure, splitExpression }) => {
+        const vs = splitExpression
+          ? (
+              await runSqlQuery(
+                baseQuery
+                  .addSelect(splitExpression.as('v'), { addToGroupBy: 'end' })
+                  
.changeOrderByExpression(measure.expression.toOrderByExpression('DESC'))
+                  .changeLimitValue(numberToStack),
+              )
+            ).getColumnByIndex(0)!
+          : undefined;
+
+        const dataset = (
+          await runSqlQuery(
+            baseQuery
+              .applyIf(splitExpression && vs && !showOthers, q =>
+                q.addWhere(splitExpression!.in(vs!)),
+              )
+              .addSelect(F.timeFloor(C(timeColumnName), 
L(timeGranularity)).as(TIME_NAME), {
+                addToGroupBy: 'end',
+                addToOrderBy: 'end',
+                direction: 'ASC',
+              })
+              .applyIf(splitExpression, q => {
+                if (!splitExpression || !vs) return q; // Should never get 
here, doing this to make peace between eslint and TS
+                return q.addSelect(
+                  (showOthers
+                    ? SqlCase.ifThenElse(splitExpression.in(vs), 
splitExpression, L(OTHERS_VALUE))
+                    : splitExpression
+                  ).as(STACK_NAME),
+                  { addToGroupBy: 'end' },
+                );
+              })
+              .addSelect(measure.expression.as(METRIC_NAME)),
+          )
+        ).toObjectArray();
+
+        const effectiveVs = vs && showOthers ? vs.concat(OTHERS_VALUE) : vs;
+        return {
+          effectiveVs,
+          sourceData: effectiveVs ? transformData(dataset, effectiveVs) : 
dataset,
+          measure,
+        };
+      },
+    });
+
+    function setupChart(container: HTMLDivElement) {
+      const myChart = echarts.init(container, 'dark');
+
+      myChart.setOption({
+        dataset: {
+          dimensions: [],
+          source: [],
+        },
+        tooltip: {
+          trigger: 'axis',
+          transitionDuration: 0,
+          axisPointer: {
+            type: 'cross',
+            label: {
+              backgroundColor: '#6a7985',
+            },
+          },
+        },
+        legend: {
+          data: [],
+        },
+        toolbox: {
+          feature: {
+            saveAsImage: {},
+          },
+        },
+        brush: {
+          toolbox: ['lineX'],
+          xAxisIndex: 0,
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '3%',
+          containLabel: true,
+        },
+        xAxis: [
+          {
+            type: 'time',
+            boundaryGap: false,
+          },
+        ],
+        yAxis: [
+          {
+            type: 'value',
+          },
+        ],
+        series: [],
+      });
+
+      // auto-enables the brush tool on load
+      myChart.dispatchAction({
+        type: 'takeGlobalCursor',
+        key: 'brush',
+        brushOption: {
+          brushType: 'lineX',
+        },
+      });
+
+      return myChart;
+    }
+
+    useEffect(() => {
+      return () => {
+        const myChart = chartRef.current;
+        if (!myChart) return;
+        myChart.dispose();
+      };
+    }, []);
+
+    useEffect(() => {
+      const myChart = chartRef.current;
+      const data = sourceDataState.data;
+      if (!myChart || !data) return;
+      const { effectiveVs, sourceData, measure } = data;
+
+      myChart.off('brush');
+      myChart.off('brushend');
+
+      myChart.on('brush', (params: any) => {
+        if (!params.areas.length) return;
+
+        // this is only used for the label and the data saved in the highlight
+        // the positioning is done with the true coordinates until the user
+        // releases the mouse button (in the `brushend` event)
+        const { start, end } = snappyHighlight
+          ? snapToGranularity(
+              params.areas[0].coordRange[0],
+              params.areas[0].coordRange[1],
+              timeGranularity,
+              // context.timezone, // ToDo:???
+            )
+          : { start: params.areas[0].coordRange[0], end: 
params.areas[0].coordRange[1] };
+
+        const x0 = myChart.convertToPixel({ xAxisIndex: 0 }, 
params.areas[0].coordRange[0]);
+        const x1 = myChart.convertToPixel({ xAxisIndex: 0 }, 
params.areas[0].coordRange[1]);
+
+        highlightStore.getState().setHighlight({
+          label: DATE_FORMAT.formatRange(start, end),
+          x: x0 + (x1 - x0) / 2,
+          y: 40,
+          data: { start, end },
+          onDrop: () => {
+            highlightStore.getState().dropHighlight();
+            myChart.dispatchAction({
+              type: 'brush',
+              command: 'clear',
+              areas: [],
+            });
+          },
+          onSave: () => {
+            setWhere(
+              where.changeClauseInWhere(
+                SqlExpression.parse(
+                  `TIME_IN_INTERVAL(${C(
+                    timeColumnName,
+                  )}, '${start.toISOString()}/${end.toISOString()}')`,
+                ),
+              ) as SqlExpression,
+            );
+            highlightStore.getState().dropHighlight();
+            myChart.dispatchAction({
+              type: 'brush',
+              command: 'clear',
+              areas: [],
+            });
+          },
+        });
+      });
+
+      // once the user is done selecting a range, this will snap the start and 
end
+      myChart.on('brushend', () => {
+        const highlight = highlightStore.getState().highlight;
+        if (!highlight) return;
+
+        // this is already snapped
+        const { start, end } = highlight.data;
+
+        const x0 = myChart.convertToPixel({ xAxisIndex: 0 }, start);
+        const x1 = myChart.convertToPixel({ xAxisIndex: 0 }, end);
+
+        // positions the bubble on the snapped start and end
+        highlightStore.getState().updateHighlight({
+          x: x0 + (x1 - x0) / 2,
+        });
+
+        // gives the chart the snapped range to highlight
+        // (will replace the area the user just selected)
+        myChart.dispatchAction({
+          type: 'brush',
+          areas: [
+            {
+              brushType: 'lineX',
+              coordRange: [start, end],
+              xAxisIndex: 0,
+            },
+          ],
+        });
+      });
+
+      const showSymbol = sourceData.length < 2;
+      myChart.setOption(
+        {
+          dataset: {
+            dimensions: [TIME_NAME].concat(effectiveVs || [METRIC_NAME]),
+            source: sourceData,
+          },
+          animation: false,
+          legend: effectiveVs
+            ? {
+                data: effectiveVs,
+              }
+            : undefined,
+          series: (effectiveVs || [METRIC_NAME]).map(v => {
+            return {
+              id: v,
+              name: effectiveVs ? v : measure.name,
+              type: 'line',
+              stack: 'Total',
+              showSymbol,
+              lineStyle: v === OTHERS_VALUE ? { color: '#ccc' } : {},
+              areaStyle: v === OTHERS_VALUE ? { color: '#ccc' } : {},
+              emphasis: {
+                focus: 'series',
+              },
+              encode: {
+                x: TIME_NAME,
+                y: v,
+                itemId: v,
+              },
+            };
+          }),
+        },
+        {
+          replaceMerge: ['legend', 'series'],
+        },
+      );
+    }, [sourceDataState.data, snappyHighlight]);
+
+    useEffect(() => {
+      const myChart = chartRef.current;
+      if (!myChart) return;
+      myChart.resize();
+    }, [stage]);
+
+    return (
+      <div
+        className="time-chart-module module"
+        ref={container => {
+          if (chartRef.current || !container) return;
+          chartRef.current = setupChart(container);
+        }}
+      />
+    );
+  },
+});
diff --git a/web-console/src/modules/timeline-module.tsx 
b/web-console/src/modules/timeline-module.tsx
index 93b819f2fd7..f470ec92b3b 100644
--- a/web-console/src/modules/timeline-module.tsx
+++ b/web-console/src/modules/timeline-module.tsx
@@ -50,7 +50,6 @@ interface TimelineParameterValues {
 ModuleRepository.registerModule<TimelineParameterValues>({
   id: 'timeline',
   title: 'Timeline',
-  description: 'A timeline of events',
   parameters: {
     time: {
       type: 'expression',
diff --git a/web-console/src/views/explore-view/explore-view.tsx 
b/web-console/src/views/explore-view/explore-view.tsx
index ef88b8a2d73..be609358283 100644
--- a/web-console/src/views/explore-view/explore-view.tsx
+++ b/web-console/src/views/explore-view/explore-view.tsx
@@ -291,6 +291,7 @@ export const ExploreView = React.memo(function 
ExploreView() {
               { id: 'overall', icon: IconNames.NUMERICAL, label: 'Overall' },
               { id: 'grouping-table', icon: IconNames.PANEL_TABLE, label: 
'Grouping table' },
               { id: 'record-table', icon: IconNames.PIVOT_TABLE, label: 
'Record table' },
+              { id: 'time-chart', icon: IconNames.CHART, label: 'Time chart' },
               { id: 'timeline', icon: IconNames.FLOW_LINEAR, label: 'Timeline' 
},
               { id: 'bar-chart', icon: IconNames.VERTICAL_BAR_CHART_DESC, 
label: 'Bar chart' },
               { id: 'pie-chart', icon: IconNames.PIE_CHART, label: 'Pie chart' 
},


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

Reply via email to