From 10abff27a081c2431186001b4329286ab0fd4411 Mon Sep 17 00:00:00 2001 From: deepaknevdepsl <102342039+deepaknevdepsl@users.noreply.github.com> Date: Thu, 16 Jun 2022 10:26:48 +0530 Subject: [PATCH] Feature/Pie chart legend, chart style, color theme and cypress test cases for same. (#776) * pie chart legends, chart color contrast and cypress test cases Signed-off-by: Deepak Nevde * Added color code in constants Signed-off-by: Deepak Nevde * Snapshots updated Signed-off-by: Deepak Nevde * Conflicts resolved Signed-off-by: Deepak Nevde * Review comment addressed Signed-off-by: Deepak Nevde * color variable changes Signed-off-by: Deepak Nevde --- .../integration/1_event_analytics.spec.js | 39 ++++++++- .../.cypress/utils/event_constants.js | 26 +++++- .../common/constants/colors.ts | 13 +++ .../__snapshots__/config_panel.test.tsx.snap | 84 ++++++++++++++++++- .../config_controls/config_button_group.tsx | 41 +++++++++ .../config_controls/config_chart_options.tsx | 9 ++ .../config_controls/config_legend.tsx | 50 +++++++++++ .../config_panes/config_controls/index.ts | 1 + .../__tests__/__snapshots__/pie.test.tsx.snap | 10 ++- .../visualizations/charts/pie/pie.tsx | 31 ++++++- .../visualizations/charts/pie/pie_type.ts | 60 +++++++++++-- 11 files changed, 344 insertions(+), 20 deletions(-) create mode 100644 dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_button_group.tsx create mode 100644 dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_legend.tsx diff --git a/dashboards-observability/.cypress/integration/1_event_analytics.spec.js b/dashboards-observability/.cypress/integration/1_event_analytics.spec.js index 4cfa67727..12c6027d0 100644 --- a/dashboards-observability/.cypress/integration/1_event_analytics.spec.js +++ b/dashboards-observability/.cypress/integration/1_event_analytics.spec.js @@ -18,7 +18,8 @@ import { landOnEventExplorer, landOnEventVisualizations, landOnPanels, - renderTreeMapchart + renderTreeMapchart, + renderPieChart } from '../utils/event_constants'; import { supressResizeObserverIssue } from '../utils/constants'; @@ -645,7 +646,6 @@ describe('Renders chart and verify Toast message if X-axis and Y-axis values are beforeEach(() => { landOnEventVisualizations(); }); - it('Renders chart, clear X-axis and Y-axis value and click on Apply button, Toast message should display with error message', () => { querySearch(TEST_QUERIES[4].query, TEST_QUERIES[4].dateRangeDOM); cy.get('[data-test-subj="configPane__vizTypeSelector"] [data-test-subj="comboBoxInput"]') @@ -811,5 +811,40 @@ describe('Renders Tree Map', () => { cy.get('.euiFormHelpText.euiFormRow__text').contains('Parent field').should('not.exist'); cy.get('.euiButton__text').contains('Preview').click(); cy.get('.trace.treemap path[style*="rgb(211, 96, 134)"]').should('exist'); + }) +}); + +describe('Render Pie chart for Legend and single color contrast change', () => { + beforeEach(() => { + landOnEventVisualizations(); + }); + it('Render Pie chart and verify legends for Position Right and Bottom', () => { + renderPieChart(); + cy.get('[data-text="Right"]').should('have.text', 'Right'); + cy.get('[data-text="Right"] [data-test-subj="v"]').should('have.attr', 'checked'); + cy.get('[data-text="Bottom"]').should('have.text', 'Bottom').click(); + cy.get('[data-text="Bottom"] [data-test-subj="h"]').should('not.have.attr', 'checked'); + cy.get('[data-test-subj="visualizeEditorRenderButton"]').click({ force: true }); + }); + + it('Render Pie chart and verify legends for Show and Hidden', () => { + renderPieChart(); + cy.get('[data-text="Show"]').should('have.text', 'Show'); + cy.get('[data-text="Show"] [data-test-subj="show"]').should('have.attr', 'checked'); + cy.get('[data-text="Hidden"]').should('have.text', 'Hidden').click(); + cy.get('[data-text="Hidden"] [data-test-subj="hidden"]').should('not.have.attr', 'checked'); + cy.get('[data-test-subj="visualizeEditorRenderButton"]').click({ force: true }); + }); + + it('Renders Pie chart with single color', () => { + renderPieChart(); + cy.get('.euiIEFlexWrapFix').eq(3).contains('Chart Styles').should('exist'); + cy.get('[data-test-subj="comboBoxInput"]').eq(3).click(); + cy.get('[name="Pie"]').click(); + cy.get('.euiSuperSelectControl').click(); + cy.get('.euiContextMenuItem.euiSuperSelect__item.euiSuperSelect__item--hasDividers').eq(1).click(); + cy.get('.euiFlexItem.euiFlexItem--flexGrowZero .euiButton__text').eq(2).click(); + cy.wait(delay); }); }); + diff --git a/dashboards-observability/.cypress/utils/event_constants.js b/dashboards-observability/.cypress/utils/event_constants.js index 0155a5d19..0dd369922 100644 --- a/dashboards-observability/.cypress/utils/event_constants.js +++ b/dashboards-observability/.cypress/utils/event_constants.js @@ -28,10 +28,14 @@ export const TEST_QUERIES = [ query: 'source = opensearch_dashboards_sample_data_logs | stats count(), avg(bytes) by host, tags', dateRangeDOM: YEAR_TO_DATE_DOM_ID }, - { - query: 'source=opensearch_dashboards_sample_data_flights | stats avg(FlightDelayMin) by DestCountry, DestCityName', - dateRangeDOM: YEAR_TO_DATE_DOM_ID -}, + { + query: 'source=opensearch_dashboards_sample_data_flights | stats avg(FlightDelayMin) by DestCountry, DestCityName', + dateRangeDOM: YEAR_TO_DATE_DOM_ID + }, + { + query:"source = opensearch_dashboards_sample_data_logs | where response='503' or response='404' | stats count() by span(timestamp,1d)", + dateRangeDOM: YEAR_TO_DATE_DOM_ID + }, ]; export const TESTING_PANEL = 'Mock Testing Panels'; @@ -92,3 +96,17 @@ export const renderTreeMapchart = () => { cy.get('[data-test-subj = "comboBoxInput"]').eq(4).click(); cy.get('button[name="Slice Dice"]').click(); }; + +export const renderPieChart = () => { + querySearch(TEST_QUERIES[5].query, TEST_QUERIES[5].dateRangeDOM); + cy.get('[data-test-subj="configPane__vizTypeSelector"] [data-test-subj="comboBoxInput"]').click(); + cy.get('[data-test-subj="comboBoxOptionsList "] button span').contains('Pie').click(); + cy.wait(delay); + cy.get('#configPanel__panelOptions .euiFieldText').click().type('Pie chart'); + cy.get('.euiFlexItem .euiFormRow [placeholder="Description"]').click().type('This is the description for Pie chart'); + cy.get('.euiIEFlexWrapFix').eq(1).contains('Value options').should('exist'); + cy.get('[data-test-subj="comboBoxInput"]').eq(1).click(); + cy.get('[name="count()"]').eq(0).click(); + cy.get('[data-test-subj="comboBoxToggleListButton"]').eq(0).click(); + cy.get('[data-test-subj="comboBoxInput"]').eq(2).click(); +}; diff --git a/dashboards-observability/common/constants/colors.ts b/dashboards-observability/common/constants/colors.ts index 5f9304ff2..865af84ad 100644 --- a/dashboards-observability/common/constants/colors.ts +++ b/dashboards-observability/common/constants/colors.ts @@ -166,3 +166,16 @@ export const COLOR_PALETTES = [ type: 'gradient', }, ]; +export const HEX_CONTRAST_COLOR = 0xFFFFFF; +export const PIE_PALETTES = [ + { + value: DEFAULT_PALETTE, + title: 'Default', + type: 'text', + }, + { + value: SINGLE_COLOR_PALETTE, + title: 'Single Color', + type: 'text', + } +]; diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap index a6d447094..72b322dc5 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap @@ -709,9 +709,63 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, Object { "editor": [Function], - "id": "chart_options", - "mapTo": "chartOptions", - "name": "Chart options", + "id": "legend", + "mapTo": "legend", + "name": "Legend", + "schemas": Array [ + Object { + "component": null, + "mapTo": "showLegend", + "name": "Show Legend", + "props": Object { + "defaultSelections": Array [ + Object { + "id": "show", + "name": "Show", + }, + ], + "options": Array [ + Object { + "id": "show", + "name": "Show", + }, + Object { + "id": "hidden", + "name": "Hidden", + }, + ], + }, + }, + Object { + "component": null, + "mapTo": "position", + "name": "Position", + "props": Object { + "defaultSelections": Array [ + Object { + "id": "v", + "name": "Right", + }, + ], + "options": Array [ + Object { + "id": "v", + "name": "Right", + }, + Object { + "id": "h", + "name": "Bottom", + }, + ], + }, + }, + ], + }, + Object { + "editor": [Function], + "id": "chart_styles", + "mapTo": "chartStyles", + "name": "Chart Styles", "schemas": Array [ Object { "component": null, @@ -737,6 +791,28 @@ exports[`Config panel component Renders config panel with visualization data 1`] ], }, }, + Object { + "component": [Function], + "defaultState": Object { + "name": "default", + }, + "eleType": "colorpicker", + "isSingleSelection": true, + "mapTo": "colorTheme", + "name": "Color Theme", + "options": Array [ + Object { + "title": "Default", + "type": "text", + "value": "default", + }, + Object { + "title": "Single Color", + "type": "text", + "value": "singleColor", + }, + ], + }, ], }, ], @@ -755,11 +831,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] "iconType": "visPie", "id": "pie", "label": "Pie", + "legendPosition": "v", "name": "pie", "selection": Object { "dataLoss": "nothing", }, "seriesAxis": "yaxis", + "showLegend": true, "type": "pie", "visConfig": Object { "config": Object { diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_button_group.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_button_group.tsx new file mode 100644 index 000000000..b497d3466 --- /dev/null +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_button_group.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { uniqueId } from 'lodash'; +import { EuiTitle, EuiSpacer, EuiButtonGroup } from '@elastic/eui'; +interface ToggleButtonOptions { + id: string; + label: string; +} +interface ToggleGroupProps { + title: string; + legend: string; + groupOptions: ToggleButtonOptions[]; + idSelected: string; + handleButtonChange: (id: string, value?: any) => void; +} +export const ButtonGroupItem: React.FC = ({ + title, legend, groupOptions, idSelected, handleButtonChange +}) => ( + <> + +

{title}

+
+ +
+ +
+ +); diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_chart_options.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_chart_options.tsx index 668a0302c..e318ec5fa 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_chart_options.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_chart_options.tsx @@ -45,6 +45,15 @@ export const ConfigChartOptions = ({ vizState, ...schema.props, }; + } else if (schema.eleType === 'colorpicker') { + params = { + title: schema.name, + selectedColor: vizState[schema.mapTo] || schema?.defaultState, + colorPalettes: schema.options || [], + onSelectChange: handleConfigurationChange(schema.mapTo), + vizState, + ...schema.props, + }; } else { params = { paddingTitle: schema.name, diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_legend.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_legend.tsx new file mode 100644 index 000000000..be9bf9d47 --- /dev/null +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_legend.tsx @@ -0,0 +1,50 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useMemo } from 'react'; +import { EuiAccordion, EuiSpacer } from '@elastic/eui'; +import { ButtonGroupItem } from './config_button_group'; +import { IConfigPanelOptionSection } from '../../../../../../../../common/types/explorer'; + +export const ConfigLegend = ({ schemas, vizState, handleConfigChange }: any) => { + const handleConfigurationChange = useCallback( + (stateFiledName) => { + return (changes) => { + handleConfigChange({ + ...vizState, + [stateFiledName]: changes, + }); + }; + }, + [handleConfigChange, vizState] + ); + + const dimensions = useMemo(() => { + return schemas.map((schema: IConfigPanelOptionSection, index: number) => { + const DimensionComponent = schema.component || ButtonGroupItem; + const params = { + title: schema.name, + legend: schema.name, + groupOptions: schema?.props?.options.map((btn: { name: string }) => ({ ...btn, label: btn.name })), + idSelected: vizState[schema.mapTo] || schema?.props?.defaultSelections[0]?.id, + handleButtonChange: handleConfigurationChange(schema.mapTo), + vizState, + ...schema.props, + }; + return ( + <> + + + + ); + }); + }, [schemas, vizState, handleConfigurationChange]);; + + return ( + + {dimensions} + + ); +}; diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/index.ts b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/index.ts index ead2f05d5..04900c51c 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/index.ts +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/index.ts @@ -11,3 +11,4 @@ export { ConfigThresholds } from './config_thresholds'; export { ConfigText } from './config_text'; export { ConfigGaugeValueOptions } from './config_gauge_options'; export { ColorPalettePicker } from './config_color_palette_picker'; +export { ConfigLegend } from './config_legend'; diff --git a/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap b/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap index fffaeaa8e..5b59e4898 100644 --- a/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap +++ b/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap @@ -382,6 +382,9 @@ exports[`Pie component Renders pie component 1`] = ` "rows": 1, }, "height": 220, + "legend": Object { + "orientation": undefined, + }, "margin": Object { "b": 15, "l": 60, @@ -389,7 +392,7 @@ exports[`Pie component Renders pie component 1`] = ` "r": 10, "t": 30, }, - "showlegend": true, + "showlegend": undefined, "title": "", } } @@ -442,8 +445,7 @@ exports[`Pie component Renders pie component 1`] = ` "height": 220, "hovermode": "closest", "legend": Object { - "orientation": "h", - "traceorder": "normal", + "orientation": undefined, }, "margin": Object { "b": 15, @@ -452,7 +454,7 @@ exports[`Pie component Renders pie component 1`] = ` "r": 10, "t": 30, }, - "showlegend": true, + "showlegend": undefined, "title": "", "xaxis": Object { "automargin": true, diff --git a/dashboards-observability/public/components/visualizations/charts/pie/pie.tsx b/dashboards-observability/public/components/visualizations/charts/pie/pie.tsx index 0c07892e9..b9ce49044 100644 --- a/dashboards-observability/public/components/visualizations/charts/pie/pie.tsx +++ b/dashboards-observability/public/components/visualizations/charts/pie/pie.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { take, isEmpty } from 'lodash'; import { Plt } from '../../plotly/plot'; +import { DEFAULT_PALETTE, HEX_CONTRAST_COLOR } from '../../../../../common/constants/colors'; export const Pie = ({ visualizations, layout, config }: any) => { const { vis } = visualizations; @@ -19,17 +20,37 @@ export const Pie = ({ visualizations, layout, config }: any) => { dataConfig?.valueOptions && dataConfig.valueOptions.xaxis ? dataConfig.valueOptions.xaxis : []; const yaxis = dataConfig?.valueOptions && dataConfig.valueOptions.yaxis ? dataConfig.valueOptions.yaxis : []; - const type = dataConfig?.chartOptions?.mode ? dataConfig?.chartOptions?.mode[0]?.modeId : 'pie'; + const type = dataConfig?.chartStyles?.mode ? dataConfig?.chartStyles?.mode[0]?.modeId : 'pie'; const lastIndex = fields.length - 1; + const colorTheme = dataConfig?.chartStyles?.colorTheme + ? dataConfig?.chartStyles?.colorTheme + : { name: DEFAULT_PALETTE }; + const showLegend = dataConfig?.legend?.showLegend === 'hidden' ? false : vis.showLegend; + const legendPosition = dataConfig?.legend?.position || vis.legendPosition; - let valueSeries; + let valueSeries; if (!isEmpty(xaxis) && !isEmpty(yaxis)) { valueSeries = [...yaxis]; } else { valueSeries = defaultAxes.yaxis || take(fields, lastIndex > 0 ? lastIndex : 1); } + const invertHex = (hex:string) => (Number(`0x1${hex}`) ^ HEX_CONTRAST_COLOR).toString(16).substr(1).toUpperCase(); + + const pies = valueSeries.map((field: any, index) => { + const marker = + colorTheme.name !== DEFAULT_PALETTE + ? { + marker: { + colors: [...Array(data[field.name].length).fill(colorTheme.childColor)], + line: { + color: invertHex(colorTheme), + width: 1, + }, + }, + } + : undefined; return { labels: data[xaxis ? xaxis[0]?.label : fields[lastIndex].name], values: data[field.name], @@ -44,6 +65,7 @@ export const Pie = ({ visualizations, layout, config }: any) => { row: Math.floor(index / 3), column: index % 3, }, + ...marker, }; }); @@ -57,6 +79,11 @@ export const Pie = ({ visualizations, layout, config }: any) => { ...layout, ...(layoutConfig.layout && layoutConfig.layout), title: dataConfig?.panelOptions?.title || layoutConfig.layout?.title || '', + legend:{ + ...layout.legend, + orientation: legendPosition, + }, + showlegend: showLegend, }; const mergedConfigs = { diff --git a/dashboards-observability/public/components/visualizations/charts/pie/pie_type.ts b/dashboards-observability/public/components/visualizations/charts/pie/pie_type.ts index 7dbc507aa..84c3c3cc5 100644 --- a/dashboards-observability/public/components/visualizations/charts/pie/pie_type.ts +++ b/dashboards-observability/public/components/visualizations/charts/pie/pie_type.ts @@ -9,7 +9,14 @@ import { LensIconChartPie } from '../../assets/chart_pie'; import { PLOTLY_COLOR } from '../../../../../common/constants/shared'; import { VizDataPanel } from '../../../event_analytics/explorer/visualizations/config_panel/config_panes/default_vis_editor'; import { ConfigEditor } from '../../../event_analytics/explorer/visualizations/config_panel/config_panes/json_editor'; -import { ConfigValueOptions } from '../../../event_analytics/explorer/visualizations/config_panel/config_panes/config_controls'; +import { + ColorPalettePicker, + ConfigChartOptions, + ConfigValueOptions, + ConfigLegend, +} from '../../../event_analytics/explorer/visualizations/config_panel/config_panes/config_controls'; +import { DEFAULT_PALETTE, PIE_PALETTES } from '../../../../../common/constants/colors'; + const sharedConfigs = getPlotlySharedConfigs(); const VIS_CATEGORY = getPlotlyCategory(); @@ -22,6 +29,8 @@ export const createPieTypeDefinition = (params: any) => ({ fullLabel: 'Pie', iconType: 'visPie', category: VIS_CATEGORY.BASICS, + showLegend: true, + legendPosition: 'v', selection: { dataLoss: 'nothing', }, @@ -59,10 +68,42 @@ export const createPieTypeDefinition = (params: any) => ({ ], }, { - id: 'chart_options', - name: 'Chart options', - editor: ConfigValueOptions, - mapTo: 'chartOptions', + id: 'legend', + name: 'Legend', + editor: ConfigLegend, + mapTo: 'legend', + schemas: [ + { + name: 'Show Legend', + mapTo: 'showLegend', + component: null, + props: { + options: [ + { name: 'Show', id: "show" }, + { name: 'Hidden', id: "hidden" }, + ], + defaultSelections: [{ name: 'Show', id: "show" }], + }, + }, + { + name: 'Position', + mapTo: 'position', + component: null, + props: { + options: [ + { name: 'Right', id: 'v' }, + { name: 'Bottom', id: 'h' }, + ], + defaultSelections: [{ name: 'Right', id: 'v' }], + }, + }, + ], + }, + { + id: 'chart_styles', + name: 'Chart Styles', + editor: ConfigChartOptions, + mapTo: 'chartStyles', schemas: [ { name: 'Mode', @@ -77,6 +118,15 @@ export const createPieTypeDefinition = (params: any) => ({ defaultSelections: [{ name: 'Pie', modeId: 'pie' }], }, }, + { + name: 'Color Theme', + isSingleSelection: true, + component: ColorPalettePicker, + mapTo: 'colorTheme', + eleType: 'colorpicker', + options: PIE_PALETTES, + defaultState: { name: DEFAULT_PALETTE }, + }, ], }, ],