Skip to content
Merged
2 changes: 2 additions & 0 deletions changelogs/fragments/10697.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Add bar gauge ([#10697](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10697))
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,6 @@ export const createBarSpec = (
: undefined,
data: { values: transformedData },
layer: layers,
// Add legend configuration if needed, or explicitly set to null if disabled
legend: styles.addLegend
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: why this is removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just cleaning, legend should be controlled by encoding.color, I cleaned some when I was doing global thresholds, I missed to clean this one.

? {
orient: styles.legendPosition?.toLowerCase() || 'right',
title: styles.legendTitle,
symbolType: styles.legendShape ?? 'circle',
}
: null,
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { BarGaugeExclusiveVisOptions } from './bar_gauge_exclusive_vis_options';
import { ExclusiveBarGaugeConfig } from './bar_gauge_vis_config';

jest.mock('@osd/i18n', () => ({
i18n: {
translate: jest.fn().mockImplementation((id, { defaultMessage }) => defaultMessage),
},
}));

describe('BarGaugeExclusiveVisOptions', () => {
const defaultStyles: ExclusiveBarGaugeConfig = {
orientation: 'vertical',
displayMode: 'gradient',
valueDisplay: 'valueColor',
showUnfilledArea: true,
};

const mockOnChange = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

it('should call onChange when orientation is changed', () => {
const { getByText } = render(
<BarGaugeExclusiveVisOptions
styles={defaultStyles}
onChange={mockOnChange}
isXaxisNumerical={false}
/>
);

fireEvent.click(getByText('Horizontal'));

expect(mockOnChange).toHaveBeenCalledWith({
...defaultStyles,
orientation: 'horizontal',
});
});

it('should call onChange when display mode is changed', () => {
const { getByText } = render(
<BarGaugeExclusiveVisOptions
styles={defaultStyles}
onChange={mockOnChange}
isXaxisNumerical={false}
/>
);

fireEvent.click(getByText('Stack'));

expect(mockOnChange).toHaveBeenCalledWith({
...defaultStyles,
displayMode: 'stack',
});
});

it('should call onChange when value display is changed', () => {
const { getByText } = render(
<BarGaugeExclusiveVisOptions
styles={defaultStyles}
onChange={mockOnChange}
isXaxisNumerical={false}
/>
);

fireEvent.click(getByText('Text Color'));

expect(mockOnChange).toHaveBeenCalledWith({
...defaultStyles,
valueDisplay: 'textColor',
});
});

it('should call onChange when unfilled area switch is toggled', () => {
const { getByRole } = render(
<BarGaugeExclusiveVisOptions
styles={defaultStyles}
onChange={mockOnChange}
isXaxisNumerical={false}
/>
);

const switchElement = getByRole('switch');
fireEvent.click(switchElement);

expect(mockOnChange).toHaveBeenCalledWith({
...defaultStyles,
showUnfilledArea: false,
});
});

it('should handle undefined styles with defaults', () => {
const { getByText } = render(
<BarGaugeExclusiveVisOptions
styles={undefined as any}
onChange={mockOnChange}
isXaxisNumerical={false}
/>
);

fireEvent.click(getByText('Horizontal'));

expect(mockOnChange).toHaveBeenCalledWith({
orientation: 'horizontal',
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { i18n } from '@osd/i18n';
import { EuiSwitch, EuiButtonGroup, EuiFormRow } from '@elastic/eui';
import React from 'react';
import { BarGaugeChartStyle } from './bar_gauge_vis_config';
import { StyleAccordion } from '../style_panel/style_accordion';

interface BarGaugeVisOptionsProps {
styles: BarGaugeChartStyle['exclusive'];
onChange: (styles: BarGaugeChartStyle['exclusive']) => void;
isXaxisNumerical: boolean;
}

const displayModeOption = [
{
id: 'gradient',
label: i18n.translate('explore.vis.barGauge.displayMode.gradient', {
defaultMessage: 'Gradient',
}),
},
{
id: 'stack',
label: i18n.translate('explore.vis.barGauge.displayMode.stack', {
defaultMessage: 'Stack',
}),
},
{
id: 'basic',
label: i18n.translate('explore.vis.barGauge.displayMode.basic', {
defaultMessage: 'Basic',
}),
},
];

const valueDisplayOption = [
{
id: 'valueColor',
label: i18n.translate('explore.vis.barGauge.valueDisplay.valueColor', {
defaultMessage: 'Value Color',
}),
},
{
id: 'textColor',
label: i18n.translate('explore.vis.barGauge.valueDisplay.textColor', {
defaultMessage: 'Text Color',
}),
},
{
id: 'hidden',
label: i18n.translate('explore.vis.barGauge.valueDisplay.hidden', {
defaultMessage: 'Hidden',
}),
},
];

export const BarGaugeExclusiveVisOptions = ({
styles,
onChange,
isXaxisNumerical,
}: BarGaugeVisOptionsProps) => {
const getOrientationOptions = () => {
const horizontalLabel = i18n.translate('explore.vis.barGauge.orientation.horizontal', {
defaultMessage: 'Horizontal',
});
const verticalLabel = i18n.translate('explore.vis.barGauge.orientation.vertical', {
defaultMessage: 'Vertical',
});

// When X-axis is numerical, the labels are swapped
const verticalOptionLabel = isXaxisNumerical ? horizontalLabel : verticalLabel;
const horizontalOptionLabel = isXaxisNumerical ? verticalLabel : horizontalLabel;

return [
{ id: 'vertical', label: verticalOptionLabel },
{ id: 'horizontal', label: horizontalOptionLabel },
];
};

const orientationOption = getOrientationOptions();

const updateExclusiveOption = (key: keyof BarGaugeChartStyle['exclusive'], value: any) => {
onChange({
...styles,
[key]: value,
});
};

return (
<StyleAccordion
id="barGaugeSection"
accordionLabel={i18n.translate('explore.stylePanel.tabs.barGauge', {
defaultMessage: 'Bar Gauge',
})}
initialIsOpen={true}
data-test-subj="barGaugeExclusivePanel"
>
<EuiFormRow
label={i18n.translate('explore.stylePanel.barGauge.exclusive.orientation', {
defaultMessage: 'Orientation',
})}
>
<EuiButtonGroup
legend={i18n.translate('explore.stylePanel.barGauge.exclusive.orientation', {
defaultMessage: 'Orientation',
})}
isFullWidth
options={orientationOption}
onChange={(optionId) => {
updateExclusiveOption('orientation', optionId);
}}
type="single"
idSelected={styles?.orientation ?? 'vertical'}
buttonSize="compressed"
/>
</EuiFormRow>

<EuiFormRow
label={i18n.translate('explore.stylePanel.barGauge.exclusive.displayMode', {
defaultMessage: 'Display mode',
})}
>
<EuiButtonGroup
legend={i18n.translate('explore.stylePanel.barGauge.exclusive.displayMode', {
defaultMessage: 'Display mode',
})}
isFullWidth
options={displayModeOption}
onChange={(optionId) => {
updateExclusiveOption('displayMode', optionId);
}}
type="single"
idSelected={styles?.displayMode ?? 'gradient'}
buttonSize="compressed"
/>
</EuiFormRow>

<EuiFormRow
label={i18n.translate('explore.stylePanel.barGauge.exclusive.valueDisplay', {
defaultMessage: 'Value display',
})}
>
<EuiButtonGroup
legend={i18n.translate('explore.stylePanel.barGauge.exclusive.valueDisplay', {
defaultMessage: 'Value display',
})}
isFullWidth
options={valueDisplayOption}
onChange={(optionId) => {
updateExclusiveOption('valueDisplay', optionId);
}}
type="single"
idSelected={styles?.valueDisplay ?? 'valueColor'}
buttonSize="compressed"
/>
</EuiFormRow>

<EuiFormRow>
<EuiSwitch
compressed
label={i18n.translate('explore.vis.heatmap.showUnfilledArea', {
defaultMessage: 'Show unfilled area',
})}
checked={styles?.showUnfilledArea ?? false}
onChange={(e) => updateExclusiveOption('showUnfilledArea', e.target.checked)}
/>
</EuiFormRow>
</StyleAccordion>
);
};
Loading
Loading