Skip to content

Commit 0172967

Browse files
[Explore Vis]Feat/add bar gauge (#10697)
Introduced bar gauge as a new type of visualization --------- Signed-off-by: Qxisylolo <[email protected]> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
1 parent db9b657 commit 0172967

23 files changed

+1722
-77
lines changed

changelogs/fragments/10697.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- Add bar gauge ([#10697](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10697))

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,6 @@ export const createBarSpec = (
120120
: undefined,
121121
data: { values: transformedData },
122122
layer: layers,
123-
// Add legend configuration if needed, or explicitly set to null if disabled
124-
legend: styles.addLegend
125-
? {
126-
orient: styles.legendPosition?.toLowerCase() || 'right',
127-
title: styles.legendTitle,
128-
symbolType: styles.legendShape ?? 'circle',
129-
}
130-
: null,
131123
};
132124
};
133125

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from 'react';
7+
import { render, fireEvent } from '@testing-library/react';
8+
import { BarGaugeExclusiveVisOptions } from './bar_gauge_exclusive_vis_options';
9+
import { ExclusiveBarGaugeConfig } from './bar_gauge_vis_config';
10+
11+
jest.mock('@osd/i18n', () => ({
12+
i18n: {
13+
translate: jest.fn().mockImplementation((id, { defaultMessage }) => defaultMessage),
14+
},
15+
}));
16+
17+
describe('BarGaugeExclusiveVisOptions', () => {
18+
const defaultStyles: ExclusiveBarGaugeConfig = {
19+
orientation: 'vertical',
20+
displayMode: 'gradient',
21+
valueDisplay: 'valueColor',
22+
showUnfilledArea: true,
23+
};
24+
25+
const mockOnChange = jest.fn();
26+
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
});
30+
31+
it('should call onChange when orientation is changed', () => {
32+
const { getByText } = render(
33+
<BarGaugeExclusiveVisOptions
34+
styles={defaultStyles}
35+
onChange={mockOnChange}
36+
isXaxisNumerical={false}
37+
/>
38+
);
39+
40+
fireEvent.click(getByText('Horizontal'));
41+
42+
expect(mockOnChange).toHaveBeenCalledWith({
43+
...defaultStyles,
44+
orientation: 'horizontal',
45+
});
46+
});
47+
48+
it('should call onChange when display mode is changed', () => {
49+
const { getByText } = render(
50+
<BarGaugeExclusiveVisOptions
51+
styles={defaultStyles}
52+
onChange={mockOnChange}
53+
isXaxisNumerical={false}
54+
/>
55+
);
56+
57+
fireEvent.click(getByText('Stack'));
58+
59+
expect(mockOnChange).toHaveBeenCalledWith({
60+
...defaultStyles,
61+
displayMode: 'stack',
62+
});
63+
});
64+
65+
it('should call onChange when value display is changed', () => {
66+
const { getByText } = render(
67+
<BarGaugeExclusiveVisOptions
68+
styles={defaultStyles}
69+
onChange={mockOnChange}
70+
isXaxisNumerical={false}
71+
/>
72+
);
73+
74+
fireEvent.click(getByText('Text Color'));
75+
76+
expect(mockOnChange).toHaveBeenCalledWith({
77+
...defaultStyles,
78+
valueDisplay: 'textColor',
79+
});
80+
});
81+
82+
it('should call onChange when unfilled area switch is toggled', () => {
83+
const { getByRole } = render(
84+
<BarGaugeExclusiveVisOptions
85+
styles={defaultStyles}
86+
onChange={mockOnChange}
87+
isXaxisNumerical={false}
88+
/>
89+
);
90+
91+
const switchElement = getByRole('switch');
92+
fireEvent.click(switchElement);
93+
94+
expect(mockOnChange).toHaveBeenCalledWith({
95+
...defaultStyles,
96+
showUnfilledArea: false,
97+
});
98+
});
99+
100+
it('should handle undefined styles with defaults', () => {
101+
const { getByText } = render(
102+
<BarGaugeExclusiveVisOptions
103+
styles={undefined as any}
104+
onChange={mockOnChange}
105+
isXaxisNumerical={false}
106+
/>
107+
);
108+
109+
fireEvent.click(getByText('Horizontal'));
110+
111+
expect(mockOnChange).toHaveBeenCalledWith({
112+
orientation: 'horizontal',
113+
});
114+
});
115+
});
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { i18n } from '@osd/i18n';
7+
import { EuiSwitch, EuiButtonGroup, EuiFormRow } from '@elastic/eui';
8+
import React from 'react';
9+
import { BarGaugeChartStyle } from './bar_gauge_vis_config';
10+
import { StyleAccordion } from '../style_panel/style_accordion';
11+
12+
interface BarGaugeVisOptionsProps {
13+
styles: BarGaugeChartStyle['exclusive'];
14+
onChange: (styles: BarGaugeChartStyle['exclusive']) => void;
15+
isXaxisNumerical: boolean;
16+
}
17+
18+
const displayModeOption = [
19+
{
20+
id: 'gradient',
21+
label: i18n.translate('explore.vis.barGauge.displayMode.gradient', {
22+
defaultMessage: 'Gradient',
23+
}),
24+
},
25+
{
26+
id: 'stack',
27+
label: i18n.translate('explore.vis.barGauge.displayMode.stack', {
28+
defaultMessage: 'Stack',
29+
}),
30+
},
31+
{
32+
id: 'basic',
33+
label: i18n.translate('explore.vis.barGauge.displayMode.basic', {
34+
defaultMessage: 'Basic',
35+
}),
36+
},
37+
];
38+
39+
const valueDisplayOption = [
40+
{
41+
id: 'valueColor',
42+
label: i18n.translate('explore.vis.barGauge.valueDisplay.valueColor', {
43+
defaultMessage: 'Value Color',
44+
}),
45+
},
46+
{
47+
id: 'textColor',
48+
label: i18n.translate('explore.vis.barGauge.valueDisplay.textColor', {
49+
defaultMessage: 'Text Color',
50+
}),
51+
},
52+
{
53+
id: 'hidden',
54+
label: i18n.translate('explore.vis.barGauge.valueDisplay.hidden', {
55+
defaultMessage: 'Hidden',
56+
}),
57+
},
58+
];
59+
60+
export const BarGaugeExclusiveVisOptions = ({
61+
styles,
62+
onChange,
63+
isXaxisNumerical,
64+
}: BarGaugeVisOptionsProps) => {
65+
const getOrientationOptions = () => {
66+
const horizontalLabel = i18n.translate('explore.vis.barGauge.orientation.horizontal', {
67+
defaultMessage: 'Horizontal',
68+
});
69+
const verticalLabel = i18n.translate('explore.vis.barGauge.orientation.vertical', {
70+
defaultMessage: 'Vertical',
71+
});
72+
73+
// When X-axis is numerical, the labels are swapped
74+
const verticalOptionLabel = isXaxisNumerical ? horizontalLabel : verticalLabel;
75+
const horizontalOptionLabel = isXaxisNumerical ? verticalLabel : horizontalLabel;
76+
77+
return [
78+
{ id: 'vertical', label: verticalOptionLabel },
79+
{ id: 'horizontal', label: horizontalOptionLabel },
80+
];
81+
};
82+
83+
const orientationOption = getOrientationOptions();
84+
85+
const updateExclusiveOption = (key: keyof BarGaugeChartStyle['exclusive'], value: any) => {
86+
onChange({
87+
...styles,
88+
[key]: value,
89+
});
90+
};
91+
92+
return (
93+
<StyleAccordion
94+
id="barGaugeSection"
95+
accordionLabel={i18n.translate('explore.stylePanel.tabs.barGauge', {
96+
defaultMessage: 'Bar Gauge',
97+
})}
98+
initialIsOpen={true}
99+
data-test-subj="barGaugeExclusivePanel"
100+
>
101+
<EuiFormRow
102+
label={i18n.translate('explore.stylePanel.barGauge.exclusive.orientation', {
103+
defaultMessage: 'Orientation',
104+
})}
105+
>
106+
<EuiButtonGroup
107+
legend={i18n.translate('explore.stylePanel.barGauge.exclusive.orientation', {
108+
defaultMessage: 'Orientation',
109+
})}
110+
isFullWidth
111+
options={orientationOption}
112+
onChange={(optionId) => {
113+
updateExclusiveOption('orientation', optionId);
114+
}}
115+
type="single"
116+
idSelected={styles?.orientation ?? 'vertical'}
117+
buttonSize="compressed"
118+
/>
119+
</EuiFormRow>
120+
121+
<EuiFormRow
122+
label={i18n.translate('explore.stylePanel.barGauge.exclusive.displayMode', {
123+
defaultMessage: 'Display mode',
124+
})}
125+
>
126+
<EuiButtonGroup
127+
legend={i18n.translate('explore.stylePanel.barGauge.exclusive.displayMode', {
128+
defaultMessage: 'Display mode',
129+
})}
130+
isFullWidth
131+
options={displayModeOption}
132+
onChange={(optionId) => {
133+
updateExclusiveOption('displayMode', optionId);
134+
}}
135+
type="single"
136+
idSelected={styles?.displayMode ?? 'gradient'}
137+
buttonSize="compressed"
138+
/>
139+
</EuiFormRow>
140+
141+
<EuiFormRow
142+
label={i18n.translate('explore.stylePanel.barGauge.exclusive.valueDisplay', {
143+
defaultMessage: 'Value display',
144+
})}
145+
>
146+
<EuiButtonGroup
147+
legend={i18n.translate('explore.stylePanel.barGauge.exclusive.valueDisplay', {
148+
defaultMessage: 'Value display',
149+
})}
150+
isFullWidth
151+
options={valueDisplayOption}
152+
onChange={(optionId) => {
153+
updateExclusiveOption('valueDisplay', optionId);
154+
}}
155+
type="single"
156+
idSelected={styles?.valueDisplay ?? 'valueColor'}
157+
buttonSize="compressed"
158+
/>
159+
</EuiFormRow>
160+
161+
<EuiFormRow>
162+
<EuiSwitch
163+
compressed
164+
label={i18n.translate('explore.vis.heatmap.showUnfilledArea', {
165+
defaultMessage: 'Show unfilled area',
166+
})}
167+
checked={styles?.showUnfilledArea ?? false}
168+
onChange={(e) => updateExclusiveOption('showUnfilledArea', e.target.checked)}
169+
/>
170+
</EuiFormRow>
171+
</StyleAccordion>
172+
);
173+
};

0 commit comments

Comments
 (0)