mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:57:08 +00:00
384 lines
9.6 KiB
Vue
384 lines
9.6 KiB
Vue
<template>
|
||
<v-card style="min-height:400px;" outlined>
|
||
<v-card-title class="headline">
|
||
Gun timing
|
||
<v-spacer></v-spacer>
|
||
<v-switch v-model="shotpoint" label="Shotpoint"></v-switch>
|
||
<v-switch class="ml-4" v-model="violinplot" label="Violin plot"></v-switch>
|
||
</v-card-title>
|
||
|
||
<v-container fluid fill-height>
|
||
<v-row>
|
||
<v-col>
|
||
<div class="graph-container" ref="graphSeries"></div>
|
||
</v-col>
|
||
</v-row>
|
||
<v-row v-show="shotpoint">
|
||
<v-col>
|
||
<div class="graph-container" ref="graphBar"></div>
|
||
</v-col>
|
||
</v-row>
|
||
<v-row v-show="violinplot">
|
||
<v-col>
|
||
<div class="graph-container" ref="graphViolin"></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: 'DougalGraphGunsTiming',
|
||
|
||
props: [ "data", "settings" ],
|
||
|
||
data () {
|
||
return {
|
||
graph: null,
|
||
graphHover: null,
|
||
busy: false,
|
||
resizeObserver: null,
|
||
shotpoint: true,
|
||
violinplot: false
|
||
};
|
||
},
|
||
|
||
computed: {
|
||
//...mapGetters(['apiUrl'])
|
||
},
|
||
|
||
watch: {
|
||
|
||
data (newVal, oldVal) {
|
||
console.log("data changed");
|
||
|
||
if (newVal === null) {
|
||
this.busy = true;
|
||
} else {
|
||
this.busy = false;
|
||
this.plot();
|
||
}
|
||
},
|
||
|
||
settings () {
|
||
for (const key in this.settings) {
|
||
this[key] = this.settings[key];
|
||
}
|
||
},
|
||
|
||
shotpoint () {
|
||
if (this.shotpoint) {
|
||
this.replot();
|
||
}
|
||
this.$emit("update:settings", {[`${this.$options.name}.shotpoint`]: this.shotpoint});
|
||
},
|
||
|
||
violinplot () {
|
||
if (this.violinplot) {
|
||
this.plotViolin();
|
||
}
|
||
this.$emit("update:settings", {[`${this.$options.name}.violinplot`]: this.violinplot});
|
||
},
|
||
|
||
"$vuetify.theme.isDark" () {
|
||
this.plot();
|
||
}
|
||
|
||
},
|
||
|
||
methods: {
|
||
|
||
plot () {
|
||
this.plotSeries();
|
||
if (this.violinplot) {
|
||
this.plotViolin();
|
||
}
|
||
},
|
||
|
||
async plotSeries () {
|
||
|
||
function transformSeries (d, src_number, otherParams={}) {
|
||
|
||
const meta = src_number
|
||
? unpack(d, "meta").filter( s => s.src_number == src_number )
|
||
: unpack(d, "meta");
|
||
const guns = unpack(meta, "guns").map(s => s.filter(g => g[2] == src_number));;
|
||
const gunTimings = guns.map(s => s.map(g => g[9]));
|
||
const gunTimingsSorted = gunTimings.map(s => d3a.sort(s));
|
||
const gunsAvgTiming = gunTimings.map( (s, sidx) => d3a.mean(s) );
|
||
|
||
const x = src_number
|
||
? unpack(d.filter(s => s.meta.src_number == src_number), "point")
|
||
: unpack(d, "point");
|
||
|
||
const tracesGunTimings = [{
|
||
type: "scatter",
|
||
mode: "lines",
|
||
x,
|
||
y: gunTimingsSorted.map(s => d3a.quantileSorted(s, 0.25)),
|
||
...aes.gunArrays[src_number || 1].min
|
||
},
|
||
{
|
||
type: "scatter",
|
||
mode: "lines",
|
||
fill: "tonexty",
|
||
x,
|
||
y: gunsAvgTiming,
|
||
...aes.gunArrays[src_number || 1].avg
|
||
},
|
||
{
|
||
type: "scatter",
|
||
mode: "lines",
|
||
fill: "tonexty",
|
||
x,
|
||
y: gunTimingsSorted.map(s => d3a.quantileSorted(s, 0.75)),
|
||
...aes.gunArrays[src_number || 1].max
|
||
}];
|
||
|
||
const tracesGunsTimingsIndividual = {
|
||
//name: `Array ${src_number} outliers`,
|
||
type: "scatter",
|
||
mode: "markers",
|
||
marker: {size: 2 },
|
||
hoverinfo: "skip",
|
||
x: gunTimingsSorted.map( (s, idx) =>
|
||
s.filter( g => g < d3a.quantileSorted(s, 0.05) || g > d3a.quantileSorted(s, 0.95))
|
||
.map( f => Array(f.length).fill(x[idx]) ).flat()
|
||
).flat(),
|
||
y: gunTimingsSorted.map( (s, idx) =>
|
||
s.filter( g => g < d3a.quantileSorted(s, 0.05) || g > d3a.quantileSorted(s, 0.95))
|
||
).flat(),
|
||
...aes.gunArrays[src_number || 1].out
|
||
};
|
||
|
||
const data = [ ...tracesGunTimings, tracesGunsTimingsIndividual ]
|
||
return data;
|
||
}
|
||
|
||
if (!this.data) {
|
||
console.log("missing data");
|
||
return;
|
||
}
|
||
|
||
const sources = [ ...new Set(unpack(this.data.items, "meta").map( s => s.src_number ))];
|
||
const data = sources.map( src_number => transformSeries(this.data.items, src_number) ).flat();
|
||
console.log("Sources", sources);
|
||
console.log(data);
|
||
this.busy = false;
|
||
|
||
const layout = {
|
||
//autosize: true,
|
||
title: {text: "Gun timings – sequence %{meta.sequence}"},
|
||
autocolorscale: true,
|
||
// colorscale: "sequential",
|
||
hovermode: "x",
|
||
yaxis: {
|
||
title: "Timing (ms)",
|
||
//zeroline: false
|
||
},
|
||
xaxis: {
|
||
title: "Shotpoint",
|
||
showspikes: true
|
||
},
|
||
font: {
|
||
color: this.$vuetify.theme.isDark ? "#fff" : undefined
|
||
},
|
||
plot_bgcolor:"rgba(0,0,0,0)",
|
||
paper_bgcolor:"rgba(0,0,0,0)",
|
||
meta: this.data.meta
|
||
};
|
||
|
||
const config = {
|
||
editable: false,
|
||
displaylogo: false
|
||
};
|
||
|
||
this.graph = Plotly.newPlot(this.$refs.graphSeries, data, layout, config);
|
||
this.$refs.graphSeries.on('plotly_hover', (d) => {
|
||
const point = d.points[0].x;
|
||
const item = this.data.items.find(s => s.point == point);
|
||
const guns = item.meta.guns.filter( g => g[2] == item.meta.src_number );
|
||
const gunIds = guns.map( g => "G"+g[1] );
|
||
const timings = unpack(guns, 9);
|
||
const data = [{
|
||
type: "bar",
|
||
x: gunIds,
|
||
y: timings,
|
||
transforms: [{
|
||
type: "groupby",
|
||
groups: unpack(guns, 0)
|
||
}],
|
||
}];
|
||
|
||
const layout = {
|
||
title: {text: "Gun timings – shot %{meta.point}"},
|
||
height: 300,
|
||
yaxis: {
|
||
title: "Timing (ms)",
|
||
range: [ Math.min(d3a.min(timings), 10), Math.max(d3a.max(timings), 20) ]
|
||
},
|
||
xaxis: {
|
||
title: "Gun number",
|
||
type: 'category'
|
||
},
|
||
font: {
|
||
color: this.$vuetify.theme.isDark ? "#fff" : undefined
|
||
},
|
||
plot_bgcolor:"rgba(0,0,0,0)",
|
||
paper_bgcolor:"rgba(0,0,0,0)",
|
||
meta: {
|
||
point
|
||
}
|
||
};
|
||
|
||
const config = { displaylogo: false };
|
||
|
||
Plotly.react(this.$refs.graphBar, data, layout, config);
|
||
});
|
||
},
|
||
|
||
async plotViolin () {
|
||
|
||
function transformViolin (d, opts = {}) {
|
||
|
||
const styles = [];
|
||
|
||
unpack(unpack(d, "meta"), "guns").flat().forEach(i => {
|
||
const gunId = i[1];
|
||
const arrayId = i[2];
|
||
if (!styles[gunId]) {
|
||
styles[gunId] = Object.assign({target: gunId}, aes.gunArrayViolins[arrayId]);
|
||
}
|
||
});
|
||
|
||
const data = {
|
||
type: 'violin',
|
||
x: unpack(unpack(unpack(d, "meta"), "guns").flat(), 1), // Gun number
|
||
y: unpack(unpack(unpack(d, "meta"), "guns").flat(), 9), // Gun timing
|
||
points: 'none',
|
||
box: {
|
||
visible: true
|
||
},
|
||
line: {
|
||
color: 'green',
|
||
},
|
||
meanline: {
|
||
visible: true
|
||
},
|
||
transforms: [{
|
||
type: 'groupby',
|
||
groups: unpack(unpack(unpack(d, "meta"), "guns").flat(), 1),
|
||
styles: styles.filter(i => !!i)
|
||
}]
|
||
}
|
||
|
||
return data;
|
||
}
|
||
|
||
|
||
console.log("plot violin");
|
||
if (!this.data) {
|
||
console.log("missing data");
|
||
return;
|
||
}
|
||
console.log("Will plot sequence", this.data.meta.project, this.data.meta.sequence);
|
||
|
||
const data = [ transformViolin(this.data.items) ];
|
||
this.busy = false;
|
||
|
||
const layout = {
|
||
//autosize: true,
|
||
showlegend: false,
|
||
title: {text: "Individual gun timings – sequence %{meta.sequence}"},
|
||
autocolorscale: true,
|
||
// colorscale: "sequential",
|
||
yaxis: {
|
||
title: "Timing (ms)",
|
||
zeroline: false
|
||
},
|
||
xaxis: {
|
||
title: "Gun number"
|
||
},
|
||
font: {
|
||
color: this.$vuetify.theme.isDark ? "#fff" : undefined
|
||
},
|
||
plot_bgcolor:"rgba(0,0,0,0)",
|
||
paper_bgcolor:"rgba(0,0,0,0)",
|
||
meta: this.data.meta
|
||
};
|
||
|
||
const config = {
|
||
editable: false,
|
||
displaylogo: false
|
||
};
|
||
|
||
this.graph = Plotly.newPlot(this.$refs.graphViolin, 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.graphSeries);
|
||
this.resizeObserver.observe(this.$refs.graphViolin);
|
||
this.resizeObserver.observe(this.$refs.graphBar);
|
||
},
|
||
|
||
beforeDestroy () {
|
||
if (this.resizeObserver) {
|
||
this.resizeObserver.unobserve(this.$refs.graphBar);
|
||
this.resizeObserver.unobserve(this.$refs.graphViolin);
|
||
this.resizeObserver.unobserve(this.$refs.graphSeries);
|
||
}
|
||
}
|
||
|
||
};
|
||
</script>
|