Skip to content
Snippets Groups Projects
Commit fbbe18aa authored by Marek Trtik's avatar Marek Trtik
Browse files

Add chart with snow height averages of all sticks and all cameras.

parent cc930cf1
No related branches found
No related tags found
No related merge requests found
<template>
<div id="AveragesChart" class="chart" :style="cssVars" :height="height">
<resize-observer @notify="handleResize" />
<svg
id="AveragesChart_SVGDaysGradient"
class="chart-gradient"
:height="spanY"
:width="width"
>
<image
id="AveragesChart_Gradient"
:href="sunImage"
:width="spanX"
:height="spanY"
:x="rangeX[0]"
preserveAspectRatio="none"
/>
<rect :width="margin.left" :height="spanY" />
<rect :width="margin.right" :height="spanY" :x="rangeX[1]" />
</svg>
<canvas
id="AveragesChart_Canvas"
class="chart-lines"
:height="height"
:width="width"
/>
<svg id="AveragesChart_SVGAxesActive" class="chart-axes" :height="height">
<g id="AveragesChart_XAxis" />
<g id="AveragesChart_YAxis" />
</svg>
</div>
</template>
<script>
import * as d3 from 'd3'
export default {
data() {
return {
width: 1000,
height: 100,
margin: { top: 15, bottom: 20, left: 30, right: 20 },
scaleExtent: [1, Infinity],
lineWidth: 1
}
},
computed: {
// --- D3 chart element accessors ---
$chart() {
return d3.select('#AveragesChart')
},
$svgAxesActive() {
return d3.select('#AveragesChart_SVGAxesActive')
},
$canvas() {
return d3.select('#AveragesChart_Canvas')
},
$xAxis() {
return d3.select('#AveragesChart_XAxis')
},
$yAxis() {
return d3.select('#AveragesChart_YAxis')
},
$gradient() {
return d3.select('#AveragesChart_Gradient')
},
// ---
// --- SCSS variables ---
cssVars() {
return {
'--chart-margin-left': this.margin.left + 'px',
'--chart-margin-top': this.margin.top + 'px'
}
},
// ---
// --- Canvas context ---
context() {
return this.$canvas.node().getContext('2d')
},
// ---
// --- Precomputed helpers ---
rangeX() {
return [this.margin.left, this.width - this.margin.right]
},
spanX() {
return this.rangeX[1] - this.rangeX[0]
},
rangeY() {
return [this.height - this.margin.bottom, this.margin.top]
},
spanY() {
return this.rangeY[0] - this.rangeY[1]
},
maxSnowHeightRounded() {
return Math.round(this.maxSnowHeight / 10) * 10
},
// --- Scales ---
xScale() {
return d3
.scaleUtc()
.domain(this.timespan)
.range(this.rangeX)
},
xScaleZoomed() {
const transform = this.normalizedSelection2Transform(
this.zoomState.selection
)
return transform.rescaleX(this.xScale)
},
yScale() {
return d3
.scaleLinear()
.domain([0, this.maxSnowHeightRounded])
.range(this.rangeY)
},
// ---
// --- Vuex store data ---
chartData() {
let result = {}
for (const camID of this.$store.getters.cameraIDs) {
result[camID] = this.$store.getters.chartData(this.cameraID)
}
return result
},
sunImage() {
return this.$store.getters.sunImage
},
timespan() {
return [
this.$store.getters.timespanStart,
this.$store.getters.timespanEnd
]
},
maxSnowHeight() {
return this.$store.getters.maxSnowHeight
},
zoomState() {
// Returns:
// zoomState - {selection: [0, 1], zoomingID: cameraID_stickID}
// selection: Start and end points of the viewport.
return this.$store.getters.zoomState
},
// --- Zoom ---
zoom() {
return d3
.zoom()
.scaleExtent(this.scaleExtent)
.extent([
[this.margin.left, 0],
[this.width - this.margin.right, this.height]
])
.translateExtent([
[this.margin.left, 0],
[this.width - this.margin.right, 0]
])
.on('zoom', this.zoomed)
},
// ---
},
watch: {
zoomState(state) {
// check if sourceEvent exists and if this is the same component
// - if true, don't update the zoom (this is to prevent the infinite loop)
if (state.zoomingID === this.chartID) {
return
}
// update zoom of the chart
if (this.isVisible) {
this.updateZoom(state.selection)
}
const transform = this.normalizedSelection2Transform(state.selection)
// then update the zoom transform internally with the new transform settings of the other components
this.$chart.node().__zoom = transform
},
width() {
this.$chart.call(this.zoom)
this.$nextTick(() => {
if (this.isVisible) {
this.updateZoom(this.zoomState.selection)
}
})
},
chartData() {
this.redrawChart(this.xScaleZoomed)
}
},
methods: {
handleResize({ width }) {
this.width = width
},
redrawChart(x) {
// Clear canvas and draw chart and gradient again.
// x: Scale of the x coordinate.
// Clear whole canvas.
this.context.clearRect(0, 0, this.width, this.height)
this.drawSnowHightAverages(this.context, x, this.yScale)
// Clear left and right sides of charts.
this.context.clearRect(0, 0, this.margin.left, this.height)
this.context.clearRect(
this.width - this.margin.right,
0,
this.margin.right,
this.height
)
},
zoomed(event) {
// Generate normalized selection from transform object.
const normSel = this.transform2NormalizedSelection(event.transform)
// Update zoom of this component.
if (this.isVisible) {
this.updateZoom(normSel)
}
// Create state object describing zoom and save it to the Vuex store.
const state = { selection: normSel, zoomingID: this.chartID }
this.$store.dispatch('setZoomState', state)
},
updateZoom(normalizedSelection) {
// Update zoom on this chart, using the transform.
// Apply transformation to the x axis and the chart points.
const transform = this.normalizedSelection2Transform(normalizedSelection)
const xz = transform.rescaleX(this.xScale)
// Redraw x axis.
this.$xAxis.call(this.xAxis, xz)
// Redraw gradient background.
this.$gradient.attr('transform', transform)
// Redraw canvas with points.
this.redrawChart(xz)
},
transform2NormalizedSelection(transform) {
const x = transform.x
const k = transform.k
if (k === 1) {
return [0, 1]
}
const s = (x + (k - 1) * this.rangeX[0]) / (-k * this.spanX)
const e = s + 1 / k
return [s, e]
},
normalizedSelection2Transform(normalizedSelection) {
const start = normalizedSelection[0]
const end = normalizedSelection[1]
const k = 1 / (end - start)
const x = -(start * k * this.spanX + (k - 1) * this.rangeX[0])
return d3.zoomIdentity.translate(x, 0).scale(k)
},
mousemoved(event) {
const mouseX = event.offsetX
const mouseY = event.offsetY
},
clicked(event) {
const mouseX = event.offsetX
const mouseY = event.offsetY
},
xAxis(g, x) {
g.attr(
'transform',
`translate(0,${this.height - this.margin.bottom})`
).call(
d3
.axisBottom(x)
.ticks(10)
.tickSizeOuter(0)
)
},
yAxis(g, y) {
g.attr('transform', `translate(${this.margin.left},0)`)
.call(
d3
.axisLeft(y)
.ticks(5)
.tickSizeOuter(0)
)
.call(g =>
g
.select('.tick:last-of-type text')
.clone()
.attr('x', 0)
.attr('y', -7)
.attr('text-anchor', 'start')
.attr('font-weight', 'bold')
.text('cm')
)
},
drawSnowHightAverages(context, x, y) {
// context: Canvas context, used for drawing into canvas.
// x: D3 scale for x axis.
// y: D3 scale for y axis.
for (const cameraID of this.$store.getters.cameraIDs) {
for (const stickID of this.$store.getters.stickIDs) {
this.drawSnowHightAverage(context, x, y, cameraID, stickID)
}
}
},
drawSnowHightAverage(context, x, y, cameraID, stickID) {
// context: Canvas context, used for drawing into canvas.
// x: D3 scale for x axis.
// y: D3 scale for y axis.
let lastTimestamp = null
let lastSnowHeightAverage = null
const images = this.$store.getters.chartData(cameraID)
if (typeof images === 'undefined') {
return
}
for (const [imageName, image] of Object.entries(images)) {
const timestamp = image.dateTime
const stick = image.sticks[stickID]
if (typeof stick === 'undefined') {
return // the stick 'stickID' is outside view of the cammera 'cameraID'
}
const snowHeightAverage = image.sticks[stickID]
.snowHeightAverage
if (lastTimestamp != null) {
this.drawLine(
context,
lastTimestamp,
lastSnowHeightAverage,
timestamp,
snowHeightAverage,
x,
y,
'black'
)
}
lastTimestamp = timestamp
lastSnowHeightAverage = snowHeightAverage
}
},
drawLine(
context,
lastTimestamp,
lastSnowHeightAverage,
timestamp,
snowHeightAverage,
x,
y,
color
) {
// context: Canvas context, used for drawing into canvas.
// lastTimestamp, timestamp: (x axis) string, containing standard time format.
// lastSnowHeightAverage, snowHeightAverage: (y axis) number.
// x: D3 scale for x axis.
// y: D3 scale for y axis.
// color: Color of the point and the line.
const lastPx = Math.floor(x(new Date(lastTimestamp)))
const px = Math.floor(x(new Date(timestamp)))
// If both points are out of bounds, don't draw the line.
if (
(px >= 0 && px <= this.width) ||
(lastPx >= 0 && lastPx <= this.width)
) {
const lastPy = Math.floor(y(lastSnowHeightAverage))
const py = Math.floor(y(snowHeightAverage))
context.beginPath()
context.strokeStyle = color || '#555555'
context.lineWidth = 1
context.moveTo(lastPx, lastPy)
context.lineTo(px, py)
context.stroke()
}
},
onZZZ() {
this.redrawChart(this.xScaleZoomed)
}
},
mounted() {
this.$chart
.call(this.zoom)
.on('mousemove', this.mousemoved)
.on('click', this.clicked)
.on('wheel', event => {
// Prevent scrolling the whole page when using mouse wheel on chart.
if (
event.deltaY > 0 &&
this.zoomState.selection[0] === 0 &&
this.zoomState.selection[1] === 1
) {
event.preventDefault()
}
})
this.$xAxis.call(this.xAxis, this.xScale)
this.$yAxis.call(this.yAxis, this.yScale)
this.width = this.$chart.node().clientWidth
}
}
</script>
<style lang="scss" scoped>
.chart {
position: relative;
background: var(--color-background);
border-radius: 5px;
}
.chart-axes {
position: absolute;
left: 0px;
top: 0px;
z-index: 2;
width: 100%;
}
.chart-gradient {
position: absolute;
// left: var(--chart-margin-left);
left: 0;
top: var(--chart-margin-top);
z-index: 0;
}
.chart-gradient rect {
fill: var(--color-background);
}
.chart-lines {
position: absolute;
left: 0px;
top: 0px;
z-index: 1;
}
</style>
......@@ -15,6 +15,7 @@
<p></p>
</div> -->
<Sticks />
<AveragesChart />
</div>
</template>
......@@ -22,6 +23,7 @@
import Sticks from '@/components/Sticks.vue'
import ImageDialog from '@/components/image-dialog/ImageDialog.vue'
import Header from '@/components/header/Header.vue'
import AveragesChart from '@/components/AveragesChart.vue'
export default {
// Component DatasetView
......@@ -30,7 +32,8 @@ export default {
components: {
ImageDialog,
Header,
Sticks
Sticks,
AveragesChart
},
computed: {
sticks() {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment