Skip to content

Commit

Permalink
Add JSON based plots
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasnordquist committed Jun 4, 2019
1 parent 09151d1 commit 9e15e28
Show file tree
Hide file tree
Showing 12 changed files with 633 additions and 61 deletions.
4 changes: 4 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
"brace": "^0.11.1",
"compare-versions": "^3.4.0",
"copy-text-to-clipboard": "^1.0.4",
"d3": "^5.9.2",
"d3-shape": "^1.3.5",
"diff": "^4.0.1",
"dot-prop": "^5.0.0",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"file-loader": "^3.0.1",
"get-value": "^3.0.1",
Expand Down Expand Up @@ -47,6 +50,7 @@
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/d3": "^5.7.2",
"@types/diff": "^4.0.1",
"@types/get-value": "^3.0.1",
"@types/node": "^10.12.18",
Expand Down
62 changes: 54 additions & 8 deletions app/src/components/Sidebar/CodeDiff/Gutters.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 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, literalPositions: Array<JsonPropertyLocation>) {
let diagram = literalPositions[line] ? <ShowChart style={{ height: '16px' }} /> : ''
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: '&nbsp;'}} />]
}
}

Expand All @@ -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)
71 changes: 61 additions & 10 deletions app/src/components/Sidebar/CodeDiff/index.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,66 @@
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 Gutters from './Gutters'
import TopicPlot from '../TopicPlot'
import { CodeBlockColors, CodeBlockColorsBraceMonokai } from '../CodeBlockColors'
import { literalsMappedByLines } from '../../../../../backend/src/JsonAstParser'
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 { Theme, withStyles } from '@material-ui/core'
import 'prismjs/components/prism-json'
import { trimNewlineRight, lineChangeStyle } from './util';
import Gutters from './Gutters'
const throttle = require('lodash.throttle')

interface Props {
messageHistory: q.MessageHistory
previous: string
current: string
nameOfCompareMessage: string
language?: 'json'
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) => {
Expand All @@ -41,22 +73,40 @@ class CodeDiff extends React.Component<Props, {}> {
})
lineNumber += changedLines

return <div key={key}>{lines}</div>
return [<div key={key}>{lines}</div>]
}

return trimNewlineRight(change.value)
.split('\n')
.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>
)
Expand All @@ -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 {
Expand All @@ -81,6 +131,7 @@ const style = (theme: Theme) => {
height: '16px',
},
codeWrapper: {
display: 'flex',
maxHeight: '15em',
overflow: 'auto',
backgroundColor: `${codeBlockColors.background}`,
Expand Down
29 changes: 26 additions & 3 deletions app/src/components/Sidebar/CodeDiff/util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ export function trimNewlineRight(str: string) {
return str
}

const gutterBaseStyle = {
width: '100%',
}
const gutterBaseStyle = {}

const additionStyle = {
...gutterBaseStyle,
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)}
Expand All @@ -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>
Expand Down
39 changes: 39 additions & 0 deletions app/src/components/Sidebar/TopicPlot.tsx
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 9e15e28

Please sign in to comment.