Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ScatterPlotWidget #129

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/react-core/__tests__/operations/scatterPlot.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { scatterPlot } from '../../src/operations/scatterPlot';
test('filter invalid values', () => {
const data = [
{ x: 0 }, // Missing y
{ y: 1 }, // Missing x
{ x: null, y: 1 }, // null x
{ x: 1, y: null }, // null y
{ x: 0, y: 0 }, // zero for both
{ x: 1, y: 2 }, // valid
{}, // no values for both
{ x: 2, y: 3 } // valid
];
expect(scatterPlot(data, 'x', 'y')).toEqual([
[0, 0],
[1, 2],
[2, 3]
]);
});
3 changes: 2 additions & 1 deletion packages/react-core/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ export { AggregationTypes } from './operations/aggregation/AggregationTypes';
export { aggregationFunctions } from './operations/aggregation/values';
export { groupValuesByColumn } from './operations/groupby';
export { histogram } from './operations/histogram';
export { scatterPlot } from './operations/scatterPlot';

export {
export {
FilterTypes as _FilterTypes,
filtersToSQL as _filtersToSQL,
getApplicableFilters as _getApplicableFilters
Expand Down
13 changes: 13 additions & 0 deletions packages/react-core/src/operations/scatterPlot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Filters invalid features and formats data
*/
export const scatterPlot = (features, xAxisColumn, yAxisColumn) =>
features
.filter((feature) => {
const xValue = feature[xAxisColumn];
const xIsValid = xValue !== null && xValue !== undefined;
const yValue = feature[yAxisColumn];
const yIsValid = yValue !== null && yValue !== undefined;
return xIsValid && yIsValid;
})
.map((feature) => [feature[xAxisColumn], feature[yAxisColumn]]);
34 changes: 34 additions & 0 deletions packages/react-ui/__tests__/widgets/ScatterPlotWidget.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { render } from '@testing-library/react';
import ScatterPlotWidgetUI from '../../src/widgets/ScatterPlotWidgetUI';
import { getMaterialUIContext, mockEcharts } from './testUtils';

describe('ScatterPlotWidgetUI', () => {
beforeAll(() => {
mockEcharts.init();
});

afterAll(() => {
mockEcharts.destroy();
});
const DATA = [
[1, 2],
[2, 4],
[3, 6]
];
const Widget = (props) =>
getMaterialUIContext(
<ScatterPlotWidgetUI data={DATA} onSelectedBarsChange={() => {}} {...props} />
);

test('renders correctly', () => {
render(<Widget />);
});

test('re-render with different data', () => {
const { rerender } = render(<Widget />);

rerender(<Widget />);
rerender(<Widget data={[...DATA, [4, 8]]} />);
});
});
2 changes: 2 additions & 0 deletions packages/react-ui/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import CategoryWidgetUI from './widgets/CategoryWidgetUI';
import FormulaWidgetUI from './widgets/FormulaWidgetUI';
import HistogramWidgetUI from './widgets/HistogramWidgetUI';
import PieWidgetUI from './widgets/PieWidgetUI';
import ScatterPlotWidgetUI from './widgets/ScatterPlotWidgetUI';

export {
cartoThemeOptions,
Expand All @@ -12,4 +13,5 @@ export {
FormulaWidgetUI,
HistogramWidgetUI,
PieWidgetUI,
ScatterPlotWidgetUI
};
104 changes: 104 additions & 0 deletions packages/react-ui/src/widgets/ScatterPlotWidgetUI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useTheme } from '@material-ui/core';
import PropTypes from 'prop-types';
import React, { useRef, useState, useEffect } from 'react';
import ReactEcharts from 'echarts-for-react';
import { isDataEqual } from './utils/chartUtils';
function __generateDefaultConfig(
{ tooltipFormatter, xAxisFormatter = (v) => v, yAxisFormatter = (v) => v },
theme
) {
return {
grid: {},
tooltip: {
padding: [theme.spacing(0.5), theme.spacing(1)],
textStyle: {
...theme.typography.caption,
fontSize: 12,
lineHeight: 16
},
backgroundColor: theme.palette.other.tooltip,
...(tooltipFormatter ? { formatter: tooltipFormatter } : {})
},
color: [theme.palette.secondary.main],
xAxis: {
axisLabel: {
...theme.typography.charts,
padding: [theme.spacing(0.5), 0, 0, 0],
formatter: (v) => {
const formatted = xAxisFormatter(v);
return typeof formatted === 'object'
? `${formatted.prefix || ''}${formatted.value}${formatted.suffix || ''}`
: formatted;
}
}
},
yAxis: {
axisLabel: {
...theme.typography.charts,
formatter: (v) => {
const formatted = yAxisFormatter(v);
return typeof formatted === 'object'
? `${formatted.prefix}${formatted.value}${formatted.suffix || ''}`
: formatted;
}
}
}
};
}

function __generateSerie({ name, data, theme }) {
return [
{
type: 'scatter',
name,
data: data
}
];
}

const EchartsWrapper = React.memo(
ReactEcharts,
({ option: optionPrev }, { option: optionNext }) => isDataEqual(optionPrev, optionNext)
);

function ScatterPlotWidgetUI(props) {
const theme = useTheme();
const { data = [], name, xAxisFormatter, yAxisFormatter, tooltipFormatter } = props;
const chartInstance = useRef();
const [options, setOptions] = useState({
series: []
});

useEffect(() => {
const config = __generateDefaultConfig(
{ xAxisFormatter, yAxisFormatter, tooltipFormatter },
theme
);
const series = __generateSerie({
name,
data: data || []
});
setOptions({
...config,
series
});
}, [data, name, theme, xAxisFormatter, yAxisFormatter, tooltipFormatter]);
return <EchartsWrapper ref={chartInstance} option={options} lazyUpdate={true} />;
}

ScatterPlotWidgetUI.defaultProps = {
name: null,
tooltipFormatter: (v) => `[${v.value[0]}, ${v.value[1]})`,
xAxisFormatter: (v) => v,
yAxisFormatter: (v) => v
};

ScatterPlotWidgetUI.propTypes = {
name: PropTypes.string,
data: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)),
tooltipFormatter: PropTypes.func,
xAxisFormatter: PropTypes.func,
yAxisFormatter: PropTypes.func
};

