Add JSON based plots
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import * as diff from 'diff'
|
||||
import * as React from 'react'
|
||||
import Add from '@material-ui/icons/Add'
|
||||
import Remove from '@material-ui/icons/Remove'
|
||||
import ShowChart from '@material-ui/icons/ShowChart'
|
||||
import { JsonPropertyLocation } from '../../../../../backend/src/JsonAstParser'
|
||||
import { lineChangeStyle, trimNewlineRight } from './util'
|
||||
@@ -10,28 +12,70 @@ interface Props {
|
||||
changes: Array<diff.Change>,
|
||||
literalPositions: Array<JsonPropertyLocation>
|
||||
classes: any
|
||||
className: string
|
||||
showDiagram: (dotPath: string, target: EventTarget) => void
|
||||
hideDiagram: () => void
|
||||
}
|
||||
|
||||
const style = (theme: Theme) => {
|
||||
return {
|
||||
gutterLine: {
|
||||
display: 'flex' as 'flex',
|
||||
textAlign: 'right' as 'right',
|
||||
paddingRight: theme.spacing(0.5),
|
||||
height: '16px',
|
||||
width: '100%',
|
||||
},
|
||||
icon: {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
marginTop: '2px',
|
||||
borderRadius: '50%',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.contrastText,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
hover: {
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function tokensForLine(change: diff.Change, line: number, literalPositions: Array<JsonPropertyLocation>) {
|
||||
let diagram = literalPositions[line] ? <ShowChart style={{ height: '16px' }} /> : ''
|
||||
function ChartIcon(props: { classes: any, literal: JsonPropertyLocation, showDiagram: (dotPath: string, target: EventTarget) => void, hideDiagram: () => void }) {
|
||||
const mouseOver = (event: React.MouseEvent<Element>) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
if ((event.target as Element).tagName !== 'path') {
|
||||
props.showDiagram(props.literal.path, event.target)
|
||||
}
|
||||
}
|
||||
|
||||
const mouseOut = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
if ((event.target as Element).tagName !== 'path') {
|
||||
props.hideDiagram()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ShowChart className={props.classes.icon} onMouseEnter={mouseOver} onMouseLeave={mouseOut} />
|
||||
)
|
||||
}
|
||||
|
||||
function tokensForLine(change: diff.Change, line: number, props: Props) {
|
||||
const { classes, literalPositions } = props
|
||||
|
||||
const literal = literalPositions[line]
|
||||
const diagram = literal ? <ChartIcon classes={{ icon: props.classes.icon, hover: props.classes.hover }} literal={literal} showDiagram={props.showDiagram} hideDiagram={props.hideDiagram}/> : null
|
||||
|
||||
if (change.added) {
|
||||
return [diagram, '+']
|
||||
return [diagram, <Add key="add" className={classes.icon} />]
|
||||
} else if (change.removed) {
|
||||
return '-'
|
||||
return [<Remove key="remove" className={classes.icon} />]
|
||||
} else {
|
||||
return [diagram, ' ']
|
||||
return [diagram, <div key="placeholder" style={{ width: '12px', display: 'inline-block' }} dangerouslySetInnerHTML={{ __html: ' '}} />]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,13 +88,15 @@ function Gutters(props: Props) {
|
||||
currentLine = !change.removed ? currentLine + 1 : currentLine
|
||||
return (
|
||||
<div key={`${key}-${idx}`} style={lineChangeStyle(change)} className={props.classes.gutterLine}>
|
||||
{tokensForLine(change, currentLine, props.literalPositions)}
|
||||
{tokensForLine(change, currentLine, props)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}).reduce((a, b) => a.concat(b), [])
|
||||
|
||||
return <div>{gutters}</div>
|
||||
return <span>
|
||||
<pre className={props.className}>{gutters}</pre>
|
||||
</span>
|
||||
}
|
||||
|
||||
export default withStyles(style)(Gutters)
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import * as diff from 'diff'
|
||||
import * as Prism from 'prismjs'
|
||||
import * as q from '../../../../../backend/src/Model'
|
||||
import * as React from 'react'
|
||||
import DiffCount from './DiffCount'
|
||||
import { CodeBlockColors, CodeBlockColorsBraceMonokai } from '../CodeBlockColors'
|
||||
import { literalsMappedByLines } from '../../../../../backend/src/JsonAstParser'
|
||||
import { selectTextWithCtrlA } from '../../../utils/handleTextSelectWithCtrlA'
|
||||
import { Theme, withStyles } from '@material-ui/core'
|
||||
import 'prismjs/components/prism-json'
|
||||
import { trimNewlineRight, lineChangeStyle } from './util';
|
||||
import Gutters from './Gutters'
|
||||
import TopicPlot from '../TopicPlot'
|
||||
import { CodeBlockColors, CodeBlockColorsBraceMonokai } from '../CodeBlockColors'
|
||||
import { isPlottable, lineChangeStyle, trimNewlineRight } from './util'
|
||||
import { JsonPropertyLocation, literalsMappedByLines } from '../../../../../backend/src/JsonAstParser'
|
||||
import { Theme, withStyles, Popper, Paper, Fade, Zoom } from '@material-ui/core'
|
||||
import { selectTextWithCtrlA } from '../../../utils/handleTextSelectWithCtrlA'
|
||||
import 'prismjs/components/prism-json'
|
||||
const throttle = require('lodash.throttle')
|
||||
|
||||
interface Props {
|
||||
messageHistory: q.MessageHistory
|
||||
previous: string
|
||||
current: string
|
||||
nameOfCompareMessage: string
|
||||
@@ -18,17 +22,45 @@ interface Props {
|
||||
classes: any
|
||||
}
|
||||
|
||||
class CodeDiff extends React.Component<Props, {}> {
|
||||
interface State {
|
||||
diagram?: DiagramOptions
|
||||
}
|
||||
|
||||
interface DiagramOptions {
|
||||
dotPath?: string
|
||||
anchorEl?: EventTarget
|
||||
}
|
||||
|
||||
class CodeDiff extends React.Component<Props, State> {
|
||||
private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre ~ pre' })
|
||||
|
||||
private updateDiagram = throttle((diagram?: DiagramOptions) => {
|
||||
this.setState({ diagram })
|
||||
}, 200)
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
private showDiagram(dotPath: string, target: EventTarget) {
|
||||
this.updateDiagram({
|
||||
dotPath,
|
||||
anchorEl: target,
|
||||
})
|
||||
}
|
||||
|
||||
private hideDiagram() {
|
||||
this.updateDiagram(undefined)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const changes = diff.diffLines(this.props.previous, this.props.current)
|
||||
const styledLines = Prism.highlight(this.props.current, Prism.languages.json, 'json').split('\n')
|
||||
const literalPositions = literalsMappedByLines(this.props.current) || []
|
||||
const literalPositions = (
|
||||
(literalsMappedByLines(this.props.current) || [])
|
||||
.map((l: JsonPropertyLocation) => isPlottable(l.value) ? l : undefined)
|
||||
) as Array<JsonPropertyLocation>
|
||||
|
||||
let lineNumber = 0
|
||||
const code = changes.map((change, key) => {
|
||||
@@ -41,7 +73,7 @@ class CodeDiff extends React.Component<Props, {}> {
|
||||
})
|
||||
lineNumber += changedLines
|
||||
|
||||
return <div key={key}>{lines}</div>
|
||||
return [<div key={key}>{lines}</div>]
|
||||
}
|
||||
|
||||
return trimNewlineRight(change.value)
|
||||
@@ -49,14 +81,32 @@ class CodeDiff extends React.Component<Props, {}> {
|
||||
.map((line, idx) => {
|
||||
return <div key={`${key}-${idx}`} style={lineChangeStyle(change)} className={this.props.classes.line}><span>{line}</span></div>
|
||||
})
|
||||
})
|
||||
}).reduce((a, b) => a.concat(b), [])
|
||||
|
||||
const { diagram } = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div tabIndex={0} onKeyDown={this.handleCtrlA} className={this.props.classes.codeWrapper}>
|
||||
<pre className={this.props.classes.gutters}><Gutters changes={changes} literalPositions={literalPositions} /></pre>
|
||||
<Gutters
|
||||
showDiagram={(dotPath, target) => this.showDiagram(dotPath, target)}
|
||||
hideDiagram={() => this.hideDiagram()}
|
||||
className={this.props.classes.gutters}
|
||||
changes={changes}
|
||||
literalPositions={literalPositions} />
|
||||
<pre className={this.props.classes.codeBlock}>{code}</pre>
|
||||
</div>
|
||||
<Popper
|
||||
open={Boolean(this.state.diagram)}
|
||||
anchorEl={diagram && diagram.anchorEl as any}
|
||||
dir="left"
|
||||
>
|
||||
<Fade in={Boolean(this.state.diagram)} timeout={300}>
|
||||
<Paper style={{ width: '300px' }}>
|
||||
{diagram ? <TopicPlot history={this.props.messageHistory} dotPath={diagram.dotPath} /> : <span/>}
|
||||
</Paper>
|
||||
</Fade>
|
||||
</Popper>
|
||||
<DiffCount changes={changes} nameOfCompareMessage={this.props.nameOfCompareMessage} />
|
||||
</div>
|
||||
)
|
||||
@@ -70,7 +120,7 @@ const style = (theme: Theme) => {
|
||||
font: "12px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace",
|
||||
display: 'inline-grid' as 'inline-grid',
|
||||
margin: '0',
|
||||
padding: '1px 0 2px 0',
|
||||
padding: '1px 0 0 0',
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -81,6 +131,7 @@ const style = (theme: Theme) => {
|
||||
height: '16px',
|
||||
},
|
||||
codeWrapper: {
|
||||
display: 'flex',
|
||||
maxHeight: '15em',
|
||||
overflow: 'auto',
|
||||
backgroundColor: `${codeBlockColors.background}`,
|
||||
|
||||
@@ -6,9 +6,7 @@ export function trimNewlineRight(str: string) {
|
||||
return str
|
||||
}
|
||||
|
||||
const gutterBaseStyle = {
|
||||
width: '100%',
|
||||
}
|
||||
const gutterBaseStyle = {}
|
||||
|
||||
const additionStyle = {
|
||||
...gutterBaseStyle,
|
||||
@@ -30,4 +28,29 @@ export function lineChangeStyle(change: Diff.Change) {
|
||||
}
|
||||
|
||||
return gutterBaseStyle
|
||||
}
|
||||
|
||||
export function toPlottableValue(value: any): number | undefined {
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 1 : 0
|
||||
}
|
||||
|
||||
const isNumber = !isNaN(value)
|
||||
const floatVal = parseFloat(value)
|
||||
if (isNumber && !isNaN(floatVal)) {
|
||||
return floatVal
|
||||
}
|
||||
|
||||
const intVal = parseInt(value)
|
||||
if (isNumber && !isNaN(intVal)) {
|
||||
return intVal
|
||||
}
|
||||
}
|
||||
|
||||
export function isPlottable(value: any) {
|
||||
return !isNaN(toPlottableValue(value) as any)
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as q from '../../../../../backend/src/Model'
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import * as React from 'react'
|
||||
import DateFormatter from '../../helper/DateFormatter'
|
||||
import DateFormatter from '../helper/DateFormatter'
|
||||
import { default as ReactResizeDetector } from 'react-resize-detector'
|
||||
import 'react-vis/dist/style.css'
|
||||
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||
const { XYPlot, LineMarkSeries, Hint, YAxis, HorizontalGridLines } = require('react-vis')
|
||||
const { XYPlot, LineMarkSeries, Hint, XAxis, YAxis, HorizontalGridLines } = require('react-vis')
|
||||
const abbreviate = require('number-abbreviate')
|
||||
|
||||
interface Props {
|
||||
data: Array<{x: number, y: number}>
|
||||
}
|
||||
// const configuredCurve = d3Shape.curveBundle.beta(1)
|
||||
|
||||
interface Stats {
|
||||
width: number
|
||||
@@ -47,9 +47,10 @@ class PlotHistory extends React.Component<Props, Stats> {
|
||||
const data = this.props.data
|
||||
|
||||
return (
|
||||
<div>
|
||||
<XYPlot width={this.state.width} height={150}>
|
||||
<div style={{ height: '150px', overflow: 'hidden' }}>
|
||||
<XYPlot width={this.state.width} height={180}>
|
||||
<HorizontalGridLines />
|
||||
<XAxis />
|
||||
<YAxis
|
||||
width={45}
|
||||
tickFormat={(num: number) => abbreviate(num)}
|
||||
@@ -59,7 +60,7 @@ class PlotHistory extends React.Component<Props, Stats> {
|
||||
onValueMouseOut={this._forgetValue}
|
||||
size={3}
|
||||
data={data}
|
||||
curve={data.length < 50 ? 'curveCardinal' : undefined}
|
||||
curve="curveMonotoneX"
|
||||
/>
|
||||
{this.state.value ? <Hint format={this.hintFormatter} value={this.state.value} /> : null}
|
||||
</XYPlot>
|
||||
39
app/src/components/Sidebar/TopicPlot.tsx
Normal file
39
app/src/components/Sidebar/TopicPlot.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import * as dotProp from 'dot-prop'
|
||||
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'
|
||||
|
||||
interface Props {
|
||||
history: q.MessageHistory
|
||||
dotPath?: string
|
||||
}
|
||||
|
||||
function nodeToHistory(history: q.MessageHistory) {
|
||||
return history
|
||||
.toArray()
|
||||
.map((message: q.Message) => {
|
||||
const value = message.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN
|
||||
return { x: message.received.getTime(), y: toPlottableValue(value) }
|
||||
}).filter(data => !isNaN(data.y as any)) as any
|
||||
}
|
||||
|
||||
function nodeDotPathToHistory(history: q.MessageHistory, dotPath: string) {
|
||||
return history
|
||||
.toArray()
|
||||
.map((message: q.Message) => {
|
||||
const json = message.value ? JSON.parse(Base64Message.toUnicodeString(message.value)) : {}
|
||||
let value = dotProp.get(json, dotPath)
|
||||
|
||||
return { x: message.received.getTime(), y: toPlottableValue(value) }
|
||||
}).filter(data => !isNaN(data.y as any)) as any
|
||||
}
|
||||
|
||||
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} />
|
||||
}
|
||||
|
||||
export default render
|
||||
@@ -4,11 +4,11 @@ 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 { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||
import { isPlottable } from '../CodeDiff/util'
|
||||
import { TopicViewModel } from '../../../model/TopicViewModel'
|
||||
|
||||
const PlotHistory = React.lazy(() => import('./PlotHistory'))
|
||||
|
||||
const throttle = require('lodash.throttle')
|
||||
|
||||
interface Props {
|
||||
@@ -76,33 +76,19 @@ class MessageHistory extends React.Component<Props, State> {
|
||||
return element
|
||||
})
|
||||
|
||||
const numericMessages = history
|
||||
.map((message: q.Message) => {
|
||||
const value = message.value ? parseFloat(Base64Message.toUnicodeString(message.value)) : NaN
|
||||
return { x: message.received.getTime(), y: value }
|
||||
}).filter(data => !isNaN(data.y))
|
||||
const showPlot = numericMessages.length >= 2
|
||||
|
||||
const isMessagePlottable = node.message && node.message.value && isPlottable(Base64Message.toUnicodeString(node.message.value))
|
||||
return (
|
||||
<div>
|
||||
<History
|
||||
items={historyElements}
|
||||
contentTypeIndicator={showPlot ? <BarChart /> : undefined}
|
||||
contentTypeIndicator={isMessagePlottable ? <BarChart /> : undefined}
|
||||
onClick={this.displayMessage}
|
||||
>
|
||||
{showPlot ? this.renderPlot(numericMessages) : null}
|
||||
{isMessagePlottable ? <TopicPlot history={node.messageHistory} /> : null}
|
||||
</History>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
public renderPlot(data: Array<{x: number, y: number}>) {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<PlotHistory data={data} />
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MessageHistory
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ValueRendererDisplayMode } from '../../../reducers/Settings'
|
||||
|
||||
interface Props {
|
||||
message: q.Message
|
||||
messageHistory: q.RingBuffer<q.Message>
|
||||
messageHistory: q.MessageHistory
|
||||
compareWith?: q.Message
|
||||
renderMode: ValueRendererDisplayMode
|
||||
}
|
||||
@@ -27,6 +27,7 @@ class ValueRenderer extends React.Component<Props, State> {
|
||||
private renderDiff(current: string = '', previous: string = '', language?: 'json') {
|
||||
return (
|
||||
<CodeDiff
|
||||
messageHistory={this.props.messageHistory}
|
||||
previous={previous}
|
||||
current={current}
|
||||
language={language}
|
||||
|
||||
Reference in New Issue
Block a user