Skip to content

Commit

Permalink
Merge branch 'time-slider' into swot-front
Browse files Browse the repository at this point in the history
  • Loading branch information
devincowan committed Jun 6, 2024
2 parents b2625cf + 7a7c43b commit 762d3e1
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 41 deletions.
160 changes: 134 additions & 26 deletions frontend/src/components/LineChart.vue
Original file line number Diff line number Diff line change
@@ -1,35 +1,60 @@
<template>
<v-container class="overflow-auto">
<v-row>
<v-col xs="12" lg="10">
<v-col xs="12" lg="9">
<v-sheet :min-height="lgAndUp ? '65vh' : '50vh'" :max-height="lgAndUp ? '100%' : '20vh'" max-width="100%"
min-width="500px">
<Line :data="chartData" :options="options" ref="line" :plugins="[zoomPlugin]" />
</v-sheet>
</v-col>
<v-col xs="12" lg="2">
<v-sheet>
<v-select label="Data Quality" v-model="dataQuality" :items="dataQualityOptions" item-title="label"
item-value="value" @update:modelValue="filterAllDatasets()" multiple chips></v-select>
<v-select label="Plot Style" v-model="plotStyle" :items="['Scatter', 'Connected',]"
@update:modelValue="updateChartLine()"></v-select>
<v-btn :loading="downloading.chart" @click="downloadChart()" class="ma-1" color="input">
<v-icon :icon="mdiDownloadBox"></v-icon>
Download Chart
</v-btn>
<v-btn :loading="downloading.csv" @click="downCsv()" class="ma-1" color="input">
<v-icon :icon="mdiFileDelimited"></v-icon>
Download CSV
</v-btn>
<v-btn :loading="downloading.json" @click="downJson()" class="ma-1" color="input">
<v-icon :icon="mdiCodeJson"></v-icon>
Download JSON
</v-btn>
<v-btn @click="resetZoom()" color="input" class="ma-1">
<v-icon :icon="mdiMagnifyMinusOutline"></v-icon>
Reset Zoom
</v-btn>
</v-sheet>
<v-col xs="12" lg="3">
<v-expansion-panels with="100%" v-model="panel" multiple>
<v-expansion-panel value="plotOptions">
<v-expansion-panel-title>Plot Options</v-expansion-panel-title>
<v-expansion-panel-text>
<v-select label="Data Quality" v-model="dataQuality" :items="dataQualityOptions" item-title="label"
item-value="value" @update:modelValue="filterAllDatasets()" multiple chips></v-select>
<v-select label="Plot Style" v-model="plotStyle" :items="['Scatter', 'Connected',]"
@update:modelValue="updateChartLine()"></v-select>
<v-btn :loading="downloading.chart" @click="downloadChart()" class="ma-1" color="input">
<v-icon :icon="mdiDownloadBox"></v-icon>
Download Chart
</v-btn>
<v-btn :loading="downloading.csv" @click="downCsv()" class="ma-1" color="input">
<v-icon :icon="mdiFileDelimited"></v-icon>
Download CSV
</v-btn>
<v-btn :loading="downloading.json" @click="downJson()" class="ma-1" color="input">
<v-icon :icon="mdiCodeJson"></v-icon>
Download JSON
</v-btn>
<v-btn @click="resetZoom()" color="input" class="ma-1">
<v-icon :icon="mdiMagnifyMinusOutline"></v-icon>
Reset Zoom
</v-btn>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel :disabled="selectedTimes.length == 0" value="selectedTimes">
<v-expansion-panel-title>Selected Timestamps</v-expansion-panel-title>
<v-expansion-panel-text>
<v-list>
<v-list-item v-for="node in selectedTimes" :key="node.datetime">
<template v-slot:append>
<v-icon :icon="mdiCloseBox" color="error" @click="removeSelectedNode(node)"></v-icon>
</template>
<v-list-item-content>
<v-list-item-title>{{ node.datetime }}</v-list-item-title>
<v-list-item-subtitle>{{ node.time_str }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</v-expansion-panel-text>
<v-btn v-if="selectedTimes.length > 0" class="ma-1 float-right" color="input" @click="viewLongProfileByDates">
<v-icon :icon="mdiChartBellCurveCumulative"></v-icon>
View Long Profile
</v-btn>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
</v-container>
Expand All @@ -49,18 +74,24 @@ import {
import { Line } from 'vue-chartjs'
import 'chartjs-adapter-date-fns';
import { enUS } from 'date-fns/locale';
import { addMinutes, subMinutes } from "date-fns";
import { useChartsStore } from '@/stores/charts'
import { useAlertStore } from '@/stores/alerts'
import { ref } from 'vue'
import { customCanvasBackgroundColor } from '@/_helpers/charts/plugins'
import { mdiDownloadBox, mdiFileDelimited, mdiCodeJson, mdiMagnifyMinusOutline } from '@mdi/js'
import { mdiDownloadBox, mdiFileDelimited, mdiCodeJson, mdiMagnifyMinusOutline, mdiChartBellCurveCumulative, mdiCloseBox } from '@mdi/js'
import { downloadCsv, downloadFeatureJson } from '../_helpers/hydroCron';
import { useDisplay } from 'vuetify'
import zoomPlugin from 'chartjs-plugin-zoom';
import { capitalizeFirstLetter } from '@/_helpers/charts/plugins'
import { NODE_DATETIME_VARIATION } from '@/constants'
const { lgAndUp } = useDisplay()
const panel = ref(["plotOptions"])
const selectedTimes = ref([])
const chartStore = useChartsStore()
const alertStore = useAlertStore()
const props = defineProps({ data: Object, chosenVariable: Object })
const line = ref(null)
const plotStyle = ref('Scatter')
Expand Down Expand Up @@ -163,11 +194,88 @@ const options = {
text: yLabel
}
}
}
},
onClick: (e) => handleTimeseriesPointClick(e),
// events: ["click", "contextmenu"],
}
const dataQualityOptions = [{ label: 'good', value: 0 }, { label: 'suspect', value: 1 }, { label: 'degraded', value: 2 }, { label: 'bad', value: 3 }]
const handleTimeseriesPointClick = (e) => {
// TODO: right click context menu
// e.native.preventDefault()
// if (e.native.button !== 2) {
// console.log("not right click")
// return
// }
const elems = line.value.chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, false)
if (elems.length <= 0) {
return
}
const datasetIndex = elems[0].datasetIndex
const index = elems[0].index
const dataset = line.value.chart.data.datasets[datasetIndex]
const data = dataset.data[index]
// console.log("clicked data:", data)
// console.log("clicked dataset:", dataset)
// // TODO: y axis variable is not being set correctly
// console.log("y axis variable:", dataset.parsing.yAxisKey)
// console.log("datetime:", data.datetime)
// const datetime = data.datetime
// const allDatasets = line.value.chart.data.datasets
// const dataForDatetime = allDatasets.map((dataset) => {
// const data = dataset.data.find((data) => data.datetime === datetime)
// return data
// })
// console.log("average data for datetime:", dataForDatetime)
addSelectedNode(data)
}
const viewLongProfileByDates = () => {
// TODO use the datetime and plot all of the nodes' data for that datetime
// chartStore.filterDatasetsToTimeRange(allDatasets, subMinutes(datetime, NODE_DATETIME_VARIATION), addMinutes(datetime, NODE_DATETIME_VARIATION))
alertStore.displayAlert({
title: 'Long Profile Filter Not Implemented',
text: `The long profile filter by dates has not been implemented yet.`,
type: 'warning',
closable: true,
duration: 3
})
}
const addSelectedNode = (node) => {
// first make sure the node is not already selected
if (selectedTimes.value.includes(node)) {
alertStore.displayAlert({
title: 'Point already selected',
text: `The point at ${node.datetime} has already been selected.`,
type: 'warning',
closable: true,
duration: 3
})
return
}
selectedTimes.value.push(node)
panel.value = ["selectedTimes"]
// TODO use datalabels to show selection?
// https://chartjs-plugin-datalabels.netlify.app/samples/events/selection.html
}
const removeSelectedNode = (node) => {
const index = selectedTimes.value.indexOf(node)
if (index > -1) {
selectedTimes.value.splice(index, 1)
}
if (selectedTimes.value.length === 0) {
panel.value = ["plotOptions"]
}
}
const resetZoom = () => {
line.value.chart.resetZoom()
}
Expand Down
28 changes: 27 additions & 1 deletion frontend/src/components/NodeChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
min-width="500px">
<Line :data="chartData" :options="options" ref="line" :plugins="[customCanvasBackgroundColor, zoomPlugin]" />
</v-sheet>
<v-sheet class="pa-2" color="input">
<TimeRangeSlider />
</v-sheet>
</v-col>
<v-col lg="2">
<v-sheet>
<v-select label="Plot Style" v-model="plotStyle" :items="['Scatter', 'Connected',]"
@update:modelValue="updateChartLine()"></v-select>
<v-btn :loading="downloading.chart" @click="downloadChart()" class="ma-1" color="input">
<v-icon :icon="mdiDownloadBox"></v-icon>
Download Chart
Expand Down Expand Up @@ -51,12 +56,20 @@ import { downloadMultiNodesCsv, downloadMultiNodesJson } from '../_helpers/hydro
import { useDisplay } from 'vuetify'
import zoomPlugin from 'chartjs-plugin-zoom';
import { capitalizeFirstLetter } from '@/_helpers/charts/plugins'
import { useChartsStore } from '../stores/charts';
import TimeRangeSlider from '@/components/TimeRangeSlider.vue'
const { lgAndUp } = useDisplay()
const props = defineProps({ data: Object, chosenVariable: Object })
const line = ref(null)
const downloading = ref({ csv: false, json: false, chart: false })
const chartStore = useChartsStore()
const plotStyle = ref('Connected')
// const timeStamps = ref(chartStore.getNodeTimeStamps())
const timeStamps = ref(['2021-01-01T00:00:00Z', '2021-01-01T00:00:00Z'])
ChartJS.register(LinearScale, TimeScale, PointElement, LineElement, Title, Tooltip, Legend, customCanvasBackgroundColor, zoomPlugin)
// TODO: might need a more efficient way of doing this instead of re-mapping the data
Expand All @@ -80,7 +93,7 @@ const options = {
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
display: true,
position: 'bottom',
},
title: {
Expand Down Expand Up @@ -126,6 +139,7 @@ const options = {
label += context.parsed.y
}
label += ` ${selectedVariable.unit}`
// add the timestamp as well
return label;
},
title: function (context) {
Expand Down Expand Up @@ -187,4 +201,16 @@ const downJson = async () => {
await downloadMultiNodesJson()
downloading.value.json = false
}
const updateChartLine = () => {
let showLine = false
if (plotStyle.value === 'Connected') {
showLine = true
}
line.value.chart.data.datasets.forEach((dataset) => {
dataset.showLine = showLine
setParsing(line.value.chart.data.datasets)
})
line.value.chart.update()
}
</script>
37 changes: 37 additions & 0 deletions frontend/src/components/TimeRangeSlider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<template>
<v-range-slider v-model="range" :min="featuresStore.minTime" :max="featuresStore.maxTime" class="align-center" hide-details>
<template v-slot:prepend>
<v-text-field
v-model="range[0]"
density="compact"
type="date"
variant="outlined"
hide-details
single-line
></v-text-field>
</template>
<template v-slot:append>
<v-text-field
v-model="range[1]"
density="compact"
type="date"
variant="outlined"
hide-details
single-line
></v-text-field>
</template>
</v-range-slider>
{{ range }}
</template>

<script setup>
import { ref } from 'vue'
import { useFeaturesStore } from '../stores/features';
const featuresStore = useFeaturesStore()
const range = ref(featuresStore.timeRange)
// TODO: convert so that the range is an array of floats (decimal years?) instead of strings
// https://vuetifyjs.com/en/components/range-sliders/#slots
</script>
2 changes: 2 additions & 0 deletions frontend/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export const ENDPOINTS = {
authenticatedRoute: `${APP_API_URL}/authenticated-route`,
userInfo: `${APP_API_URL}/users/me`,
};

export const NODE_DATETIME_VARIATION = 1; // minutes
Loading

0 comments on commit 762d3e1

Please sign in to comment.