Skip to content

Commit 3af700d

Browse files
[Explore Vis] migrate line and facet bar chart to echart (#11113)
* add line chart Signed-off-by: Qxisylolo <qianxisy@amazon.com> * add facet bar Signed-off-by: Qxisylolo <qianxisy@amazon.com> * adjust and comments Signed-off-by: Qxisylolo <qianxisy@amazon.com> * Changeset file for PR #11113 created/updated * comments Signed-off-by: Qxisylolo <qianxisy@amazon.com> * add linebar Signed-off-by: Qxisylolo <qianxisy@amazon.com> * comments Signed-off-by: Qxisylolo <qianxisy@amazon.com> --------- Signed-off-by: Qxisylolo <qianxisy@amazon.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
1 parent d053e90 commit 3af700d

File tree

9 files changed

+755
-59
lines changed

9 files changed

+755
-59
lines changed

changelogs/fragments/11113.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- Migrate line and facet bar chart to echart ([#11113](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/11113))

src/plugins/explore/public/components/visualizations/bar/bar_chart_utils.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,16 +207,11 @@ export const createBarSeries = <T extends BaseChartStyle>({
207207
categoryField: string;
208208
seriesFields: string[] | ((headers?: string[]) => string[]);
209209
}): PipelineFn<T> => (state) => {
210-
const { axisConfig, axisColumnMappings, transformedData = [] } = state;
210+
const { axisColumnMappings, transformedData = [] } = state;
211211
const newState = { ...state };
212-
const source = transformedData[transformedData?.length - 1];
213-
214-
if (!axisConfig) {
215-
throw new Error('axisConfig must be derived before createBarSeries');
216-
}
217212

218213
if (!Array.isArray(seriesFields)) {
219-
seriesFields = seriesFields(source[0]);
214+
seriesFields = seriesFields(transformedData[0]);
220215
}
221216

222217
const thresholdLines = generateThresholdLines(styles?.thresholdOptions, styles?.switchAxes);
@@ -251,3 +246,54 @@ export const createBarSeries = <T extends BaseChartStyle>({
251246

252247
return newState;
253248
};
249+
250+
export const createFacetBarSeries = <T extends BaseChartStyle>({
251+
styles,
252+
categoryField,
253+
seriesFields,
254+
}: {
255+
styles: BarChartStyle;
256+
categoryField: string;
257+
seriesFields: (headers?: string[]) => string[];
258+
}): PipelineFn<T> => (state) => {
259+
const { transformedData } = state;
260+
261+
const newState = { ...state };
262+
const thresholdLines = generateThresholdLines(styles?.thresholdOptions, styles?.switchAxes);
263+
264+
const allSeries = transformedData?.map((seriesData: any[], index: number) => {
265+
const header = seriesData[0];
266+
const cateColumns = seriesFields(header);
267+
268+
return cateColumns.map((item: string, i: number) => ({
269+
name: String(item),
270+
type: 'bar',
271+
stack: `stack_${index}`, // each grid should have a exclusive stack key
272+
encode: {
273+
[adjustOppositeSymbol(styles?.switchAxes, 'x')]: categoryField,
274+
[adjustOppositeSymbol(styles?.switchAxes, 'y')]: item,
275+
},
276+
datasetIndex: index,
277+
gridIndex: index,
278+
xAxisIndex: index,
279+
yAxisIndex: index,
280+
emphasis: {
281+
focus: 'self',
282+
},
283+
barWidth: styles.barSizeMode === 'manual' ? `${(styles.barWidth || 0.7) * 100}%` : undefined,
284+
barCategoryGap:
285+
styles.barSizeMode === 'manual' ? `${(styles.barPadding || 0.1) * 100}%` : undefined,
286+
...(styles.showBarBorder && {
287+
itemStyle: {
288+
borderWidth: styles.barBorderWidth,
289+
borderColor: styles.barBorderColor,
290+
},
291+
}),
292+
...(i === 0 && thresholdLines),
293+
}));
294+
});
295+
296+
newState.series = allSeries?.flat() as BarSeriesOption[];
297+
298+
return newState;
299+
};

src/plugins/explore/public/components/visualizations/bar/to_expression.ts

Lines changed: 125 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
buildTooltipEncoding,
2929
buildThresholdColorEncoding,
3030
createBarSeries,
31+
createFacetBarSeries,
3132
} from './bar_chart_utils';
3233
import { DEFAULT_OPACITY } from '../constants';
3334
import { createTimeRangeBrush, createTimeRangeUpdater } from '../utils/time_range_brush';
@@ -38,7 +39,13 @@ import {
3839
assembleSpec,
3940
buildVisMap,
4041
} from '../utils/echarts_spec';
41-
import { aggregate, convertTo2DArray, transform, pivot } from '../utils/data_transformation';
42+
import {
43+
aggregate,
44+
convertTo2DArray,
45+
transform,
46+
pivot,
47+
facetTransform,
48+
} from '../utils/data_transformation';
4249

4350
// Only set size and binSpacing in manual mode
4451
const configureBarSizeAndSpacing = (barMark: any, styles: BarChartStyle) => {
@@ -76,9 +83,6 @@ export const createBarSpec = (
7683
} else if (yAxis.schema === VisFieldType.Categorical) {
7784
categoryField = yAxis.column;
7885
valueField = xAxis.column;
79-
} else {
80-
categoryField = styles.switchAxes ? yAxis.column : xAxis.column;
81-
valueField = styles.switchAxes ? xAxis.column : yAxis.column;
8286
}
8387

8488
const aggregationType = styles.bucket.aggregationType ?? AggregationType.SUM;
@@ -93,7 +97,9 @@ export const createBarSpec = (
9397
),
9498
createBaseConfig,
9599
buildAxisConfigs,
96-
buildVisMap({ seriesFields: [valueField] }),
100+
buildVisMap({
101+
seriesFields: (headers) => (headers ?? []).filter((h) => h !== categoryField),
102+
}),
97103
createBarSeries({ styles, categoryField, seriesFields: [valueField] }),
98104
assembleSpec
99105
)({
@@ -220,9 +226,6 @@ export const createTimeBarChart = (
220226
} else if (yAxis.schema === VisFieldType.Date) {
221227
timeField = yAxis.column;
222228
valueField = xAxis.column;
223-
} else {
224-
timeField = styles.switchAxes ? yAxis.column : xAxis.column;
225-
valueField = styles.switchAxes ? xAxis.column : yAxis.column;
226229
}
227230

228231
const timeUnit = styles.bucket?.bucketTimeUnit ?? TimeUnit.AUTO;
@@ -239,7 +242,9 @@ export const createTimeBarChart = (
239242
),
240243
createBaseConfig,
241244
buildAxisConfigs,
242-
buildVisMap({ seriesFields: [valueField] }),
245+
buildVisMap({
246+
seriesFields: (headers) => (headers ?? []).filter((h) => h !== timeField),
247+
}),
243248
createBarSeries({
244249
styles,
245250
categoryField: timeField,
@@ -379,9 +384,6 @@ export const createGroupedTimeBarChart = (
379384
} else if (yAxis.schema === VisFieldType.Date) {
380385
timeField = yAxis.column;
381386
valueField = xAxis.column;
382-
} else {
383-
timeField = styles.switchAxes ? yAxis.column : xAxis.column;
384-
valueField = styles.switchAxes ? xAxis.column : yAxis.column;
385387
}
386388

387389
const timeUnit = styles?.bucket?.bucketTimeUnit ?? TimeUnit.AUTO;
@@ -404,7 +406,9 @@ export const createGroupedTimeBarChart = (
404406
),
405407
createBaseConfig,
406408
buildAxisConfigs,
407-
buildVisMap({ seriesFields: (headers) => (headers ?? []).filter((h) => h !== timeField) }),
409+
buildVisMap({
410+
seriesFields: (headers) => (headers ?? []).filter((h) => h !== timeField),
411+
}),
408412
createBarSeries({
409413
styles,
410414
categoryField: timeField,
@@ -541,6 +545,65 @@ export const createFacetedTimeBarChart = (
541545
axisColumnMappings?: AxisColumnMappings,
542546
timeRange?: { from: string; to: string }
543547
): any => {
548+
if (getChartRender() === 'echarts') {
549+
const styles = { ...defaultBarChartStyles, ...styleOptions };
550+
const axisConfig = getSwappedAxisRole(styles, axisColumnMappings);
551+
const xAxis = axisConfig.xAxis;
552+
const yAxis = axisConfig.yAxis;
553+
const colorColumn = axisColumnMappings?.[AxisRole.COLOR];
554+
const colorField = colorColumn?.column;
555+
556+
const facetColumn = axisColumnMappings?.[AxisRole.FACET]?.column;
557+
558+
if (!xAxis || !yAxis || !colorField || !facetColumn) {
559+
throw Error('Missing axis config for facet time bar chart');
560+
}
561+
562+
let timeField = '';
563+
let valueField = '';
564+
if (xAxis.schema === VisFieldType.Date) {
565+
timeField = xAxis.column;
566+
valueField = yAxis.column;
567+
} else if (yAxis.schema === VisFieldType.Date) {
568+
timeField = yAxis.column;
569+
valueField = xAxis.column;
570+
}
571+
572+
const timeUnit = styles?.bucket?.bucketTimeUnit ?? TimeUnit.AUTO;
573+
const aggregationType = styles?.bucket?.aggregationType ?? AggregationType.SUM;
574+
575+
const result = pipe(
576+
facetTransform(
577+
facetColumn,
578+
pivot({
579+
groupBy: timeField,
580+
pivot: colorField,
581+
field: valueField,
582+
timeUnit,
583+
aggregationType,
584+
}),
585+
convertTo2DArray()
586+
),
587+
createBaseConfig,
588+
buildAxisConfigs,
589+
buildVisMap({
590+
seriesFields: (headers) => (headers ?? []).filter((h) => h !== timeField),
591+
}),
592+
createFacetBarSeries({
593+
styles,
594+
categoryField: timeField,
595+
seriesFields: (headers) => (headers ?? []).filter((h) => h !== timeField),
596+
}),
597+
assembleSpec
598+
)({
599+
data: transformedData,
600+
styles,
601+
axisConfig,
602+
axisColumnMappings: axisColumnMappings ?? {},
603+
});
604+
return result.spec;
605+
}
606+
544607
const styles = { ...defaultBarChartStyles, ...styleOptions };
545608
const { xAxis, xAxisStyle, yAxis, yAxisStyle } = getSwappedAxisRole(styles, axisColumnMappings);
546609
const colorMapping = axisColumnMappings?.[AxisRole.COLOR];
@@ -680,9 +743,6 @@ export const createStackedBarSpec = (
680743
} else if (yAxis.schema === VisFieldType.Categorical) {
681744
categoryField = yAxis.column;
682745
valueField = xAxis.column;
683-
} else {
684-
categoryField = styles.switchAxes ? yAxis.column : xAxis.column;
685-
valueField = styles.switchAxes ? xAxis.column : yAxis.column;
686746
}
687747

688748
const aggregationType = styles?.bucket?.aggregationType ?? AggregationType.SUM;
@@ -818,6 +878,55 @@ export const createDoubleNumericalBarChart = (
818878
styleOptions: BarChartStyle,
819879
axisColumnMappings?: AxisColumnMappings
820880
): any => {
881+
if (getChartRender() === 'echarts') {
882+
const styles = { ...defaultBarChartStyles, ...styleOptions };
883+
const axisConfig = getSwappedAxisRole(styles, axisColumnMappings);
884+
const xAxis = axisConfig.xAxis;
885+
const yAxis = axisConfig.yAxis;
886+
887+
if (!xAxis || !yAxis) {
888+
throw Error('Missing axis config for Bar chart');
889+
}
890+
891+
let categoryField = '';
892+
let valueField = '';
893+
894+
categoryField = styles.switchAxes ? yAxis.column : xAxis.column;
895+
valueField = styles.switchAxes ? xAxis.column : yAxis.column;
896+
897+
const aggregationType = styles.bucket.aggregationType ?? AggregationType.SUM;
898+
const result = pipe(
899+
transform(
900+
aggregate({
901+
groupBy: categoryField,
902+
field: valueField,
903+
aggregationType,
904+
}),
905+
convertTo2DArray()
906+
),
907+
createBaseConfig,
908+
buildAxisConfigs,
909+
buildVisMap({
910+
seriesFields: (headers) => (headers ?? []).filter((h) => h !== categoryField),
911+
}),
912+
createBarSeries({ styles, categoryField, seriesFields: [valueField] }),
913+
assembleSpec
914+
)({
915+
data: transformedData,
916+
styles,
917+
axisConfig,
918+
axisColumnMappings: axisColumnMappings ?? {},
919+
});
920+
921+
if (styles.switchAxes) {
922+
result.yAxisConfig.type = 'category';
923+
} else {
924+
result.xAxisConfig.type = 'category';
925+
}
926+
927+
return result.spec;
928+
}
929+
821930
const styles = { ...defaultBarChartStyles, ...styleOptions };
822931
const { xAxis, xAxisStyle, yAxis, yAxisStyle } = getSwappedAxisRole(styles, axisColumnMappings);
823932

src/plugins/explore/public/components/visualizations/echarts_render.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export const EchartsRender = ({ spec }: Props) => {
2424
[]
2525
);
2626

27+
const gridArray = Array.isArray(spec?.grid) ? spec.grid : [];
28+
const shouldScroll = gridArray.length > 10;
29+
const widthPercentage = shouldScroll ? `${Math.ceil(gridArray.length / 10) * 100}%` : '100%';
30+
2731
useEffect(() => {
2832
if (containerRef.current) {
2933
const echartsInstance = echarts.init(containerRef.current);
@@ -46,12 +50,20 @@ export const EchartsRender = ({ spec }: Props) => {
4650
instance.setOption(
4751
{
4852
...spec,
49-
grid: { top: 60, bottom: 60, left: 60, right: 60 },
5053
},
5154
{ notMerge: true } // this is a must to update compulsorily otherwise will merge with previous option
5255
);
5356
}
5457
}, [spec, instance]);
5558

56-
return <div style={{ height: '100%', width: '100%' }} ref={containerRef} />;
59+
return (
60+
<div
61+
style={{
62+
height: '100%',
63+
overflowX: 'auto',
64+
...(shouldScroll && { width: widthPercentage }),
65+
}}
66+
ref={containerRef}
67+
/>
68+
);
5769
};

0 commit comments

Comments
 (0)