mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat(plugin-chart-echarts): add Gantt Chart plugin (#33716)
This commit is contained in:
committed by
GitHub
parent
cb6342fc73
commit
9f0523977d
@@ -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',
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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 |
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>],
|
||||
|
||||
@@ -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>],
|
||||
[
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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>],
|
||||
|
||||
@@ -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>],
|
||||
|
||||
@@ -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>],
|
||||
|
||||
@@ -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>],
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
);
|
||||
|
||||
33
superset-frontend/src/components/TimePicker/index.tsx
Normal file
33
superset-frontend/src/components/TimePicker/index.tsx
Normal 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} />
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
65
superset/examples/configs/charts/Featured Charts/Gantt.yaml
Normal file
65
superset/examples/configs/charts/Featured Charts/Gantt.yaml
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user