Allow custom options for charts
This commit is contained in:
@@ -8,7 +8,8 @@ import { bindActionCreators } from 'redux'
|
||||
import { chartActions } from '../../actions'
|
||||
import { ChartParameters } from '../../reducers/Charts'
|
||||
import { connect } from 'react-redux'
|
||||
import { Fade, Paper, Theme, Typography, withStyles } from '@material-ui/core'
|
||||
import { Paper, Theme, Typography, withStyles } from '@material-ui/core'
|
||||
import { SettingsButton } from './ChartSettings/SettingsButton'
|
||||
const throttle = require('lodash.throttle')
|
||||
|
||||
interface Props {
|
||||
@@ -28,41 +29,17 @@ function Chart(props: Props) {
|
||||
const { tree, parameters } = props
|
||||
const initialTreeNode = tree.findNode(parameters.topic)
|
||||
const [treeNode, setTreeNode] = React.useState<q.TreeNode<any> | undefined>(initialTreeNode)
|
||||
const [lastUpdate, setLastUpdate] = React.useState(0)
|
||||
usePollingToFetchTreeNode(treeNode, tree, parameters, setTreeNode)
|
||||
|
||||
/** If a node is not available when the plot is shown, keep polling until it has been created */
|
||||
function pollForTreeNode() {
|
||||
const onUpdateCallback = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
|
||||
let intervalTimer: any
|
||||
|
||||
if (!treeNode) {
|
||||
intervalTimer = setInterval(() => {
|
||||
const node = tree.findNode(parameters.topic)
|
||||
if (node) {
|
||||
setTreeNode(node)
|
||||
node.onMessage.subscribe(onUpdateCallback)
|
||||
clearInterval(intervalTimer)
|
||||
}
|
||||
}, 500)
|
||||
} else {
|
||||
treeNode.onMessage.subscribe(onUpdateCallback)
|
||||
}
|
||||
|
||||
return function cleanup() {
|
||||
treeNode && treeNode.onMessage.unsubscribe(onUpdateCallback)
|
||||
intervalTimer && clearInterval(intervalTimer)
|
||||
}
|
||||
}
|
||||
React.useEffect(pollForTreeNode)
|
||||
|
||||
const onClick = React.useCallback(() => {
|
||||
const onRemove = React.useCallback(() => {
|
||||
props.actions.chart.removeChart(props.parameters)
|
||||
}, [props.parameters])
|
||||
|
||||
return (
|
||||
<Paper style={{ padding: '8px' }}>
|
||||
<div style={{ float: 'right' }}>
|
||||
<CustomIconButton tooltip="Remove chart" onClick={onClick}>
|
||||
<SettingsButton parameters={parameters} />
|
||||
<CustomIconButton tooltip="Remove chart" onClick={onRemove}>
|
||||
<Clear />
|
||||
</CustomIconButton>
|
||||
</div>
|
||||
@@ -74,7 +51,16 @@ function Chart(props: Props) {
|
||||
{parameters.topic}
|
||||
</Typography>
|
||||
<br />
|
||||
{treeNode ? <TopicPlot history={treeNode.messageHistory} dotPath={parameters.dotPath} /> : <span>No data</span>}
|
||||
{treeNode ? (
|
||||
<TopicPlot
|
||||
interpolation={props.parameters.interpolation}
|
||||
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
|
||||
history={treeNode.messageHistory}
|
||||
dotPath={parameters.dotPath}
|
||||
/>
|
||||
) : (
|
||||
<span>No data</span>
|
||||
)}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
@@ -106,3 +92,37 @@ export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(Chart))
|
||||
|
||||
/**
|
||||
* If a node is not available when the plot is shown, keep polling until it has been created
|
||||
*/
|
||||
function usePollingToFetchTreeNode(
|
||||
treeNode: q.TreeNode<any> | undefined,
|
||||
tree: q.Tree<any>,
|
||||
parameters: ChartParameters,
|
||||
setTreeNode: React.Dispatch<React.SetStateAction<q.TreeNode<any> | undefined>>
|
||||
) {
|
||||
const [lastUpdate, setLastUpdate] = React.useState(0)
|
||||
|
||||
function pollForTreeNode() {
|
||||
const onUpdateCallback = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
|
||||
let intervalTimer: any
|
||||
if (!treeNode) {
|
||||
intervalTimer = setInterval(() => {
|
||||
const node = tree.findNode(parameters.topic)
|
||||
if (node) {
|
||||
setTreeNode(node)
|
||||
node.onMessage.subscribe(onUpdateCallback)
|
||||
clearInterval(intervalTimer)
|
||||
}
|
||||
}, 500)
|
||||
} else {
|
||||
treeNode.onMessage.subscribe(onUpdateCallback)
|
||||
}
|
||||
return function cleanup() {
|
||||
treeNode && treeNode.onMessage.unsubscribe(onUpdateCallback)
|
||||
intervalTimer && clearInterval(intervalTimer)
|
||||
}
|
||||
}
|
||||
React.useEffect(pollForTreeNode)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as React from 'react'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { chartActions } from '../../../actions'
|
||||
import { ChartParameters, PlotCurveTypes } from '../../../reducers/Charts'
|
||||
import { connect } from 'react-redux'
|
||||
import { Menu, MenuItem } from '@material-ui/core'
|
||||
|
||||
function chartParametersForAction(chart: ChartParameters, action: string) {
|
||||
return {
|
||||
topic: chart.topic,
|
||||
dotPath: chart.dotPath,
|
||||
interpolation: action as any,
|
||||
}
|
||||
}
|
||||
|
||||
const curves: Array<PlotCurveTypes> = ['curve', 'linear', 'step_after', 'step_before', 'cubic_basis_spline']
|
||||
|
||||
function InterpolationSettings(props: {
|
||||
chart: ChartParameters
|
||||
actions: {
|
||||
chart: typeof chartActions
|
||||
}
|
||||
anchorEl?: Element
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
const callbacks = React.useMemo(() => {
|
||||
const createCurveCallback = (curve: PlotCurveTypes) => () => {
|
||||
props.actions.chart.updateChart(chartParametersForAction(props.chart, curve))
|
||||
}
|
||||
|
||||
const callbacks: { [key: string]: () => void } = {}
|
||||
for (const curve of curves) {
|
||||
callbacks[curve] = createCurveCallback(curve)
|
||||
}
|
||||
return callbacks
|
||||
}, [curves])
|
||||
|
||||
const menuItems = React.useMemo(() => {
|
||||
return curves.map(curve => (
|
||||
<MenuItem key={curve} onClick={callbacks[curve]} selected={props.chart.interpolation === curve}>
|
||||
{curve.replace(/_/g, ' ')}
|
||||
</MenuItem>
|
||||
))
|
||||
}, [curves, props.chart])
|
||||
|
||||
return (
|
||||
<Menu id="long-menu" anchorEl={props.anchorEl} open={props.open} onClose={props.onClose}>
|
||||
{menuItems}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: {
|
||||
chart: bindActionCreators(chartActions, dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(InterpolationSettings)
|
||||
@@ -0,0 +1,78 @@
|
||||
import * as React from 'react'
|
||||
import { ChartParameters } from '../../../reducers/Charts'
|
||||
import { Menu, MenuItem, TextField, Typography } from '@material-ui/core'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { chartActions } from '../../../actions'
|
||||
|
||||
function RangeSettings(props: {
|
||||
actions: { chart: typeof chartActions }
|
||||
chart: ChartParameters
|
||||
anchorEl?: Element
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
const dismissClick = React.useCallback((e: React.MouseEvent) => e.stopPropagation(), [])
|
||||
const [rangeFrom, setRangeFrom] = React.useState<string | number | undefined>(
|
||||
props.chart.range && props.chart.range.from
|
||||
)
|
||||
const [rangeTo, setRangeTo] = React.useState<string | number | undefined>(props.chart.range && props.chart.range.to)
|
||||
|
||||
React.useEffect(() => {
|
||||
const from = parseFloat(rangeFrom as any)
|
||||
const to = parseFloat(rangeTo as any)
|
||||
props.actions.chart.updateChart({
|
||||
topic: props.chart.topic,
|
||||
dotPath: props.chart.dotPath,
|
||||
range: {
|
||||
from: isNaN(from) ? undefined : from,
|
||||
to: isNaN(to) ? undefined : to,
|
||||
},
|
||||
})
|
||||
}, [rangeFrom, rangeTo])
|
||||
|
||||
const setFromHandler = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => setRangeFrom(e.target.value), [])
|
||||
const setToHandler = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => setRangeTo(e.target.value), [])
|
||||
return (
|
||||
<Menu
|
||||
style={{ textAlign: 'center' }}
|
||||
id="long-menu"
|
||||
anchorEl={props.anchorEl}
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<Typography>Define custom ranges for the Y-Axis</Typography>
|
||||
<div style={{ padding: '0 16px' }}>
|
||||
<TextField
|
||||
style={{ marginTop: '0' }}
|
||||
onClick={dismissClick}
|
||||
label="from"
|
||||
value={rangeFrom}
|
||||
onChange={setFromHandler}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
style={{ marginLeft: '8px', marginTop: '0' }}
|
||||
onClick={dismissClick}
|
||||
label="to"
|
||||
value={rangeTo}
|
||||
onChange={setToHandler}
|
||||
margin="normal"
|
||||
/>
|
||||
</div>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: {
|
||||
chart: bindActionCreators(chartActions, dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(RangeSettings)
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react'
|
||||
import ChartSettings from '.'
|
||||
import CustomIconButton from '../../helper/CustomIconButton'
|
||||
import MoreVertIcon from '@material-ui/icons/Settings'
|
||||
import { ChartParameters } from '../../../reducers/Charts'
|
||||
|
||||
export function SettingsButton(props: { parameters: ChartParameters }) {
|
||||
const [visible, setVisible] = React.useState(false)
|
||||
const settingsRef = React.useRef()
|
||||
const toggleSettings = React.useCallback(() => {
|
||||
setVisible(!visible)
|
||||
}, [visible])
|
||||
|
||||
const close = React.useCallback(() => {
|
||||
setVisible(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<span>
|
||||
<ChartSettings open={visible} close={close} anchorEl={settingsRef} chart={props.parameters} />
|
||||
<CustomIconButton tooltip="Chart settings" onClick={toggleSettings}>
|
||||
<MoreVertIcon ref={settingsRef as any} />
|
||||
</CustomIconButton>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
51
app/src/components/ChartPanel/ChartSettings/index.tsx
Normal file
51
app/src/components/ChartPanel/ChartSettings/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react'
|
||||
import InterpolationSettings from './InterpolationSettings'
|
||||
import { ChartParameters } from '../../../reducers/Charts'
|
||||
import { Menu, MenuItem } from '@material-ui/core'
|
||||
import RangeSettings from './RangeSettings'
|
||||
|
||||
function ChartSettings(props: {
|
||||
open: boolean
|
||||
close: () => void
|
||||
chart: ChartParameters
|
||||
anchorEl: React.MutableRefObject<undefined>
|
||||
}) {
|
||||
const [rangeVisible, setRangeVisible] = React.useState(false)
|
||||
const [interpolationVisible, setInterpolationVisible] = React.useState(false)
|
||||
|
||||
const toggleRange = React.useCallback(() => {
|
||||
if (!rangeVisible && open) {
|
||||
props.close()
|
||||
}
|
||||
setRangeVisible(!rangeVisible)
|
||||
}, [rangeVisible, open])
|
||||
|
||||
const toggleInterpolation = React.useCallback(() => {
|
||||
if (!interpolationVisible && open) {
|
||||
props.close()
|
||||
}
|
||||
setInterpolationVisible(!interpolationVisible)
|
||||
}, [interpolationVisible, open])
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Menu id="long-menu" anchorEl={props.anchorEl.current} open={props.open} onClose={props.close}>
|
||||
<MenuItem key="range" onClick={toggleRange}>
|
||||
Set range
|
||||
</MenuItem>
|
||||
<MenuItem key="interpolation" onClick={toggleInterpolation}>
|
||||
Curve interpolation
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<RangeSettings chart={props.chart} anchorEl={props.anchorEl.current} open={rangeVisible} onClose={toggleRange} />
|
||||
<InterpolationSettings
|
||||
chart={props.chart}
|
||||
anchorEl={props.anchorEl.current}
|
||||
open={interpolationVisible}
|
||||
onClose={toggleInterpolation}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChartSettings
|
||||
@@ -60,7 +60,7 @@ function ChartPanel(props: Props) {
|
||||
))
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', padding: '8px', borderTop: '1px solid #999' }}>
|
||||
<div style={{ width: '100%', height: '100%', padding: '8px' }}>
|
||||
<Grid container spacing={1}>
|
||||
<TransitionGroup component={null} className="example">
|
||||
{charts}
|
||||
|
||||
Reference in New Issue
Block a user