Add Graph component.

It displays a series of data plots.
This commit is contained in:
D. Berge
2021-09-04 19:13:57 +02:00
parent e212dc8b92
commit 019561229c
6 changed files with 2007 additions and 0 deletions

View File

@@ -0,0 +1,347 @@
<template>
<v-card style="min-height:400px;">
<v-card-title class="headline">
Gun depth
<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: 'DougalGraphGunsDepth',
props: [ "data" ],
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();
}
},
violinplot () {
if (this.violinplot) {
this.plotViolin();
}
}
},
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 gunDepths = guns.map(s => s.map(g => g[10]));
const gunDepthsSorted = gunDepths.map(s => d3a.sort(s));
const gunsAvgDepth = gunDepths.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 tracesGunDepths = [{
type: "scatter",
mode: "lines",
x,
y: gunDepthsSorted.map(s => d3a.quantileSorted(s, 0.25)),
...aes.gunArrays[src_number || 1].min
},
{
type: "scatter",
mode: "lines",
fill: "tonexty",
x,
y: gunsAvgDepth,
...aes.gunArrays[src_number || 1].avg
},
{
type: "scatter",
mode: "lines",
fill: "tonexty",
x,
y: gunDepthsSorted.map(s => d3a.quantileSorted(s, 0.75)),
...aes.gunArrays[src_number || 1].max
}];
const tracesGunsDepthsIndividual = {
name: `Array ${src_number} outliers`,
type: "scatter",
mode: "markers",
marker: {size: 2 },
hoverinfo: "skip",
x: gunDepthsSorted.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: gunDepthsSorted.map( (s, idx) =>
s.filter( g => g < d3a.quantileSorted(s, 0.05) || g > d3a.quantileSorted(s, 0.95))
).flat()
};
const data = [ ...tracesGunDepths, tracesGunsDepthsIndividual ]
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 depths sequence %{meta.sequence}"},
autocolorscale: true,
// colorscale: "sequential",
hovermode: "x",
yaxis: {
title: "Depth (m)",
//zeroline: false
},
xaxis: {
title: "Shotpoint",
showspikes: true
},
meta: this.data.meta
};
const config = {
editable: true,
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 depths = unpack(guns, 10);
const data = [{
type: "bar",
x: gunIds,
y: depths,
transforms: [{
type: "groupby",
groups: unpack(guns, 0)
}],
}];
const layout = {
title: {text: "Gun depths shot %{meta.point}"},
height: 300,
yaxis: {
title: "Depth (m)",
range: [ Math.min(d3a.min(depths)-0.1, 5), Math.max(d3a.max(depths)+0.1, 7) ]
},
xaxis: {
title: "Gun number",
type: 'category'
},
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(), 10), // Gun depth
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 depths sequence %{meta.sequence}"},
autocolorscale: true,
// colorscale: "sequential",
yaxis: {
title: "Depth (m)",
zeroline: false
},
xaxis: {
title: "Gun number"
},
meta: this.data.meta
};
const config = {
editable: true,
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);
},
beforeDestroy () {
if (this.resizeObserver) {
this.resizeObserver.observe(this.$refs.graphViolin);
this.resizeObserver.unobserve(this.$refs.graphSeries);
}
}
};
</script>