Files
dougal-software/lib/www/client/source/src/components/graph-guns-heatmap.vue
D. Berge e3a3bdb153 Clean up whitespace.
Commands used:

find . -type f -name '*.js'| while read FILE; do if echo $FILE |grep -qv node_modules; then sed -ri 's/^\s+$//' "$FILE"; fi; done
find . -type f -name '*.vue'| while read FILE; do if echo $FILE |grep -qv node_modules; then sed -ri 's/^\s+$//' "$FILE"; fi; done
find . -type f -name '*.py'| while read FILE; do if echo $FILE |grep -qv node_modules; then sed -ri 's/^\s+$//' "$FILE"; fi; done
2022-04-29 14:48:21 +02:00

406 lines
9.1 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<v-card style="min-height:400px;">
<v-card-title class="headline">
Gun details
</v-card-title>
<v-container fluid fill-height>
<v-row>
<v-col>
<div class="graph-container" ref="graphHeat"></div>
</v-col>
</v-row>
</v-container>
<v-overlay :value="busy" absolute z-index="1">
<v-progress-circular indeterminate></v-progress-circular>
</v-overlay>
</v-card>
</template>
<style scoped>
.graph-container {
width: 100%;
height: 100%;
}
</style>
<script>
import * as d3a from 'd3-array';
import Plotly from 'plotly.js-dist';
import { mapActions, mapGetters } from 'vuex';
import unpack from '@/lib/unpack.js';
import * as aes from '@/lib/graphs/aesthetics.js';
export default {
name: 'DougalGraphGunsDepth',
props: [ "data" ],
data () {
return {
graph: null,
graphHover: null,
busy: false,
resizeObserver: null,
// TODO: aspects should be a prop
aspects: [
"Mode", "Detect", "Autofire", "Aimpoint", "Firetime", "Delay",
"Delta",
"Depth", "Pressure", "Volume", "Filltime"
]
};
},
computed: {
//...mapGetters(['apiUrl'])
},
watch: {
data (newVal, oldVal) {
console.log("data changed");
if (newVal === null) {
this.busy = true;
} else {
this.busy = false;
this.plot();
}
},
violinplot () {
if (this.violinplot) {
this.plotViolin();
}
}
},
methods: {
plot () {
this.plotHeat();
},
async plotHeat () {
if (!this.data) {
console.log("missing data");
return;
}
function transform (data, aspects=["Depth", "Pressure"]) {
const facets = [
// Mode
{
params: {
name: "Mode",
hovertemplate: "SP%{x}<br>%{y}<br>%{text}",
},
text: [ "Off", "Auto", "Manual", "Disabled" ],
conversion: (gun, shot) => {
switch (gun[3]) {
case "A":
return 1;
case "M":
return 2;
case "O":
return 0;
case "D":
return 3;
}
}
},
// Detect
{
params: {
name: "Detect",
hovertemplate: "SP%{x}<br>%{y}<br>%{text}",
},
text: [ "Zero", "Peak", "Level" ],
conversion: (gun, shot) => {
switch (gun[4]) {
case "P":
return 1;
case "Z":
return 0;
case "L":
return 2;
}
}
},
// Autofire
{
params: {
name: "Autofire",
hovertemplate: "SP%{x}<br>%{y}<br>%{text}",
},
text: [ "False", "True" ],
conversion: (gun, shot) => {
return gun[5] ? 1 : 0;
}
},
// Aimpoint
{
params: {
name: "Aimpoint",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} ms"
},
conversion: (gun, shot) => gun[7]
},
// Firetime
{
params: {
name: "Firetime",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} ms"
},
conversion: (gun, shot) => gun[2] == shot.meta.src_number ? gun[8] : null
},
// Delta
{
params: {
name: "Delta",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} ms",
// NOTE: These values are based on
// Grane + Snorre's ±1.5 ms spec. While a fairly
// common range, I still consider these min / max
// numbers to have been chosen semi-arbitrarily.
zmin: -2,
zmax: 2
},
conversion: (gun, shot) => gun[2] == shot.meta.src_number ? gun[7]-gun[8] : null
},
// Delay
{
params: {
name: "Delay",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} ms"
},
conversion: (gun, shot) => gun[9]
},
// Depth
{
params: {
name: "Depth",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} m"
},
conversion: (gun, shot) => gun[10]
},
// Pressure
{
params: {
name: "Pressure",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} psi"
},
conversion: (gun, shot) => gun[11]
},
// Volume
{
params: {
name: "Volume",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} in³"
},
conversion: (gun, shot) => gun[12]
},
// Filltime
{
params: {
name: "Filltime",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} ms"
},
// NOTE that filltime is applicable to the *non* firing guns
conversion: (gun, shot) => gun[2] == shot.meta.src_number ? null : gun[13]
}
];
// Get gun numbers
const guns = [...new Set(data.map( s => s.meta.guns.map( g => g[1] ) ).flat())];
// z eventually will have the structure:
// z = {
// [aspect]: [ // First shotpoint
// [ // Value for gun 0, gun 1, … ],
// …more shotpoints…
// ]
// }
const z = {};
// x is an array of shotpoints
const x = [];
// y is an array of gun numbers
const y = guns.map( gun => `G${gun}` );
// Build array of guns (i.e., populate z)
// We prefer to do this outside the shot-to-shot loop
// for efficiency
for (const facet of facets) {
const label = facet.params.name;
z[label] = Array(guns.length);
for (let i=0; i<guns.length; i++) {
z[label][i] = [];
}
}
// Populate array of guns with shotpoint data
for (let shot of data) {
x.push(shot.point);
for (const facet of facets) {
const label = facet.params.name;
const facetGunsArray = z[label];
for (const gun of shot.meta.guns) {
const gunIndex = gun[1]-1;
const facetGun = facetGunsArray[gunIndex];
facetGun.push(facet.conversion(gun, shot));
}
}
}
return aspects.map( (aspect, idx) => {
const facet = facets.find(el => el.params.name == aspect) || {};
const defaultParams = {
name: aspect,
type: "heatmap",
showscale: false,
x,
y,
z: z[aspect],
text: facet.text ? z[aspect].map(row => row.map(v => facet.text[v])) : undefined,
xaxis: "x",
yaxis: "y" + (idx > 0 ? idx+1 : "")
}
return Object.assign({}, defaultParams, facet.params);
});
}
const data = transform(this.data.items, this.aspects);
this.busy = false;
const layout = {
title: {text: "Gun details sequence %{meta.sequence}"},
height: 200*this.aspects.length,
//autocolorscale: true,
/*
grid: {
rows: this.aspects.length,
columns: 1,
pattern: "coupled",
roworder: "bottom to top"
},
*/
//autosize: true,
// colorscale: "sequential",
xaxis: {
title: "Shotpoint",
showspikes: true
},
meta: this.data.meta
};
this.aspects.forEach ( (aspect, idx) => {
const num = idx+1;
const key = "yaxis" + num;
const anchor = "y" + num;
const segment = (1/this.aspects.length);
const margin = segment/20;
const domain = [
segment*idx + margin,
segment*num - margin
];
layout[key] = {
title: aspect,
anchor,
domain
}
});
const config = {
//editable: true,
displaylogo: false
};
this.graph = Plotly.newPlot(this.$refs.graphHeat, data, layout, config);
},
replot () {
if (!this.graph) {
return;
}
console.log("Replotting");
Object.values(this.$refs).forEach( ref => {
if (ref.data) {
console.log("Replotting", ref, ref.clientWidth, ref.clientHeight);
Plotly.relayout(ref, {
width: ref.clientWidth,
height: ref.clientHeight
});
}
});
},
...mapActions(["api"])
},
mounted () {
if (this.data) {
this.plot();
} else {
this.busy = true;
}
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graphHeat);
},
beforeDestroy () {
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.$refs.graphHeat);
}
}
};
</script>