mirror of
https://github.com/apache/superset.git
synced 2026-04-19 16:14:52 +00:00
feat(plugin-chart-echarts): add Gantt Chart plugin (#33716)
This commit is contained in:
committed by
GitHub
parent
cb6342fc73
commit
9f0523977d
@@ -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'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user