Added charts
This commit is contained in:
parent
1d438bc349
commit
f85346aea9
@ -3,6 +3,12 @@ import { withStyles } from '@material-ui/styles';
|
||||
import Services from './services/Services';
|
||||
import { blueGrey } from '@material-ui/core/colors';
|
||||
import { Box, Grid, IconButton, Paper, Typography } from '@material-ui/core';
|
||||
import DonutChart from './charts/DonutChart';
|
||||
import HeatmapChart from './charts/HeatmapChart';
|
||||
import BarChart from './charts/BarChart';
|
||||
import LineChart from './charts/LineChart';
|
||||
import DevicesContext from '../../contexts/DevicesContext';
|
||||
import C from '../../common/Constants';
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
@ -10,27 +16,304 @@ const styles = theme => ({
|
||||
padding: '64px',
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
},
|
||||
typo: {
|
||||
fontSize: theme.typography.pxToRem(20),
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
},
|
||||
paper: {
|
||||
backgroundColor: blueGrey[50],
|
||||
height: '60px',
|
||||
padding: '16px',
|
||||
}
|
||||
});
|
||||
|
||||
class Dashboard extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
deviceSeries: [],
|
||||
sensorSeries: [],
|
||||
heatmapSecondsSeries: [],
|
||||
heatmapMinutesSeries: [],
|
||||
barSeries: [],
|
||||
barCategories: [],
|
||||
lineSeries: [],
|
||||
};
|
||||
|
||||
this.updateSeries = this.updateSeries.bind(this);
|
||||
this.updateDynamic = this.updateDynamic.bind(this);
|
||||
}
|
||||
|
||||
static contextType = DevicesContext;
|
||||
|
||||
getItemsWithStatus(iterate, status) {
|
||||
const items = [];
|
||||
|
||||
for (var d of iterate) {
|
||||
if (d.status == status) {
|
||||
items.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
getDevicesWithStatus(status) {
|
||||
return this.getItemsWithStatus(this.context.devices, status);
|
||||
}
|
||||
|
||||
getSensorsWithStatus(status) {
|
||||
const sensors = [];
|
||||
|
||||
for (var d of this.context.devices) {
|
||||
sensors.push(...d.sensors)
|
||||
}
|
||||
|
||||
return this.getItemsWithStatus(sensors, status);
|
||||
}
|
||||
|
||||
getDeviceSeries() {
|
||||
var online = this.getDevicesWithStatus("Online").length;
|
||||
var offline = this.getDevicesWithStatus("Offline").length;
|
||||
var error = this.getDevicesWithStatus("Error").length;
|
||||
|
||||
return [online, offline, error]
|
||||
}
|
||||
|
||||
getSensorSeries() {
|
||||
var online = this.getSensorsWithStatus("Online").length;
|
||||
var offline = this.getSensorsWithStatus("Offline").length;
|
||||
var unknown = this.getSensorsWithStatus("Unknown").length;
|
||||
|
||||
return [online, offline, unknown]
|
||||
}
|
||||
|
||||
updateSeries() {
|
||||
this.setState({
|
||||
deviceSeries: this.getDeviceSeries(),
|
||||
sensorSeries: this.getSensorSeries(),
|
||||
heatmapSecondsSeries: this.getHeatmapSecondsSeries(),
|
||||
heatmapMinutesSeries: this.getHeatmapMinutesSeries(),
|
||||
barSeries: this.getBarSeries(),
|
||||
barCategories: this.getBarCategories(),
|
||||
lineSeries: this.getLineSeries(),
|
||||
});
|
||||
}
|
||||
|
||||
updateDynamic() {
|
||||
this.setState({
|
||||
heatmapSecondsSeries: this.getHeatmapSecondsSeries(),
|
||||
heatmapMinutesSeries: this.getHeatmapMinutesSeries(),
|
||||
barSeries: this.getBarSeries(),
|
||||
barCategories: this.getBarCategories(),
|
||||
lineSeries: this.getLineSeries(),
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.context.addHandler(C.update_all_method_name, this.updateSeries);
|
||||
this.context.addHandler(C.update_method_name, this.updateSeries);
|
||||
this.updateSeries();
|
||||
window.setInterval(() => {
|
||||
this.updateDynamic();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.context.removeHandler(C.update_all_method_name, this.updateSeries);
|
||||
this.context.removeHandler(C.update_method_name, this.updateSeries);
|
||||
}
|
||||
|
||||
getHeatmapSecondsSeries() {
|
||||
const minuteAgo = new Date( Date.now() - 1000 * 60 );
|
||||
|
||||
const devicePoints = {};
|
||||
|
||||
for (var d of this.context.devices) {
|
||||
devicePoints[d.id] = Array(60).fill(0);
|
||||
}
|
||||
|
||||
for (var p of this.context.heatmapPoints) {
|
||||
if (p.date > minuteAgo) {
|
||||
var seconds = Math.floor((p.date.getTime() - minuteAgo.getTime()) / 1000);
|
||||
var oldProb = devicePoints[p.deviceId][seconds];
|
||||
if (oldProb < p.prob) {
|
||||
devicePoints[p.deviceId][seconds] = p.prob;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const series = [];
|
||||
|
||||
var i = 0;
|
||||
for (var p in devicePoints) {
|
||||
series.push({
|
||||
name: "Device " + i,
|
||||
data: devicePoints[p].map((value, index) => ({
|
||||
x: new Date( Date.now() - (60 - index) * 1000 ).toLocaleTimeString('hu-HU'),
|
||||
y: value
|
||||
})),
|
||||
});
|
||||
i++;
|
||||
};
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
getHeatmapMinutesSeries() {
|
||||
const hourAgo = new Date( Date.now() - 1000 * 60 * 60 );
|
||||
|
||||
const devicePoints = {};
|
||||
|
||||
for (var d of this.context.devices) {
|
||||
devicePoints[d.id] = Array(60).fill(0);
|
||||
}
|
||||
|
||||
for (var p of this.context.heatmapPoints) {
|
||||
if (p.date > hourAgo) {
|
||||
var minutes = Math.floor((p.date.getTime() - hourAgo.getTime()) / (1000 * 60));
|
||||
var oldProb = devicePoints[p.deviceId][minutes];
|
||||
if (oldProb < p.prob) {
|
||||
devicePoints[p.deviceId][minutes] = p.prob;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const series = [];
|
||||
|
||||
var i = 0;
|
||||
for (var p in devicePoints) {
|
||||
series.push({
|
||||
name: "Device " + i,
|
||||
data: devicePoints[p].map((value, index) => ({
|
||||
x: new Date( Date.now() - (60 - index) * 1000 * 60 ).toLocaleTimeString('hu-HU').substring(0, 5),
|
||||
y: value
|
||||
})),
|
||||
});
|
||||
i++;
|
||||
};
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
getBarSeries() {
|
||||
const devicePoints = {};
|
||||
for (var d of this.context.devices) {
|
||||
devicePoints[d.id] = Array(3).fill(0);
|
||||
}
|
||||
|
||||
for (var p of this.context.heatmapPoints) {
|
||||
if (p.prob > 0.5 && p.prob <= 0.7) {
|
||||
devicePoints[p.deviceId][0] += 1;
|
||||
}
|
||||
if (p.prob > 0.7 && p.prob <= 0.9) {
|
||||
devicePoints[p.deviceId][1] += 1;
|
||||
}
|
||||
if (p.prob > 0.9) {
|
||||
devicePoints[p.deviceId][2] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const series = [];
|
||||
const getCount = column => {
|
||||
var counts = [];
|
||||
|
||||
for (var p in devicePoints) {
|
||||
counts.unshift(devicePoints[p][column]);
|
||||
}
|
||||
|
||||
return counts;
|
||||
};
|
||||
|
||||
series.push({
|
||||
name: "Prob > 0.5",
|
||||
data: getCount(0),
|
||||
});
|
||||
series.push({
|
||||
name: "Prob > 0.7",
|
||||
data: getCount(1),
|
||||
});
|
||||
series.push({
|
||||
name: "Prob > 0.9",
|
||||
data: getCount(2),
|
||||
});
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
getBarCategories() {
|
||||
const categories = [];
|
||||
|
||||
for (var i = this.context.devices.length - 1; i >= 0; i--) {
|
||||
categories.push("Device " + i)
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
getLineSeries() {
|
||||
const xSecondsAgo = new Date( Date.now() - 1000 * 2 );
|
||||
const aSecondAgo = new Date( Date.now() - 1000 * 1);
|
||||
const messages = {};
|
||||
|
||||
var counter = 0;
|
||||
for (var p of this.context.heatmapPoints) {
|
||||
var shortDate = p.date.toUTCString();
|
||||
var message = messages[shortDate];
|
||||
if (message === undefined) {
|
||||
messages[shortDate] = 1;
|
||||
} else {
|
||||
messages[shortDate] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const series = [{data: []}];
|
||||
for (var m in messages) {
|
||||
series[0].data.push({
|
||||
x: new Date(m).getTime(),
|
||||
y: messages[m],
|
||||
})
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={8}>
|
||||
<Grid item xs={12}>
|
||||
<Services isAdmin={this.props.isAdmin}/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Paper className={classes.paper} />
|
||||
<Paper className={classes.paper} />
|
||||
<Grid item xs={6}>
|
||||
<Paper className={classes.paper}>
|
||||
<DonutChart totalLabel="Devices" series={this.state.deviceSeries}/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Paper className={classes.paper}>
|
||||
<DonutChart totalLabel="Sensors" series={this.state.sensorSeries}/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Paper className={classes.paper} />
|
||||
<Paper className={classes.paper}>
|
||||
<HeatmapChart label="Highest probability per devices by seconds" series={this.state.heatmapSecondsSeries}/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Paper className={classes.paper}>
|
||||
<HeatmapChart label="Highest probability per devices by minutes" series={this.state.heatmapMinutesSeries}/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Paper className={classes.paper}>
|
||||
<BarChart label="# of messages per device" series={this.state.barSeries} categories={this.state.barCategories}/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Paper className={classes.paper}>
|
||||
<LineChart label="# of messages by second" series={this.state.lineSeries}/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
@ -0,0 +1,92 @@
|
||||
import React, { Component } from 'react';
|
||||
import Chart from 'react-apexcharts';
|
||||
import { blueGrey, green, red, orange, amber } from '@material-ui/core/colors';
|
||||
|
||||
export class BarChart extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
options: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.categories !== this.props.categories) {
|
||||
this.setState({options: {
|
||||
chart: {
|
||||
stacked: true,
|
||||
animations: {
|
||||
enabled: false,
|
||||
easing: 'linear',
|
||||
speed: 1000,
|
||||
animateGradually: {
|
||||
enabled: false,
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: true,
|
||||
speed: 500
|
||||
}
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
},
|
||||
},
|
||||
colors: [blueGrey[500], blueGrey[700], blueGrey[900]],
|
||||
stroke: {
|
||||
width: 1,
|
||||
colors: ['#fff']
|
||||
},
|
||||
title: {
|
||||
text: this.props.label,
|
||||
style: {
|
||||
fontSize: '22px',
|
||||
fontWeight: 600,
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
categories: this.props.categories,
|
||||
labels: {
|
||||
formatter: function (val) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: undefined
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: function (val) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
},
|
||||
fill: {
|
||||
opacity: 1
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
}
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Chart
|
||||
options={this.state.options}
|
||||
series={this.props.series}
|
||||
type="bar"
|
||||
height={600}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default BarChart;
|
@ -0,0 +1,81 @@
|
||||
import React, { Component } from 'react';
|
||||
import Chart from 'react-apexcharts';
|
||||
import { blueGrey, green, red } from '@material-ui/core/colors';
|
||||
|
||||
|
||||
export class DonutChart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
options: {
|
||||
chart: {
|
||||
animations: {
|
||||
enabled: false,
|
||||
easing: 'linear',
|
||||
speed: 1000,
|
||||
animateGradually: {
|
||||
enabled: false,
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: true,
|
||||
speed: 500
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
fontSize: '18px',
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
startAngle: 0,
|
||||
expandOnClick: false,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
customScale: 1,
|
||||
dataLabels: {
|
||||
offset: 0,
|
||||
minAngleToShowLabel: 10
|
||||
},
|
||||
donut: {
|
||||
size: '65%',
|
||||
background: 'transparent',
|
||||
labels: {
|
||||
show: true,
|
||||
total: {
|
||||
show: true,
|
||||
showAlways: true,
|
||||
label: props.totalLabel,
|
||||
fontSize: '22px',
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
fontWeight: 600,
|
||||
color: '#373d3f',
|
||||
formatter: function (w) {
|
||||
return w.globals.seriesTotals.reduce((a, b) => {
|
||||
return a + b
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
colors: [green[500], blueGrey[500], red[500]],
|
||||
labels: ['Online', 'Offline', 'Error / Unknown']},
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Chart
|
||||
options={this.state.options}
|
||||
series={this.props.series}
|
||||
type="donut"/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DonutChart;
|
@ -0,0 +1,54 @@
|
||||
import React, { Component } from 'react';
|
||||
import Chart from 'react-apexcharts';
|
||||
import { blueGrey, green, red } from '@material-ui/core/colors';
|
||||
|
||||
export class HeatmapChart extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
options: {
|
||||
chart: {
|
||||
animations: {
|
||||
enabled: false,
|
||||
easing: 'linear',
|
||||
speed: 1000,
|
||||
animateGradually: {
|
||||
enabled: false,
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: true,
|
||||
speed: 500
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
colors: [blueGrey[900]],
|
||||
title: {
|
||||
text: props.label,
|
||||
style: {
|
||||
fontSize: '22px',
|
||||
fontWeight: 600,
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Chart
|
||||
options={this.state.options}
|
||||
series={this.props.series}
|
||||
type="heatmap"
|
||||
height={600}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default HeatmapChart
|
@ -0,0 +1,74 @@
|
||||
import React, { Component } from 'react';
|
||||
import Chart from 'react-apexcharts';
|
||||
import { blueGrey, green, red } from '@material-ui/core/colors';
|
||||
|
||||
export class LineChart extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
options: {
|
||||
chart: {
|
||||
animations: {
|
||||
enabled: false,
|
||||
easing: 'linear',
|
||||
speed: 1000,
|
||||
animateGradually: {
|
||||
enabled: false,
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: true,
|
||||
speed: 500
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
colors: [blueGrey[900]],
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight'
|
||||
},
|
||||
title: {
|
||||
text: this.props.label,
|
||||
align: 'left',
|
||||
style: {
|
||||
fontSize: '22px',
|
||||
fontWeight: 600,
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
row: {
|
||||
colors: ['#f3f3f3', 'transparent'], // takes an array which will be repeated on columns
|
||||
opacity: 0.5
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
formatter: function (val) {
|
||||
return new Date(val).toLocaleTimeString('hu-HU');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Chart
|
||||
options={this.state.options}
|
||||
series={this.props.series}
|
||||
type="line"
|
||||
height={600}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LineChart
|
@ -134,7 +134,7 @@ class Services extends Component {
|
||||
<Refresh fontSize="large" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<AddNewDialog open={this.state.isDialogOpen} handleClose={() => this.setState({ isDialogOpen: false })} handleAdd={this.addDevice}/>
|
||||
<AddNewDialog open={this.state.isDialogOpen} handleClose={() => this.setState({ isDialogOpen: false })} handleAdd={this.addDevice} />
|
||||
</Grid>
|
||||
</Paper>
|
||||
{this.state.isLoading ? Skeletons : ServiceComponents}
|
||||
|
@ -63,8 +63,8 @@ class DeviceComponent extends Component {
|
||||
if (status == "Online") {
|
||||
return { color: green[600] };
|
||||
} else if (status == "Offline") {
|
||||
return { color: orange[900] };
|
||||
} else /* if (device.status == "unknown") */ {
|
||||
return { color: blueGrey[500] };
|
||||
} else /* if (device.status == "Unknown" || device.status == "Error") */ {
|
||||
return { color: red[800] };
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ export default class DevicesContextProvider extends Component {
|
||||
}
|
||||
service.getall().then(result => {
|
||||
this.setState({ devices: result });
|
||||
this.invokeHandlers(C.update_all_method_name, null);
|
||||
}).catch(ex => {
|
||||
console.log(ex);
|
||||
});
|
||||
@ -101,7 +102,7 @@ export default class DevicesContextProvider extends Component {
|
||||
newConnection.on(C.probability_method_name, (id, date, prob) => {
|
||||
//console.log(method_name + " recieved: [id: " + id + ", date: " + date + ", prob: " + prob + "]");
|
||||
var device = this.state.devices.filter(function (x) { return x.id === id })[0]
|
||||
var newPoint = { lat: device.coordinates.latitude, lng: device.coordinates.longitude, prob: prob, date: date };
|
||||
var newPoint = { deviceId: device.id, lat: device.coordinates.latitude, lng: device.coordinates.longitude, prob: prob, date: new Date(date) };
|
||||
this.setState({
|
||||
heatmapPoints: [...this.state.heatmapPoints, newPoint]
|
||||
});
|
||||
@ -111,7 +112,6 @@ export default class DevicesContextProvider extends Component {
|
||||
|
||||
newConnection.on(C.update_all_method_name, () => {
|
||||
this.updateAllDevicesInternal(service);
|
||||
this.invokeHandlers(C.update_all_method_name, null);
|
||||
});
|
||||
|
||||
newConnection.on(C.update_method_name, (id) => this.updateDeviceInternal(id, service));
|
||||
|
Loading…
Reference in New Issue
Block a user