Add time range setting for charts
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"moving-average": "^1.0.0",
|
"moving-average": "^1.0.0",
|
||||||
"number-abbreviate": "^2.0.0",
|
"number-abbreviate": "^2.0.0",
|
||||||
|
"parse-duration": "^0.1.1",
|
||||||
"prismjs": "^1.15.0",
|
"prismjs": "^1.15.0",
|
||||||
"react": "16.8",
|
"react": "16.8",
|
||||||
"react-ace": "^7.0.1",
|
"react-ace": "^7.0.1",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ function Chart(props: Props) {
|
|||||||
<TopicPlot
|
<TopicPlot
|
||||||
color={props.parameters.color}
|
color={props.parameters.color}
|
||||||
interpolation={props.parameters.interpolation}
|
interpolation={props.parameters.interpolation}
|
||||||
|
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined}
|
||||||
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
|
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
|
||||||
history={freezedHistory ? freezedHistory : props.treeNode.messageHistory}
|
history={freezedHistory ? freezedHistory : props.treeNode.messageHistory}
|
||||||
dotPath={parameters.dotPath}
|
dotPath={parameters.dotPath}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import React, { ChangeEvent, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { Button, Menu, TextField, Typography } from '@material-ui/core'
|
||||||
|
import { chartActions } from '../../../actions'
|
||||||
|
import { ChartParameters } from '../../../reducers/Charts'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { KeyCodes } from '../../../utils/KeyCodes'
|
||||||
|
const parseDuration = require('parse-duration')
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
actions: { chart: typeof chartActions }
|
||||||
|
chart: ChartParameters
|
||||||
|
anchorEl?: Element
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimeRangeSettings(props: Props) {
|
||||||
|
const dismissClick = useCallback((e: MouseEvent) => e.stopPropagation(), [])
|
||||||
|
const [value, setValue] = useState<string | undefined>(
|
||||||
|
props.chart.timeRange ? props.chart.timeRange.until : undefined
|
||||||
|
)
|
||||||
|
const ranges = ['all', '10s', '30s', '1m', '5m', '15m', '1h', '6h', '1d']
|
||||||
|
|
||||||
|
const manuallySetIntervalHandler = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLInputElement>) => setValue(e.currentTarget.value),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const canBeParsed = Boolean(parseDuration(value))
|
||||||
|
if (canBeParsed) {
|
||||||
|
props.actions.chart.updateChart({
|
||||||
|
...props.chart,
|
||||||
|
timeRange: {
|
||||||
|
until: value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const createRangeHandler = (range: string) => (e: React.MouseEvent) => setValue(range === 'all' ? undefined : range)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
style={{ textAlign: 'center' }}
|
||||||
|
keepMounted={true}
|
||||||
|
anchorEl={props.anchorEl}
|
||||||
|
open={props.open}
|
||||||
|
onClose={props.onClose}
|
||||||
|
>
|
||||||
|
<Typography>Discard values older then</Typography>
|
||||||
|
<div style={{ padding: '0 16px', width: '300px', textAlign: 'center' }}>
|
||||||
|
{ranges.map(r => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
style={{ margin: '4px', textTransform: 'none' }}
|
||||||
|
variant="contained"
|
||||||
|
key={r}
|
||||||
|
onClick={createRangeHandler(r)}
|
||||||
|
>
|
||||||
|
{r}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<TextField
|
||||||
|
style={{ marginLeft: '8px', marginTop: '0' }}
|
||||||
|
onClick={dismissClick}
|
||||||
|
label="interval"
|
||||||
|
value={value || ''}
|
||||||
|
onKeyPress={manuallySetIntervalHandler}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}, [value, props.open])
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
|
return {
|
||||||
|
actions: {
|
||||||
|
chart: bindActionCreators(chartActions, dispatch),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
undefined,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TimeRangeSettings)
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import ColorSettings from './ColorSettings'
|
||||||
import InterpolationSettings from './InterpolationSettings'
|
import InterpolationSettings from './InterpolationSettings'
|
||||||
import { ChartParameters } from '../../../reducers/Charts'
|
import MoveUp from './MoveUp'
|
||||||
import { Menu, MenuItem } from '@material-ui/core'
|
|
||||||
import RangeSettings from './RangeSettings'
|
import RangeSettings from './RangeSettings'
|
||||||
import Size from './Size'
|
import Size from './Size'
|
||||||
import MoveUp from './MoveUp'
|
import TimeRangeSettings from './TimeRangeSettings'
|
||||||
import ColorSettings from './ColorSettings'
|
import { ChartParameters } from '../../../reducers/Charts'
|
||||||
|
import { Menu, MenuItem } from '@material-ui/core'
|
||||||
|
|
||||||
function ChartSettings(props: {
|
function ChartSettings(props: {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -14,6 +15,7 @@ function ChartSettings(props: {
|
|||||||
anchorEl: React.MutableRefObject<undefined>
|
anchorEl: React.MutableRefObject<undefined>
|
||||||
}) {
|
}) {
|
||||||
const [rangeVisible, setRangeVisible] = React.useState(false)
|
const [rangeVisible, setRangeVisible] = React.useState(false)
|
||||||
|
const [timeRangeVisible, setTimeRangeVisible] = React.useState(false)
|
||||||
const [interpolationVisible, setInterpolationVisible] = React.useState(false)
|
const [interpolationVisible, setInterpolationVisible] = React.useState(false)
|
||||||
const [sizeVisible, setSizeVisible] = React.useState(false)
|
const [sizeVisible, setSizeVisible] = React.useState(false)
|
||||||
const [colorVisible, setColorVisible] = React.useState(false)
|
const [colorVisible, setColorVisible] = React.useState(false)
|
||||||
@@ -25,6 +27,13 @@ function ChartSettings(props: {
|
|||||||
setRangeVisible(!rangeVisible)
|
setRangeVisible(!rangeVisible)
|
||||||
}, [rangeVisible, open])
|
}, [rangeVisible, open])
|
||||||
|
|
||||||
|
const toggleTimeRange = React.useCallback(() => {
|
||||||
|
if (open) {
|
||||||
|
props.close()
|
||||||
|
}
|
||||||
|
setTimeRangeVisible(!timeRangeVisible)
|
||||||
|
}, [timeRangeVisible, open])
|
||||||
|
|
||||||
const toggleInterpolation = React.useCallback(() => {
|
const toggleInterpolation = React.useCallback(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
props.close()
|
props.close()
|
||||||
@@ -52,6 +61,9 @@ function ChartSettings(props: {
|
|||||||
<MenuItem key="range" onClick={toggleRange}>
|
<MenuItem key="range" onClick={toggleRange}>
|
||||||
Set range
|
Set range
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem key="timeRange" onClick={toggleTimeRange}>
|
||||||
|
Time range
|
||||||
|
</MenuItem>
|
||||||
<MenuItem key="interpolation" onClick={toggleInterpolation}>
|
<MenuItem key="interpolation" onClick={toggleInterpolation}>
|
||||||
Curve interpolation
|
Curve interpolation
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -64,6 +76,12 @@ function ChartSettings(props: {
|
|||||||
<MoveUp chart={props.chart} close={props.close} />
|
<MoveUp chart={props.chart} close={props.close} />
|
||||||
</Menu>
|
</Menu>
|
||||||
<RangeSettings chart={props.chart} anchorEl={props.anchorEl.current} open={rangeVisible} onClose={toggleRange} />
|
<RangeSettings chart={props.chart} anchorEl={props.anchorEl.current} open={rangeVisible} onClose={toggleRange} />
|
||||||
|
<TimeRangeSettings
|
||||||
|
chart={props.chart}
|
||||||
|
anchorEl={props.anchorEl.current}
|
||||||
|
open={timeRangeVisible}
|
||||||
|
onClose={toggleTimeRange}
|
||||||
|
/>
|
||||||
<InterpolationSettings
|
<InterpolationSettings
|
||||||
chart={props.chart}
|
chart={props.chart}
|
||||||
anchorEl={props.anchorEl.current}
|
anchorEl={props.anchorEl.current}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface Props {
|
|||||||
theme: Theme
|
theme: Theme
|
||||||
interpolation?: PlotCurveTypes
|
interpolation?: PlotCurveTypes
|
||||||
range?: [number?, number?]
|
range?: [number?, number?]
|
||||||
|
timeRangeStart?: number
|
||||||
color?: string
|
color?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +56,12 @@ export default withTheme((props: Props) => {
|
|||||||
return React.useMemo(() => {
|
return React.useMemo(() => {
|
||||||
const data = props.data
|
const data = props.data
|
||||||
const calculatedDomain = domainForData(data)
|
const calculatedDomain = domainForData(data)
|
||||||
let yDomain: [number, number] = props.range
|
const yDomain: [number, number] = props.range
|
||||||
? [props.range[0] || calculatedDomain[0], props.range[1] || calculatedDomain[1]]
|
? [props.range[0] || calculatedDomain[0], props.range[1] || calculatedDomain[1]]
|
||||||
: calculatedDomain
|
: calculatedDomain
|
||||||
|
|
||||||
|
const xDomain = props.timeRangeStart ? [Date.now() - props.timeRangeStart, Date.now()] : undefined
|
||||||
|
|
||||||
let color: string =
|
let color: string =
|
||||||
props.theme.palette.type === 'light' ? props.theme.palette.secondary.dark : props.theme.palette.primary.light
|
props.theme.palette.type === 'light' ? props.theme.palette.secondary.dark : props.theme.palette.primary.light
|
||||||
if (props.color) {
|
if (props.color) {
|
||||||
@@ -67,7 +70,7 @@ export default withTheme((props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '150px', overflow: 'hidden' }}>
|
<div style={{ height: '150px', overflow: 'hidden' }}>
|
||||||
<XYPlot width={width} height={180} yDomain={yDomain}>
|
<XYPlot width={width} height={180} yDomain={yDomain} xDomain={xDomain}>
|
||||||
<HorizontalGridLines />
|
<HorizontalGridLines />
|
||||||
<XAxis />
|
<XAxis />
|
||||||
<YAxis width={45} tickFormat={(num: number) => abbreviate(num)} />
|
<YAxis width={45} tickFormat={(num: number) => abbreviate(num)} />
|
||||||
|
|||||||
@@ -5,18 +5,28 @@ import PlotHistory from './Sidebar/PlotHistory'
|
|||||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||||
import { toPlottableValue } from './Sidebar/CodeDiff/util'
|
import { toPlottableValue } from './Sidebar/CodeDiff/util'
|
||||||
import { PlotCurveTypes } from '../reducers/Charts'
|
import { PlotCurveTypes } from '../reducers/Charts'
|
||||||
|
const parseDuration = require('parse-duration')
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
history: q.MessageHistory
|
history: q.MessageHistory
|
||||||
dotPath?: string
|
dotPath?: string
|
||||||
|
timeInterval?: string
|
||||||
interpolation?: PlotCurveTypes
|
interpolation?: PlotCurveTypes
|
||||||
range?: [number?, number?]
|
range?: [number?, number?]
|
||||||
color?: string
|
color?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeToHistory(history: q.MessageHistory) {
|
function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Message>) {
|
||||||
return history
|
if (startTime) {
|
||||||
.toArray()
|
const threshold = new Date(Date.now() - startTime)
|
||||||
|
return data.filter(d => d.received >= threshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory) {
|
||||||
|
return filterUsingTimeRange(startTime, history.toArray())
|
||||||
.map((message: q.Message) => {
|
.map((message: q.Message) => {
|
||||||
const value = message.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN
|
const value = message.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN
|
||||||
return { x: message.received.getTime(), y: toPlottableValue(value) }
|
return { x: message.received.getTime(), y: toPlottableValue(value) }
|
||||||
@@ -24,9 +34,8 @@ function nodeToHistory(history: q.MessageHistory) {
|
|||||||
.filter(data => !isNaN(data.y as any)) as any
|
.filter(data => !isNaN(data.y as any)) as any
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeDotPathToHistory(history: q.MessageHistory, dotPath: string) {
|
function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string) {
|
||||||
return history
|
return filterUsingTimeRange(startTime, history.toArray())
|
||||||
.toArray()
|
|
||||||
.map((message: q.Message) => {
|
.map((message: q.Message) => {
|
||||||
let json = {}
|
let json = {}
|
||||||
try {
|
try {
|
||||||
@@ -41,8 +50,20 @@ function nodeDotPathToHistory(history: q.MessageHistory, dotPath: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render(props: Props) {
|
function render(props: Props) {
|
||||||
const data = props.dotPath ? nodeDotPathToHistory(props.history, props.dotPath) : nodeToHistory(props.history)
|
const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined
|
||||||
return <PlotHistory color={props.color} range={props.range} interpolation={props.interpolation} data={data} />
|
const data = props.dotPath
|
||||||
|
? nodeDotPathToHistory(startOffset, props.history, props.dotPath)
|
||||||
|
: nodeToHistory(startOffset, props.history)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlotHistory
|
||||||
|
timeRangeStart={startOffset}
|
||||||
|
color={props.color}
|
||||||
|
range={props.range}
|
||||||
|
interpolation={props.interpolation}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default render
|
export default render
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export interface ChartParameters {
|
|||||||
from?: number
|
from?: number
|
||||||
to?: number
|
to?: number
|
||||||
}
|
}
|
||||||
|
timeRange?: {
|
||||||
|
until: string
|
||||||
|
}
|
||||||
width?: 'big' | 'medium' | 'small'
|
width?: 'big' | 'medium' | 'small'
|
||||||
color?: string
|
color?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4252,6 +4252,11 @@ parse-asn1@^5.0.0:
|
|||||||
pbkdf2 "^3.0.3"
|
pbkdf2 "^3.0.3"
|
||||||
safe-buffer "^5.1.1"
|
safe-buffer "^5.1.1"
|
||||||
|
|
||||||
|
parse-duration@^0.1.1:
|
||||||
|
version "0.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-0.1.1.tgz#13114ddc9891c1ecd280036244554de43647a226"
|
||||||
|
integrity sha1-ExFN3JiRwezSgANiRFVN5DZHoiY=
|
||||||
|
|
||||||
parse-json@^4.0.0:
|
parse-json@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
|
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
|
||||||
|
|||||||
Reference in New Issue
Block a user