feat(plugin-chart-echarts): add Gantt Chart plugin (#33716)

This commit is contained in:
Vladislav Korenkov
2025-07-04 07:23:50 +10:00
committed by GitHub
parent cb6342fc73
commit 9f0523977d
36 changed files with 1845 additions and 108 deletions

View File

@@ -29,6 +29,7 @@ export const TITLE_POSITION_OPTIONS: [string, string][] = [
['Left', t('Left')],
['Top', t('Top')],
];
export const titleControls: ControlPanelSectionConfig = {
label: t('Chart Title'),
tabOverride: 'customize',

View File

@@ -17,7 +17,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryColumn, t, validateNonEmpty } from '@superset-ui/core';
import {
GenericDataType,
QueryColumn,
t,
validateNonEmpty,
} from '@superset-ui/core';
import {
ExtraControlProps,
SharedControlConfig,
@@ -52,6 +57,19 @@ type Control = {
* feature flags are set and when they're checked.
*/
function filterOptions(
options: (ColumnMeta | QueryColumn)[],
allowedDataTypes?: GenericDataType[],
) {
if (!allowedDataTypes) {
return options;
}
return options.filter(
o =>
o.type_generic !== undefined && allowedDataTypes.includes(o.type_generic),
);
}
export const dndGroupByControl: SharedControlConfig<
'DndColumnSelect' | 'SelectControl',
ColumnMeta
@@ -81,14 +99,20 @@ export const dndGroupByControl: SharedControlConfig<
const newState: ExtraControlProps = {};
const { datasource } = state;
if (datasource?.columns[0]?.hasOwnProperty('groupby')) {
const options = (datasource as Dataset).columns.filter(c => c.groupby);
const options = filterOptions(
(datasource as Dataset).columns.filter(c => c.groupby),
controlState?.allowedDataTypes,
);
if (controlState?.includeTime) {
options.unshift(DATASET_TIME_COLUMN_OPTION);
}
newState.options = options;
newState.savedMetrics = (datasource as Dataset).metrics || [];
} else {
const options = (datasource?.columns as QueryColumn[]) || [];
const options = filterOptions(
(datasource?.columns as QueryColumn[]) || [],
controlState?.allowedDataTypes,
);
if (controlState?.includeTime) {
options.unshift(QUERY_TIME_COLUMN_OPTION);
}
@@ -177,6 +201,19 @@ export const dndAdhocMetricControl: typeof dndAdhocMetricsControl = {
),
};
export const dndTooltipColumnsControl: typeof dndColumnsControl = {
...dndColumnsControl,
label: t('Tooltip (columns)'),
description: t('Columns to show in the tooltip.'),
};
export const dndTooltipMetricsControl: typeof dndAdhocMetricsControl = {
...dndAdhocMetricsControl,
label: t('Tooltip (metrics)'),
description: t('Metrics to show in the tooltip.'),
validators: [],
};
export const dndAdhocMetricControl2: typeof dndAdhocMetricControl = {
...dndAdhocMetricControl,
label: t('Right Axis Metric'),

View File

@@ -45,6 +45,7 @@ import {
isDefined,
NO_TIME_RANGE,
validateMaxValue,
getColumnLabel,
} from '@superset-ui/core';
import {
@@ -82,6 +83,8 @@ import {
dndSeriesControl,
dndAdhocMetricControl2,
dndXAxisControl,
dndTooltipColumnsControl,
dndTooltipMetricsControl,
} from './dndControls';
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
@@ -373,6 +376,14 @@ const temporal_columns_lookup: SharedControlConfig<'HiddenControl'> = {
),
};
const zoomable: SharedControlConfig<'CheckboxControl'> = {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: false,
renderTrigger: true,
description: t('Enable data zooming controls'),
};
const sort_by_metric: SharedControlConfig<'CheckboxControl'> = {
type: 'CheckboxControl',
label: t('Sort by metric'),
@@ -381,6 +392,26 @@ const sort_by_metric: SharedControlConfig<'CheckboxControl'> = {
),
};
const order_by_cols: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl',
label: t('Ordering'),
description: t('Order results by selected columns'),
multi: true,
default: [],
shouldMapStateToProps: () => true,
mapStateToProps: ({ datasource }) => ({
choices: (datasource?.columns || [])
.map(col =>
[true, false].map(asc => [
JSON.stringify([col.column_name, asc]),
`${getColumnLabel(col.column_name)} [${asc ? 'asc' : 'desc'}]`,
]),
)
.flat(),
}),
resetOnHide: false,
};
export default {
metrics: dndAdhocMetricsControl,
metric: dndAdhocMetricControl,
@@ -392,6 +423,8 @@ export default {
secondary_metric: dndSecondaryMetricControl,
groupby: dndGroupByControl,
columns: dndColumnsControl,
tooltip_columns: dndTooltipColumnsControl,
tooltip_metrics: dndTooltipMetricsControl,
granularity,
granularity_sqla: dndGranularitySqlaControl,
time_grain_sqla,
@@ -417,8 +450,10 @@ export default {
legacy_order_by: dndSortByControl,
truncate_metric,
x_axis: dndXAxisControl,
zoomable,
show_empty_columns,
temporal_columns_lookup,
currency_format,
sort_by_metric,
order_by_cols,
};

View File

@@ -31,6 +31,7 @@ export enum VizType {
Compare = 'compare',
CountryMap = 'country_map',
Funnel = 'funnel',
Gantt = 'gantt_chart',
Gauge = 'gauge_chart',
Graph = 'graph_chart',
Handlebars = 'handlebars',

View File

@@ -0,0 +1,89 @@
/**
* 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 { useEffect, useRef, useState } from 'react';
import { sharedControlComponents } from '@superset-ui/chart-controls';
import { t } from '@superset-ui/core';
import Echart from '../components/Echart';
import { EchartsGanttChartTransformedProps } from './types';
import { EventHandlers } from '../types';
const { RadioButtonControl } = sharedControlComponents;
export default function EchartsGantt(props: EchartsGanttChartTransformedProps) {
const {
height,
width,
echartOptions,
selectedValues,
refs,
formData,
setControlValue,
onLegendStateChanged,
} = props;
const extraControlRef = useRef<HTMLDivElement>(null);
const [extraHeight, setExtraHeight] = useState(0);
useEffect(() => {
const updatedHeight = extraControlRef.current?.offsetHeight ?? 0;
setExtraHeight(updatedHeight);
}, [formData.showExtraControls]);
const eventHandlers: EventHandlers = {
legendselectchanged: payload => {
requestAnimationFrame(() => {
onLegendStateChanged?.(payload.selected);
});
},
legendselectall: payload => {
requestAnimationFrame(() => {
onLegendStateChanged?.(payload.selected);
});
},
legendinverseselect: payload => {
requestAnimationFrame(() => {
onLegendStateChanged?.(payload.selected);
});
},
};
return (
<>
<div ref={extraControlRef} css={{ textAlign: 'center' }}>
{formData.showExtraControls ? (
<RadioButtonControl
options={[
[false, t('Plain')],
[true, t('Subcategories')],
]}
value={formData.subcategories}
onChange={v => setControlValue?.('subcategories', v)}
/>
) : null}
</div>
<Echart
refs={refs}
height={height - extraHeight}
width={width}
echartOptions={echartOptions}
selectedValues={selectedValues}
eventHandlers={eventHandlers}
/>
</>
);
}

View File

@@ -0,0 +1,61 @@
/**
* 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 {
QueryFormData,
QueryObject,
buildQueryContext,
ensureIsArray,
} from '@superset-ui/core';
export default function buildQuery(formData: QueryFormData) {
const {
start_time,
end_time,
y_axis,
series,
tooltip_columns,
tooltip_metrics,
order_by_cols,
} = formData;
const groupBy = ensureIsArray(series);
const orderby = ensureIsArray(order_by_cols).map(
expr => JSON.parse(expr) as [string, boolean],
);
const columns = Array.from(
new Set([
start_time,
end_time,
y_axis,
...groupBy,
...ensureIsArray(tooltip_columns),
...orderby.map(v => v[0]),
]),
);
return buildQueryContext(formData, (baseQueryObject: QueryObject) => [
{
...baseQueryObject,
columns,
metrics: ensureIsArray(tooltip_metrics),
orderby,
series_columns: groupBy,
},
]);
}

View File

@@ -0,0 +1,26 @@
/**
* 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.
*/
export const ELEMENT_HEIGHT_SCALE = 0.85 as const;
export enum Dimension {
StartTime = 'startTime',
EndTime = 'endTime',
Index = 'index',
SeriesCount = 'seriesCount',
}

View File

@@ -0,0 +1,136 @@
/**
* 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 {
ControlPanelConfig,
ControlSubSectionHeader,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
import { GenericDataType, t } from '@superset-ui/core';
import {
legendSection,
showExtraControls,
tooltipTimeFormatControl,
tooltipValuesFormatControl,
} from '../controls';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
[
{
name: 'start_time',
config: {
...sharedControls.entity,
label: t('Start Time'),
description: undefined,
allowedDataTypes: [GenericDataType.Temporal],
},
},
],
[
{
name: 'end_time',
config: {
...sharedControls.entity,
label: t('End Time'),
description: undefined,
allowedDataTypes: [GenericDataType.Temporal],
},
},
],
[
{
name: 'y_axis',
config: {
...sharedControls.x_axis,
label: t('Y-axis'),
description: t('Dimension to use on y-axis.'),
initialValue: () => undefined,
},
},
],
['series'],
[
{
name: 'subcategories',
config: {
type: 'CheckboxControl',
label: t('Subcategories'),
description: t(
'Divides each category into subcategories based on the values in ' +
'the dimension. It can be used to exclude intersections.',
),
renderTrigger: true,
default: false,
visibility: ({ controls }) => !!controls?.series?.value,
},
},
],
['tooltip_metrics'],
['tooltip_columns'],
['adhoc_filters'],
['order_by_cols'],
['row_limit'],
],
},
{
...sections.titleControls,
controlSetRows: [...sections.titleControls.controlSetRows.slice(0, -1)],
},
{
label: t('Chart Options'),
expanded: true,
tabOverride: 'customize',
controlSetRows: [
['color_scheme'],
...legendSection,
['zoomable'],
[showExtraControls],
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
[
{
name: 'x_axis_time_bounds',
config: {
type: 'TimeRangeControl',
label: t('Bounds'),
description: t(
'Bounds for the X-axis. Selected time merges with ' +
'min/max date of the data. When left empty, bounds ' +
'dynamically defined based on the min/max of the data.',
),
renderTrigger: true,
allowClear: true,
allowEmpty: [true, true],
},
},
],
['x_axis_time_format'],
[<ControlSubSectionHeader>{t('Tooltip')}</ControlSubSectionHeader>],
[tooltipTimeFormatControl],
[tooltipValuesFormatControl],
],
},
],
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -0,0 +1,54 @@
/**
* 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 { Behavior, t } from '@superset-ui/core';
import transformProps from './transformProps';
import controlPanel from './controlPanel';
import buildQuery from './buildQuery';
import { EchartsChartPlugin } from '../types';
import thumbnail from './images/thumbnail.png';
import example1 from './images/example1.png';
import example2 from './images/example2.png';
export default class EchartsGanttChartPlugin extends EchartsChartPlugin {
constructor() {
super({
buildQuery,
controlPanel,
loadChart: () => import('./EchartsGantt'),
metadata: {
behaviors: [
Behavior.InteractiveChart,
Behavior.DrillToDetail,
Behavior.DrillBy,
],
credits: ['https://echarts.apache.org'],
name: t('Gantt Chart'),
description: t(
'Gantt chart visualizes important events over a time span. ' +
'Every data point displayed as a separate event along a ' +
'horizontal line.',
),
tags: [t('ECharts'), t('Featured'), t('Timeline'), t('Time')],
thumbnail,
exampleGallery: [{ url: example1 }, { url: example2 }],
},
transformProps,
});
}
}

View File

@@ -0,0 +1,440 @@
/**
* 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 {
CustomSeriesOption,
CustomSeriesRenderItem,
EChartsCoreOption,
LineSeriesOption,
} from 'echarts';
import {
AxisType,
CategoricalColorNamespace,
DataRecord,
DataRecordValue,
GenericDataType,
getColumnLabel,
getNumberFormatter,
t,
tooltipHtml,
} from '@superset-ui/core';
import { CallbackDataParams } from 'echarts/types/src/util/types';
import dayjs from 'dayjs';
import {
Cartesian2dCoordSys,
EchartsGanttChartProps,
EchartsGanttFormData,
} from './types';
import { DEFAULT_FORM_DATA, TIMESERIES_CONSTANTS } from '../constants';
import { Refs } from '../types';
import { getLegendProps, groupData } from '../utils/series';
import {
getTooltipTimeFormatter,
getXAxisFormatter,
} from '../utils/formatters';
import { defaultGrid } from '../defaults';
import { getPadding } from '../Timeseries/transformers';
import { convertInteger } from '../utils/convertInteger';
import { getTooltipLabels } from '../utils/tooltip';
import { Dimension, ELEMENT_HEIGHT_SCALE } from './constants';
const renderItem: CustomSeriesRenderItem = (params, api) => {
const startX = api.value(Dimension.StartTime);
const endX = api.value(Dimension.EndTime);
const index = Number(api.value(Dimension.Index));
const seriesCount = Number(api.value(Dimension.SeriesCount));
if (Number.isNaN(index)) {
return null;
}
const startY = seriesCount - 1 - index;
const endY = startY - 1;
const startCoord = api.coord([startX, startY]);
const endCoord = api.coord([endX, endY]);
const baseHeight = endCoord[1] - startCoord[1];
const height = baseHeight * ELEMENT_HEIGHT_SCALE;
const coordSys = params.coordSys as Cartesian2dCoordSys;
const bounds = [coordSys.x, coordSys.x + coordSys.width];
// left bound
startCoord[0] = Math.max(startCoord[0], bounds[0]);
endCoord[0] = Math.max(startCoord[0], endCoord[0]);
// right bound
startCoord[0] = Math.min(startCoord[0], bounds[1]);
endCoord[0] = Math.min(endCoord[0], bounds[1]);
const width = endCoord[0] - startCoord[0];
if (width <= 0 || height <= 0) {
return null;
}
return {
type: 'rect',
transition: ['shape'],
shape: {
x: startCoord[0],
y: startCoord[1] - height - (baseHeight - height) / 2,
width,
height,
},
style: api.style(),
};
};
export default function transformProps(chartProps: EchartsGanttChartProps) {
const {
formData,
queriesData,
height,
hooks,
filterState,
width,
theme,
emitCrossFilters,
datasource,
legendState,
} = chartProps;
const {
startTime,
endTime,
yAxis,
series: dimension,
tooltipMetrics,
tooltipColumns,
xAxisTimeFormat,
tooltipTimeFormat,
tooltipValuesFormat,
colorScheme,
sliceId,
zoomable,
legendMargin,
legendOrientation,
legendType,
showLegend,
yAxisTitle,
yAxisTitleMargin,
xAxisTitle,
xAxisTitleMargin,
xAxisTimeBounds,
subcategories,
}: EchartsGanttFormData = {
...DEFAULT_FORM_DATA,
...formData,
};
const { setControlValue, onLegendStateChanged } = hooks;
const { data = [], colnames = [], coltypes = [] } = queriesData[0];
const refs: Refs = {};
const startTimeLabel = getColumnLabel(startTime);
const endTimeLabel = getColumnLabel(endTime);
const yAxisLabel = getColumnLabel(yAxis);
const dimensionLabel = dimension ? getColumnLabel(dimension) : undefined;
const tooltipLabels = getTooltipLabels({ tooltipMetrics, tooltipColumns });
const seriesMap = groupData(data, dimensionLabel);
const seriesInCategoriesMap = new Map<
DataRecordValue | undefined,
Map<DataRecordValue | undefined, number>
>();
data.forEach(datum => {
const category = datum[yAxisLabel];
let dimensionValue: DataRecordValue | undefined;
if (dimensionLabel) {
if (legendState && !legendState[String(datum[dimensionLabel])]) {
return;
}
if (subcategories) {
dimensionValue = datum[dimensionLabel];
}
}
const seriesMap = seriesInCategoriesMap.get(category);
if (seriesMap) {
const dimensionMapValue = seriesMap.get(dimensionValue);
if (dimensionMapValue === undefined) {
seriesMap.set(dimensionValue, seriesMap.size);
}
} else {
seriesInCategoriesMap.set(category, new Map([[dimensionValue, 0]]));
}
});
let seriesCount = 0;
const categoryAndSeriesToIndexMap: typeof seriesInCategoriesMap = new Map();
Array.from(seriesInCategoriesMap.entries()).forEach(([key, map]) => {
categoryAndSeriesToIndexMap.set(
key,
new Map(
Array.from(map.entries()).map(([key2, idx]) => [
key2,
seriesCount + idx,
]),
),
);
seriesCount += map.size;
});
const borderLines: { yAxis: number }[] = [];
const categoryLines: { yAxis: number; name?: string }[] = [];
let sum = 0;
let prevSum = 0;
Array.from(seriesInCategoriesMap.entries()).forEach(([key, map]) => {
sum += map.size;
categoryLines.push({
yAxis: seriesCount - (sum + prevSum) / 2,
name: key ? String(key) : undefined,
});
borderLines.push({ yAxis: seriesCount - sum });
prevSum = sum;
});
const xAxisFormatter = getXAxisFormatter(xAxisTimeFormat);
const tooltipTimeFormatter = getTooltipTimeFormatter(tooltipTimeFormat);
const tooltipValuesFormatter = getNumberFormatter(tooltipValuesFormat);
const bounds: [number | undefined, number | undefined] = [
undefined,
undefined,
];
if (xAxisTimeBounds?.[0]) {
const minDate = Math.min(
...data.map(datum => Number(datum[startTimeLabel] ?? 0)),
);
const time = dayjs(xAxisTimeBounds[0], 'HH:mm:ss');
bounds[0] = +dayjs
.utc(minDate)
.hour(time.hour())
.minute(time.minute())
.second(time.second());
}
if (xAxisTimeBounds?.[1]) {
const maxDate = Math.min(
...data.map(datum => Number(datum[endTimeLabel] ?? 0)),
);
const time = dayjs(xAxisTimeBounds[1], 'HH:mm:ss');
bounds[1] = +dayjs
.utc(maxDate)
.hour(time.hour())
.minute(time.minute())
.second(time.second());
}
const padding = getPadding(
showLegend && seriesMap.size > 1,
legendOrientation,
false,
zoomable,
legendMargin,
!!xAxisTitle,
'Left',
convertInteger(yAxisTitleMargin),
convertInteger(xAxisTitleMargin),
);
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
const getIndex = (datum: DataRecord) => {
const seriesMap = categoryAndSeriesToIndexMap.get(datum[yAxisLabel]);
const series =
subcategories && dimensionLabel ? datum[dimensionLabel] : undefined;
return seriesMap ? seriesMap.get(series) : undefined;
};
const series: (CustomSeriesOption | LineSeriesOption)[] = Array.from(
seriesMap.entries(),
)
.map(([key, data], idx) => ({
name: key as string | undefined,
// For some reason items can visually disappear if progressive enabled.
progressive: 0,
itemStyle: {
color: colorScale(String(key), sliceId ?? idx),
},
type: 'custom' as const,
renderItem,
data: data.map(datum => ({
value: [
datum[startTimeLabel],
datum[endTimeLabel],
getIndex(datum),
seriesCount,
...Object.values(datum),
],
})),
dimensions: [...Object.values(Dimension), ...colnames],
encode: {
x: [0, 1],
},
}))
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
series.push(
{
animation: false,
type: 'line' as const,
markLine: {
silent: true,
symbol: ['none', 'none'],
lineStyle: {
type: 'dashed',
// eslint-disable-next-line theme-colors/no-literal-colors
color: '#dbe0ea',
},
label: {
show: false,
},
data: borderLines,
},
},
{
animation: false,
type: 'line',
markLine: {
silent: true,
symbol: ['none', 'none'],
lineStyle: {
type: 'solid',
// eslint-disable-next-line theme-colors/no-literal-colors
color: '#00000000',
},
label: {
show: true,
position: 'start',
formatter: '{b}',
},
data: categoryLines,
},
},
);
const tooltipFormatterMap = {
[GenericDataType.Numeric]: tooltipValuesFormatter,
[GenericDataType.String]: undefined,
[GenericDataType.Temporal]: tooltipTimeFormatter,
[GenericDataType.Boolean]: undefined,
};
const echartOptions: EChartsCoreOption = {
useUTC: true,
tooltip: {
formatter: (params: CallbackDataParams) =>
tooltipHtml(
tooltipLabels.map(label => {
const offset = Object.keys(Dimension).length;
const dimensionNames = params.dimensionNames!.slice(offset);
const data = (params.value as any[]).slice(offset);
const idx = dimensionNames.findIndex(v => v === label)!;
const value = data[idx];
const type = coltypes[idx];
return [label, tooltipFormatterMap[type]?.(value) ?? value];
}),
dimensionLabel ? params.seriesName : undefined,
),
},
legend: {
...getLegendProps(
legendType,
legendOrientation,
showLegend,
theme,
zoomable,
legendState,
),
},
grid: {
...defaultGrid,
...padding,
},
dataZoom: zoomable && [
{
type: 'slider',
filterMode: 'none',
start: TIMESERIES_CONSTANTS.dataZoomStart,
end: TIMESERIES_CONSTANTS.dataZoomEnd,
bottom: TIMESERIES_CONSTANTS.zoomBottom,
},
],
toolbox: {
show: zoomable,
top: TIMESERIES_CONSTANTS.toolboxTop,
right: TIMESERIES_CONSTANTS.toolboxRight,
feature: {
dataZoom: {
yAxisIndex: false,
title: {
zoom: t('zoom area'),
back: t('restore zoom'),
},
},
},
},
series,
xAxis: {
name: xAxisTitle,
nameLocation: 'middle',
type: AxisType.Time,
nameGap: convertInteger(xAxisTitleMargin),
axisLabel: {
formatter: xAxisFormatter,
hideOverlap: true,
},
min: bounds[0],
max: bounds[1],
},
yAxis: {
name: yAxisTitle,
nameGap: convertInteger(yAxisTitleMargin),
nameLocation: 'middle',
axisLabel: {
show: false,
},
splitLine: {
show: false,
},
type: AxisType.Value,
min: 0,
max: seriesCount,
},
};
return {
formData,
queriesData,
echartOptions,
height,
filterState,
width,
theme,
hooks,
emitCrossFilters,
datasource,
refs,
setControlValue,
onLegendStateChanged,
};
}

View File

@@ -0,0 +1,71 @@
/**
* 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 {
ChartDataResponseResult,
ChartProps,
QueryFormColumn,
QueryFormData,
QueryFormMetric,
} from '@superset-ui/core';
import {
BaseTransformedProps,
CrossFilterTransformedProps,
LegendFormData,
} from '../types';
export type EchartsGanttChartTransformedProps =
BaseTransformedProps<EchartsGanttFormData> & CrossFilterTransformedProps;
export type EchartsGanttFormData = QueryFormData &
LegendFormData & {
viz_type: 'gantt_chart';
startTime: QueryFormColumn;
endTime: QueryFormColumn;
yAxis: QueryFormColumn;
tooltipMetrics: QueryFormMetric[];
tooltipColumns: QueryFormColumn[];
series?: QueryFormColumn;
xAxisTimeFormat?: string;
tooltipTimeFormat?: string;
tooltipValuesFormat?: string;
colorScheme?: string;
zoomable?: boolean;
xAxisTitle?: string;
xAxisTitleMargin?: number;
yAxisTitle?: string;
yAxisTitleMargin?: number;
yAxisTitlePosition?: string;
xAxisTimeBounds?: [string | null, string | null];
subcategories?: boolean;
showExtraControls?: boolean;
};
export interface EchartsGanttChartProps
extends ChartProps<EchartsGanttFormData> {
formData: EchartsGanttFormData;
queriesData: ChartDataResponseResult[];
}
export interface Cartesian2dCoordSys {
type: 'cartesian2d';
x: number;
y: number;
width: number;
height: number;
}

View File

@@ -58,7 +58,6 @@ const {
stack,
truncateYAxis,
yAxisBounds,
zoomable,
yAxisIndex,
} = DEFAULT_FORM_DATA;
@@ -352,18 +351,7 @@ const config: ControlPanelConfig = {
['time_shift_color'],
...createCustomizeSection(t('Query A'), ''),
...createCustomizeSection(t('Query B'), 'B'),
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
['zoomable'],
[minorTicks],
...legendSection,
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],

View File

@@ -54,7 +54,6 @@ const {
seriesType,
truncateYAxis,
yAxisBounds,
zoomable,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -171,18 +170,7 @@ const config: ControlPanelConfig = {
},
],
[minorTicks],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
['zoomable'],
...legendSection,
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
[

View File

@@ -48,14 +48,8 @@ import {
} from '../../constants';
import { StackControlsValue } from '../../../constants';
const {
logAxis,
minorSplitLine,
truncateYAxis,
yAxisBounds,
zoomable,
orientation,
} = DEFAULT_FORM_DATA;
const { logAxis, minorSplitLine, truncateYAxis, yAxisBounds, orientation } =
DEFAULT_FORM_DATA;
function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
const isXAxis = axis === 'x';
@@ -363,18 +357,7 @@ const config: ControlPanelConfig = {
},
],
[minorTicks],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
['zoomable'],
...legendSection,
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
...createAxisControl('x'),

View File

@@ -55,7 +55,6 @@ const {
seriesType,
truncateYAxis,
yAxisBounds,
zoomable,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -158,18 +157,7 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
['zoomable'],
[minorTicks],
...legendSection,
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],

View File

@@ -51,7 +51,6 @@ const {
rowLimit,
truncateYAxis,
yAxisBounds,
zoomable,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -100,18 +99,7 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
['zoomable'],
[minorTicks],
...legendSection,
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],

View File

@@ -51,7 +51,6 @@ const {
rowLimit,
truncateYAxis,
yAxisBounds,
zoomable,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -100,18 +99,7 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
['zoomable'],
[minorTicks],
...legendSection,
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],

View File

@@ -51,7 +51,6 @@ const {
rowLimit,
truncateYAxis,
yAxisBounds,
zoomable,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -152,18 +151,7 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
['zoomable'],
[minorTicks],
...legendSection,
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],

View File

@@ -48,10 +48,12 @@ import {
TreemapChart,
HeatmapChart,
SunburstChart,
CustomChart,
} from 'echarts/charts';
import { CanvasRenderer } from 'echarts/renderers';
import {
TooltipComponent,
TitleComponent,
GridComponent,
VisualMapComponent,
LegendComponent,
@@ -83,6 +85,7 @@ use([
CanvasRenderer,
BarChart,
BoxplotChart,
CustomChart,
FunnelChart,
GaugeChart,
GraphChart,
@@ -104,6 +107,7 @@ use([
LegendComponent,
ToolboxComponent,
TooltipComponent,
TitleComponent,
VisualMapComponent,
LabelLayout,
]);

View File

@@ -22,6 +22,7 @@ import {
ControlSetItem,
ControlSetRow,
ControlSubSectionHeader,
CustomControlItem,
DEFAULT_SORT_SERIES_DATA,
SORT_SERIES_CHOICES,
sharedControls,
@@ -185,7 +186,7 @@ const richTooltipControl: ControlSetItem = {
},
};
const tooltipTimeFormatControl: ControlSetItem = {
export const tooltipTimeFormatControl: ControlSetItem = {
name: 'tooltipTimeFormat',
config: {
...sharedControls.x_axis_time_format,
@@ -195,6 +196,15 @@ const tooltipTimeFormatControl: ControlSetItem = {
},
};
export const tooltipValuesFormatControl: CustomControlItem = {
name: 'tooltipValuesFormat',
config: {
...sharedControls.y_axis_format,
label: t('Number format'),
clearable: false,
},
};
const tooltipSortByMetricControl: ControlSetItem = {
name: 'tooltipSortByMetric',
config: {
@@ -367,3 +377,13 @@ export const forceCategorical: ControlSetItem = {
description: t('Make the x-axis categorical'),
},
};
export const showExtraControls: CustomControlItem = {
name: 'show_extra_controls',
config: {
type: 'CheckboxControl',
label: t('Extra Controls'),
renderTrigger: true,
default: false,
},
};

View File

@@ -43,6 +43,7 @@ export { default as EchartsSunburstChartPlugin } from './Sunburst';
export { default as EchartsBubbleChartPlugin } from './Bubble';
export { default as EchartsSankeyChartPlugin } from './Sankey';
export { default as EchartsWaterfallChartPlugin } from './Waterfall';
export { default as EchartsGanttChartPlugin } from './Gantt';
export { default as BoxPlotTransformProps } from './BoxPlot/transformProps';
export { default as FunnelTransformProps } from './Funnel/transformProps';
@@ -60,6 +61,7 @@ export { default as BubbleTransformProps } from './Bubble/transformProps';
export { default as WaterfallTransformProps } from './Waterfall/transformProps';
export { default as HistogramTransformProps } from './Histogram/transformProps';
export { default as SankeyTransformProps } from './Sankey/transformProps';
export { default as GanttTransformProps } from './Gantt/transformProps';
export { DEFAULT_FORM_DATA as TimeseriesDefaultFormData } from './Timeseries/constants';

View File

@@ -685,3 +685,20 @@ export function extractTooltipKeys(
}
return [forecastValue[0][TOOLTIP_SERIES_KEY]];
}
export function groupData(data: DataRecord[], by?: string | null) {
const seriesMap: Map<DataRecordValue | undefined, DataRecord[]> = new Map();
if (by) {
data.forEach(datum => {
const value = seriesMap.get(datum[by]);
if (value) {
value.push(datum);
} else {
seriesMap.set(datum[by], [datum]);
}
});
} else {
seriesMap.set(undefined, data);
}
return seriesMap;
}

View File

@@ -18,6 +18,12 @@
*/
import type { CallbackDataParams } from 'echarts/types/src/util/types';
import {
QueryFormColumn,
QueryFormMetric,
getColumnLabel,
getMetricLabel,
} from '@superset-ui/core';
import { TOOLTIP_OVERFLOW_MARGIN, TOOLTIP_POINTER_MARGIN } from '../constants';
import { Refs } from '../types';
@@ -80,3 +86,16 @@ export function getDefaultTooltip(refs: Refs) {
},
};
}
export function getTooltipLabels({
tooltipMetrics,
tooltipColumns,
}: {
tooltipMetrics?: QueryFormMetric[];
tooltipColumns?: QueryFormColumn[];
}) {
return [
...(tooltipMetrics ?? []).map(v => getMetricLabel(v)),
...(tooltipColumns ?? []).map(v => getColumnLabel(v)),
];
}

View File

@@ -0,0 +1,64 @@
/**
* 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 { QueryFormData } from '@superset-ui/core';
import buildQuery from '../../src/Gantt/buildQuery';
describe('Gantt buildQuery', () => {
const formData: QueryFormData = {
datasource: '1__table',
viz_type: 'gantt_chart',
start_time: 'start_time',
end_time: 'end_time',
y_axis: {
label: 'Y Axis',
sqlExpression: 'SELECT 1',
expressionType: 'SQL',
},
series: 'series',
tooltip_metrics: ['tooltip_metric'],
tooltip_columns: ['tooltip_column'],
order_by_cols: [
JSON.stringify(['start_time', true]),
JSON.stringify(['order_col', false]),
],
};
it('should build query', () => {
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.metrics).toStrictEqual(['tooltip_metric']);
expect(query.columns).toStrictEqual([
'start_time',
'end_time',
{
label: 'Y Axis',
sqlExpression: 'SELECT 1',
expressionType: 'SQL',
},
'series',
'tooltip_column',
'order_col',
]);
expect(query.series_columns).toStrictEqual(['series']);
expect(query.orderby).toStrictEqual([
['start_time', true],
['order_col', false],
]);
});
});

View File

@@ -0,0 +1,269 @@
/**
* 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 { AxisType, ChartProps, supersetTheme } from '@superset-ui/core';
import {
LegendOrientation,
LegendType,
} from '@superset-ui/plugin-chart-echarts';
import transformProps from '../../src/Gantt/transformProps';
import {
EchartsGanttChartProps,
EchartsGanttFormData,
} from '../../src/Gantt/types';
describe('Gantt transformProps', () => {
const formData: EchartsGanttFormData = {
viz_type: 'gantt_chart',
datasource: '1__table',
startTime: 'startTime',
endTime: 'endTime',
yAxis: {
label: 'Y Axis',
sqlExpression: 'y_axis',
expressionType: 'SQL',
},
tooltipMetrics: ['tooltip_metric'],
tooltipColumns: ['tooltip_column'],
series: 'series',
xAxisTimeFormat: '%H:%M',
tooltipTimeFormat: '%H:%M',
tooltipValuesFormat: 'DURATION_SEC',
colorScheme: 'bnbColors',
zoomable: true,
xAxisTitleMargin: undefined,
yAxisTitleMargin: undefined,
xAxisTimeBounds: [null, '19:00:00'],
subcategories: true,
legendMargin: 0,
legendOrientation: LegendOrientation.Top,
legendType: LegendType.Scroll,
showLegend: true,
sortSeriesAscending: true,
};
const queriesData = [
{
data: [
{
startTime: Date.UTC(2025, 1, 1, 13, 0, 0),
endTime: Date.UTC(2025, 1, 1, 14, 0, 0),
'Y Axis': 'first',
tooltip_column: 'tooltip value 1',
series: 'series value 1',
},
{
startTime: Date.UTC(2025, 1, 1, 18, 0, 0),
endTime: Date.UTC(2025, 1, 1, 20, 0, 0),
'Y Axis': 'second',
tooltip_column: 'tooltip value 2',
series: 'series value 2',
},
],
colnames: ['startTime', 'endTime', 'Y Axis', 'tooltip_column', 'series'],
},
];
const chartPropsConfig = {
formData,
queriesData,
theme: supersetTheme,
};
it('should transform chart props', () => {
const chartProps = new ChartProps(chartPropsConfig);
const transformedProps = transformProps(
chartProps as EchartsGanttChartProps,
);
expect(transformedProps.echartOptions.series).toHaveLength(4);
const series = transformedProps.echartOptions.series as any[];
const series0 = series[0];
const series1 = series[1];
// exclude renderItem because it can't be serialized
expect(typeof series0.renderItem).toBe('function');
delete series0.renderItem;
expect(typeof series1.renderItem).toBe('function');
delete series1.renderItem;
delete transformedProps.echartOptions.series;
expect(transformedProps).toEqual(
expect.objectContaining({
echartOptions: expect.objectContaining({
useUTC: true,
xAxis: {
name: '',
nameGap: 0,
nameLocation: 'middle',
max: Date.UTC(2025, 1, 1, 19, 0, 0),
min: undefined,
type: AxisType.Time,
axisLabel: {
hideOverlap: true,
formatter: expect.anything(),
},
},
yAxis: {
name: '',
nameGap: 0,
nameLocation: 'middle',
type: AxisType.Value,
// always 0
min: 0,
// equals unique categories count
max: 2,
axisLabel: {
show: false,
},
splitLine: {
show: false,
},
},
legend: expect.objectContaining({
show: true,
type: 'scroll',
selector: ['all', 'inverse'],
}),
tooltip: {
formatter: expect.anything(),
},
dataZoom: [
expect.objectContaining({
type: 'slider',
filterMode: 'none',
}),
],
}),
}),
);
expect(series0).toEqual({
name: 'series value 1',
type: 'custom',
progressive: 0,
itemStyle: {
color: expect.anything(),
},
data: [
{
value: [
Date.UTC(2025, 1, 1, 13, 0, 0),
Date.UTC(2025, 1, 1, 14, 0, 0),
0,
2,
Date.UTC(2025, 1, 1, 13, 0, 0),
Date.UTC(2025, 1, 1, 14, 0, 0),
'first',
'tooltip value 1',
'series value 1',
],
},
],
dimensions: [
'startTime',
'endTime',
'index',
'seriesCount',
'startTime',
'endTime',
'Y Axis',
'tooltip_column',
'series',
],
encode: {
x: [0, 1],
},
});
expect(series1).toEqual({
name: 'series value 2',
type: 'custom',
progressive: 0,
itemStyle: {
color: expect.anything(),
},
data: [
{
value: [
Date.UTC(2025, 1, 1, 18, 0, 0),
Date.UTC(2025, 1, 1, 20, 0, 0),
1,
2,
Date.UTC(2025, 1, 1, 18, 0, 0),
Date.UTC(2025, 1, 1, 20, 0, 0),
'second',
'tooltip value 2',
'series value 2',
],
},
],
dimensions: [
'startTime',
'endTime',
'index',
'seriesCount',
'startTime',
'endTime',
'Y Axis',
'tooltip_column',
'series',
],
encode: {
x: [0, 1],
},
});
expect(series[2]).toEqual({
// just for markLines
type: 'line',
animation: false,
markLine: {
data: [{ yAxis: 1 }, { yAxis: 0 }],
label: {
show: false,
},
silent: true,
symbol: ['none', 'none'],
lineStyle: {
type: 'dashed',
color: '#dbe0ea',
},
},
});
expect(series[3]).toEqual({
type: 'line',
animation: false,
markLine: {
data: [
{ yAxis: 1.5, name: 'first' },
{ yAxis: 0.5, name: 'second' },
],
label: {
show: true,
position: 'start',
formatter: '{b}',
},
lineStyle: expect.objectContaining({
color: '#00000000',
type: 'solid',
}),
silent: true,
symbol: ['none', 'none'],
},
});
});
});

View File

@@ -164,6 +164,8 @@ class ChartRenderer extends Component {
nextProps.labelsColorMap !== this.props.labelsColorMap ||
nextProps.formData.color_scheme !== this.props.formData.color_scheme ||
nextProps.formData.stack !== this.props.formData.stack ||
nextProps.formData.subcategories !==
this.props.formData.subcategories ||
nextProps.cacheBusterProp !== this.props.cacheBusterProp ||
nextProps.emitCrossFilters !== this.props.emitCrossFilters
);

View File

@@ -0,0 +1,33 @@
/**
* 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 {
TimePicker as AntdTimePicker,
TimePickerProps,
TimeRangePickerProps,
} from 'antd';
const commonCss = { width: '100%' };
export const TimePicker = (props: TimePickerProps) => (
<AntdTimePicker css={commonCss} {...props} />
);
export const TimeRangePicker = (props: TimeRangePickerProps) => (
<AntdTimePicker.RangePicker css={commonCss} {...props} />
);

View File

@@ -0,0 +1,58 @@
/**
* 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 dayjs from 'dayjs';
import { TimeRangePicker } from 'src/components/TimePicker';
import ControlHeader, { ControlHeaderProps } from '../../ControlHeader';
type TimeRangeValueType = [string, string];
export interface TimeRangeControlProps extends ControlHeaderProps {
value?: TimeRangeValueType;
onChange?: (value: TimeRangeValueType, errors: any) => void;
allowClear?: boolean;
showNow?: boolean;
allowEmpty?: [boolean, boolean];
}
export default function TimeRangeControl({
value: stringValue,
onChange,
allowClear,
showNow,
allowEmpty,
...rest
}: TimeRangeControlProps) {
const dayjsValue: [dayjs.Dayjs | null, dayjs.Dayjs | null] = [
stringValue?.[0] ? dayjs.utc(stringValue[0], 'HH:mm:ss') : null,
stringValue?.[1] ? dayjs.utc(stringValue[1], 'HH:mm:ss') : null,
];
return (
<div>
<ControlHeader {...rest} />
<TimeRangePicker
value={dayjsValue}
onChange={(_, stringValue) => onChange?.(stringValue, null)}
allowClear={allowClear}
showNow={showNow}
allowEmpty={allowEmpty}
/>
</div>
);
}

View File

@@ -55,6 +55,7 @@ import LayerConfigsControl from './LayerConfigsControl/LayerConfigsControl';
import MapViewControl from './MapViewControl/MapViewControl';
import ZoomConfigControl from './ZoomConfigControl/ZoomConfigControl';
import NumberControl from './NumberControl';
import TimeRangeControl from './TimeRangeControl';
const extensionsRegistry = getExtensionsRegistry();
const DateFilterControlExtension = extensionsRegistry.get(
@@ -99,6 +100,7 @@ const controlMap = {
TimeOffsetControl,
ZoomConfigControl,
NumberControl,
TimeRangeControl,
...sharedControlComponents,
};
export default controlMap;

View File

@@ -68,6 +68,7 @@ import {
EchartsWaterfallChartPlugin,
BigNumberPeriodOverPeriodChartPlugin,
EchartsHeatmapChartPlugin,
EchartsGanttChartPlugin,
} from '@superset-ui/plugin-chart-echarts';
import {
SelectFilterPlugin,
@@ -111,6 +112,7 @@ export default class MainPreset extends Preset {
new EchartsFunnelChartPlugin().configure({ key: VizType.Funnel }),
new EchartsSankeyChartPlugin().configure({ key: VizType.Sankey }),
new EchartsTreemapChartPlugin().configure({ key: VizType.Treemap }),
new EchartsGanttChartPlugin().configure({ key: VizType.Gantt }),
new EchartsGaugeChartPlugin().configure({ key: VizType.Gauge }),
new EchartsGraphChartPlugin().configure({ key: VizType.Graph }),
new EchartsRadarChartPlugin().configure({ key: VizType.Radar }),

View File

@@ -0,0 +1,65 @@
# 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.
slice_name: Gantt
description: null
certified_by: null
certification_details: null
viz_type: gantt_chart
params:
datasource: 61__table
viz_type: gantt_chart
slice_id: 1495
start_time: start_time
end_time: end_time
y_axis: status
series: priority
subcategories: true
tooltip_columns:
- project
- phase
adhoc_filters:
- clause: WHERE
comparator: No filter
expressionType: SIMPLE
operator: TEMPORAL_RANGE
subject: start_time
order_by_cols:
- '["status",false]'
row_limit: 10000
x_axis_title_margin: 15
y_axis_title_margin: 50
color_scheme: supersetAndPresetColors
show_legend: true
legendType: plain
legendOrientation: right
legendMargin: 100
zoomable: false
show_extra_controls: false
x_axis_time_bounds:
- '08:00:00'
- '19:00:00'
x_axis_time_format: smart_date
tooltipTimeFormat: smart_date
tooltipValuesFormat: SMART_NUMBER
extra_form_data: {}
dashboards:
- 9
query_context: null
cache_timeout: null
uuid: c91c242e-ec16-43e4-84fd-1c69336e0a99
version: 1.0.0
dataset_uuid: d638a239-f255-44fc-b0c1-c3f3b7f00ee0

View File

@@ -135,6 +135,20 @@ position:
- GRID_ID
- ROW-cUv-aKn4Yt
type: CHART
CHART-Df3UIo8Y9s:
children: []
id: CHART-Df3UIo8Y9s
meta:
chartId: 1495
height: 50
sliceName: Gantt
uuid: c91c242e-ec16-43e4-84fd-1c69336e0a99
width: 4
parents:
- ROOT_ID
- GRID_ID
- ROW-W7YILGiS0-
type: CHART
CHART-DqaJJ8Fse6:
children: []
id: CHART-DqaJJ8Fse6
@@ -370,6 +384,7 @@ position:
- ROW-Jq9auQfs6-
- ROW-3XARWMYOfz
- ROW-ux6j1ePT8I
- ROW-FHBKXbZT5Z
id: GRID_ID
parents:
- ROOT_ID
@@ -386,9 +401,9 @@ position:
type: ROOT
ROW-3XARWMYOfz:
children:
- CHART-Yi0u5d9otw
- CHART-33vjmwrGX1
- CHART-3tEC_8e-uS
- CHART-A4qrvR24Ne
id: ROW-3XARWMYOfz
meta:
background: BACKGROUND_TRANSPARENT
@@ -396,11 +411,21 @@ position:
- ROOT_ID
- GRID_ID
type: ROW
ROW-FHBKXbZT5Z:
children:
- CHART-KE7lk61Tbt
id: ROW-FHBKXbZT5Z
meta:
background: BACKGROUND_TRANSPARENT
parents:
- ROOT_ID
- GRID_ID
type: ROW
ROW-Jq9auQfs6-:
children:
- CHART-jzyy9Sa3pS
- CHART-qZh51tuuRH
- CHART-j2o9aZo4HY
- CHART-Yi0u5d9otw
id: ROW-Jq9auQfs6-
meta:
background: BACKGROUND_TRANSPARENT
@@ -434,9 +459,9 @@ position:
type: ROW
ROW-UxgGmS9gb3:
children:
- CHART-EpsTnvUMuW
- CHART-4Zm6Q1VGY5
- CHART-AFzv0kyWG_
- CHART-jzyy9Sa3pS
id: ROW-UxgGmS9gb3
meta:
background: BACKGROUND_TRANSPARENT
@@ -447,8 +472,8 @@ position:
ROW-W7YILGiS0-:
children:
- CHART-gfrGP3BD76
- CHART-Df3UIo8Y9s
- CHART-jC2_mEgWeL
- CHART-SjTqfJNmup
id: ROW-W7YILGiS0-
meta:
background: BACKGROUND_TRANSPARENT
@@ -458,9 +483,9 @@ position:
type: ROW
ROW-cUv-aKn4Yt:
children:
- CHART-SjTqfJNmup
- CHART-CR0-igYucm
- CHART-t5t_tQe43g
- CHART-EpsTnvUMuW
id: ROW-cUv-aKn4Yt
meta:
background: BACKGROUND_TRANSPARENT
@@ -470,9 +495,9 @@ position:
type: ROW
ROW-ux6j1ePT8I:
children:
- CHART-A4qrvR24Ne
- CHART-DqaJJ8Fse6
- CHART-XwFZukVv8E
- CHART-KE7lk61Tbt
id: ROW-ux6j1ePT8I
meta:
background: BACKGROUND_TRANSPARENT

View File

@@ -0,0 +1,293 @@
# 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.
table_name: project_management
main_dttm_col: start_time
description: null
default_endpoint: null
offset: 0
cache_timeout: null
catalog: examples
schema: public
sql: |-
SELECT
1718870400000 AS start_time,
1718874000000 AS end_time,
'Project Alpha' AS project,
'Design Phase' AS phase,
'Initial design and architecture planning for Alpha.' AS description,
'Completed' AS status,
'High' AS priority
UNION ALL
SELECT
1718872800000,
1718877200000,
'Project Alpha',
'Development Phase',
'Core feature development for Alpha project.',
'In Progress',
'High'
UNION ALL
SELECT
1718876400000,
1718880000000,
'Project Alpha',
'Testing Phase',
'Internal testing and bug fixing for Alpha features.',
'Planned',
'Medium'
UNION ALL
SELECT
1718878800000,
1718882400000,
'Project Alpha',
'Deployment Phase',
'Preparation and execution of Alpha deployment.',
'On Hold',
'High'
UNION ALL
SELECT
1718880000000,
1718883600000,
'Project Beta',
'Design Phase',
'Gathering requirements and conceptual design for Beta.',
'Completed',
'Medium'
UNION ALL
SELECT
1718882400000,
1718886000000,
'Project Beta',
'Development Phase',
'Module-wise development for Beta project.',
'In Progress',
'Medium'
UNION ALL
SELECT
1718884800000,
1718888400000,
'Project Beta',
'Testing Phase',
'User acceptance testing for Beta release.',
'Planned',
'High'
UNION ALL
SELECT
1718887200000,
1718890800000,
'Project Beta',
'Deployment Phase',
'Final checks and release of Beta version.',
'Planned',
'Medium'
UNION ALL
SELECT
1718889600000,
1718893200000,
'Project Gamma',
'Design Phase',
'System design and database schema for Gamma.',
'Completed',
'Low'
UNION ALL
SELECT
1718892000000,
1718895600000,
'Project Gamma',
'Development Phase',
'Backend API and frontend integration for Gamma.',
'In Progress',
'High'
UNION ALL
SELECT
1718894400000,
1718898000000,
'Project Gamma',
'Testing Phase',
'Automated test suite execution for Gamma.',
'Planned',
'Medium'
UNION ALL
SELECT
1718896800000,
1718900400000,
'Project Gamma',
'Deployment Phase',
'Handover and post-deployment support for Gamma.',
'Planned',
'Low'
UNION ALL
SELECT
1718900000000,
1718904000000,
'Project Alpha',
'Risk Assessment',
'Analyzing potential risks and mitigation strategies.',
'Completed',
'High'
UNION ALL
SELECT
1718902000000,
1718906000000,
'Project Beta',
'Client Review',
'Review meeting with key stakeholders for Beta.',
'In Progress',
'High'
UNION ALL
SELECT
1718904000000,
1718908000000,
'Project Gamma',
'Documentation',
'Creating technical and user documentation.',
'Planned',
'Low'
UNION ALL
SELECT
1718906000000,
1718910000000,
'Project Alpha',
'Feature Implementation',
'Implementing new requested features for Alpha.',
'In Progress',
'High'
UNION ALL
SELECT
1718908000000,
1718912000000,
'Project Beta',
'User Acceptance Testing',
'Final UAT before production release.',
'Planned',
'High'
UNION ALL
SELECT
1718910000000,
1718914000000,
'Project Gamma',
'Bug Fixing',
'Addressing critical bugs reported post-release.',
'In Progress',
'Medium';
params: null
template_params: null
filter_select_enabled: true
fetch_values_predicate: null
extra: null
normalize_columns: false
always_filter_main_dttm: false
folders: null
uuid: d638a239-f255-44fc-b0c1-c3f3b7f00ee0
metrics:
- metric_name: count
verbose_name: COUNT(*)
metric_type: count
expression: COUNT(*)
description: null
d3format: null
currency: null
extra:
warning_markdown: ''
warning_text: null
columns:
- column_name: start_time
verbose_name: null
is_dttm: true
is_active: true
type: LONGINTEGER
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: {}
- column_name: end_time
verbose_name: null
is_dttm: true
is_active: true
type: LONGINTEGER
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: {}
- column_name: phase
verbose_name: null
is_dttm: false
is_active: true
type: STRING
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: {}
- column_name: status
verbose_name: null
is_dttm: false
is_active: true
type: STRING
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: {}
- column_name: description
verbose_name: null
is_dttm: false
is_active: true
type: STRING
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: {}
- column_name: project
verbose_name: null
is_dttm: false
is_active: true
type: STRING
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: {}
- column_name: priority
verbose_name: null
is_dttm: false
is_active: true
type: STRING
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: {}
version: 1.0.0
database_uuid: a2dc77af-e654-49bb-b321-40f6b559a1ee