mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat(plugin-chart-echarts): supports sunburst chart v2 [WIP] (#21625)
Co-authored-by: Michael S. Molina <michael.s.molina@gmail.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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 React, { useCallback } from 'react';
|
||||
import { BinaryQueryObjectFilterClause } from '@superset-ui/core';
|
||||
import { SunburstTransformedProps } from './types';
|
||||
import Echart from '../components/Echart';
|
||||
import { EventHandlers, TreePathInfo } from '../types';
|
||||
|
||||
export const extractTreePathInfo = (treePathInfo: TreePathInfo[] | undefined) =>
|
||||
(treePathInfo ?? [])
|
||||
.map(pathInfo => pathInfo?.name || '')
|
||||
.filter(path => path !== '');
|
||||
|
||||
export default function EchartsSunburst(props: SunburstTransformedProps) {
|
||||
const {
|
||||
height,
|
||||
width,
|
||||
echartOptions,
|
||||
setDataMask,
|
||||
labelMap,
|
||||
selectedValues,
|
||||
formData,
|
||||
onContextMenu,
|
||||
refs,
|
||||
} = props;
|
||||
|
||||
const { emitFilter, columns } = formData;
|
||||
|
||||
const handleChange = useCallback(
|
||||
(values: string[]) => {
|
||||
if (!emitFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = values.map(value => labelMap[value]);
|
||||
|
||||
setDataMask({
|
||||
extraFormData: {
|
||||
filters:
|
||||
values.length === 0 || !columns
|
||||
? []
|
||||
: columns.map((col, idx) => {
|
||||
const val = labels.map(v => v[idx]);
|
||||
if (val === null || val === undefined)
|
||||
return {
|
||||
col,
|
||||
op: 'IS NULL',
|
||||
};
|
||||
return {
|
||||
col,
|
||||
op: 'IN',
|
||||
val: val as (string | number | boolean)[],
|
||||
};
|
||||
}),
|
||||
},
|
||||
filterState: {
|
||||
value: labels.length ? labels : null,
|
||||
selectedValues: values.length ? values : null,
|
||||
},
|
||||
});
|
||||
},
|
||||
[emitFilter, setDataMask, columns, labelMap],
|
||||
);
|
||||
|
||||
const eventHandlers: EventHandlers = {
|
||||
click: props => {
|
||||
const { treePathInfo } = props;
|
||||
const treePath = extractTreePathInfo(treePathInfo);
|
||||
const name = treePath.join(',');
|
||||
const values = Object.values(selectedValues);
|
||||
if (values.includes(name)) {
|
||||
handleChange(values.filter(v => v !== name));
|
||||
} else {
|
||||
handleChange([name]);
|
||||
}
|
||||
},
|
||||
contextmenu: eventParams => {
|
||||
if (onContextMenu) {
|
||||
eventParams.event.stop();
|
||||
const treePath = extractTreePathInfo(eventParams.treePathInfo);
|
||||
if (treePath.length > 0) {
|
||||
const pointerEvent = eventParams.event.event;
|
||||
const filters: BinaryQueryObjectFilterClause[] = [];
|
||||
if (columns) {
|
||||
treePath.forEach((path, i) =>
|
||||
filters.push({
|
||||
col: columns[i],
|
||||
op: '==',
|
||||
val: path,
|
||||
formattedVal: path,
|
||||
}),
|
||||
);
|
||||
}
|
||||
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Echart
|
||||
refs={refs}
|
||||
height={height}
|
||||
width={width}
|
||||
echartOptions={echartOptions}
|
||||
eventHandlers={eventHandlers}
|
||||
selectedValues={selectedValues}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 { buildQueryContext, QueryFormData } from '@superset-ui/core';
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
const { metric, sort_by_metric } = formData;
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
...(sort_by_metric && { orderby: [[metric, false]] }),
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
ControlPanelsContainerProps,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
emitFilterControl,
|
||||
getStandardizedControls,
|
||||
sections,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
|
||||
const { labelType, numberFormat, showLabels } = DEFAULT_FORM_DATA;
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
sections.legacyRegularTime,
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['columns'],
|
||||
['metric'],
|
||||
['secondary_metric'],
|
||||
['adhoc_filters'],
|
||||
emitFilterControl,
|
||||
['row_limit'],
|
||||
[
|
||||
{
|
||||
name: 'sort_by_metric',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Sort by metric'),
|
||||
description: t(
|
||||
'Whether to sort results by the selected metric in descending order.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
['linear_color_scheme'],
|
||||
[<div className="section-header">{t('Labels')}</div>],
|
||||
[
|
||||
{
|
||||
name: 'show_labels',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Labels'),
|
||||
renderTrigger: true,
|
||||
default: showLabels,
|
||||
description: t('Whether to display the labels.'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_labels_threshold',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Percentage threshold'),
|
||||
renderTrigger: true,
|
||||
isFloat: true,
|
||||
default: 5,
|
||||
description: t(
|
||||
'Minimum threshold in percentage points for showing labels.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_total',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Total'),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
description: t('Whether to display the aggregate count'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'label_type',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Label Type'),
|
||||
default: labelType,
|
||||
renderTrigger: true,
|
||||
choices: [
|
||||
['key', t('Category Name')],
|
||||
['value', t('Value')],
|
||||
['key_value', t('Category and Value')],
|
||||
],
|
||||
description: t('What should be shown on the label?'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'number_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Number format'),
|
||||
renderTrigger: true,
|
||||
default: numberFormat,
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
description: `${t(
|
||||
'D3 format syntax: https://github.com/d3/d3-format',
|
||||
)} ${t('Only applies when "Label Type" is set to show values.')}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'date_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Date format'),
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
default: 'smart_date',
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
metric: {
|
||||
label: t('Primary Metric'),
|
||||
description: t(
|
||||
'The primary metric is used to define the arc segment sizes',
|
||||
),
|
||||
},
|
||||
secondary_metric: {
|
||||
label: t('Secondary Metric'),
|
||||
default: null,
|
||||
description: t(
|
||||
'[optional] this secondary metric is used to ' +
|
||||
'define the color as a ratio against the primary metric. ' +
|
||||
'When omitted, the color is categorical and based on labels',
|
||||
),
|
||||
},
|
||||
color_scheme: {
|
||||
description: t(
|
||||
'When only a primary metric is provided, a categorical color scale is used.',
|
||||
),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
Boolean(
|
||||
!controls?.secondary_metric?.value ||
|
||||
controls?.secondary_metric?.value === controls?.metric.value,
|
||||
),
|
||||
},
|
||||
linear_color_scheme: {
|
||||
description: t(
|
||||
'When a secondary metric is provided, a linear color scale is used.',
|
||||
),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
Boolean(
|
||||
controls?.secondary_metric?.value &&
|
||||
controls?.secondary_metric?.value !== controls?.metric.value,
|
||||
),
|
||||
},
|
||||
groupby: {
|
||||
label: t('Hierarchy'),
|
||||
description: t('This defines the level of the hierarchy'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
groupby: getStandardizedControls().popAllColumns(),
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
secondary_metric: getStandardizedControls().shiftMetric(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import controlPanel from './controlPanel';
|
||||
import buildQuery from './buildQuery';
|
||||
|
||||
export default class EchartsSunburstChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
buildQuery,
|
||||
controlPanel,
|
||||
loadChart: () => import('./EchartsSunburst'),
|
||||
metadata: new ChartMetadata({
|
||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
||||
category: t('Part of a Whole'),
|
||||
credits: ['https://echarts.apache.org'],
|
||||
description: t(
|
||||
'Uses circles to visualize the flow of data through different stages of a system. Hover over individual paths in the visualization to understand the stages a value took. Useful for multi-stage, multi-group visualizing funnels and pipelines.',
|
||||
),
|
||||
exampleGallery: [],
|
||||
name: t('Sunburst Chart v2'),
|
||||
tags: [
|
||||
t('ECharts'),
|
||||
t('Aesthetic'),
|
||||
t('Multi-Levels'),
|
||||
t('Proportional'),
|
||||
],
|
||||
thumbnail,
|
||||
}),
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* 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 {
|
||||
CategoricalColorNamespace,
|
||||
getColumnLabel,
|
||||
getMetricLabel,
|
||||
getNumberFormatter,
|
||||
getSequentialSchemeRegistry,
|
||||
getTimeFormatter,
|
||||
NumberFormats,
|
||||
NumberFormatter,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import { EChartsCoreOption } from 'echarts';
|
||||
import { SunburstSeriesNodeItemOption } from 'echarts/types/src/chart/sunburst/SunburstSeries';
|
||||
import { CallbackDataParams } from 'echarts/types/src/util/types';
|
||||
import { OpacityEnum } from '../constants';
|
||||
import { defaultGrid, defaultTooltip } from '../defaults';
|
||||
import { Refs } from '../types';
|
||||
import { formatSeriesName, getColtypesMapping } from '../utils/series';
|
||||
import { treeBuilder, TreeNode } from '../utils/treeBuilder';
|
||||
import {
|
||||
EchartsSunburstChartProps,
|
||||
EchartsSunburstLabelType,
|
||||
SunburstTransformedProps,
|
||||
} from './types';
|
||||
|
||||
export function getLinearDomain(
|
||||
treeData: TreeNode[],
|
||||
callback: (treeNode: TreeNode) => number,
|
||||
) {
|
||||
let min = 0;
|
||||
let max = 0;
|
||||
let temp = null;
|
||||
function traverse(tree: TreeNode[]) {
|
||||
tree.forEach(treeNode => {
|
||||
if (treeNode.children?.length) {
|
||||
traverse(treeNode.children);
|
||||
}
|
||||
temp = callback(treeNode);
|
||||
if (temp !== null) {
|
||||
if (min > temp) min = temp;
|
||||
if (max < temp) max = temp;
|
||||
}
|
||||
});
|
||||
}
|
||||
traverse(treeData);
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
export function formatLabel({
|
||||
params,
|
||||
labelType,
|
||||
numberFormatter,
|
||||
}: {
|
||||
params: CallbackDataParams;
|
||||
labelType: EchartsSunburstLabelType;
|
||||
numberFormatter: NumberFormatter;
|
||||
}): string {
|
||||
const { name = '', value } = params;
|
||||
const formattedValue = numberFormatter(value as number);
|
||||
|
||||
switch (labelType) {
|
||||
case EchartsSunburstLabelType.Key:
|
||||
return name;
|
||||
case EchartsSunburstLabelType.Value:
|
||||
return formattedValue;
|
||||
case EchartsSunburstLabelType.KeyValue:
|
||||
return `${name}: ${formattedValue}`;
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTooltip({
|
||||
params,
|
||||
numberFormatter,
|
||||
colorByCategory,
|
||||
totalValue,
|
||||
metricLabel,
|
||||
secondaryMetricLabel,
|
||||
}: {
|
||||
params: CallbackDataParams & {
|
||||
treePathInfo: {
|
||||
name: string;
|
||||
dataIndex: number;
|
||||
value: number;
|
||||
}[];
|
||||
};
|
||||
numberFormatter: NumberFormatter;
|
||||
colorByCategory: boolean;
|
||||
totalValue: number;
|
||||
metricLabel: string;
|
||||
secondaryMetricLabel?: string;
|
||||
}): string {
|
||||
const { data, treePathInfo = [] } = params;
|
||||
treePathInfo.shift();
|
||||
const node = data as TreeNode;
|
||||
const formattedValue = numberFormatter(node.value);
|
||||
const formattedSecondaryValue = numberFormatter(node.secondaryValue);
|
||||
|
||||
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
|
||||
const compareValuePercentage = percentFormatter(
|
||||
node.secondaryValue / node.value,
|
||||
);
|
||||
const absolutePercentage = percentFormatter(node.value / totalValue);
|
||||
const parentNode = treePathInfo[treePathInfo.length - 1];
|
||||
const result = [
|
||||
`<div style="font-size: 14px;font-weight: 600">${absolutePercentage} of total</div>`,
|
||||
];
|
||||
if (parentNode) {
|
||||
const conditionalPercentage = percentFormatter(
|
||||
node.value / parentNode.value,
|
||||
);
|
||||
result.push(`
|
||||
<div style="font-size: 12px;">
|
||||
${conditionalPercentage} of parent
|
||||
</div>`);
|
||||
}
|
||||
result.push(
|
||||
`<div style="color: '#666666'">
|
||||
${metricLabel}: ${formattedValue}${
|
||||
colorByCategory
|
||||
? ''
|
||||
: `, ${secondaryMetricLabel}: ${formattedSecondaryValue}`
|
||||
}
|
||||
</div>`,
|
||||
colorByCategory
|
||||
? ''
|
||||
: `<div style="color: '#666666'">
|
||||
${metricLabel}/${secondaryMetricLabel}: ${compareValuePercentage}
|
||||
</div>`,
|
||||
);
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
export default function transformProps(
|
||||
chartProps: EchartsSunburstChartProps,
|
||||
): SunburstTransformedProps {
|
||||
const {
|
||||
formData,
|
||||
height,
|
||||
hooks,
|
||||
filterState,
|
||||
queriesData,
|
||||
width,
|
||||
theme,
|
||||
inContextMenu,
|
||||
} = chartProps;
|
||||
const { data = [] } = queriesData[0];
|
||||
const coltypeMapping = getColtypesMapping(queriesData[0]);
|
||||
const {
|
||||
groupby = [],
|
||||
columns = [],
|
||||
metric = '',
|
||||
secondaryMetric = '',
|
||||
colorScheme,
|
||||
linearColorScheme,
|
||||
labelType,
|
||||
numberFormat,
|
||||
dateFormat,
|
||||
showLabels,
|
||||
showLabelsThreshold,
|
||||
showTotal,
|
||||
sliceId,
|
||||
emitFilter,
|
||||
} = formData;
|
||||
const refs: Refs = {};
|
||||
const numberFormatter = getNumberFormatter(numberFormat);
|
||||
const formatter = (params: CallbackDataParams) =>
|
||||
formatLabel({
|
||||
params,
|
||||
numberFormatter,
|
||||
labelType,
|
||||
});
|
||||
const minShowLabelAngle = (showLabelsThreshold || 0) * 3.6;
|
||||
const padding = {
|
||||
top: theme.gridUnit * 3,
|
||||
right: theme.gridUnit,
|
||||
bottom: theme.gridUnit * 3,
|
||||
left: theme.gridUnit,
|
||||
};
|
||||
const containerWidth = width;
|
||||
const containerHeight = height;
|
||||
const visWidth = containerWidth - padding.left - padding.right;
|
||||
const visHeight = containerHeight - padding.top - padding.bottom;
|
||||
const radius = Math.min(visWidth, visHeight) / 2;
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
const columnsLabelMap = new Map<string, string[]>();
|
||||
const metricLabel = getMetricLabel(metric);
|
||||
const secondaryMetricLabel = secondaryMetric
|
||||
? getMetricLabel(secondaryMetric)
|
||||
: undefined;
|
||||
const columnLabels = columns.map(getColumnLabel);
|
||||
const treeData = treeBuilder(
|
||||
data,
|
||||
columnLabels,
|
||||
metricLabel,
|
||||
secondaryMetricLabel,
|
||||
);
|
||||
const totalValue = treeData.reduce(
|
||||
(result, treeNode) => result + treeNode.value,
|
||||
0,
|
||||
);
|
||||
const totalSecondaryValue = treeData.reduce(
|
||||
(result, treeNode) => result + treeNode.secondaryValue,
|
||||
0,
|
||||
);
|
||||
|
||||
const categoricalColorScale = CategoricalColorNamespace.getScale(
|
||||
colorScheme as string,
|
||||
);
|
||||
let linearColorScale: any;
|
||||
let colorByCategory = true;
|
||||
if (secondaryMetric && metric !== secondaryMetric) {
|
||||
const domain = getLinearDomain(
|
||||
treeData,
|
||||
node => node.secondaryValue / node.value,
|
||||
);
|
||||
colorByCategory = false;
|
||||
linearColorScale = getSequentialSchemeRegistry()
|
||||
?.get(linearColorScheme)
|
||||
?.createLinearScale(domain);
|
||||
}
|
||||
|
||||
// add a base color to keep feature parity
|
||||
if (colorByCategory) {
|
||||
categoricalColorScale(metricLabel, sliceId);
|
||||
} else {
|
||||
linearColorScale(totalSecondaryValue / totalValue);
|
||||
}
|
||||
|
||||
const traverse = (treeNodes: TreeNode[], path: string[]) =>
|
||||
treeNodes.map(treeNode => {
|
||||
const { name: nodeName, value, secondaryValue, groupBy } = treeNode;
|
||||
let name = formatSeriesName(nodeName, {
|
||||
numberFormatter,
|
||||
timeFormatter: getTimeFormatter(dateFormat),
|
||||
...(coltypeMapping[groupBy] && {
|
||||
coltype: coltypeMapping[groupBy],
|
||||
}),
|
||||
});
|
||||
const newPath = path.concat(name);
|
||||
let item: SunburstSeriesNodeItemOption = {
|
||||
name,
|
||||
value,
|
||||
// @ts-ignore
|
||||
secondaryValue,
|
||||
itemStyle: {
|
||||
color: colorByCategory
|
||||
? categoricalColorScale(name, sliceId)
|
||||
: linearColorScale(secondaryValue / value),
|
||||
},
|
||||
};
|
||||
if (treeNode.children?.length) {
|
||||
item.children = traverse(treeNode.children, newPath);
|
||||
} else {
|
||||
name = newPath.join(',');
|
||||
}
|
||||
columnsLabelMap.set(name, newPath);
|
||||
if (filterState.selectedValues?.[0]?.includes(name) === false) {
|
||||
item = {
|
||||
...item,
|
||||
itemStyle: {
|
||||
...item.itemStyle,
|
||||
opacity: OpacityEnum.SemiTransparent,
|
||||
},
|
||||
label: {
|
||||
color: `rgba(0, 0, 0, ${OpacityEnum.SemiTransparent})`,
|
||||
},
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
const echartOptions: EChartsCoreOption = {
|
||||
grid: {
|
||||
...defaultGrid,
|
||||
},
|
||||
tooltip: {
|
||||
...defaultTooltip,
|
||||
show: !inContextMenu,
|
||||
trigger: 'item',
|
||||
formatter: (params: any) =>
|
||||
formatTooltip({
|
||||
params,
|
||||
numberFormatter,
|
||||
colorByCategory,
|
||||
totalValue,
|
||||
metricLabel,
|
||||
secondaryMetricLabel,
|
||||
}),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'sunburst',
|
||||
...padding,
|
||||
nodeClick: false,
|
||||
emphasis: {
|
||||
focus: 'ancestor',
|
||||
label: {
|
||||
show: showLabels,
|
||||
},
|
||||
},
|
||||
label: {
|
||||
width: (radius * 0.6) / (columns.length || 1),
|
||||
show: showLabels,
|
||||
formatter,
|
||||
color: theme.colors.grayscale.dark2,
|
||||
minAngle: minShowLabelAngle,
|
||||
overflow: 'breakAll',
|
||||
},
|
||||
radius: [radius * 0.3, radius],
|
||||
data: traverse(treeData, []),
|
||||
},
|
||||
],
|
||||
graphic: showTotal
|
||||
? {
|
||||
type: 'text',
|
||||
top: 'center',
|
||||
left: 'center',
|
||||
style: {
|
||||
text: t('Total: %s', numberFormatter(totalValue)),
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
z: 10,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
return {
|
||||
formData,
|
||||
width,
|
||||
height,
|
||||
echartOptions,
|
||||
setDataMask,
|
||||
emitFilter,
|
||||
labelMap: Object.fromEntries(columnsLabelMap),
|
||||
groupby,
|
||||
selectedValues: filterState.selectedValues || [],
|
||||
onContextMenu,
|
||||
refs,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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,
|
||||
ContextMenuTransformedProps,
|
||||
CrossFilterTransformedProps,
|
||||
} from '../types';
|
||||
|
||||
export type EchartsSunburstFormData = QueryFormData & {
|
||||
groupby: QueryFormColumn[];
|
||||
metric: QueryFormMetric;
|
||||
secondaryMetric?: QueryFormMetric;
|
||||
colorScheme?: string;
|
||||
linearColorScheme?: string;
|
||||
emitFilter: boolean;
|
||||
};
|
||||
|
||||
export enum EchartsSunburstLabelType {
|
||||
Key = 'key',
|
||||
Value = 'value',
|
||||
KeyValue = 'key_value',
|
||||
}
|
||||
|
||||
export const DEFAULT_FORM_DATA: Partial<EchartsSunburstFormData> = {
|
||||
groupby: [],
|
||||
numberFormat: 'SMART_NUMBER',
|
||||
labelType: EchartsSunburstLabelType.Key,
|
||||
showLabels: false,
|
||||
dateFormat: 'smart_date',
|
||||
emitFilter: false,
|
||||
};
|
||||
|
||||
export interface EchartsSunburstChartProps
|
||||
extends ChartProps<EchartsSunburstFormData> {
|
||||
formData: EchartsSunburstFormData;
|
||||
queriesData: ChartDataResponseResult[];
|
||||
}
|
||||
|
||||
export type SunburstTransformedProps =
|
||||
BaseTransformedProps<EchartsSunburstFormData> &
|
||||
ContextMenuTransformedProps &
|
||||
CrossFilterTransformedProps;
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { TreePathInfo } from './types';
|
||||
import { TreePathInfo } from '../types';
|
||||
|
||||
export const COLOR_SATURATION = [0.7, 0.4];
|
||||
export const LABEL_FONTSIZE = 11;
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
import {
|
||||
CategoricalColorNamespace,
|
||||
DataRecord,
|
||||
getColumnLabel,
|
||||
getMetricLabel,
|
||||
getNumberFormatter,
|
||||
@@ -26,7 +25,6 @@ import {
|
||||
NumberFormats,
|
||||
NumberFormatter,
|
||||
} from '@superset-ui/core';
|
||||
import { groupBy, isNumber, transform } from 'lodash';
|
||||
import { TreemapSeriesNodeItemOption } from 'echarts/types/src/chart/treemap/TreemapSeries';
|
||||
import { EChartsCoreOption, TreemapSeriesOption } from 'echarts';
|
||||
import {
|
||||
@@ -49,6 +47,7 @@ import {
|
||||
import { OpacityEnum } from '../constants';
|
||||
import { getDefaultTooltip } from '../utils/tooltip';
|
||||
import { Refs } from '../types';
|
||||
import { treeBuilder, TreeNode } from '../utils/treeBuilder';
|
||||
|
||||
export function formatLabel({
|
||||
params,
|
||||
@@ -151,97 +150,58 @@ export default function transformProps(
|
||||
});
|
||||
|
||||
const columnsLabelMap = new Map<string, string[]>();
|
||||
|
||||
const transformer = (
|
||||
data: DataRecord[],
|
||||
groupbyLabels: string[],
|
||||
metric: string,
|
||||
depth: number,
|
||||
path: string[],
|
||||
): TreemapSeriesNodeItemOption[] => {
|
||||
const [currGroupby, ...restGroupby] = groupbyLabels;
|
||||
const currGrouping = groupBy(data, currGroupby);
|
||||
if (!restGroupby.length) {
|
||||
return transform(
|
||||
currGrouping,
|
||||
(result, value, key) => {
|
||||
(value ?? []).forEach(datum => {
|
||||
const name = formatSeriesName(key, {
|
||||
numberFormatter,
|
||||
timeFormatter: getTimeFormatter(dateFormat),
|
||||
...(coltypeMapping[currGroupby] && {
|
||||
coltype: coltypeMapping[currGroupby],
|
||||
}),
|
||||
});
|
||||
const item: TreemapSeriesNodeItemOption = {
|
||||
name,
|
||||
value: isNumber(datum[metric]) ? (datum[metric] as number) : 0,
|
||||
};
|
||||
const joinedName = path.concat(name).join(',');
|
||||
// map(joined_name: [columnLabel_1, columnLabel_2, ...])
|
||||
columnsLabelMap.set(joinedName, path.concat(name));
|
||||
if (
|
||||
filterState.selectedValues &&
|
||||
!filterState.selectedValues.includes(joinedName)
|
||||
) {
|
||||
item.itemStyle = {
|
||||
colorAlpha: OpacityEnum.SemiTransparent,
|
||||
};
|
||||
item.label = {
|
||||
color: `rgba(0, 0, 0, ${OpacityEnum.SemiTransparent})`,
|
||||
};
|
||||
}
|
||||
result.push(item);
|
||||
});
|
||||
},
|
||||
[] as TreemapSeriesNodeItemOption[],
|
||||
);
|
||||
}
|
||||
const sortedData = transform(
|
||||
currGrouping,
|
||||
(result, value, key) => {
|
||||
const name = formatSeriesName(key, {
|
||||
numberFormatter,
|
||||
timeFormatter: getTimeFormatter(dateFormat),
|
||||
...(coltypeMapping[currGroupby] && {
|
||||
coltype: coltypeMapping[currGroupby],
|
||||
}),
|
||||
});
|
||||
const children = transformer(
|
||||
value,
|
||||
restGroupby,
|
||||
metric,
|
||||
depth + 1,
|
||||
path.concat(name),
|
||||
);
|
||||
result.push({
|
||||
name,
|
||||
children,
|
||||
value: children.reduce(
|
||||
(prev, cur) => prev + (cur.value as number),
|
||||
0,
|
||||
),
|
||||
});
|
||||
result.sort((a, b) => (b.value as number) - (a.value as number));
|
||||
},
|
||||
[] as TreemapSeriesNodeItemOption[],
|
||||
);
|
||||
// sort according to the area and then take the color value in order
|
||||
return sortedData.map(child => ({
|
||||
...child,
|
||||
colorSaturation: COLOR_SATURATION,
|
||||
itemStyle: {
|
||||
borderColor: BORDER_COLOR,
|
||||
color: colorFn(`${child.name}`, sliceId),
|
||||
borderWidth: BORDER_WIDTH,
|
||||
gapWidth: GAP_WIDTH,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const metricLabel = getMetricLabel(metric);
|
||||
const groupbyLabels = groupby.map(getColumnLabel);
|
||||
const initialDepth = 1;
|
||||
const treeData = treeBuilder(data, groupbyLabels, metricLabel);
|
||||
const traverse = (treeNodes: TreeNode[], path: string[]) =>
|
||||
treeNodes.map(treeNode => {
|
||||
const { name: nodeName, value, groupBy } = treeNode;
|
||||
const name = formatSeriesName(nodeName, {
|
||||
numberFormatter,
|
||||
timeFormatter: getTimeFormatter(dateFormat),
|
||||
...(coltypeMapping[groupBy] && {
|
||||
coltype: coltypeMapping[groupBy],
|
||||
}),
|
||||
});
|
||||
const newPath = path.concat(name);
|
||||
let item: TreemapSeriesNodeItemOption = {
|
||||
name,
|
||||
value,
|
||||
};
|
||||
if (treeNode.children?.length) {
|
||||
item = {
|
||||
...item,
|
||||
children: traverse(treeNode.children, newPath),
|
||||
colorSaturation: COLOR_SATURATION,
|
||||
itemStyle: {
|
||||
borderColor: BORDER_COLOR,
|
||||
color: colorFn(name, sliceId),
|
||||
borderWidth: BORDER_WIDTH,
|
||||
gapWidth: GAP_WIDTH,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const joinedName = newPath.join(',');
|
||||
// map(joined_name: [columnLabel_1, columnLabel_2, ...])
|
||||
columnsLabelMap.set(joinedName, newPath);
|
||||
if (
|
||||
filterState.selectedValues &&
|
||||
!filterState.selectedValues.includes(joinedName)
|
||||
) {
|
||||
item = {
|
||||
...item,
|
||||
itemStyle: {
|
||||
colorAlpha: OpacityEnum.SemiTransparent,
|
||||
},
|
||||
label: {
|
||||
color: `rgba(0, 0, 0, ${OpacityEnum.SemiTransparent})`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
const transformedData: TreemapSeriesNodeItemOption[] = [
|
||||
{
|
||||
name: metricLabel,
|
||||
@@ -255,7 +215,7 @@ export default function transformProps(
|
||||
upperLabel: {
|
||||
show: false,
|
||||
},
|
||||
children: transformer(data, groupbyLabels, metricLabel, initialDepth, []),
|
||||
children: traverse(treeData, []),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
ContextMenuTransformedProps,
|
||||
CrossFilterTransformedProps,
|
||||
LabelPositionEnum,
|
||||
TreePathInfo,
|
||||
} from '../types';
|
||||
|
||||
export type EchartsTreemapFormData = QueryFormData & {
|
||||
@@ -67,12 +68,6 @@ export const DEFAULT_FORM_DATA: Partial<EchartsTreemapFormData> = {
|
||||
dateFormat: 'smart_date',
|
||||
emitFilter: false,
|
||||
};
|
||||
|
||||
export interface TreePathInfo {
|
||||
name: string;
|
||||
dataIndex: number;
|
||||
value: number | number[];
|
||||
}
|
||||
export interface TreemapSeriesCallbackDataParams extends CallbackDataParams {
|
||||
treePathInfo?: TreePathInfo[];
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export { default as EchartsFunnelChartPlugin } from './Funnel';
|
||||
export { default as EchartsTreeChartPlugin } from './Tree';
|
||||
export { default as EchartsTreemapChartPlugin } from './Treemap';
|
||||
export { BigNumberChartPlugin, BigNumberTotalChartPlugin } from './BigNumber';
|
||||
export { default as EchartsSunburstChartPlugin } from './Sunburst';
|
||||
|
||||
export { default as BoxPlotTransformProps } from './BoxPlot/transformProps';
|
||||
export { default as FunnelTransformProps } from './Funnel/transformProps';
|
||||
@@ -44,6 +45,7 @@ export { default as RadarTransformProps } from './Radar/transformProps';
|
||||
export { default as TimeseriesTransformProps } from './Timeseries/transformProps';
|
||||
export { default as TreeTransformProps } from './Tree/transformProps';
|
||||
export { default as TreemapTransformProps } from './Treemap/transformProps';
|
||||
export { default as SunburstTransformProps } from './Sunburst/transformProps';
|
||||
|
||||
export { DEFAULT_FORM_DATA as TimeseriesDefaultFormData } from './Timeseries/constants';
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
ChartDataResponseResult,
|
||||
ChartProps,
|
||||
HandlerFunction,
|
||||
PlainObject,
|
||||
QueryFormColumn,
|
||||
SetDataMaskHook,
|
||||
} from '@superset-ui/core';
|
||||
@@ -111,7 +112,7 @@ export enum LabelPositionEnum {
|
||||
InsideBottomRight = 'insideBottomRight',
|
||||
}
|
||||
|
||||
export interface BaseChartProps<T> extends ChartProps<T> {
|
||||
export interface BaseChartProps<T extends PlainObject> extends ChartProps<T> {
|
||||
queriesData: ChartDataResponseResult[];
|
||||
}
|
||||
|
||||
@@ -155,4 +156,10 @@ export interface TitleFormData {
|
||||
|
||||
export type StackType = boolean | null | Partial<AreaChartExtraControlsValue>;
|
||||
|
||||
export interface TreePathInfo {
|
||||
name: string;
|
||||
dataIndex: number;
|
||||
value: number | number[];
|
||||
}
|
||||
|
||||
export * from './Timeseries/types';
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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 { DataRecord } from '@superset-ui/core';
|
||||
import _ from 'lodash';
|
||||
|
||||
export type TreeNode = {
|
||||
name: string;
|
||||
value: number;
|
||||
secondaryValue: number;
|
||||
groupBy: string;
|
||||
children?: TreeNode[];
|
||||
};
|
||||
|
||||
export function treeBuilder(
|
||||
data: DataRecord[],
|
||||
groupBy: string[],
|
||||
metric: string,
|
||||
secondaryMetric?: string,
|
||||
): TreeNode[] {
|
||||
const [curGroupBy, ...restGroupby] = groupBy;
|
||||
const curData = _.groupBy(data, curGroupBy);
|
||||
return _.transform(
|
||||
curData,
|
||||
(result, value, name) => {
|
||||
if (!restGroupby.length) {
|
||||
(value ?? []).forEach(datum => {
|
||||
const metricValue = getMetricValue(datum, metric);
|
||||
const secondaryValue = secondaryMetric
|
||||
? getMetricValue(datum, secondaryMetric)
|
||||
: metricValue;
|
||||
const item = {
|
||||
name,
|
||||
value: metricValue,
|
||||
secondaryValue,
|
||||
groupBy: curGroupBy,
|
||||
};
|
||||
result.push(item);
|
||||
});
|
||||
} else {
|
||||
const children = treeBuilder(
|
||||
value,
|
||||
restGroupby,
|
||||
metric,
|
||||
secondaryMetric,
|
||||
);
|
||||
const metricValue = children.reduce(
|
||||
(prev, cur) => prev + (cur.value as number),
|
||||
0,
|
||||
);
|
||||
const secondaryValue = secondaryMetric
|
||||
? children.reduce(
|
||||
(prev, cur) => prev + (cur.secondaryValue as number),
|
||||
0,
|
||||
)
|
||||
: metricValue;
|
||||
result.push({
|
||||
name,
|
||||
children,
|
||||
value: metricValue,
|
||||
secondaryValue,
|
||||
groupBy: curGroupBy,
|
||||
});
|
||||
}
|
||||
},
|
||||
[] as TreeNode[],
|
||||
);
|
||||
}
|
||||
|
||||
function getMetricValue(datum: DataRecord, metric: string) {
|
||||
return _.isNumber(datum[metric]) ? (datum[metric] as number) : 0;
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* 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 { treeBuilder } from '../../src/utils/treeBuilder';
|
||||
|
||||
describe('test treeBuilder', () => {
|
||||
const data = [
|
||||
{
|
||||
foo: 'a-1',
|
||||
bar: 'a',
|
||||
count: 2,
|
||||
count2: 3,
|
||||
},
|
||||
{
|
||||
foo: 'a-2',
|
||||
bar: 'a',
|
||||
count: 2,
|
||||
count2: 3,
|
||||
},
|
||||
{
|
||||
foo: 'b-1',
|
||||
bar: 'b',
|
||||
count: 2,
|
||||
count2: 3,
|
||||
},
|
||||
{
|
||||
foo: 'b-2',
|
||||
bar: 'b',
|
||||
count: 2,
|
||||
count2: 3,
|
||||
},
|
||||
{
|
||||
foo: 'c-1',
|
||||
bar: 'c',
|
||||
count: 2,
|
||||
count2: 3,
|
||||
},
|
||||
{
|
||||
foo: 'c-2',
|
||||
bar: 'c',
|
||||
count: 2,
|
||||
count2: 3,
|
||||
},
|
||||
{
|
||||
foo: 'd-1',
|
||||
bar: 'd',
|
||||
count: 2,
|
||||
count2: 3,
|
||||
},
|
||||
];
|
||||
it('should build tree as expected', () => {
|
||||
const tree = treeBuilder(data, ['foo', 'bar'], 'count');
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'a',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'a-1',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'a',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'a-2',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'b',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'b-1',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'b',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'b-2',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'c',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'c-1',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'c',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'c-2',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'd',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'd-1',
|
||||
secondaryValue: 2,
|
||||
value: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should build tree with secondaryValue as expected', () => {
|
||||
const tree = treeBuilder(data, ['foo', 'bar'], 'count', 'count2');
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'a',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'a-1',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'a',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'a-2',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'b',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'b-1',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'b',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'b-2',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'c',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'c-1',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'c',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'c-2',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
groupBy: 'bar',
|
||||
name: 'd',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
groupBy: 'foo',
|
||||
name: 'd-1',
|
||||
secondaryValue: 3,
|
||||
value: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user