Let Plot components size themselves

pull/12168/head
Christopher Henn 2019-02-23 15:49:14 -08:00 committed by Chris Henn
parent 049e8f8c90
commit 0d774cecb0
3 changed files with 175 additions and 154 deletions

View File

@ -1,132 +1,34 @@
import React, {useReducer, useRef, useMemo, SFC, CSSProperties} from 'react'
import React, {SFC} from 'react'
import {AutoSizer} from 'react-virtualized'
import {Table, PlotEnv} from 'src/minard'
import {Axes} from 'src/minard/components/Axes'
import {useMousePos} from 'src/minard/utils/useMousePos'
import {useMountedEffect} from 'src/minard/utils/useMountedEffect'
import {
setDimensions,
setTable,
setControlledXDomain,
setControlledYDomain,
setXAxisLabel,
setYAxisLabel,
} from 'src/minard/utils/plotEnvActions'
import {plotEnvReducer, INITIAL_PLOT_ENV} from 'src/minard/utils/plotEnvReducer'
SizedPlot,
Props as SizedPlotProps,
} from 'src/minard/components/SizedPlot'
export interface Props {
//
// Required props
// ==============
//
table: Table
width: number
height: number
children: (env: PlotEnv) => JSX.Element
type Props = Pick<
SizedPlotProps,
Exclude<keyof SizedPlotProps, 'width' | 'height'>
> & {width?: number; height?: number}
//
// Miscellaneous options
// =====================
//
axesStroke?: string
tickFont?: string
tickFill?: string
xAxisLabel?: string
yAxisLabel?: string
// The x domain of the plot can be explicitly set. If this prop is passed,
// then the component is operating in a "controlled" mode, where it always
// uses the passed x domain. Any interaction with the plot that should change
// the x domain (clicking, brushing, etc.) will call the `onSetXDomain` prop
// when the component is in controlled mode. If the `xDomain` prop is not
// passed, then the component is "uncontrolled". It will compute and set the
// `xDomain` automatically.
xDomain?: [number, number]
onSetXDomain?: (xDomain: [number, number]) => void
// See the `xDomain` and `onSetXDomain` props
yDomain?: [number, number]
onSetYDomain?: (yDomain: [number, number]) => void
}
export const Plot: SFC<Props> = ({
width,
height,
table,
children,
axesStroke = '#31313d',
tickFont = 'bold 10px Roboto',
tickFill = '#8e91a1',
xAxisLabel = '',
yAxisLabel = '',
xDomain = null,
yDomain = null,
}) => {
const [env, dispatch] = useReducer(plotEnvReducer, {
...INITIAL_PLOT_ENV,
width,
height,
xDomain,
yDomain,
xAxisLabel,
yAxisLabel,
baseLayer: {...INITIAL_PLOT_ENV.baseLayer, table},
})
useMountedEffect(() => dispatch(setTable(table)), [table])
useMountedEffect(() => dispatch(setControlledXDomain(xDomain)), [xDomain])
useMountedEffect(() => dispatch(setControlledYDomain(yDomain)), [yDomain])
useMountedEffect(() => dispatch(setXAxisLabel(xAxisLabel)), [xAxisLabel])
useMountedEffect(() => dispatch(setYAxisLabel(yAxisLabel)), [yAxisLabel])
useMountedEffect(() => dispatch(setDimensions(width, height)), [
width,
height,
])
const mouseRegion = useRef<HTMLDivElement>(null)
const {x: hoverX, y: hoverY} = useMousePos(mouseRegion.current)
const childProps = useMemo(
() => ({
...env,
hoverX,
hoverY,
dispatch,
}),
[env, hoverX, hoverY, dispatch]
)
const plotStyle: CSSProperties = {
position: 'relative',
width: `${width}px`,
height: `${height}px`,
}
const layersStyle: CSSProperties = {
position: 'absolute',
top: `${env.margins.top}px`,
right: `${env.margins.right}px`,
bottom: `${env.margins.bottom}px`,
left: `${env.margins.left}px`,
/*
Works just like a `SizedPlot`, except it will measure the width and height of
the containing element if no `width` and `height` props are passed.
*/
export const Plot: SFC<Props> = props => {
if (props.width && props.height) {
return <SizedPlot {...props} width={props.width} height={props.height} />
}
return (
<div className="minard-plot" style={plotStyle}>
<Axes
env={env}
axesStroke={axesStroke}
tickFont={tickFont}
tickFill={tickFill}
>
<div className="minard-layers" style={layersStyle}>
{children(childProps)}
</div>
<div
className="minard-interaction-region"
style={layersStyle}
ref={mouseRegion}
/>
</Axes>
</div>
<AutoSizer>
{({width, height}) => {
if (width === 0 || height === 0) {
return null
}
return <SizedPlot {...props} width={width} height={height} />
}}
</AutoSizer>
)
}

View File

@ -0,0 +1,132 @@
import React, {useReducer, useRef, useMemo, SFC, CSSProperties} from 'react'
import {Table, PlotEnv} from 'src/minard'
import {Axes} from 'src/minard/components/Axes'
import {useMousePos} from 'src/minard/utils/useMousePos'
import {useMountedEffect} from 'src/minard/utils/useMountedEffect'
import {
setDimensions,
setTable,
setControlledXDomain,
setControlledYDomain,
setXAxisLabel,
setYAxisLabel,
} from 'src/minard/utils/plotEnvActions'
import {plotEnvReducer, INITIAL_PLOT_ENV} from 'src/minard/utils/plotEnvReducer'
export interface Props {
//
// Required props
// ==============
//
table: Table
width: number
height: number
children: (env: PlotEnv) => JSX.Element
//
// Miscellaneous options
// =====================
//
axesStroke?: string
tickFont?: string
tickFill?: string
xAxisLabel?: string
yAxisLabel?: string
// The x domain of the plot can be explicitly set. If this prop is passed,
// then the component is operating in a "controlled" mode, where it always
// uses the passed x domain. Any interaction with the plot that should change
// the x domain (clicking, brushing, etc.) will call the `onSetXDomain` prop
// when the component is in controlled mode. If the `xDomain` prop is not
// passed, then the component is "uncontrolled". It will compute and set the
// `xDomain` automatically.
xDomain?: [number, number]
onSetXDomain?: (xDomain: [number, number]) => void
// See the `xDomain` and `onSetXDomain` props
yDomain?: [number, number]
onSetYDomain?: (yDomain: [number, number]) => void
}
export const SizedPlot: SFC<Props> = ({
width,
height,
table,
children,
axesStroke = '#31313d',
tickFont = 'bold 10px Roboto',
tickFill = '#8e91a1',
xAxisLabel = '',
yAxisLabel = '',
xDomain = null,
yDomain = null,
}) => {
const [env, dispatch] = useReducer(plotEnvReducer, {
...INITIAL_PLOT_ENV,
width,
height,
xDomain,
yDomain,
xAxisLabel,
yAxisLabel,
baseLayer: {...INITIAL_PLOT_ENV.baseLayer, table},
})
useMountedEffect(() => dispatch(setTable(table)), [table])
useMountedEffect(() => dispatch(setControlledXDomain(xDomain)), [xDomain])
useMountedEffect(() => dispatch(setControlledYDomain(yDomain)), [yDomain])
useMountedEffect(() => dispatch(setXAxisLabel(xAxisLabel)), [xAxisLabel])
useMountedEffect(() => dispatch(setYAxisLabel(yAxisLabel)), [yAxisLabel])
useMountedEffect(() => dispatch(setDimensions(width, height)), [
width,
height,
])
const mouseRegion = useRef<HTMLDivElement>(null)
const {x: hoverX, y: hoverY} = useMousePos(mouseRegion.current)
const childProps = useMemo(
() => ({
...env,
hoverX,
hoverY,
dispatch,
}),
[env, hoverX, hoverY, dispatch]
)
const plotStyle: CSSProperties = {
position: 'relative',
width: `${width}px`,
height: `${height}px`,
}
const layersStyle: CSSProperties = {
position: 'absolute',
top: `${env.margins.top}px`,
right: `${env.margins.right}px`,
bottom: `${env.margins.bottom}px`,
left: `${env.margins.left}px`,
}
return (
<div className="minard-plot" style={plotStyle}>
<Axes
env={env}
axesStroke={axesStroke}
tickFont={tickFont}
tickFill={tickFill}
>
<div className="minard-layers" style={layersStyle}>
{children(childProps)}
</div>
<div
className="minard-interaction-region"
style={layersStyle}
ref={mouseRegion}
/>
</Axes>
</div>
)
}

View File

@ -1,7 +1,6 @@
// Libraries
import React, {useMemo, useEffect, SFC} from 'react'
import {connect} from 'react-redux'
import {AutoSizer} from 'react-virtualized'
import {
Plot as MinardPlot,
Histogram as MinardHistogram,
@ -99,36 +98,24 @@ const Histogram: SFC<Props> = ({
}
return (
<AutoSizer>
{({width, height}) => {
if (width === 0 || height === 0) {
return null
}
return (
<MinardPlot
table={table}
width={width}
height={height}
xAxisLabel={xAxisLabel}
xDomain={xDomain}
onSetXDomain={setXDomain}
>
{env => (
<MinardHistogram
env={env}
x={mappings.x}
fill={fill}
binCount={binCount}
position={position}
tooltip={HistogramTooltip}
colors={colorHexes}
/>
)}
</MinardPlot>
)
}}
</AutoSizer>
<MinardPlot
table={table}
xAxisLabel={xAxisLabel}
xDomain={xDomain}
onSetXDomain={setXDomain}
>
{env => (
<MinardHistogram
env={env}
x={mappings.x}
fill={fill}
binCount={binCount}
position={position}
tooltip={HistogramTooltip}
colors={colorHexes}
/>
)}
</MinardPlot>
)
}