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));