Add histogram
This commit is contained in:
parent
ebedcb0f17
commit
1e9dbb4df6
10 changed files with 274 additions and 39 deletions
|
|
@ -20,10 +20,6 @@ hr {
|
|||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.huge {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
background-color: #222;
|
||||
background-size: cover;
|
||||
|
|
|
|||
BIN
ui/src/assets/images/loader.gif
Normal file
BIN
ui/src/assets/images/loader.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
|
|
@ -14,6 +14,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img v-if="graphIsLoading" ref="loader" style="margin: auto" :src="loaderUrl" />
|
||||
<img ref="graph" style="width: 100%; height: 100%" />
|
||||
</div>
|
||||
|
||||
|
|
@ -30,7 +31,6 @@
|
|||
import { Datetime } from 'vue-datetime'
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
const FROM_DEFAULT = moment().add('-24', 'hours').toISOString();
|
||||
const TO_DEFAULT = moment().toISOString();
|
||||
|
||||
|
|
@ -42,6 +42,8 @@ export default {
|
|||
to: TO_DEFAULT,
|
||||
lastFrom: FROM_DEFAULT,
|
||||
lastTo: TO_DEFAULT,
|
||||
graphIsLoading: true,
|
||||
loaderUrl: require('../assets/images/loader.gif'),
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -50,6 +52,8 @@ export default {
|
|||
mounted() {
|
||||
this.loadGraphImage();
|
||||
this.updateDownloadUrl();
|
||||
|
||||
this.$refs.graph.onload = () => this.graphIsLoading = false;
|
||||
},
|
||||
methods: {
|
||||
rangeUpdated() {
|
||||
|
|
@ -75,6 +79,8 @@ export default {
|
|||
let root = this.$http.options.root;
|
||||
let binSize = 1;
|
||||
|
||||
this.$refs.graph.src = '';
|
||||
this.graphIsLoading = true;
|
||||
let imageUrl = `${root}histogram.png?from=${from}&to=${to}&bin_size=${binSize}`;
|
||||
this.$refs.graph.src = imageUrl;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,19 +1,35 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<Histogram></Histogram>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<Location :longitude=lastLongitude :latitude=lastLatitude></Location>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-columns">
|
||||
<Value :value=lastTemperature icon="fa-thermometer-quarter" title="Temperature"></Value>
|
||||
<Value :value=lastHumidity icon="fa-thermometer-quarter" title="Humidity"></Value>
|
||||
<Value :value=lastPressure icon="fa-thermometer-quarter" title="Pressure"></Value>
|
||||
|
||||
<TimeSeries title="Temperature (C)" dkey="TemperatureC"></TimeSeries>
|
||||
<Value :value=lastAcceleration icon="fa-tachometer" title="Acceleration"></Value>
|
||||
<Value :value=lastMagnet icon="fa-compass" title="Magnet"></Value>
|
||||
<Value :value=detectorInfo icon="fa-info-circle" title="Info"></Value>
|
||||
<Value :value=lastLocation icon="fa-map-marker" title="Location"></Value>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Value from './Value.vue'
|
||||
import TimeSeries from './TimeSeries.vue'
|
||||
import Value from './Value.vue';
|
||||
import TimeSeries from './TimeSeries.vue';
|
||||
import Location from './Location.vue';
|
||||
import Histogram from './Histogram.vue';
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: { Value, TimeSeries },
|
||||
components: { Value, Histogram, Location },
|
||||
computed: {
|
||||
times() {
|
||||
return this.$store.getters.getTimeSeries();
|
||||
|
|
@ -25,6 +41,47 @@ export default {
|
|||
let value = this.$store.getters.getLastValue('Humidity');
|
||||
return (!isNaN(value)) ? value.toFixed(2) : value;
|
||||
},
|
||||
lastAcceleration() {
|
||||
let x = this.$store.getters.getLastValue('AccelX');
|
||||
let y = this.$store.getters.getLastValue('AccelY');
|
||||
let z = this.$store.getters.getLastValue('AccelZ');
|
||||
x = (!isNaN(x)) ? x.toFixed(3) : x;
|
||||
y = (!isNaN(x)) ? y.toFixed(3) : y;
|
||||
z = (!isNaN(x)) ? z.toFixed(3) : z;
|
||||
return `${x}\n${y}\n${z}`;
|
||||
},
|
||||
lastMagnet() {
|
||||
let x = this.$store.getters.getLastValue('MagX');
|
||||
let y = this.$store.getters.getLastValue('MagY');
|
||||
let z = this.$store.getters.getLastValue('MagZ');
|
||||
x = (!isNaN(x)) ? x.toFixed(4) : x;
|
||||
y = (!isNaN(x)) ? y.toFixed(4) : y;
|
||||
z = (!isNaN(x)) ? z.toFixed(4) : z;
|
||||
return `${x}\n${y}\n${z}`;
|
||||
},
|
||||
lastLocation() {
|
||||
let long = this.$store.getters.getLastValue('Longitude');
|
||||
let lati = this.$store.getters.getLastValue('Latitude');
|
||||
long = (!isNaN(long)) ? long.toFixed(6) : long;
|
||||
lati = (!isNaN(lati)) ? lati.toFixed(6) : lati;
|
||||
return `${long}\n${lati}`;
|
||||
},
|
||||
lastLongitude() {
|
||||
return this.$store.getters.getLastValue('Longitude');
|
||||
},
|
||||
lastLatitude() {
|
||||
return this.$store.getters.getLastValue('Latitude');
|
||||
},
|
||||
lastPressure() {
|
||||
let value = this.$store.getters.getLastValue('Pressure');
|
||||
return (!isNaN(value)) ? value.toFixed(0) : value;
|
||||
},
|
||||
detectorInfo() {
|
||||
let name = this.$store.getters.getLastValue('DetectorName');
|
||||
let version = this.$store.getters.getLastValue('DetectorVersion');
|
||||
let serial = this.$store.getters.getLastValue('HardwareSerial');
|
||||
return `${name}\n${version}\n${serial}`;
|
||||
},
|
||||
},
|
||||
beforeCreate() {
|
||||
this.$store.dispatch('requestSeries');
|
||||
|
|
|
|||
108
ui/src/components/dashboard/Histogram.vue
Normal file
108
ui/src/components/dashboard/Histogram.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<div class="card card-default">
|
||||
<div class="card-header">
|
||||
<h5>Histogram</h5>
|
||||
<p class="small">
|
||||
Showing data in period {{from}} - {{to}} ({{period}}s),
|
||||
<b>{{ numberOfEvents }}</b> events with
|
||||
bin size <b>{{ binSize }}s</b>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas ref="canvas" style="height:300px"></canvas>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<input v-model="binSize" class="form-control" min="1" type="number"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Chart from 'chart.js';
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
const DEFAULT_BIN_SIZE = 1;
|
||||
|
||||
|
||||
export default {
|
||||
name: 'Histogram',
|
||||
props: ['title', 'dkey'],
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
binSize: DEFAULT_BIN_SIZE,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
times() {
|
||||
return this.$store.getters.getSeries('UTCUnixTime');
|
||||
},
|
||||
from() {
|
||||
return moment(this.times[0] * 1000).format('LLL');
|
||||
},
|
||||
to() {
|
||||
return moment(this.times[this.times.length - 1] * 1000).format('LLL');
|
||||
},
|
||||
numberOfEvents() {
|
||||
return this.$store.getters.getNumberOfEvents();
|
||||
},
|
||||
period() {
|
||||
return moment.duration(this.$store.getters.getPeriod() * 1000).asSeconds();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'binSize': function() { this.generateGraph() },
|
||||
'times': function() { this.generateGraph() },
|
||||
},
|
||||
methods: {
|
||||
generateGraph() {
|
||||
let list = this.times;
|
||||
|
||||
// Create histogram
|
||||
let histogram = {}
|
||||
list = list.map(x => x - (x - list[0]) % this.binSize);
|
||||
list.forEach(x => histogram[x] = (histogram[x] || 0) + 1);
|
||||
|
||||
// Put histogram in chart
|
||||
this.chart.data.datasets[0].data = Object.values(histogram);
|
||||
this.chart.data.labels = Object.keys(histogram).map(x => moment(x * 1000).format('MMMM DD, YYYY HH:mm:ss'))
|
||||
this.chart.update();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.chart = new Chart(this.$refs.canvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: this.times,
|
||||
datasets: [{
|
||||
label: this.title,
|
||||
data: this.values,
|
||||
backgroundColor: "rgba(153,51,255,0.4)"
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
animation: false,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: false
|
||||
}]
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.small {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
38
ui/src/components/dashboard/Location.vue
Normal file
38
ui/src/components/dashboard/Location.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div class="card card-default">
|
||||
<div class="card-header">
|
||||
<h5>Location</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<iframe class="google-map" ref="map" :src=googleMapsUrl></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const MAPS_KEY = 'AIzaSyBjX-IrwBEp7lncv8Q-OXsY549c5zNh_kY';
|
||||
|
||||
export default {
|
||||
name: 'Location',
|
||||
props: ['latitude', 'longitude'],
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
googleMapsUrl() {
|
||||
return `https://www.google.com/maps/embed/v1/place?q=${this.latitude},${this.longitude}&key=${MAPS_KEY}`;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.google-map {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
<template>
|
||||
<div class="col-lg-6">
|
||||
<div class="card card-default">
|
||||
<div class="card-header">
|
||||
<h5>{{ title }}</h5>
|
||||
|
|
@ -8,7 +7,6 @@
|
|||
<canvas ref="canvas" style="height:300px"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -70,3 +68,9 @@ export default {
|
|||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
<template>
|
||||
<div class="col-bg-3 col-md-4 col-sm-6 col-xs-12 item">
|
||||
<div class="card card-default bg-primary">
|
||||
<div class="card card-default bg-light">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<i :class="[ ['fa', icon, 'fa-5x'].join(' ') ]"></i>
|
||||
</div>
|
||||
<div class="col-9 text-right">
|
||||
<div class="huge">{{ value }}</div>
|
||||
<pre class="huge">{{ value }}</pre>
|
||||
<div class="card-title">{{ title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -24,7 +22,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<style>
|
||||
.item {
|
||||
padding-bottom: 20px;
|
||||
.huge {
|
||||
font-size: 24px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,7 +3,7 @@ import Vuex from 'vuex';
|
|||
import moment from 'moment';
|
||||
|
||||
|
||||
const SERIES_MAX_SIZE = 30;
|
||||
const SERIES_MAX_SIZE = 100;
|
||||
Vue.use(Vuex);
|
||||
|
||||
|
||||
|
|
@ -32,6 +32,15 @@ const getters = {
|
|||
}
|
||||
return 'NA';
|
||||
},
|
||||
getNumberOfEvents: (state) => () => {
|
||||
return state.series.length;
|
||||
},
|
||||
getPeriod: (state) => () => {
|
||||
if (state.series.length > 1) {
|
||||
return state.series[state.series.length - 1].UTCUnixTime - state.series[0].UTCUnixTime;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
isLogged: (state) => () => {
|
||||
return (state.token !== null);
|
||||
},
|
||||
|
|
@ -52,7 +61,11 @@ const getters = {
|
|||
|
||||
const actions = {
|
||||
requestSeries({ commit }) {
|
||||
Vue.http.get('series?format=json').then(response => {
|
||||
let params = {
|
||||
format: 'json',
|
||||
limit: SERIES_MAX_SIZE,
|
||||
};
|
||||
Vue.http.get('series', { params }).then(response => {
|
||||
commit('setSeries', response.body);
|
||||
});
|
||||
},
|
||||
|
|
@ -71,7 +84,15 @@ const actions = {
|
|||
|
||||
const mutations = {
|
||||
setSeries(state, data) {
|
||||
state.series.push(...data);
|
||||
data.sort((l, r) => l.UTCUnixTime + l.SubSeconds > r.UTCUnixTime + r.SubSeconds ? 1 : -1);
|
||||
for (let item of data) {
|
||||
let last = state.series[state.series.length - 1];
|
||||
let lastUTCUnixTime = last ? last.UTCUnixTime : 0;
|
||||
let lastSubSeconds = last ? last.SubSeconds : 0;
|
||||
if (item.UTCUnixTime + item.SubSeconds >= lastUTCUnixTime + lastSubSeconds) {
|
||||
state.series.push(item);
|
||||
}
|
||||
}
|
||||
state.series = state.series.slice(-SERIES_MAX_SIZE);
|
||||
},
|
||||
setAuth(state, token) {
|
||||
|
|
|
|||
|
|
@ -19,12 +19,19 @@ module.exports = {
|
|||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg)$/,
|
||||
test: /\.(png|jpg|svg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]?[hash]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.gif$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: 'style-loader!css-loader'
|
||||
|
|
|
|||
Loading…
Reference in a new issue