mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 10:07:08 +00:00
Add Graph component.
It displays a series of data plots.
This commit is contained in:
347
lib/www/client/source/src/components/graph-guns-depth.vue
Normal file
347
lib/www/client/source/src/components/graph-guns-depth.vue
Normal 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>
|
||||
Reference in New Issue
Block a user