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]
