Skip to content

Commit

Permalink
#8588: Multi-chart widget improvements (#8730)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsuren1 authored Oct 27, 2022
1 parent 6f9327b commit 5a44c71
Show file tree
Hide file tree
Showing 20 changed files with 264 additions and 58 deletions.
Binary file modified docs/user-guide/img/widgets/chart-options.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions docs/user-guide/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ Selecting a *Layer* or *Layers*, the following *Chart* options is presented to u

<img src="../img/widgets/chart-options.jpg" class="ms-docimage" style="max-width:450px;"/>

From here it is possible to choose between *Bar Chart*, *Pie Chart* or *Line Chart*, or simply go back to widget type selection through the <img src="../img/button/back.jpg" class="ms-docbutton"/> button. <br>
By default, the bar chart is selected.
From the chart configuration page, the user can perform the following operation

* Edit chart name <img src="../img/button/edit_button.jpg" class="ms-docbutton"/>
* Choose between *Bar Chart*, *Pie Chart* or *Line Chart*. By default, the bar chart is selected.

From the toolbar of this panel <img src="../img/widgets/widget-options.jpg" class="ms-docbutton"/> the user is allowed to:

Expand Down
2 changes: 1 addition & 1 deletion web/client/components/charts/WidgetChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ export const toPlotly = (props) => {
const classificationType = getChartClassificationType(classificationAttr, classificationAttributeType, autoColorOptions, customColorEnabled);
return {
layout: {
showlegend: legend,
showlegend: legend ?? false, // Set false when legend is undefined, else pie-chart attempts to display legend
// https://plotly.com/javascript/setting-graph-size/
// automargin: true ok for big widgets.
// small widgets should be adapted accordingly
Expand Down
8 changes: 4 additions & 4 deletions web/client/components/charts/__tests__/WidgetChart-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('WidgetChart', () => {
it('rendering pie', (done) => {
const check = ({ data, layout }, graphDiv) => {
expect(graphDiv).toExist();
expect(layout.showLegend).toBeFalsy();
expect(layout.showlegend).toBeFalsy();
expect(layout.autosize).toBeFalsy();
expect(layout.automargin).toBeFalsy();
expect(layout.legend).toBeFalsy();
Expand All @@ -60,7 +60,7 @@ describe('WidgetChart', () => {
it('rendering line', (done) => {
const check = ({ data, layout }, graphDiv) => {
expect(graphDiv).toExist();
expect(layout.showLegend).toBeFalsy();
expect(layout.showlegend).toBeFalsy();
expect(layout.autosize).toBeFalsy();
expect(layout.automargin).toBeFalsy();
expect(layout.hovermode).toBe('x unified');
Expand All @@ -78,7 +78,7 @@ describe('WidgetChart', () => {
it('rendering bar', (done) => {
const check = ({ data, layout }, graphDiv) => {
expect(graphDiv).toExist();
expect(layout.showLegend).toBeFalsy();
expect(layout.showlegend).toBeFalsy();
expect(layout.autosize).toBeFalsy();
expect(layout.automargin).toBeFalsy();
expect(layout.hovermode).toBe('x unified');
Expand Down Expand Up @@ -108,7 +108,7 @@ describe('Widget Chart: data conversions ', () => {
it('defaults', () => {
testAllTypes(DATASET_1, ({data, config, layout}) => {
// default settings valid for all chart types
expect(layout.showLegend).toBeFalsy();
expect(layout.showlegend).toBeFalsy();
expect(layout.autosize).toBeFalsy();
expect(layout.automargin).toBeFalsy();
expect(data.length).toEqual(1);
Expand Down
4 changes: 2 additions & 2 deletions web/client/components/widgets/builder/wizard/ChartWizard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ const renderPreview = ({ data = {}, layer, dependencies = {}, setValid = () => {
layer={data.layer || layer}
charts={data.charts || []}
selectedChartId={data.selectedChartId}
filter={data.filter}
filter={data.filter || {}}
geomProp={data.geomProp}
mapSync={data.mapSync}
autoColorOptions={data.autoColorOptions}
options={data.options}
options={data.options || []}
yAxis={data.yAxis}
xAxisAngle={data.xAxisAngle}
yAxisLabel={data.yAxisLabel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import ReactSelect from "react-select";
import { Glyphicon } from "react-bootstrap";
import {FormControl as FC, Glyphicon} from "react-bootstrap";
import isEmpty from "lodash/isEmpty";
import get from "lodash/get";
import tooltip from "../../../../misc/enhancers/tooltip";
Expand All @@ -16,6 +16,7 @@ import localizedProps from "../../../../misc/enhancers/localizedProps";
import Message from "../../../../I18N/Message";
import ButtonRB from "../../../../misc/Button";
const Select = localizedProps(["noResultsText"])(ReactSelect);
const FormControl = localizedProps("placeholder")(FC);
const Button = tooltip(ButtonRB);


Expand All @@ -36,6 +37,8 @@ export default ({
width,
...props
}) => {
const [showInput, setShowInput] = useState(false);
const [inputVal, setInputVal] = useState(selectedChart?.name);
const renderChartSwitchSelector = (options) => {
if (options.length === 1) {
return null;
Expand All @@ -49,21 +52,53 @@ export default ({
<Glyphicon glyph="info-sign" />
</Button>);
}
return (<Select
className={className}
disabled={disabled}
noResultsText="widgets.chartSwitcher.noResults"
options={isEmpty(options)
? []
: options.map(m => ({
label: m?.layer?.title,
value: m.chartId
}))
}
onChange={(val) => val.value && onChange("selectedChartId", val.value)}
value={value || options?.[0]?.chartId}
clearable={false}
/>);
const formValue = inputVal ?? options?.find(o => o.chartId === value)?.name;
return (
<div className={"chart-fields"}>
{showInput
? <FormControl
type="text"
placeholder={"widgets.chartSwitcher.placeholder"}
style={{
textOverflow: "ellipsis"
}}
value={formValue}
onChange={(e) => setInputVal(e.target.value)}/>
: <Select
className={className}
disabled={disabled}
noResultsText="widgets.chartSwitcher.noResults"
options={isEmpty(options)
? []
: options.map(m => ({
label: m?.name || (m?.layer?.title),
value: m.chartId
}))
}
onChange={(chart) => {
if (chart.value) {
onChange("selectedChartId", chart.value);
setInputVal(null);
}
}}
value={value || options?.[0]?.chartId}
clearable={false}
/>}
{withContainer && <Button
bsStyle="primary"
disabled={showInput && isEmpty(formValue)}
onClick={() => {
if (!showInput) {
setShowInput(true);
} else {
inputVal && onChange(`charts[${selectedChart?.chartId}].name`, inputVal);
setShowInput(false);
}
}}>
<Glyphicon glyph={showInput ? "ok" : "pencil"}/>
</Button>}
</div>
);
};

if (!withContainer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('ChartSwitcher component', () => {
const switcherDropdown = container.querySelector('.Select');
expect(switcherDropdown).toBeTruthy();
});
it('ChartSwitcher render icon when map size is small', () => {
it('ChartSwitcher render icon when widget size is small', () => {
ReactDOM.render(<ChartSwitcher width={400} charts={charts} value={1} />, document.getElementById("container"));
const container = document.getElementById('container');
const el = container.querySelector('.chart-switcher');
Expand Down Expand Up @@ -92,4 +92,85 @@ describe('ChartSwitcher component', () => {
const button = container.querySelector('button');
expect(button.textContent).toBe('TEST');
});
it('ChartSwitcher with chart fields', () => {
ReactDOM.render(<ChartSwitcher withContainer editorData={{charts}} />, document.getElementById("container"));
const container = document.getElementById('container');
const el = container.querySelector('.chart-switcher');
expect(el).toBeTruthy();
const fieldsEl = container.querySelector('.chart-fields');
expect(fieldsEl).toBeTruthy();
const selectArrow = container.querySelector('.Select-arrow');
expect(selectArrow).toBeTruthy();

const button = container.querySelector('.chart-fields button');
expect(button).toBeTruthy();
ReactTestUtils.Simulate.click(button);

const inputEl = container.querySelector('.chart-fields input');
expect(inputEl).toBeTruthy();
});
it('ChartSwitcher on chart name change', () => {
const action = { onChange: () => {} };
const spyOnChange = expect.spyOn(action, 'onChange');
ReactDOM.render(<ChartSwitcher withContainer editorData={{charts}} selectedChart={{...charts[0]}} onChange={action.onChange} />, document.getElementById("container"));
const container = document.getElementById('container');
const el = container.querySelector('.chart-switcher');
expect(el).toBeTruthy();
let button = container.querySelector('.chart-fields button');
expect(button).toBeTruthy();
ReactTestUtils.Simulate.click(button);
const inputEl = container.querySelector('.chart-fields input');
expect(inputEl).toBeTruthy();
ReactTestUtils.Simulate.change(inputEl, {target: {value: "ChartEdited"}});
ReactTestUtils.Simulate.click(button);
expect(spyOnChange).toHaveBeenCalled();
spyOnChange.calls.forEach(c => {
const args = c.arguments;
if (args[0].includes('charts')) {
expect(args).toEqual(["charts[1].name", "ChartEdited"]);
} else {
expect(args).toEqual(["selectedChartId", 1]);
}
});
});
it('ChartSwitcher on chart name empty', () => {
ReactDOM.render(<ChartSwitcher withContainer editorData={{charts}} selectedChart={{...charts[0]}} />, document.getElementById("container"));
const container = document.getElementById('container');
const el = container.querySelector('.chart-switcher');
expect(el).toBeTruthy();
let button = container.querySelector('.chart-fields button');
expect(button).toBeTruthy();
// Edit button
ReactTestUtils.Simulate.click(button);
const inputEl = container.querySelector('.chart-fields input');
expect(inputEl).toBeTruthy();
expect(button.classList.contains('disabled')).toBeTruthy();
});
it('ChartSwitcher onChange chart name unmodified', () => {
const action = { onChange: () => {} };
const spyOnChange = expect.spyOn(action, 'onChange');
ReactDOM.render(<ChartSwitcher withContainer value={1} editorData={{charts: charts.map((c, i) =>({...c, name: `Chart-${i}`}))}} selectedChart={{...charts[0]}} onChange={action.onChange} />, document.getElementById("container"));
const container = document.getElementById('container');
const el = container.querySelector('.chart-switcher');
expect(el).toBeTruthy();
let button = container.querySelector('.chart-fields button');
expect(button).toBeTruthy();
// Edit button
ReactTestUtils.Simulate.click(button);
const inputEl = container.querySelector('.chart-fields input');
expect(inputEl).toBeTruthy();
expect(button.classList.contains('disabled')).toBeFalsy();
expect(spyOnChange).toHaveBeenCalled();
spyOnChange.calls.forEach(c => {
// onChange name is not dispatched
expect(c.arguments).toEqual(["selectedChartId", 1]);
});
});
it('ChartSwitcher hide chart edit in widget view', () => {
ReactDOM.render(<ChartSwitcher editorData={{charts}} selectedChart={{...charts[0]}} />, document.getElementById("container"));
const container = document.getElementById('container');
expect(container).toBeTruthy();
const button = container.querySelector('.chart-fields button');
expect(button).toBeFalsy();
});
});
3 changes: 1 addition & 2 deletions web/client/plugins/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ import {
import dashboardReducers from '../reducers/dashboard';
import dashboardEpics from '../epics/dashboard';
import widgetsEpics from '../epics/widgets';
import { getSelectedWidgetData } from "../utils/WidgetsUtils";

const WidgetsView = compose(
connect(
Expand Down Expand Up @@ -111,7 +110,7 @@ const WidgetsView = compose(
target.widgetType === "table" &&
(editingWidget.widgetType !== "map" &&
editingWidget.widgetType === "chart"
? (target.layer && editingWidget && target.layer.name === (getSelectedWidgetData(editingWidget)?.layer?.name || ''))
? (target.layer && editingWidget && editingWidget?.charts?.map(c => c?.layer?.name)?.includes(target.layer.name))
: (target.layer && editingWidget.layer && target.layer.name === editingWidget.layer.name)
|| editingWidget.widgetType === "map") && !target.mapSync
) && target.id !== editingWidget.id
Expand Down
3 changes: 1 addition & 2 deletions web/client/plugins/widgetbuilder/ChartLayerSelector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ export default connect((state) =>({
const onProceed = () => {
const isUpdate = showLayers && !isEmpty(layers);
const key = isUpdate ? 'chart-add' : 'chart-layers';
const _layers = isUpdate ? layers.concat(layer) : layer;
onLayerChoice(key, _layers);
onLayerChoice(key, layer);
toggleLayerSelector(false);
};
const stepButton = !showLayers ? stepButtons : withBackButton({toggleLayerSelector});
Expand Down
19 changes: 11 additions & 8 deletions web/client/plugins/widgetbuilder/enhancers/chartLayerSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,25 @@ const layerSelector = compose(
...props
}))
),
withProps(({selected, setSelected, layers}) => ({
withProps(({selected, setSelected}) => ({
getItems: (items) => items.map(i =>
!isEmpty(selected)
&& i && i.record
&& selected.some(s => s.identifier === i.record.identifier)
? { ...i, selected: true }
: !isEmpty(layers)
&& layers.some(l => i?.record?.identifier === l.name)
? { ...i, className: 'disabled' }
: i
: i
),
onItemClick: ({record} = {}, props, event) => {
if (event.ctrlKey) {
return setSelected(isEmpty(selected)
? castArray(record)
: castArray(selected).concat(record));
const selectedArray = castArray(selected);
if (isEmpty(selected)) {
return setSelected(castArray(record));
}
const present = selectedArray.find((s) => s?.identifier === record?.identifier);
if (present) {
return setSelected(selectedArray.filter(s => s?.identifier !== record?.identifier));
}
return setSelected(selectedArray.concat(record));
}
return setSelected(castArray(record));
}
Expand Down
53 changes: 53 additions & 0 deletions web/client/selectors/__tests__/widgets-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ describe('widgets selectors', () => {
const state = set(`widgets.builder.editor`, { filter: { name: "TEST" } }, {});
expect(getEditingWidgetFilter(state)).toExist();
});
it('getEditingWidgetFilter - chart', () => {
const state = set(`widgets.builder.editor`, {widgetType: "chart", charts: [{chartId: "1", filter: { name: "TEST" } }], selectedChartId: "1" }, {});
expect(getEditingWidgetFilter(state)).toBeTruthy();
});
it('getEditorSettings', () => {
const state = set(`widgets.builder.settings`, { flag: true }, {});
expect(getEditorSettings(state)).toExist();
Expand Down Expand Up @@ -223,6 +227,55 @@ describe('widgets selectors', () => {
expect(availableDeps[0]).toBe('widgets[tableId]');
expect(availableDeps[1]).toBe('widgets[otherTableId]');
});
it('availableDependenciesForEditingWidgetSelector for chart', () => {
const stateInput = {
widgets: {
containers: {
[DEFAULT_TARGET]: {
widgets: [{
widgetType: "table",
id: "tableId",
layer: {
name: "layername"
}
}, {
widgetType: "table",
id: "otherTableId",
layer: {
name: "layername"
}
},
{
id: "WIDGET",
maps: [{mapId: "MAPS"}],
widgetType: "map"
}]
}
},
builder: {
editor: {
charts: [
{
chartId: "1",
layer: {
name: "layername"
}
}
],
widgetType: "chart",
id: "chartId"
}
}
}
};
const state = availableDependenciesForEditingWidgetSelector(stateInput);
const availableDeps = state.availableDependencies;
expect(availableDeps).toExist();
expect(availableDeps.length).toBe(3);
expect(availableDeps[0]).toBe('widgets[WIDGET].maps[MAPS].map');
expect(availableDeps[1]).toBe('widgets[tableId]');
expect(availableDeps[2]).toBe('widgets[otherTableId]');
});
it('availableDependenciesForEditingWidgetSelector for counter', () => {
const stateInput = {
widgets: {
Expand Down
Loading

0 comments on commit 5a44c71

Please sign in to comment.