diff --git a/Birdmap.API/ClientApp/src/components/dashboard/Dashboard.jsx b/Birdmap.API/ClientApp/src/components/dashboard/Dashboard.jsx
index f51286b..8f55d68 100644
--- a/Birdmap.API/ClientApp/src/components/dashboard/Dashboard.jsx
+++ b/Birdmap.API/ClientApp/src/components/dashboard/Dashboard.jsx
@@ -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 (
-
+
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Birdmap.API/ClientApp/src/components/dashboard/charts/BarChart.jsx b/Birdmap.API/ClientApp/src/components/dashboard/charts/BarChart.jsx
new file mode 100644
index 0000000..8032d25
--- /dev/null
+++ b/Birdmap.API/ClientApp/src/components/dashboard/charts/BarChart.jsx
@@ -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 (
+
+ )
+ }
+}
+
+export default BarChart;
diff --git a/Birdmap.API/ClientApp/src/components/dashboard/charts/DonutChart.jsx b/Birdmap.API/ClientApp/src/components/dashboard/charts/DonutChart.jsx
new file mode 100644
index 0000000..75c2f43
--- /dev/null
+++ b/Birdmap.API/ClientApp/src/components/dashboard/charts/DonutChart.jsx
@@ -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 (
+
+ )
+ }
+}
+
+export default DonutChart;
diff --git a/Birdmap.API/ClientApp/src/components/dashboard/charts/HeatmapChart.jsx b/Birdmap.API/ClientApp/src/components/dashboard/charts/HeatmapChart.jsx
new file mode 100644
index 0000000..bbd45cd
--- /dev/null
+++ b/Birdmap.API/ClientApp/src/components/dashboard/charts/HeatmapChart.jsx
@@ -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 (
+
+ )
+ }
+}
+
+export default HeatmapChart
diff --git a/Birdmap.API/ClientApp/src/components/dashboard/charts/LineChart.jsx b/Birdmap.API/ClientApp/src/components/dashboard/charts/LineChart.jsx
new file mode 100644
index 0000000..49cf99f
--- /dev/null
+++ b/Birdmap.API/ClientApp/src/components/dashboard/charts/LineChart.jsx
@@ -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 (
+
+ )
+ }
+}
+
+export default LineChart
diff --git a/Birdmap.API/ClientApp/src/components/dashboard/services/Services.jsx b/Birdmap.API/ClientApp/src/components/dashboard/services/Services.jsx
index 032c291..523577c 100644
--- a/Birdmap.API/ClientApp/src/components/dashboard/services/Services.jsx
+++ b/Birdmap.API/ClientApp/src/components/dashboard/services/Services.jsx
@@ -108,7 +108,7 @@ class Services extends Component {
const Skeletons = this.state.serviceCount.map((i, index) => (
- ));
+ ));
return (
@@ -134,7 +134,7 @@ class Services extends Component {
- this.setState({ isDialogOpen: false })} handleAdd={this.addDevice}/>
+ this.setState({ isDialogOpen: false })} handleAdd={this.addDevice} />
{this.state.isLoading ? Skeletons : ServiceComponents}
diff --git a/Birdmap.API/ClientApp/src/components/devices/DeviceComponent.jsx b/Birdmap.API/ClientApp/src/components/devices/DeviceComponent.jsx
index 6e73d8d..df66a48 100644
--- a/Birdmap.API/ClientApp/src/components/devices/DeviceComponent.jsx
+++ b/Birdmap.API/ClientApp/src/components/devices/DeviceComponent.jsx
@@ -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] };
}
}
diff --git a/Birdmap.API/ClientApp/src/contexts/DevicesContextProvider.js b/Birdmap.API/ClientApp/src/contexts/DevicesContextProvider.js
index 9680b77..7ce11c8 100644
--- a/Birdmap.API/ClientApp/src/contexts/DevicesContextProvider.js
+++ b/Birdmap.API/ClientApp/src/contexts/DevicesContextProvider.js
@@ -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));