266 lines
10 KiB
Vue
266 lines
10 KiB
Vue
<template>
|
|
<div style="font-family: monospace;" role="region" tabindex="0" v-if="(resultset) && (resultset.length > 0)">
|
|
<table class="table-sticky table-data">
|
|
<thead>
|
|
<tr><th class="name">{{name}}</th><th class="date" v-for="(group, idx) in groups" :class="{ first: (idx == 0) || (group.datehdr.datestr != groups[idx - 1].datehdr.datestr) }" ref="headers"><div class="year">{{group.datehdr.year}}</div><div class="monthdate">{{group.datehdr.monthdate}}</div><div class="hourminute" :class="{ daily }">{{group.datehdr.hourminute}}</div></th></tr>
|
|
</thead>
|
|
<tbody v-for="report in reports">
|
|
<template v-for="name in report">
|
|
<tr v-if="names[name]">
|
|
<th>{{name}}</th>
|
|
<td v-for="(group, idx) in groups" :class="{ first: (idx == 0) || (group.datehdr.datestr != groups[idx - 1].datehdr.datestr), abnormal_ref: abnormal_ref(group.values[name]), abnormal_ref_low: abnormal_ref_low(group.values[name]), abnormal_ref_high: abnormal_ref_high(group.values[name]), abnormal_iqr: abnormal_iqr(group.values[name]), abnormal_iqr_low: abnormal_iqr_low(group.values[name]), abnormal_iqr_high: abnormal_iqr_high(group.values[name]) }" :title="tooltip(group.values[name])">{{group.values[name] ? group.values[name].value : ''}}</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
<tbody>
|
|
<template v-for="name in names">
|
|
<tr v-if="!names_excluded[name]">
|
|
<th>{{name}}</th>
|
|
<td v-for="(group, idx) in groups" :class="{ first: (idx == 0) || (group.datehdr.datestr != groups[idx - 1].datehdr.datestr), abnormal_ref: abnormal_ref(group.values[name]), abnormal_ref_low: abnormal_ref_low(group.values[name]), abnormal_ref_high: abnormal_ref_high(group.values[name]), abnormal_iqr: abnormal_iqr(group.values[name]), abnormal_iqr_low: abnormal_iqr_low(group.values[name]), abnormal_iqr_high: abnormal_iqr_high(group.values[name]) }" :title="tooltip(group.values[name])">{{group.values[name] ? group.values[name].value : ''}}</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
table.table-sticky {
|
|
border: 2px solid #dee2e6;
|
|
}
|
|
table.table-sticky th:first-child {
|
|
border-right: 2px solid #dee2e6;
|
|
}
|
|
table.table-sticky tbody {
|
|
border-top: 2px solid #dee2e6;
|
|
}
|
|
table.table-sticky tbody tr {
|
|
border-top: 1px dashed #dee2e6;
|
|
}
|
|
table.table-sticky td:nth-of-type(odd) {
|
|
background-color: rgba(0, 0, 0, 0.05);
|
|
}
|
|
table.table-sticky tbody th, table.table-sticky th.name {
|
|
font-weight: bold;
|
|
text-align: center;
|
|
padding-left: 1rem;
|
|
padding-right: 1rem;
|
|
}
|
|
table.table-sticky th.date {
|
|
font-size: 80%;
|
|
font-weight: normal;
|
|
text-align: center;
|
|
}
|
|
table.table-sticky th.date .monthdate {
|
|
font-size: 125%;
|
|
font-weight: bold;
|
|
}
|
|
table.table-sticky th.date .hourminute.daily {
|
|
display: none;
|
|
}
|
|
table.table-sticky tbody td {
|
|
padding: 0 0.5rem;
|
|
max-width: 12rem;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.first {
|
|
border-left: 1px solid #dee2e6;
|
|
}
|
|
.year, .monthdate {
|
|
visibility: hidden;
|
|
}
|
|
.first .year, .first .monthdate {
|
|
visibility: visible;
|
|
}
|
|
[role="region"][tabindex] {
|
|
max-height: 75vh;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
import { uniq, groupByArray, quantile_sorted, inherit } from './util.mjs';
|
|
|
|
function isNumeric(x) {
|
|
return (x !== '') && (x !== false) && (x !== null) && (!isNaN(x));
|
|
}
|
|
|
|
function statistics(resultset) {
|
|
var res = {}, group, item;
|
|
for(var i = resultset.length - 1; i >= 0; --i) {
|
|
item = resultset[i];
|
|
if(isNumeric(item.value)) {
|
|
if(res[item.name]) res[item.name].push(+item.value);
|
|
else res[item.name] = [+item.value];
|
|
}
|
|
}
|
|
for(var k in res) if(res.hasOwnProperty(k)) {
|
|
item = res[k].sort((a, b) => a - b);
|
|
item = res[k] = {
|
|
n: item.length,
|
|
q25: quantile_sorted(item, 0.25),
|
|
q50: quantile_sorted(item, 0.50),
|
|
q75: quantile_sorted(item, 0.75)
|
|
}
|
|
item.range = item.q25 != item.q75 ? ('IQR: ' + item.q25 + ' - ' + item.q75) : ('Median: ' + item.q50);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function date_header(date) {
|
|
var datestr = date.toLocaleDateString('en-CA');
|
|
var timestr = date.toLocaleTimeString('en-GB');
|
|
return {
|
|
datestr, timestr,
|
|
year: datestr.substring(0, 4),
|
|
monthdate: datestr.substring(5),
|
|
hourminute: timestr.substring(0, 5),
|
|
second: timestr.substring(6)
|
|
};
|
|
}
|
|
|
|
export default {
|
|
props: {
|
|
name: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
resultset: {
|
|
type: Array,
|
|
default: []
|
|
},
|
|
daily: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
constants: {
|
|
type: Object,
|
|
default: {}
|
|
},
|
|
calculations: {
|
|
type: Array,
|
|
default: []
|
|
},
|
|
reports: {
|
|
type: Array,
|
|
default: []
|
|
}
|
|
},
|
|
data() {
|
|
return {};
|
|
},
|
|
computed: {
|
|
resultset_all() {
|
|
return this.resultset_calculated.length > 0 ? this.resultset.concat(this.resultset_calculated) : this.resultset;
|
|
},
|
|
resultset_calculated() {
|
|
var self = this, snapshots = [], results = [], history, update, item;
|
|
groupByArray(this.resultset, x => x.time).map(group => group.values.reduce(((acc, x) => (acc.values[x.name] = x, acc)), { key: group.key, datehdr: date_header(group.key), values: {}})).sort((a, b) => (a.key > b.key) - (a.key < b.key)).forEach(function(group) {
|
|
snapshots.push({ key: group.key, values: history = Object.assign(snapshots.length > 0 ? inherit(snapshots[snapshots.length - 1].values) : inherit(self.constants), update = group.values) });
|
|
history['Time'] = update['Time'] = { time: group.key, value: group.key };
|
|
for(var i = 0; i < self.calculations.length; ++i) {
|
|
var calculation = self.calculations[i], deps = calculation.deps;
|
|
for(var j = deps.length - 1, satisfied = true, updated = false; j >= 0; --j) if(!history[deps[j]]) { satisfied = false; break; }
|
|
else if(update[deps[j]]) updated = true;
|
|
if((satisfied) && (updated)) {
|
|
item = calculation.calc(...calculation.deps.map(x => history[x].value), history[calculation.name] && history[calculation.name].value);
|
|
if((item !== undefined) && (item !== null)) {
|
|
results.push(history[calculation.name] = update[calculation.name] = Object.assign({ time: group.key, value: item }, calculation));
|
|
if((item.hasOwnProperty('rangeL')) && (item.value < item.rangeL)) item.flag = 'L';
|
|
else if((item.hasOwnProperty('rangeH')) && (item.value > item.rangeH)) item.flag = 'H';
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return results;
|
|
},
|
|
groups() {
|
|
if(this.daily) return groupByArray(this.resultset_all, x => new Date(x.time.getFullYear(), x.time.getMonth(), x.time.getDate())).map(function(group) {
|
|
group = group.values.reduce(((acc, x) => ((acc.values[x.name] || (acc.values[x.name] = [])).push(x), acc)), { key: group.key, datehdr: date_header(group.key), values: {}});
|
|
for(var k in group.values) if(group.values.hasOwnProperty(k)) {
|
|
var items = group.values[k].sort((a, b) => a.time - b.time);
|
|
var strings = items.map(item => item.time.toLocaleTimeString('en-GB') + ' • ' + item.value + (item.unit ? ' ' + item.unit : '') + (item.flag ? ' [' + item.flag + ']' : '') + (item.comment && item.comment.indexOf('\n') < 0 ? ' • ' + item.comment : ''));
|
|
var flags = uniq(items.map(item => item.flag).filter(x => x).map(x => x.charAt(0)));
|
|
var comments = uniq(items.map(item => item.comment).filter(x => x && x.indexOf('\n') >= 0));
|
|
var numbers = uniq(items.map(item => item.value).filter(x => isNumeric(x)));
|
|
var min = Math.min.apply(null, numbers);
|
|
var max = Math.max.apply(null, numbers);
|
|
group.values[k] = {
|
|
time: group.key,
|
|
name: k,
|
|
unit: items[0].unit,
|
|
range: items[0].range,
|
|
value: numbers.length > 1 ? min + ' - ' + max : numbers.length == 1 ? numbers[0] : items.length == 1 ? items[0].value : 'MULTIPLE', min: min, max: max,
|
|
flag: flags.length > 1 ? '*' : flags.length == 1 ? flags[0] : null,
|
|
comment: (strings.join('\n') + '\n\n' + comments.join('\n\n')).replace(/^\s+|\s+$/g, '')
|
|
};
|
|
}
|
|
return group;
|
|
}).sort((a, b) => (a.key > b.key) - (a.key < b.key));
|
|
else return groupByArray(this.resultset_all, x => x.time).map(group => group.values.reduce(((acc, x) => (acc.values[x.name] = x, acc)), { key: group.key, datehdr: date_header(group.key), values: {}})).sort((a, b) => (a.key > b.key) - (a.key < b.key));
|
|
},
|
|
names() {
|
|
var res = uniq(this.resultset_all.map(x => x.name));
|
|
return res.reduce((acc, x) => (acc[x] = true, acc), res);
|
|
},
|
|
names_excluded() {
|
|
var res = {};
|
|
this.reports.forEach(report => report.reduce((acc, x) => (acc[x] = true, acc), res));
|
|
return res;
|
|
},
|
|
statistics() {
|
|
return statistics(this.resultset_all);
|
|
}
|
|
},
|
|
watch: {
|
|
async resultset(value) {
|
|
this.$nextTick(() => this.$refs.headers ? this.$refs.headers.scrollIntoView({ block: 'nearest', inline: 'end' }) : null);
|
|
}
|
|
},
|
|
methods: {
|
|
tooltip(item) {
|
|
if(item) {
|
|
var res = [], stat;
|
|
if(item.range) res.push('Ref: ' + item.range + ' ' + item.unit + (item.flag ? ' [' + item.flag + ']' : ''));
|
|
if(stat = this.statistics[item.name]) res.push(stat.range + (item.range ? ' ' + item.unit : '') + (isNaN(parseFloat(item.value)) ? '' : item.value < stat.q25 ? ' [L]' : item.value > stat.q75 ? ' [H]' : ''));
|
|
if(item.comment) {
|
|
if(res.length > 0) res.push('');
|
|
res.push(item.comment);
|
|
}
|
|
return res.join('\n');
|
|
}
|
|
},
|
|
abnormal_ref(item) {
|
|
return (item) && (item.flag);
|
|
},
|
|
abnormal_ref_low(item) {
|
|
return (item) && (item.flag) && (item.flag.indexOf('L') >= 0);
|
|
},
|
|
abnormal_ref_high(item) {
|
|
return (item) && (item.flag) && (item.flag.indexOf('H') >= 0);
|
|
},
|
|
abnormal_iqr(item) {
|
|
var stat;
|
|
if((item) && (stat = this.statistics[item.name]) && (stat.n > 2)) {
|
|
if((item.hasOwnProperty('min')) && (item.hasOwnProperty('max'))) return (item.min < stat.q25) || (item.max > stat.q75);
|
|
else if(isNumeric(item.value)) return (item.value < stat.q25) || (item.value > stat.q75);
|
|
}
|
|
},
|
|
abnormal_iqr_low(item) {
|
|
var stat;
|
|
if((item) && (stat = this.statistics[item.name]) && (stat.n > 2)) {
|
|
if((item.hasOwnProperty('min')) && (item.hasOwnProperty('max'))) return item.min < stat.q25;
|
|
else if(isNumeric(item.value)) return item.value < stat.q25;
|
|
}
|
|
},
|
|
abnormal_iqr_high(item) {
|
|
var stat;
|
|
if((item) && (stat = this.statistics[item.name]) && (stat.n > 2)) {
|
|
if((item.hasOwnProperty('min')) && (item.hasOwnProperty('max'))) return item.max > stat.q75;
|
|
else if(isNumeric(item.value)) return item.value > stat.q75;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
</script>
|