export default ScatterPlotWidgetUI;
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react';
import { Provider } from 'react-redux';
import {
Title,
Subtitle,
Primary,
ArgsTable,
Stories,
PRIMARY_STORY
} from '@storybook/addon-docs/blocks';
import * as cartoSlice from '@carto/react-redux/src/slices/cartoSlice';
import ScatterPlotWidget from '../../../../react-widgets/src/widgets/ScatterPlotWidget';
import { mockAppStoreConfiguration } from './utils';
import { buildReactPropsAsString } from '../../utils';

const store = mockAppStoreConfiguration();
store.dispatch(
cartoSlice.setWidgetLoadingState({ widgetId: 'sb-scatter-id', isLoading: false })
);
store.dispatch(
cartoSlice.setViewportFeatures({
sourceId: 'sb-data-source',
features: [
{ 'sb-x-column': 5000, 'sb-y-column': 3000 },
{ 'sb-x-column': 2000, 'sb-y-column': 1000 }
]
})
);

const options = {
title: 'Widgets/ScatterPlotWidget',
component: ScatterPlotWidget,
decorators: [
(Story) => (
<Provider store={store}>
<Story />
</Provider>
)
],
parameters: {
docs: {
page: () => (
<>
<Title />
<Subtitle />
<Primary />
<ArgsTable story={PRIMARY_STORY} />
<Stories />
</>
)
}
}
};

export default options;

const Template = (args) => <ScatterPlotWidget {...args} />;

const DEFAULT_PROPS = {
id: 'sb-scatter-id',
title: 'wrapper title',
dataSource: 'sb-data-source',
xAxisColumn: 'sb-x-column',
yAxisColumn: 'sb-y-column'
};

export const Default = Template.bind({});
Default.args = DEFAULT_PROPS;
Default.parameters = buildReactPropsAsString(DEFAULT_PROPS, 'ScatterPlotWidget');

export const xAxisFormatter = Template.bind({});
const xAxisFormatterProps = { ...DEFAULT_PROPS, xAxisFormatter: (v) => `${v}$` };
xAxisFormatter.args = xAxisFormatterProps;
xAxisFormatter.parameters = buildReactPropsAsString(
xAxisFormatterProps,
'ScatterPlotWidget'
);

export const yAxisFormatter = Template.bind({});
const yAxisFormatterProps = { ...DEFAULT_PROPS, yAxisFormatter: (v) => `$${v}` };
yAxisFormatter.args = yAxisFormatterProps;
yAxisFormatter.parameters = buildReactPropsAsString(
yAxisFormatterProps,
'ScatterPlotWidget'
);

export const tooltipFormatter = Template.bind({});
const tooltipFormatterProps = {
...DEFAULT_PROPS,
tooltipFormatter: (v) => `Price: $ ${v.value[0]}`
};
tooltipFormatter.args = tooltipFormatterProps;
tooltipFormatter.parameters = buildReactPropsAsString(
tooltipFormatterProps,
'ScatterPlotWidget'
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import ScatterPlotWidgetUI from '../../../src/widgets/ScatterPlotWidgetUI';
import { buildReactPropsAsString } from '../../utils';

const options = {
title: 'Widgets UI/ScatterPlotWidgetUI',
component: ScatterPlotWidgetUI
};

export default options;

const dataDefault = [
[1000.0, 800.04],
[800.07, 600.95]
];

const Template = (args) => <ScatterPlotWidgetUI {...args} />;

export const Default = Template.bind({});
const DefaultProps = { data: dataDefault, name: 'name' };
Default.args = DefaultProps;
Default.parameters = buildReactPropsAsString(DefaultProps, 'ScatterPlotWidgetUI');

export const xAxisFormatter = Template.bind({});
const xAxisFormatterProps = {
name: 'xFormatter',
data: dataDefault,
xAxisFormatter: (v) => `${v / 1000}k`
};
xAxisFormatter.args = xAxisFormatterProps;
xAxisFormatter.parameters = buildReactPropsAsString(
xAxisFormatterProps,
'ScatterPlotWidgetUI'
);

export const yAxisFormatter = Template.bind({});
const yAxisFormatterProps = {
name: 'yFormatter',
data: dataDefault,
yAxisFormatter: (v) => ({ prefix: `$`, value: v })
};
yAxisFormatter.args = yAxisFormatterProps;
yAxisFormatter.parameters = buildReactPropsAsString(
yAxisFormatterProps,
'ScatterPlotWidgetUI'
);

export const tooltipFormatter = Template.bind({});
const tooltipFormatterProps = {
name: 'tooltipFormatter',
data: dataDefault,
tooltipFormatter: (v) => `Price $ ${v.value[1]} Sales: ${v.value[0]}`
};

tooltipFormatter.args = tooltipFormatterProps;
tooltipFormatter.parameters = buildReactPropsAsString(
tooltipFormatterProps,
'ScatterPlotWidgetUI'
);
Loading