mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:37:08 +00:00
Add Graph component.
It displays a series of data plots.
This commit is contained in:
364
lib/www/client/source/src/components/graph-arrays-ij-scatter.vue
Normal file
364
lib/www/client/source/src/components/graph-arrays-ij-scatter.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<v-card style="min-height:400px;">
|
||||
<v-card-title class="headline">
|
||||
Array inline / crossline error
|
||||
<v-spacer></v-spacer>
|
||||
<v-switch v-model="scatterplot" label="Scatterplot"></v-switch>
|
||||
<v-switch class="ml-4" v-model="histogram" label="Histogram"></v-switch>
|
||||
</v-card-title>
|
||||
|
||||
<v-container fluid fill-height>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="graph-container" ref="graph0"></div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-show="scatterplot">
|
||||
<v-col>
|
||||
<div class="graph-container" ref="graph1"></div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-show="histogram">
|
||||
<v-col>
|
||||
<div class="graph-container" ref="graph2"></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 {
|
||||
background-color: red;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
import Plotly from 'plotly.js-dist';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import unpack from '@/lib/unpack.js';
|
||||
|
||||
export default {
|
||||
name: 'DougalGraphArraysIJScatter',
|
||||
|
||||
props: [ "data" ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
graph: [],
|
||||
busy: false,
|
||||
resizeObserver: null,
|
||||
scatterplot: false,
|
||||
histogram: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
//...mapGetters(['apiUrl'])
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
data (newVal, oldVal) {
|
||||
console.log("scatter data changed");
|
||||
|
||||
if (newVal === null) {
|
||||
this.busy = true;
|
||||
} else {
|
||||
this.busy = false;
|
||||
this.plot();
|
||||
}
|
||||
},
|
||||
|
||||
histogram () {
|
||||
this.plot();
|
||||
this.saveUserPreference([`${this.$options.name}.histogram`, this.histogram]);
|
||||
},
|
||||
|
||||
|
||||
scatterplot () {
|
||||
this.plot();
|
||||
this.saveUserPreference([`${this.$options.name}.scatterplot`, this.scatterplot]);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
plot () {
|
||||
|
||||
this.plotSeries();
|
||||
|
||||
if (this.histogram) {
|
||||
this.plotHistogram();
|
||||
}
|
||||
|
||||
if (this.scatterplot) {
|
||||
this.plotScatter();
|
||||
}
|
||||
},
|
||||
|
||||
plotSeries () {
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
function transform (d, idx=0, otherParams={}) {
|
||||
const errortype = d.errorfinal ? "errorfinal" : "errorraw";
|
||||
const coords = unpack(unpack(d, errortype), "coordinates");
|
||||
const x = unpack(d, "point");
|
||||
const y = unpack(coords, idx);
|
||||
const data = {
|
||||
type: "scatter",
|
||||
mode: "lines",
|
||||
x,
|
||||
y,
|
||||
transforms: [{
|
||||
type: "groupby",
|
||||
groups: unpack(unpack(d, "meta"), "src_number"),
|
||||
styles: [
|
||||
{target: 1, value: {line: {color: "red"}}},
|
||||
{target: 2, value: {line: {color: "green"}}}
|
||||
]
|
||||
}],
|
||||
...otherParams
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
const data = [
|
||||
transform(this.data.items, 1, {
|
||||
xaxis: 'x',
|
||||
yaxis: 'y',
|
||||
name: 'Crossline'
|
||||
}),
|
||||
transform(this.data.items, 0, {
|
||||
xaxis: 'x',
|
||||
yaxis: 'y2',
|
||||
name: 'Inline'
|
||||
})
|
||||
];
|
||||
this.busy = false;
|
||||
|
||||
const layout = {
|
||||
//autosize: true,
|
||||
title: {text: "Inline / crossline error – sequence %{meta.sequence}"},
|
||||
autocolorscale: true,
|
||||
// colorscale: "sequential",
|
||||
yaxis2: {
|
||||
title: "Crossline (m)",
|
||||
anchor: "y2",
|
||||
domain: [ 0.55, 1 ]
|
||||
},
|
||||
yaxis: {
|
||||
title: "Inline (m)",
|
||||
anchor: "y1",
|
||||
domain: [ 0, 0.45 ]
|
||||
},
|
||||
xaxis: {
|
||||
title: "Shotpoint",
|
||||
anchor: "x1"
|
||||
},
|
||||
meta: this.data.meta
|
||||
};
|
||||
|
||||
const config = {
|
||||
editable: true,
|
||||
displaylogo: false
|
||||
};
|
||||
|
||||
this.graph[0] = Plotly.newPlot(this.$refs.graph0, data, layout, config);
|
||||
},
|
||||
|
||||
plotScatter () {
|
||||
|
||||
console.log("plot");
|
||||
|
||||
if (!this.data) {
|
||||
console.log("missing data");
|
||||
return;
|
||||
}
|
||||
console.log("Will plot sequence", this.data.meta.project, this.data.meta.sequence);
|
||||
|
||||
function transform (d) {
|
||||
const errortype = d.errorfinal ? "errorfinal" : "errorraw";
|
||||
const coords = unpack(unpack(d, errortype), "coordinates");
|
||||
const x = unpack(coords, 0);
|
||||
const y = unpack(coords, 1);
|
||||
const data = [{
|
||||
type: "scatter",
|
||||
mode: "markers",
|
||||
x,
|
||||
y,
|
||||
transforms: [{
|
||||
type: "groupby",
|
||||
groups: unpack(unpack(d, "meta"), "src_number"),
|
||||
styles: [
|
||||
{target: 1, value: {line: {color: "red"}}},
|
||||
{target: 2, value: {line: {color: "green"}}}
|
||||
]
|
||||
}]
|
||||
}];
|
||||
return data;
|
||||
}
|
||||
|
||||
const data = transform(this.data.items);
|
||||
this.busy = false;
|
||||
|
||||
const layout = {
|
||||
//autosize: true,
|
||||
//title: {text: "Inline / crossline error – sequence %{meta.sequence}"},
|
||||
autocolorscale: true,
|
||||
// colorscale: "sequential",
|
||||
yaxis: {
|
||||
title: "Inline (m)",
|
||||
//zeroline: false
|
||||
},
|
||||
xaxis: {
|
||||
title: "Crossline (m)"
|
||||
},
|
||||
meta: this.data.meta
|
||||
};
|
||||
|
||||
const config = {
|
||||
editable: true,
|
||||
displaylogo: false
|
||||
};
|
||||
|
||||
this.graph[1] = Plotly.newPlot(this.$refs.graph1, data, layout, config);
|
||||
},
|
||||
|
||||
plotHistogram () {
|
||||
|
||||
if (!this.data) {
|
||||
console.log("missing data");
|
||||
return;
|
||||
}
|
||||
|
||||
function transform (d, idx=0, otherParams={}) {
|
||||
const errortype = d.errorfinal ? "errorfinal" : "errorraw";
|
||||
const coords = unpack(unpack(d, errortype), "coordinates");
|
||||
const x = unpack(coords, idx);
|
||||
const data = {
|
||||
type: "histogram",
|
||||
histnorm: 'probability',
|
||||
x,
|
||||
transforms: [{
|
||||
type: "groupby",
|
||||
groups: unpack(unpack(d, "meta"), "src_number"),
|
||||
styles: [
|
||||
{target: 1, value: {marker: {color: "rgba(229, 115, 115, 0.9)"}}},
|
||||
{target: 2, value: {marker: {color: "rgba(129, 199, 132, 0.9)"}}}
|
||||
]
|
||||
}],
|
||||
...otherParams
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
const data = [
|
||||
transform(this.data.items, 0, {
|
||||
xaxis: 'x',
|
||||
yaxis: 'y',
|
||||
name: 'Crossline'
|
||||
}),
|
||||
transform(this.data.items, 1, {
|
||||
xaxis: 'x2',
|
||||
yaxis: 'y',
|
||||
name: 'Inline'
|
||||
})
|
||||
];
|
||||
|
||||
const layout = {
|
||||
//autosize: true,
|
||||
//title: {text: "Inline / crossline error – sequence %{meta.sequence}"},
|
||||
legend: {
|
||||
title: { text: "Array" }
|
||||
},
|
||||
xaxis: {
|
||||
title: "Crossline distance (m)",
|
||||
domain: [ 0, 0.45 ],
|
||||
anchor: 'x1'
|
||||
},
|
||||
yaxis: {
|
||||
title: "Frequency (0‒1)",
|
||||
domain: [ 0, 1 ],
|
||||
anchor: 'y1'
|
||||
},
|
||||
xaxis2: {
|
||||
title: "Inline distance (m)",
|
||||
domain: [ 0.55, 1 ],
|
||||
anchor: 'x2'
|
||||
},
|
||||
meta: this.data.meta
|
||||
};
|
||||
|
||||
const config = {
|
||||
editable: false,
|
||||
displaylogo: false
|
||||
};
|
||||
this.busy = false;
|
||||
console.log(data);
|
||||
console.log(layout);
|
||||
|
||||
this.graph[2] = Plotly.newPlot(this.$refs.graph2, data, layout, config);
|
||||
},
|
||||
|
||||
replot () {
|
||||
if (!this.graph.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Replotting");
|
||||
this.graph.forEach( (graph, idx) => {
|
||||
const ref = this.$refs["graph"+idx];
|
||||
Plotly.relayout(ref, {
|
||||
width: ref.clientWidth,
|
||||
height: ref.clientHeight
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
...mapActions(["loadUserPreference", "saveUserPreference"])
|
||||
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
|
||||
if (this.data) {
|
||||
this.plot();
|
||||
} else {
|
||||
this.busy = true;
|
||||
}
|
||||
|
||||
this.resizeObserver = new ResizeObserver(this.replot)
|
||||
this.resizeObserver.observe(this.$refs.graph0);
|
||||
this.resizeObserver.observe(this.$refs.graph1);
|
||||
this.resizeObserver.observe(this.$refs.graph2);
|
||||
|
||||
this.histogram = await this.loadUserPreference([`${this.$options.name}.histogram`, false]);
|
||||
this.scatterplot = await this.loadUserPreference([`${this.$options.name}.scatterplot`, false]);
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.unobserve(this.$refs.graph2);
|
||||
this.resizeObserver.unobserve(this.$refs.graph1);
|
||||
this.resizeObserver.unobserve(this.$refs.graph0);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
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>
|
||||
295
lib/www/client/source/src/components/graph-guns-heatmap.vue
Normal file
295
lib/www/client/source/src/components/graph-guns-heatmap.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<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",
|
||||
"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 labels = [
|
||||
"Mode", "Detect", "Autofire", "Aimpoint", "Firetime", "Delay",
|
||||
"Depth", "Pressure", "Volume", "Filltime"
|
||||
];
|
||||
const indices = [
|
||||
3, 4, 5, 7, 8, 9, 10, 11, 12, 13
|
||||
];
|
||||
const conversions = [
|
||||
// Mode
|
||||
(v) => {
|
||||
switch (v) {
|
||||
case "A":
|
||||
return 1;
|
||||
case "M":
|
||||
return 2;
|
||||
case "O":
|
||||
return 0;
|
||||
case "D":
|
||||
return -1;
|
||||
}
|
||||
},
|
||||
|
||||
// Detect
|
||||
(v) => {
|
||||
switch (v) {
|
||||
case "P":
|
||||
return 1;
|
||||
case "Z":
|
||||
return 0;
|
||||
case "L":
|
||||
return -1;
|
||||
}
|
||||
},
|
||||
|
||||
// Autofire
|
||||
(v) => v ? 1 : 0
|
||||
];
|
||||
const meta = {
|
||||
// Mode
|
||||
Mode: {
|
||||
text: [ "Auto", "Manual", "Off", "Disable" ]
|
||||
},
|
||||
// Detect
|
||||
Detect: {
|
||||
text: [ "Peak", "Zero", "Level" ]
|
||||
},
|
||||
// Autofire
|
||||
Autofire: {
|
||||
text: [ "False", "True" ]
|
||||
}
|
||||
};
|
||||
const hovertemplate = {
|
||||
// Mode
|
||||
Mode: "%{x}",
|
||||
// Detect
|
||||
Detect: "%{x}",
|
||||
// Autofire
|
||||
Autofire:
|
||||
"x: %{x}<br>" +
|
||||
"y: %{y}<br>" +
|
||||
"z: %{meta.text[%{z}]}"
|
||||
};
|
||||
const guns = [...new Set(data.map( s => s.meta.guns.map( g => g[1] ) ).flat())];
|
||||
const z = {};
|
||||
const x = [];
|
||||
const y = guns.map( gun => `G${gun}` );
|
||||
|
||||
// Build array of guns
|
||||
for (let label of labels) {
|
||||
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);
|
||||
indices.forEach( (index, i) => {
|
||||
const parameter = z[labels[i]];
|
||||
const conversion = conversions[i] || ((v) => v);
|
||||
shot.meta.guns.forEach ( gun => {
|
||||
const gunParameter = parameter[gun[1]-1];
|
||||
gunParameter.push(conversion(gun[index]));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log("X, Y, Z", x, y, z);
|
||||
console.log("ASPECTS", aspects);
|
||||
return aspects.map ( (aspect, idx) => {
|
||||
return {
|
||||
name: aspect,
|
||||
type: "heatmap",
|
||||
showscale: false,
|
||||
x,
|
||||
y,
|
||||
z: z[aspect],
|
||||
xaxis: "x",
|
||||
yaxis: "y" + (idx > 0 ? idx+1 : ""),
|
||||
text: meta[aspect]?.text ? z[aspect].map( row => row.map( v => meta[aspect].text[v] )) : undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const data = transform(this.data.items, this.aspects);
|
||||
this.busy = false;
|
||||
|
||||
console.log("DATA", data);
|
||||
console.log("ASPECTS", this.aspects, this.aspects.length);
|
||||
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>
|
||||
364
lib/www/client/source/src/components/graph-guns-pressure.vue
Normal file
364
lib/www/client/source/src/components/graph-guns-pressure.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<v-card style="min-height:400px;">
|
||||
<v-card-title class="headline">
|
||||
Gun pressures
|
||||
<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: 'DougalGraphGunsPressure',
|
||||
|
||||
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 gunPressures = guns.map(s => s.map(g => g[11]));
|
||||
const gunPressuresSorted = gunPressures.map(s => d3a.sort(s));
|
||||
const gunVolumes = guns.map(s => s.map(g => g[12]));
|
||||
const gunPressureWeights = gunVolumes.map( (s, sidx) => s.map( v => v/meta[sidx].volume ));
|
||||
const gunsWeightedAvgPressure = gunPressures.map( (s, sidx) =>
|
||||
d3a.sum(s.map( (pressure, gidx) => pressure * gunPressureWeights[sidx][gidx] )) / d3a.sum(gunPressureWeights[sidx])
|
||||
);
|
||||
|
||||
const manifold = unpack(meta, "manifold");
|
||||
const x = src_number
|
||||
? unpack(d.filter(s => s.meta.src_number == src_number), "point")
|
||||
: unpack(d, "point");
|
||||
|
||||
const traceManifold = {
|
||||
name: "Manifold",
|
||||
type: "scatter",
|
||||
mode: "lines",
|
||||
line: { ...aes.gunArrays[src_number || 1].avg.line, dash: "dot", width: 1 },
|
||||
x,
|
||||
y: manifold,
|
||||
};
|
||||
|
||||
const tracesGunPressures = [{
|
||||
type: "scatter",
|
||||
mode: "lines",
|
||||
x,
|
||||
y: gunPressuresSorted.map(s => d3a.quantileSorted(s, 0.25)),
|
||||
...aes.gunArrays[src_number || 1].min
|
||||
},
|
||||
{
|
||||
type: "scatter",
|
||||
mode: "lines",
|
||||
fill: "tonexty",
|
||||
x,
|
||||
y: gunsWeightedAvgPressure,
|
||||
...aes.gunArrays[src_number || 1].avg
|
||||
},
|
||||
{
|
||||
type: "scatter",
|
||||
mode: "lines",
|
||||
fill: "tonexty",
|
||||
x,
|
||||
y: gunPressuresSorted.map(s => d3a.quantileSorted(s, 0.75)),
|
||||
...aes.gunArrays[src_number || 1].max
|
||||
}];
|
||||
|
||||
const tracesGunsPressuresIndividual = {
|
||||
name: `Array ${src_number} outliers`,
|
||||
type: "scatter",
|
||||
mode: "markers",
|
||||
marker: {size: 2 },
|
||||
hoverinfo: "skip",
|
||||
x: gunPressuresSorted.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: gunPressuresSorted.map( (s, idx) =>
|
||||
s.filter( g => g < d3a.quantileSorted(s, 0.05) || g > d3a.quantileSorted(s, 0.95))
|
||||
).flat()
|
||||
};
|
||||
|
||||
const data = [ traceManifold, ...tracesGunPressures, tracesGunsPressuresIndividual ]
|
||||
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 pressures – sequence %{meta.sequence}"},
|
||||
autocolorscale: true,
|
||||
// colorscale: "sequential",
|
||||
hovermode: "x",
|
||||
yaxis: {
|
||||
title: "Pressure (psi)",
|
||||
//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 pressures = unpack(guns, 11);
|
||||
const volumes = unpack(guns, 12);
|
||||
const maxVolume = d3a.max(volumes);
|
||||
const data = [{
|
||||
type: "bar",
|
||||
x: gunIds,
|
||||
y: pressures,
|
||||
width: volumes.map( v => v/maxVolume ),
|
||||
transforms: [{
|
||||
type: "groupby",
|
||||
groups: unpack(guns, 0)
|
||||
}],
|
||||
}];
|
||||
|
||||
const layout = {
|
||||
title: {text: "Gun pressures – shot %{meta.point}"},
|
||||
height: 300,
|
||||
yaxis: {
|
||||
title: "Pressure (psi)",
|
||||
range: [ Math.min(d3a.min(pressures), 1950), Math.max(d3a.max(pressures), 2050) ]
|
||||
},
|
||||
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(), 11), // Gun pressure
|
||||
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 pressures – sequence %{meta.sequence}"},
|
||||
autocolorscale: true,
|
||||
// colorscale: "sequential",
|
||||
yaxis: {
|
||||
title: "Pressure (psi)",
|
||||
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>
|
||||
347
lib/www/client/source/src/components/graph-guns-timing.vue
Normal file
347
lib/www/client/source/src/components/graph-guns-timing.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<v-card style="min-height:400px;">
|
||||
<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" ],
|
||||
|
||||
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 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()
|
||||
};
|
||||
|
||||
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
|
||||
},
|
||||
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 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'
|
||||
},
|
||||
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"
|
||||
},
|
||||
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>
|
||||
290
lib/www/client/source/src/views/Graphs.vue
Normal file
290
lib/www/client/source/src/views/Graphs.vue
Normal file
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<v-card>
|
||||
|
||||
<v-toolbar v-if="$route.params.sequence" class="fixed">
|
||||
|
||||
<v-toolbar-title>
|
||||
Sequence {{$route.params.sequence}}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn icon
|
||||
:disabled="!($route.params.sequence > firstSequence)"
|
||||
:to="{name: 'graphsBySequence', params: { sequence: firstSequence }}"
|
||||
>
|
||||
<v-icon>mdi-skip-backward</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon
|
||||
:disabled="!prevSequence"
|
||||
:to="{name: 'graphsBySequence', params: { sequence: prevSequence }}"
|
||||
>
|
||||
<v-icon>mdi-skip-previous</v-icon>
|
||||
</v-btn>
|
||||
<v-menu
|
||||
:close-on-content-click="false"
|
||||
:disabled="!sequences.length"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on" :disabled="!sequences.length">
|
||||
<v-icon>mdi-debug-step-over</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-autocomplete
|
||||
:value="$route.params.sequence*1"
|
||||
:items="sequences"
|
||||
item-text="sequence"
|
||||
item-value="sequence"
|
||||
@change="(sequence) => $router.push({name: 'graphsBySequence', params: {sequence}})"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn icon
|
||||
:disabled="!nextSequence"
|
||||
:to="{name: 'graphsBySequence', params: { sequence: nextSequence }}"
|
||||
>
|
||||
<v-icon>mdi-skip-next</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon
|
||||
:disabled="!($route.params.sequence < lastSequence)"
|
||||
:to="{name: 'graphsBySequence', params: { sequence: lastSequence }}"
|
||||
>
|
||||
<v-icon>mdi-skip-forward</v-icon>
|
||||
</v-btn>
|
||||
|
||||
</v-toolbar>
|
||||
|
||||
<v-toolbar v-else-if="$route.params.sequence0">
|
||||
|
||||
<v-toolbar-title>
|
||||
Sequences {{$route.params.sequence0}}‒{{$route.params.sequence1}}
|
||||
</v-toolbar-title>
|
||||
|
||||
</v-toolbar>
|
||||
|
||||
<v-toolbar v-else-if="$route.params.date">
|
||||
|
||||
<v-toolbar-title>
|
||||
Date {{$route.params.date}}
|
||||
</v-toolbar-title>
|
||||
|
||||
</v-toolbar>
|
||||
|
||||
<v-toolbar v-else-if="$route.params.date0">
|
||||
|
||||
<v-toolbar-title>
|
||||
Dates {{$route.params.date0}}‒{{$route.params.date1}}
|
||||
</v-toolbar-title>
|
||||
|
||||
</v-toolbar>
|
||||
|
||||
|
||||
<v-toolbar flat>
|
||||
<!--
|
||||
This is a “ghost” toolbar so that elements further down in the page are
|
||||
not hidden behind the (now position: fixed) real toolbar.
|
||||
-->
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<v-row v-for="(item, idx) in items" :key="idx">
|
||||
<v-col>
|
||||
<component :is="item.component" :data="attributesFor(item)"></component>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.v-toolbar.fixed {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.empty-cell {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 1px dashed lightgray;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import DougalGraphGunsPressure from '@/components/graph-guns-pressure.vue';
|
||||
import DougalGraphGunsTiming from '@/components/graph-guns-timing.vue';
|
||||
import DougalGraphGunsDepth from '@/components/graph-guns-depth.vue';
|
||||
import DougalGraphGunsHeatmap from '@/components/graph-guns-heatmap.vue';
|
||||
import GraphArraysIJScatter from '@/components/graph-arrays-ij-scatter.vue';
|
||||
|
||||
export default {
|
||||
name: "Graphs",
|
||||
|
||||
components: {
|
||||
GraphArraysIJScatter,
|
||||
DougalGraphGunsPressure,
|
||||
DougalGraphGunsTiming,
|
||||
DougalGraphGunsDepth,
|
||||
DougalGraphGunsHeatmap
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
component: "DougalGraphGunsPressure",
|
||||
},
|
||||
{
|
||||
component: "DougalGraphGunsTiming",
|
||||
},
|
||||
{
|
||||
component: "DougalGraphGunsDepth",
|
||||
},
|
||||
{
|
||||
component: "DougalGraphGunsHeatmap",
|
||||
},
|
||||
{
|
||||
component: "GraphArraysIJScatter",
|
||||
attributes: {
|
||||
}
|
||||
}
|
||||
],
|
||||
data: null,
|
||||
sequences: [],
|
||||
jumpToSequence: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
getRows() {
|
||||
return Array(this.rows).fill().map( (el, idx) => idx );
|
||||
},
|
||||
|
||||
getCols () {
|
||||
return Array(this.cols).fill().map( (el, idx) => idx );
|
||||
},
|
||||
|
||||
firstSequence () {
|
||||
return this.sequences[this.sequences.length-1]?.sequence;
|
||||
},
|
||||
|
||||
prevSequence () {
|
||||
const seq = Number(this.$route.params.sequence);
|
||||
const val = this.sequences
|
||||
.filter(i => i.sequence < seq)
|
||||
.map(i => i.sequence)
|
||||
.reduce( (acc, cur) => Math.max(acc, cur), -Infinity);
|
||||
|
||||
return isFinite(val) ? val : undefined;
|
||||
},
|
||||
|
||||
nextSequence () {
|
||||
const seq = Number(this.$route.params.sequence);
|
||||
const val = this.sequences
|
||||
.filter(i => i.sequence > seq)
|
||||
.map(i => i.sequence)
|
||||
.reduce( (acc, cur) => Math.min(acc, cur), +Infinity);
|
||||
|
||||
return isFinite(val) ? val : undefined;
|
||||
},
|
||||
|
||||
lastSequence () {
|
||||
return this.sequences[0]?.sequence;
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
attributesFor (item) {
|
||||
return this.data
|
||||
? Object.assign({
|
||||
items: this.data,
|
||||
meta: {...this.$route.params}
|
||||
}, item?.attributes)
|
||||
: null;
|
||||
},
|
||||
|
||||
gotoSequence(seq) {
|
||||
this.$route.params.sequence = seq;
|
||||
},
|
||||
|
||||
...mapActions(["api", "showSnack"])
|
||||
},
|
||||
|
||||
beforeRouteLeave (to, from, next) {
|
||||
this.data = null;
|
||||
console.log("beforeRouteLeave");
|
||||
next();
|
||||
},
|
||||
|
||||
async beforeRouteUpdate (to, from, next) {
|
||||
console.log("beforeRouteUpdate");
|
||||
this.data = null;
|
||||
next();
|
||||
|
||||
const url = `/project/${this.$route.params.project}/sequence/${this.$route.params.sequence}?project=sequence,point,tstamp,geometrypreplot,errorraw,errorfinal,meta&path=$.raw.smsrc`;
|
||||
this.data = Object.freeze(await this.api([url]));
|
||||
this.sequences = await this.api([`/project/${this.$route.params.project}/sequence`]);
|
||||
},
|
||||
|
||||
async beforeRouteEnter (to, from, next) {
|
||||
console.log("beforeRouteEnter enter");
|
||||
|
||||
next( async vm => {
|
||||
if (vm.$route.params.sequence) {
|
||||
const url = `/project/${vm.$route.params.project}/sequence/${vm.$route.params.sequence}?project=sequence,point,tstamp,geometrypreplot,errorraw,errorfinal,meta&path=$.raw.smsrc`;
|
||||
|
||||
vm.data = null;
|
||||
vm.api([url]).then( d => vm.data = Object.freeze(d) );
|
||||
vm.api([`/project/${vm.$route.params.project}/sequence`]).then( d => vm.sequences = d );
|
||||
} else {
|
||||
// FIXME Ultra-dirty hack to get a result when navigating directly to ‘Graphs’
|
||||
if (!vm.sequences.length) {
|
||||
vm.sequences = await vm.api([`/project/${vm.$route.params.project}/sequence`]);
|
||||
}
|
||||
vm.$router.push({name: "graphsBySequence", params: {
|
||||
project: vm.$route.params.project,
|
||||
sequence: vm.sequences[0]?.sequence
|
||||
}});
|
||||
}
|
||||
|
||||
console.log("beforeRouteEnter exit");
|
||||
});
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
console.log("Graphs mounted");
|
||||
this.sequences = await this.api([`/project/${this.$route.params.project}/sequence`]);
|
||||
|
||||
if (!this.$route.params.sequence) {
|
||||
this.$router.push({name: "graphsBySequence", params: {
|
||||
project: this.$route.params.project,
|
||||
sequence: this.sequences[0]?.sequence
|
||||
}});
|
||||
}
|
||||
|
||||
const url = `/project/${this.$route.params.project}/sequence/${this.$route.params.sequence}?project=sequence,point,tstamp,geometrypreplot,errorraw,errorfinal,meta&path=$.raw.smsrc`;
|
||||
|
||||
this.data = Object.freeze(await this.api([url]));
|
||||
console.log("Mount finished");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user