Skip to content

Commit

Permalink
Migrate GenericPlot to functional component. (#19540)
Browse files Browse the repository at this point in the history
* Migrate `GenericPlot` to functional component.

* Improve types.

* Add correct type for `onRelayout` prop.
  • Loading branch information
linuspahl committed Jun 4, 2024
1 parent 42d5b91 commit 51ccd67
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 165 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import React from 'react';
import PropTypes from 'prop-types';

import { Spinner } from 'components/common';
import type { PlotLayout } from 'views/components/visualizations/GenericPlot';
import GenericPlot from 'views/components/visualizations/GenericPlot';

type Props = {
traffic: { [key: string]: number },
width: number,
layoutExtension?: {},
layoutExtension?: Partial<PlotLayout>,
};

const TrafficGraph = ({ width, traffic, layoutExtension }: Props) => {
Expand All @@ -36,7 +37,7 @@ const TrafficGraph = ({ width, traffic, layoutExtension }: Props) => {
x: Object.keys(traffic),
y: Object.values(traffic),
}];
const layout = {
const layout: Partial<PlotLayout> = {
showlegend: false,
margin: {
l: 60,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,33 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import PropTypes from 'prop-types';
import type { DefaultTheme } from 'styled-components';
import styled, { css, withTheme } from 'styled-components';
import { useContext, useMemo, useCallback } from 'react';
import styled, { css, useTheme } from 'styled-components';
import merge from 'lodash/merge';
import type * as Plotly from 'plotly.js';
import type { Layout } from 'plotly.js';

import Plot from 'views/components/visualizations/plotly/AsyncPlot';
import type ColorMapper from 'views/components/visualizations/ColorMapper';
import { EVENT_COLOR, eventsDisplayName } from 'views/logic/searchtypes/events/EventHandler';
import { ROOT_FONT_SIZE } from 'theme/constants';

import ChartColorContext from './ChartColorContext';
import styles from './GenericPlot.lazy.css';

import InteractiveContext from '../contexts/InteractiveContext';
import RenderCompletionCallback from '../widgets/RenderCompletionCallback';

export type PlotLayout = Layout

const StyledPlot = styled(Plot)(({ theme }) => css`
div.plotly-notifier {
visibility: hidden;
}
.customPopover .popover-content {
padding: 0;
}
.hoverlayer .hovertext {
rect {
fill: ${theme.colors.global.contentBackground} !important;
Expand Down Expand Up @@ -61,12 +70,6 @@ export type OnHoverMarkerEvent = {
y: string,
}

type LegendConfig = {
name: string,
target: HTMLElement,
color?: string,
};

type ChartMarker = {
colors?: Array<string>,
color?: string,
Expand All @@ -92,20 +95,14 @@ export type ChartColor = {

type Props = {
chartData: Array<any>,
layout: {},
onZoom: (from: string, to: string) => boolean,
layout?: Partial<PlotLayout>,
onZoom?: (from: string, to: string) => void,
setChartColor?: (data: ChartConfig, color: ColorMapper) => ChartColor,
onClickMarker?: (event: OnClickMarkerEvent) => void
onHoverMarker?: (event: OnHoverMarkerEvent) => void,
onUnhoverMarker?: () => void,
};

type GenericPlotProps = Props & { theme: DefaultTheme };

type State = {
legendConfig?: LegendConfig,
};

type Axis = {
autosize: boolean,
};
Expand All @@ -120,55 +117,18 @@ const style = { height: '100%', width: '100%' };

const config = { displayModeBar: false, doubleClick: false as const, responsive: true };

class GenericPlot extends React.Component<GenericPlotProps, State> {
static propTypes = {
chartData: PropTypes.array.isRequired,
layout: PropTypes.object,
onZoom: PropTypes.func,
setChartColor: PropTypes.func,
};

static defaultProps = {
layout: {},
onZoom: () => true,
setChartColor: undefined,
onClickMarker: (_event) => {},
onHoverMarker: (_event) => {},
onUnhoverMarker: () => {},
};

constructor(props: GenericPlotProps) {
super(props);
this.state = {};
}
const usePlotLayout = (layout: {}) => {
const theme = useTheme();
const interactive = useContext(InteractiveContext);
const { colors } = useContext(ChartColorContext);

componentDidMount() {
styles.use();
}

componentWillUnmount() {
styles.unuse();
}

_onRelayout = (axis: Axis) => {
if (!axis.autosize && axis['xaxis.range[0]'] && axis['xaxis.range[1]']) {
const { onZoom } = this.props;
const from = axis['xaxis.range[0]'];
const to = axis['xaxis.range[1]'];

return onZoom(from, to);
}

return true;
};

render() {
const { chartData, layout, setChartColor, theme, onClickMarker, onHoverMarker, onUnhoverMarker } = this.props;
return useMemo(() => {
const fontSettings = {
color: theme.colors.global.textDefault,
size: ROOT_FONT_SIZE * Number(theme.fonts.size.small.replace(/rem|em/i, '')),
family: theme.fonts.family.body,
};

const defaultLayout = {
shapes: [],
autosize: true,
Expand Down Expand Up @@ -208,82 +168,101 @@ class GenericPlot extends React.Component<GenericPlotProps, State> {
},
},
};

const plotLayout = merge({}, defaultLayout, layout);

const _onHoverMarker = (event: unknown) => {
const { points } = event as { points: Array<{ bbox: { x0: number, y0: number }, y: string, x: string }> };
plotLayout.shapes = plotLayout.shapes.map((shape) => ({
...shape,
line: { color: shape?.line?.color || colors.get(eventsDisplayName, EVENT_COLOR) },
}));

onHoverMarker?.({
positionX: points[0].bbox.x0,
positionY: points[0].bbox.y0,
x: points[0].x,
y: points[0].y,
});
};
return interactive ? plotLayout : merge({}, nonInteractiveLayout, plotLayout);
}, [colors, interactive, layout, theme.colors.global.textDefault, theme.colors.variant.lightest.default, theme.fonts.family.body, theme.fonts.size.small]);
};

const _onMarkerClick = ({ points }: Readonly<Plotly.PlotMouseEvent>) => {
onClickMarker?.({
x: points[0].x as string,
y: points[0].y as string,
});
};
const usePlotChatData = (chartData: Array<any>, setChartColor: (data: ChartConfig, color: ColorMapper) => ChartColor) => {
const theme = useTheme();
const { colors } = useContext(ChartColorContext);

return (
<ChartColorContext.Consumer>
{({ colors }) => {
plotLayout.shapes = plotLayout.shapes.map((shape) => ({
...shape,
line: { color: shape?.line?.color || colors.get(eventsDisplayName, EVENT_COLOR) },
}));

const newChartData = chartData.map((chart) => {
if (setChartColor && colors) {
const conf = setChartColor(chart, colors);

if (chart.type === 'pie') {
conf.outsidetextfont = { color: theme.colors.global.textDefault };
}

if (chart?.name === eventsDisplayName) {
const eventColor = colors.get(eventsDisplayName, EVENT_COLOR);

conf.marker = { color: eventColor, size: 5 };
}

if (conf.line || conf.marker) {
return merge(chart, conf);
}

return chart;
}

return chart;
});

return (
<InteractiveContext.Consumer>
{(interactive) => (
<RenderCompletionCallback.Consumer>
{(onRenderComplete) => (
<StyledPlot data={newChartData}
useResizeHandler
layout={interactive ? plotLayout : merge({}, nonInteractiveLayout, plotLayout)}
style={style}
onAfterPlot={onRenderComplete}
onClick={interactive ? _onMarkerClick : () => false}
onHover={_onHoverMarker}
onUnhover={onUnhoverMarker}
onRelayout={interactive ? this._onRelayout : () => false}
config={config} />
)}
</RenderCompletionCallback.Consumer>
)}
</InteractiveContext.Consumer>
);
}}
</ChartColorContext.Consumer>
);
}
}
return useMemo(() => chartData.map((chart) => {
if (setChartColor && colors) {
const conf = setChartColor(chart, colors);

if (chart.type === 'pie') {
conf.outsidetextfont = { color: theme.colors.global.textDefault };
}

if (chart?.name === eventsDisplayName) {
const eventColor = colors.get(eventsDisplayName, EVENT_COLOR);

conf.marker = { color: eventColor, size: 5 };
}

if (conf.line || conf.marker) {
return merge(chart, conf);
}

return chart;
}

return chart;
}), [chartData, colors, setChartColor, theme.colors.global.textDefault]);
};

const GenericPlot = ({ chartData, layout, setChartColor, onClickMarker, onHoverMarker, onUnhoverMarker, onZoom }: Props) => {
const interactive = useContext(InteractiveContext);
const plotLayout = usePlotLayout(layout);
const plotChartData = usePlotChatData(chartData, setChartColor);
const onRenderComplete = useContext(RenderCompletionCallback);

const _onRelayout = useCallback((axis: Axis) => {
if (!axis.autosize && axis['xaxis.range[0]'] && axis['xaxis.range[1]']) {
const from = axis['xaxis.range[0]'];
const to = axis['xaxis.range[1]'];

onZoom(from, to);
}
}, [onZoom]);

const _onHoverMarker = useCallback((event: unknown) => {
const { points } = event as { points: Array<{ bbox: { x0: number, y0: number }, y: string, x: string }> };

onHoverMarker?.({
positionX: points[0].bbox.x0,
positionY: points[0].bbox.y0,
x: points[0].x,
y: points[0].y,
});
}, [onHoverMarker]);

const _onMarkerClick = useCallback(({ points }: Readonly<Plotly.PlotMouseEvent>) => {
onClickMarker?.({
x: points[0].x as string,
y: points[0].y as string,
});
}, [onClickMarker]);

return (
<StyledPlot data={plotChartData}
useResizeHandler
layout={plotLayout}
style={style}
onAfterPlot={onRenderComplete}
onClick={interactive ? _onMarkerClick : () => false}
onHover={_onHoverMarker}
onUnhover={onUnhoverMarker}
onRelayout={interactive ? _onRelayout : () => {}}
config={config} />
);
};

GenericPlot.defaultProps = {
layout: {},
onZoom: () => {},
setChartColor: undefined,
onClickMarker: (_event) => {},
onHoverMarker: (_event) => {},
onUnhoverMarker: () => {},
};

export default withTheme(GenericPlot);
export default GenericPlot;
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import assertUnreachable from 'logic/assertUnreachable';
import useAppDispatch from 'stores/useAppDispatch';

import GenericPlot from './GenericPlot';
import type { ChartColor, ChartConfig } from './GenericPlot';
import type { ChartColor, ChartConfig, PlotLayout } from './GenericPlot';
import OnZoom from './OnZoom';

import CustomPropTypes from '../CustomPropTypes';
Expand All @@ -43,7 +43,7 @@ export type Props = {
},
height?: number;
setChartColor?: (config: ChartConfig, color: ColorMapper) => ChartColor,
plotLayout?: any,
plotLayout?: Partial<PlotLayout>,
onZoom?: (from: string, to: string, userTimezone: string) => boolean,
};

Expand All @@ -59,13 +59,6 @@ const yLegendPosition = (containerHeight: number) => {
return -0.14;
};

type Layout = {
yaxis: { fixedrange?: boolean },
legend?: { y?: number },
showlegend?: boolean,
hovermode: 'x',
};

const mapAxisType = (axisType: AxisType): 'linear' | 'log' => {
switch (axisType) {
case 'linear': return 'linear';
Expand All @@ -87,8 +80,8 @@ const XYPlot = ({
onZoom,
}: Props) => {
const { formatTime, userTimezone } = useUserDateTime();
const yaxis = { fixedrange: true, rangemode: 'tozero', tickformat: ',~r', type: mapAxisType(axisType) };
const defaultLayout: Layout = {
const yaxis = { fixedrange: true, rangemode: 'tozero', tickformat: ',~r', type: mapAxisType(axisType) } as const;
const defaultLayout: Partial<PlotLayout> = {
yaxis,
hovermode: 'x',
};
Expand All @@ -97,7 +90,7 @@ const XYPlot = ({
defaultLayout.legend = { y: yLegendPosition(height) };
}

const layout = { ...defaultLayout, ...plotLayout };
const layout: Partial<PlotLayout> = { ...defaultLayout, ...plotLayout };
const dispatch = useAppDispatch();
// eslint-disable-next-line react-hooks/exhaustive-deps
const _onZoom = useCallback(config.isTimeline
Expand Down
Loading

0 comments on commit 51ccd67

Please sign in to comment.