Add numeric chart panel

This commit is contained in:
Thomas Nordquist
2019-06-16 19:10:37 +02:00
parent 4ec8cdf0ff
commit 209899c3b8
28 changed files with 719 additions and 219 deletions

View File

@@ -97,19 +97,16 @@ const styles = (theme: Theme) => {
const drawerWidth = 300
const contentBaseStyle = {
width: '100vw',
overflow: 'hidden' as 'hidden',
backgroundColor: theme.palette.background.default,
}
return {
heightProperty: {
height: 'calc(100vh - 64px) !important',
height: '100%', // 'calc(100vh - 64px) !important',
},
paneDefaults: {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
overflowY: 'scroll' as 'scroll',
overflowX: 'hidden' as 'hidden',
display: 'block' as 'block',
height: 'calc(100vh - 64px)',
},

View File

@@ -0,0 +1,107 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import Clear from '@material-ui/icons/Clear'
import CustomIconButton from '../helper/CustomIconButton'
import TopicPlot from '../TopicPlot'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../actions'
import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux'
import { Paper, Theme, Typography, withStyles, Fade } from '@material-ui/core'
interface Props {
parameters: ChartParameters
tree?: q.Tree<any>
classes: any
actions: {
chart: typeof chartActions
}
}
function Chart(props: Props) {
if (!props.tree) {
return null
}
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)
/** If a node is not available when the plot is shown, keep polling until it has been created */
function pollForTreeNode() {
const onUpdateCallback = () => setLastUpdate(treeNode ? treeNode.lastUpdate : 0)
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(() => {
props.actions.chart.removeChart(props.parameters)
}, [props.parameters])
return (
<Paper style={{ padding: '8px' }}>
<div style={{ float: 'right' }}>
<CustomIconButton tooltip="Remove chart" onClick={onClick}>
<Clear />
</CustomIconButton>
</div>
<Typography variant="caption" className={props.classes.topic}>
{parameters.dotPath ? parameters.dotPath : ''}
</Typography>
<br />
<Typography variant="caption" className={props.classes.topic}>
{parameters.topic}
</Typography>
<br />
{treeNode ? <TopicPlot history={treeNode.messageHistory} dotPath={parameters.dotPath} /> : <span>No data</span>}
</Paper>
)
}
const mapStateToProps = (state: AppState) => {
return {
tree: state.tree.get('tree'),
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
const styles = (theme: Theme) => ({
topic: {
wordBreak: 'break-all' as 'break-all',
whiteSpace: 'nowrap' as 'nowrap',
overflow: 'hidden' as 'hidden',
textOverflow: 'ellipsis' as 'ellipsis',
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(Chart))

View File

@@ -0,0 +1,124 @@
import * as React from 'react'
import Chart from './Chart'
import ShowChart from '@material-ui/icons/ShowChart'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../actions'
import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux'
import { Grid, Theme, Typography, withStyles } from '@material-ui/core'
import { List } from 'immutable'
const { TransitionGroup, CSSTransition } = require('react-transition-group/esm')
interface Props {
charts: List<ChartParameters>
connectionId?: string
actions: {
chart: typeof chartActions
}
}
function spacingForChartCount(count: number): 4 | 6 | 12 {
if (count >= 5) {
return 4
} else if (count >= 2) {
return 6
} else {
return 12
}
}
// function FadingChart(props: { chartParameters: ChartParameters; chartsInView: number; key: any }) {
// const { chartsInView, chartParameters } = props
// const [spacing, setSpacing] = React.useState(spacingForChartCount(chartsInView))
// // Update spacing after animations have completed
// React.useEffect(() => {
// const newSpacing = spacingForChartCount(chartsInView)
// if (spacing !== newSpacing) {
// setSpacing(newSpacing)
// // setTimeout(() => , 500)
// }
// })
// return (
// )
// }
function ChartPanel(props: Props) {
const chartsInView = props.charts.count()
const [spacing, setSpacing] = React.useState(spacingForChartCount(chartsInView))
React.useEffect(() => {
props.actions.chart.loadCharts()
}, [props.connectionId])
// Update spacing after animations have completed
React.useEffect(() => {
const newSpacing = spacingForChartCount(chartsInView)
if (newSpacing > spacing) {
setTimeout(() => setSpacing(newSpacing), 500)
} else {
setSpacing(newSpacing)
}
}, [chartsInView])
const charts = props.charts.map(chartParameters => (
<CSSTransition
key={`${chartParameters.topic}-${chartParameters.dotPath || ''}`}
timeout={{ enter: 500, exit: 500 }}
classNames="example"
>
<Grid item xs={spacing}>
<Chart parameters={chartParameters} />
</Grid>
</CSSTransition>
))
return (
<div style={{ width: '100%', height: '100%', padding: '8px', borderTop: '1px solid #999' }}>
<Grid container spacing={1}>
<TransitionGroup component={null} className="example">
{charts}
</TransitionGroup>
{chartsInView === 0 ? <NoCharts key="noCharts" /> : null}
</Grid>
</div>
)
}
function NoCharts() {
return (
<div style={{ width: '100%', textAlign: 'center' }}>
<Typography variant="h2">No charts selected</Typography>
<Typography>Select a numeric values from the value preview.</Typography>
<Typography>
Click on <ShowChart /> to add a topic / value to this panel.
</Typography>
</div>
)
}
const mapStateToProps = (state: AppState) => {
return {
charts: state.charts.get('charts'),
connectionId: state.connection.connectionId,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
const styles = (theme: Theme) => ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ChartPanel))

View File

@@ -104,6 +104,7 @@ const styles = (theme: Theme) => ({
},
textColor: {
color: theme.palette.text.primary,
userSelect: 'all' as 'all',
},
centered: {
textAlign: 'center' as 'center',

View File

@@ -1,27 +1,43 @@
import * as React from 'react'
import ReactSplitPane from 'react-split-pane'
import { Sidebar } from '../Sidebar'
import Tree from '../Tree/Tree'
import ChartPanel from '../ChartPanel'
import { Sidebar } from '../Sidebar'
export default function ContentView(props: { heightProperty: any; paneDefaults: any; connectionId: any }) {
const [height, setHeight] = React.useState(0)
return (
<ReactSplitPane
step={20}
primary="second"
className={props.heightProperty}
split="vertical"
minSize={250}
defaultSize={500}
allowResize={true}
style={{ position: 'relative' }}
pane1Style={{ overflow: 'hidden' }}
>
<div className={props.paneDefaults}>
<Tree />
</div>
<div className={props.paneDefaults}>
<Sidebar connectionId={props.connectionId} />
</div>
</ReactSplitPane>
<div className={props.paneDefaults}>
<ReactSplitPane
step={10}
split="horizontal"
minSize={0}
defaultSize={'100%'}
allowResize={true}
style={{ height: 'calc(100vh - 64px)' }}
pane1Style={{ maxHeight: '100%' }}
pane2Style={{ maxWidth: '100%', overflow: 'hidden auto' }}
onChange={setHeight}
>
<ReactSplitPane
step={20}
primary="second"
className={props.heightProperty}
split="vertical"
minSize={250}
defaultSize={500}
allowResize={true}
style={{ height: '100%' }}
pane1Style={{ overflowX: 'hidden' }}
resizerStyle={{ height: '100%' }}
>
<Tree />
<div className={props.paneDefaults} style={{ height: '100%', overflowY: 'auto', overflowX: 'hidden' }}>
<Sidebar connectionId={props.connectionId} />
</div>
</ReactSplitPane>
<ChartPanel />
</ReactSplitPane>
</div>
)
}

View File

@@ -0,0 +1,82 @@
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import ShowChart from '@material-ui/icons/ShowChart'
import TopicPlot from '../../TopicPlot'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { connect } from 'react-redux'
import { Fade, Paper, Popper, Tooltip } from '@material-ui/core'
import { JsonPropertyLocation } from '../../../../../backend/src/JsonAstParser'
interface Props {
treeNode: q.TreeNode<any>
classes: any
literal: JsonPropertyLocation
actions: {
chart: typeof chartActions
}
}
function ChartPreview(props: Props) {
const chartIconRef = React.useRef(null)
const [open, setOpen] = React.useState(false)
const onClick = React.useCallback(() => {
props.actions.chart.addChart({
topic: props.treeNode.path(),
dotPath: props.literal.path,
})
}, [props.literal.path, props.treeNode])
const mouseOver = React.useCallback(() => {
setOpen(true)
}, [])
const mouseOut = React.useCallback(() => {
setOpen(false)
}, [])
const hasEnoughDataToDisplayDiagrams = props.treeNode.messageHistory.count() > 1
let preview = hasEnoughDataToDisplayDiagrams ? (
<Tooltip title="Click to add to chart panel">
<ShowChart
ref={chartIconRef}
className={props.classes.icon}
onMouseEnter={mouseOver}
onMouseLeave={mouseOut}
onClick={onClick}
/>
</Tooltip>
) : (
<Tooltip title="Click to add to chart panel, not enough data for preview">
<ShowChart onClick={onClick} className={props.classes.icon} style={{ color: '#aaa' }} />
</Tooltip>
)
return (
<span>
{preview}
<Popper open={open} anchorEl={chartIconRef.current} placement="left-end">
<Fade in={open} timeout={300}>
<Paper style={{ width: '300px' }}>
{open ? <TopicPlot history={props.treeNode.messageHistory} dotPath={props.literal.path} /> : <span />}
</Paper>
</Fade>
</Popper>
</span>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(
undefined,
mapDispatchToProps
)(ChartPreview)

View File

@@ -2,10 +2,10 @@ import * as diff from 'diff'
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import Add from '@material-ui/icons/Add'
import ChartPreview from './ChartPreview'
import Remove from '@material-ui/icons/Remove'
import ShowChart from '@material-ui/icons/ShowChart'
import TopicPlot from '../TopicPlot'
import { Fade, Paper, Popper, Theme, Tooltip } from '@material-ui/core'
import { Theme, Tooltip } from '@material-ui/core'
import { JsonPropertyLocation } from '../../../../../backend/src/JsonAstParser'
import { lineChangeStyle, trimNewlineRight } from './util'
import { withStyles } from '@material-ui/styles'
@@ -15,7 +15,7 @@ interface Props {
literalPositions: Array<JsonPropertyLocation>
classes: any
className: string
messageHistory: q.MessageHistory
treeNode: q.TreeNode<any>
}
const style = (theme: Theme) => {
@@ -29,12 +29,9 @@ const style = (theme: Theme) => {
return {
icon,
iconDisabled: {
...icon,
color: theme.palette.text.disabled,
},
iconButton: {
...icon,
marginTop: '0px',
width: '16px',
height: '16px',
padding: '2px',
@@ -52,66 +49,24 @@ const style = (theme: Theme) => {
}
}
function ChartIcon(props: { messageHistory: q.MessageHistory; classes: any; literal: JsonPropertyLocation }) {
const chartIconRef = React.useRef(null)
const [open, setOpen] = React.useState(false)
const mouseOver = React.useCallback(
(event: React.MouseEvent<Element>) => {
setOpen(true)
},
[props.literal.path]
)
const mouseOut = React.useCallback(() => {
setOpen(false)
}, [])
return (
<span>
<ShowChart ref={chartIconRef} className={props.classes.icon} onMouseEnter={mouseOver} onMouseLeave={mouseOut} />
<Popper open={open} anchorEl={chartIconRef.current} placement="left-end">
<Fade in={open} timeout={300}>
<Paper style={{ width: '300px' }}>
{open ? <TopicPlot history={props.messageHistory} dotPath={props.literal.path} /> : <span />}
</Paper>
</Fade>
</Popper>
</span>
)
}
function tokensForLine(change: diff.Change, line: number, props: Props) {
const { classes, literalPositions } = props
const hasEnoughDataToDisplayDiagrams = props.messageHistory.count() > 1
const literal = literalPositions[line]
let chartIcon = null
let chartPreview = null
if (literal) {
if (hasEnoughDataToDisplayDiagrams) {
chartIcon = (
<ChartIcon
messageHistory={props.messageHistory}
classes={{ icon: props.classes.iconButton }}
literal={literal}
/>
)
} else {
chartIcon = (
<Tooltip title="Not enough data">
<ShowChart className={props.classes.iconDisabled} style={{ color: '#aaa' }} />
</Tooltip>
)
}
chartPreview = (
<ChartPreview treeNode={props.treeNode} classes={{ icon: props.classes.iconButton }} literal={literal} />
)
}
if (change.added) {
return [chartIcon, <Add key="add" className={classes.icon} />]
return [chartPreview, <Add key="add" className={classes.icon} />]
} else if (change.removed) {
return [<Remove key="remove" className={classes.icon} />]
} else {
return [
chartIcon,
chartPreview,
<div
key="placeholder"
style={{ width: '12px', display: 'inline-block' }}

View File

@@ -12,7 +12,7 @@ import { withStyles } from '@material-ui/core'
import 'prismjs/components/prism-json'
interface Props {
messageHistory: q.MessageHistory
treeNode: q.TreeNode<any>
previous: string
current: string
nameOfCompareMessage: string
@@ -93,7 +93,7 @@ class CodeDiff extends React.Component<Props, State> {
<Gutters
className={this.props.classes.gutters}
changes={changes}
messageHistory={this.props.messageHistory}
treeNode={this.props.treeNode}
literalPositions={literalPositions}
/>
<pre className={this.props.classes.codeBlock}>{code}</pre>

View File

@@ -188,7 +188,6 @@ const mapDispatchToProps = (dispatch: any) => {
const styles = (theme: Theme) => ({
drawer: {
display: 'block' as 'block',
height: '100%',
},
badge: {
top: '3px',

View File

@@ -4,7 +4,7 @@ import BarChart from '@material-ui/icons/BarChart'
import Copy from '../../helper/Copy'
import DateFormatter from '../../helper/DateFormatter'
import History from '../HistoryDrawer'
import TopicPlot from '../TopicPlot'
import TopicPlot from '../../TopicPlot'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { isPlottable } from '../CodeDiff/util'
import { TopicViewModel } from '../../../model/TopicViewModel'

View File

@@ -53,13 +53,7 @@ class ValuePanel extends React.Component<Props, State> {
return null
}
return (
<ValueRenderer
message={node.message}
messageHistory={node.messageHistory}
compareWith={this.props.compareMessage}
/>
)
return <ValueRenderer treeNode={node} message={node.message} compareWith={this.props.compareMessage} />
}
private renderViewOptions() {

View File

@@ -9,7 +9,7 @@ import { ValueRendererDisplayMode } from '../../../reducers/Settings'
interface Props {
message: q.Message
messageHistory: q.MessageHistory
treeNode: q.TreeNode<any>
compareWith?: q.Message
renderMode: ValueRendererDisplayMode
}
@@ -27,7 +27,7 @@ class ValueRenderer extends React.Component<Props, State> {
private renderDiff(current: string = '', previous: string = '', language?: 'json') {
return (
<CodeDiff
messageHistory={this.props.messageHistory}
treeNode={this.props.treeNode}
previous={previous}
current={current}
language={language}
@@ -65,9 +65,8 @@ class ValueRenderer extends React.Component<Props, State> {
}
public renderValue() {
const { message, messageHistory, compareWith, renderMode } = this.props
const previousMessages = messageHistory.toArray()
const { message, treeNode, compareWith, renderMode } = this.props
const previousMessages = treeNode.messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
let compareMessage = compareWith || previousMessage || message
if (renderMode === 'raw') {

View File

@@ -1,9 +1,9 @@
import * as dotProp from 'dot-prop'
import * as q from '../../../../backend/src/Model'
import * as q from '../../../backend/src/Model'
import * as React from 'react'
import PlotHistory from './PlotHistory'
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
import { toPlottableValue } from './CodeDiff/util'
import PlotHistory from './Sidebar/PlotHistory'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { toPlottableValue } from './Sidebar/CodeDiff/util'
interface Props {
history: q.MessageHistory
@@ -38,7 +38,6 @@ function nodeDotPathToHistory(history: q.MessageHistory, dotPath: string) {
function render(props: Props) {
const data = props.dotPath ? nodeDotPathToHistory(props.history, props.dotPath) : nodeToHistory(props.history)
console.log(props.dotPath, data)
return <PlotHistory data={data} />
}

View File

@@ -107,6 +107,10 @@ class TreeComponent extends React.PureComponent<Props, State> {
const style: React.CSSProperties = {
lineHeight: '1.1',
cursor: 'default',
overflowY: 'scroll',
overflowX: 'hidden',
height: '100%',
paddingBottom: '16px', // avoid conflict with chart panel Resizer
}
return (

View File

@@ -15,6 +15,9 @@ const styles = (theme: Theme) => ({
width: '32px',
height: '32px',
},
label: {
marginTop: '-2px',
},
})
class CustomIconButton extends React.Component<Props, {}> {
@@ -30,7 +33,7 @@ class CustomIconButton extends React.Component<Props, {}> {
public render() {
return (
<IconButton className={this.props.classes.button} onClick={this.onClick}>
<Tooltip title={this.props.tooltip}>
<Tooltip title={this.props.tooltip} className={this.props.classes.label}>
<span>{this.props.children}</span>
</Tooltip>
</IconButton>