Skip to content

Commit 8e867b7

Browse files
[Explore Vis] Separate histogram from bar (#10823)
--------- Signed-off-by: Qxisylolo <[email protected]> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
1 parent 964bdfb commit 8e867b7

27 files changed

+1211
-313
lines changed

changelogs/fragments/10823.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fix:
2+
- Separate histogram from bar ([#10823](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10823))

src/plugins/explore/public/components/visualizations/bar/bar_exclusive_vis_options.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jest.mock('../utils/use_debounced_value', () => ({
1616

1717
describe('BarExclusiveVisOptions', () => {
1818
const defaultProps = {
19+
type: 'bar',
1920
barSizeMode: 'manual' as 'manual' | 'auto',
2021
barWidth: 0.7,
2122
barPadding: 0.1,

src/plugins/explore/public/components/visualizations/bar/bar_exclusive_vis_options.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { DebouncedFieldNumber } from '../style_panel/utils';
1919
import { defaultBarChartStyles } from './bar_vis_config';
2020

2121
interface BarExclusiveVisOptionsProps {
22+
type: 'bar' | 'histogram';
2223
barSizeMode: 'auto' | 'manual';
2324
barWidth: number;
2425
barPadding: number;
@@ -37,6 +38,7 @@ interface BarExclusiveVisOptionsProps {
3738
}
3839

3940
export const BarExclusiveVisOptions = ({
41+
type,
4042
barSizeMode,
4143
barWidth,
4244
barPadding,
@@ -68,14 +70,17 @@ export const BarExclusiveVisOptions = ({
6870
},
6971
];
7072

73+
const barAccordionMessage =
74+
type === 'bar'
75+
? i18n.translate('explore.vis.barChart.exclusiveSettings', {
76+
defaultMessage: 'Bar',
77+
})
78+
: i18n.translate('explore.vis.histogramChart.exclusiveSettings', {
79+
defaultMessage: 'Histogram',
80+
});
81+
7182
return (
72-
<StyleAccordion
73-
id="barSection"
74-
accordionLabel={i18n.translate('explore.vis.barChart.exclusiveSettings', {
75-
defaultMessage: 'Bar',
76-
})}
77-
initialIsOpen={true}
78-
>
83+
<StyleAccordion id="barSection" accordionLabel={barAccordionMessage} initialIsOpen={true}>
7984
{!shouldDisableUseThresholdColor && (
8085
<EuiFormRow>
8186
<EuiSwitch

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,6 @@ export const createBarConfig = (): VisualizationType<'bar'> => ({
234234
[AxisRole.X]: { type: VisFieldType.Numerical, index: 0 },
235235
[AxisRole.Y]: { type: VisFieldType.Numerical, index: 1 },
236236
},
237-
{
238-
[AxisRole.X]: { type: VisFieldType.Numerical, index: 0 },
239-
},
240237
],
241238
},
242239
});

src/plugins/explore/public/components/visualizations/bar/bar_vis_options.test.tsx

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -536,19 +536,11 @@ describe('BarVisStyleControls', () => {
536536
});
537537
});
538538

539-
test('render bucket panel with num bucket type', async () => {
539+
test('render bucket panel with category bucket', async () => {
540540
const propsWithNumBucket = {
541541
...defaultProps,
542542
axisColumnMappings: {
543543
...mockAxisColumnMappings,
544-
x: {
545-
id: 1,
546-
name: 'Numerical X',
547-
schema: VisFieldType.Numerical,
548-
column: 'numX',
549-
validValuesCount: 100,
550-
uniqueValuesCount: 50,
551-
},
552544
},
553545
};
554546

@@ -559,21 +551,9 @@ describe('BarVisStyleControls', () => {
559551
);
560552

561553
expect(screen.getByTestId('mockBucketOptionsPanel')).toBeInTheDocument();
562-
expect(screen.getByTestId('bucketType')).toHaveTextContent('num');
563-
expect(screen.getByTestId('mockUpdateBucketSize')).toBeInTheDocument();
564-
expect(screen.getByTestId('mockUpdateBucketCount')).toBeInTheDocument();
554+
expect(screen.getByTestId('bucketType')).toHaveTextContent('cate');
565555
expect(screen.getByTestId('mockUpdateAggregation')).toBeInTheDocument();
566556

567-
await userEvent.click(screen.getByTestId('mockUpdateBucketSize'));
568-
expect(defaultProps.onStyleChange).toHaveBeenCalledWith({
569-
bucket: { bucketTimeUnit: 'auto', aggregationType: 'sum', bucketSize: 100 },
570-
});
571-
572-
await userEvent.click(screen.getByTestId('mockUpdateBucketCount'));
573-
expect(defaultProps.onStyleChange).toHaveBeenCalledWith({
574-
bucket: { bucketTimeUnit: 'auto', aggregationType: 'sum', bucketCount: 20 },
575-
});
576-
577557
await userEvent.click(screen.getByTestId('mockUpdateAggregation'));
578558
expect(defaultProps.onStyleChange).toHaveBeenCalledWith({
579559
bucket: { bucketTimeUnit: 'auto', aggregationType: 'sum' },

src/plugins/explore/public/components/visualizations/bar/bar_vis_options.tsx

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,14 @@ export const BarVisStyleControls: React.FC<BarVisStyleControlsProps> = ({
3939
};
4040

4141
const axes = [axisColumnMappings[AxisRole.X], axisColumnMappings[AxisRole.Y]];
42-
const hasCategory = axes.some((axis) => axis?.schema === VisFieldType.Categorical);
43-
const hasNum = axes.some((axis) => axis?.schema === VisFieldType.Numerical);
4442
const hasDate = axes.some((axis) => axis?.schema === VisFieldType.Date);
4543

46-
// 4 bucket types for bar chart:
47-
// 1. time-numerical(Regular histogram): requires one axis to be date type, bucket options: time interval + aggregationType
44+
// 3 bucket types for bar chart:
45+
// 1. time-numerical: requires one axis to be date type, bucket options: time interval + aggregationType
4846
// 2. categorical-numerical: requires one axis to be categorical type, bucket options: aggregationType only
49-
// 3. numerical-numerical: both x and y axes are numerical, bucket options: bucket size + bucket count + aggregationType
50-
// 4. single-numerical: only x-axis mapped (numerical), y-axis displays count of records, bucket options: bucket size + bucket count
51-
const bucketType = hasDate
52-
? 'time'
53-
: hasCategory
54-
? 'cate'
55-
: hasNum && axisColumnMappings[AxisRole.Y] !== undefined
56-
? 'num'
57-
: 'single';
47+
// 3. numerical-numerical: both x and y axes are numerical, treated as categorical, bucket options: aggregationType only
48+
// Note: single-numerical has been moved to histogram
49+
const bucketType = hasDate ? 'time' : 'cate';
5850

5951
// The mapping object will be an empty object if no fields are selected on the axes selector. No
6052
// visualization is generated in this case so we shouldn't display style option panels.
@@ -78,6 +70,7 @@ export const BarVisStyleControls: React.FC<BarVisStyleControlsProps> = ({
7870
<>
7971
<EuiFlexItem grow={false}>
8072
<BarExclusiveVisOptions
73+
type="bar"
8174
barSizeMode={styleOptions.barSizeMode}
8275
barWidth={styleOptions.barWidth}
8376
barPadding={styleOptions.barPadding}

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

Lines changed: 5 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import {
99
createTimeBarChart,
1010
createGroupedTimeBarChart,
1111
createFacetedTimeBarChart,
12-
createNumericalHistogramBarChart,
13-
createSingleBarChart,
12+
createDoubleNumericalBarChart,
1413
} from './to_expression';
1514
import { defaultBarChartStyles, BarChartStyle } from './bar_vis_config';
1615
import {
@@ -1151,7 +1150,7 @@ describe('bar to_expression', () => {
11511150
});
11521151
});
11531152

1154-
describe('createNumericalHistogramBarChart', () => {
1153+
describe('createDoubleNumericalBarChart', () => {
11551154
const mockNumericalColumn2 = {
11561155
id: 2,
11571156
name: 'sum',
@@ -1166,7 +1165,7 @@ describe('bar to_expression', () => {
11661165
[AxisRole.Y]: mockNumericalColumn2,
11671166
};
11681167
test('creates a numerical histogram bar chart spec', () => {
1169-
const spec = createNumericalHistogramBarChart(
1168+
const spec = createDoubleNumericalBarChart(
11701169
mockData,
11711170
[mockNumericalColumn, mockNumericalColumn2],
11721171
defaultBarChartStyles,
@@ -1181,7 +1180,7 @@ describe('bar to_expression', () => {
11811180
expect(mainLayer.mark.type).toBe('bar');
11821181
expect(mainLayer.mark.tooltip).toBe(true);
11831182
expect(mainLayer.encoding.x.field).toBe('count');
1184-
expect(mainLayer.encoding.x.type).toBe('quantitative');
1183+
expect(mainLayer.encoding.x.type).toBe('nominal');
11851184
expect(mainLayer.encoding.y.field).toBe('sum');
11861185
expect(mainLayer.encoding.y.aggregate).toBe('sum');
11871186
expect(mainLayer.encoding.y.type).toBe('quantitative');
@@ -1199,73 +1198,18 @@ describe('bar to_expression', () => {
11991198
const stylesWithBucket = {
12001199
...defaultBarChartStyles,
12011200
bucket: {
1202-
bucketSize: 50,
12031201
aggregationType: AggregationType.SUM,
12041202
},
12051203
};
12061204

1207-
const spec = createNumericalHistogramBarChart(
1205+
const spec = createDoubleNumericalBarChart(
12081206
mockData,
12091207
[mockNumericalColumn, mockNumericalColumn2],
12101208
stylesWithBucket,
12111209
mockAxisColumnMappings
12121210
);
12131211

1214-
expect(spec.layer[0].encoding.x.bin.step).toBe(50);
12151212
expect(spec.layer[0].encoding.y.aggregate).toBe('sum');
12161213
});
12171214
});
1218-
1219-
describe('createSingleBarChart', () => {
1220-
const mockAxisColumnMappings = {
1221-
[AxisRole.X]: mockNumericalColumn,
1222-
};
1223-
test('creates a numerical histogram bar chart spec', () => {
1224-
const spec = createSingleBarChart(
1225-
mockData,
1226-
[mockNumericalColumn],
1227-
defaultBarChartStyles,
1228-
mockAxisColumnMappings
1229-
);
1230-
1231-
expect(spec.$schema).toBe(VEGASCHEMA);
1232-
expect(spec.data.values).toBe(mockData);
1233-
expect(spec.layer).toHaveLength(1);
1234-
1235-
const mainLayer = spec.layer[0];
1236-
expect(mainLayer.mark.type).toBe('bar');
1237-
expect(mainLayer.mark.tooltip).toBe(true);
1238-
expect(mainLayer.encoding.x.field).toBe('count');
1239-
expect(mainLayer.encoding.x.type).toBe('quantitative');
1240-
expect(spec.layer[0].encoding.y.aggregate).toBe('count');
1241-
1242-
// select time range params
1243-
expect(spec.params).not.toEqual(
1244-
expect.arrayContaining([expect.objectContaining({ name: 'applyTimeFilter' })])
1245-
);
1246-
expect(mainLayer.params).not.toEqual(
1247-
expect.arrayContaining([expect.objectContaining({ name: 'timeRangeBrush' })])
1248-
);
1249-
});
1250-
1251-
test('applies bucket options correctly, single bar aggregation should only be count', () => {
1252-
const stylesWithBucket = {
1253-
...defaultBarChartStyles,
1254-
bucket: {
1255-
bucketSize: 50,
1256-
aggregationType: AggregationType.SUM,
1257-
},
1258-
};
1259-
1260-
const spec = createSingleBarChart(
1261-
mockData,
1262-
[mockNumericalColumn],
1263-
stylesWithBucket,
1264-
mockAxisColumnMappings
1265-
);
1266-
1267-
expect(spec.layer[0].encoding.x.bin.step).toBe(50);
1268-
expect(spec.layer[0].encoding.y.aggregate).toBe('count');
1269-
});
1270-
});
12711215
});

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

Lines changed: 3 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
VisColumn,
1111
VisFieldType,
1212
TimeUnit,
13-
AggregationType,
1413
} from '../types';
1514
import { BarChartStyle, defaultBarChartStyles } from './bar_vis_config';
1615
import { createThresholdLayer } from '../style_panel/threshold/threshold_utils';
@@ -25,7 +24,6 @@ import {
2524
inferTimeIntervals,
2625
buildEncoding,
2726
buildTooltipEncoding,
28-
adjustBucketBins,
2927
buildThresholdColorEncoding,
3028
} from './bar_chart_utils';
3129
import { DEFAULT_OPACITY } from '../constants';
@@ -595,7 +593,7 @@ export const createStackedBarSpec = (
595593
return spec;
596594
};
597595

598-
export const createNumericalHistogramBarChart = (
596+
export const createDoubleNumericalBarChart = (
599597
transformedData: Array<Record<string, any>>,
600598
numericalColumns: VisColumn[],
601599
styleOptions: BarChartStyle,
@@ -632,8 +630,7 @@ export const createNumericalHistogramBarChart = (
632630
encoding: {
633631
x: {
634632
field: xAxis?.column,
635-
type: getSchemaByAxis(xAxis),
636-
bin: adjustBucketBins(styles?.bucket, transformedData, xAxis?.column),
633+
type: 'nominal',
637634
axis: applyAxisStyling({ axis: xAxis, axisStyle: xAxisStyle }),
638635
},
639636
y: {
@@ -647,8 +644,7 @@ export const createNumericalHistogramBarChart = (
647644
tooltip: [
648645
{
649646
field: xAxis?.column,
650-
type: getSchemaByAxis(xAxis),
651-
bin: adjustBucketBins(styles?.bucket, transformedData, xAxis?.column),
647+
type: 'nominal',
652648
title: xAxisStyle?.title?.text || xAxis?.name,
653649
},
654650
{
@@ -680,85 +676,3 @@ export const createNumericalHistogramBarChart = (
680676
layer: layers,
681677
};
682678
};
683-
684-
export const createSingleBarChart = (
685-
transformedData: Array<Record<string, any>>,
686-
numericalColumns: VisColumn[],
687-
styleOptions: BarChartStyle,
688-
axisColumnMappings?: AxisColumnMappings
689-
): any => {
690-
// Check if we have the required columns
691-
if (numericalColumns.length < 1) {
692-
throw new Error('Histogram bar chart requires at least one numerical column');
693-
}
694-
695-
const styles = { ...defaultBarChartStyles, ...styleOptions };
696-
const { xAxis, xAxisStyle, yAxis, yAxisStyle } = getSwappedAxisRole(styles, axisColumnMappings);
697-
698-
const layers: any[] = [];
699-
700-
// Configure bar mark
701-
const barMark: any = {
702-
type: 'bar',
703-
tooltip: styles.tooltipOptions?.mode !== 'hidden',
704-
};
705-
706-
const colorEncodingLayer = buildThresholdColorEncoding(undefined, styleOptions);
707-
708-
configureBarSizeAndSpacing(barMark, styles);
709-
710-
// Add border if enabled
711-
if (styles.showBarBorder) {
712-
barMark.stroke = styles.barBorderColor || '#000000';
713-
barMark.strokeWidth = styles.barBorderWidth || 1;
714-
}
715-
716-
const mainLayer = {
717-
mark: barMark,
718-
encoding: {
719-
x: {
720-
field: xAxis?.column,
721-
type: getSchemaByAxis(xAxis),
722-
bin: adjustBucketBins(styles?.bucket, transformedData, xAxis?.column),
723-
axis: applyAxisStyling({ axis: xAxis, axisStyle: xAxisStyle }),
724-
},
725-
y: {
726-
aggregate: AggregationType.COUNT,
727-
type: 'quantitative',
728-
axis: applyAxisStyling({ axis: yAxis, axisStyle: yAxisStyle }),
729-
},
730-
color: styleOptions?.useThresholdColor ? colorEncodingLayer : [],
731-
...(styles.tooltipOptions?.mode !== 'hidden' && {
732-
tooltip: [
733-
{
734-
field: xAxis?.column,
735-
type: getSchemaByAxis(xAxis),
736-
bin: adjustBucketBins(styles?.bucket, transformedData, xAxis?.column),
737-
title: xAxisStyle?.title?.text || xAxis?.name,
738-
},
739-
{
740-
aggregate: AggregationType.COUNT,
741-
title: yAxisStyle?.title?.text || yAxis?.name,
742-
},
743-
],
744-
}),
745-
},
746-
};
747-
748-
layers.push(mainLayer);
749-
750-
// Add threshold layer if enabled
751-
const thresholdLayer = createThresholdLayer(styles?.thresholdOptions, 'y');
752-
if (thresholdLayer) {
753-
layers.push(...thresholdLayer.layer);
754-
}
755-
756-
return {
757-
$schema: VEGASCHEMA,
758-
title: styles.titleOptions?.show
759-
? styles.titleOptions?.titleName || `Record counts of ${xAxis?.name}`
760-
: undefined,
761-
data: { values: transformedData },
762-
layer: layers,
763-
};
764-
};

src/plugins/explore/public/components/visualizations/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export const CHART_METADATA: Record<ChartType, ChartMetadata> = {
1919
gauge: { type: 'gauge', name: 'Gauge', icon: 'visGauge' },
2020
state_timeline: { type: 'state_timeline', name: 'State timeline', icon: 'visBarHorizontal' },
2121
bar_gauge: { type: 'bar_gauge', name: 'Bar Gauge', icon: 'visBarHorizontal' },
22+
// TODO add histogram icons
23+
histogram: { type: 'histogram', name: 'Histogram', icon: 'visBarVertical' },
2224
};
2325

2426
// Map both OSD_FIELD_TYPES and OPENSEARCH_FIELD_TYPES to VisFieldType

0 commit comments

Comments
 (0)