Skip to content

Commit

Permalink
Merge branch 'time-slider' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
devincowan committed Jun 27, 2024
2 parents 32a417c + 0805bdd commit 5405d85
Show file tree
Hide file tree
Showing 7 changed files with 415 additions and 42 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
39 changes: 37 additions & 2 deletions 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 @update="resetData" />
</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 All @@ -25,6 +30,10 @@
<v-icon :icon="mdiMagnifyMinusOutline"></v-icon>
Reset Zoom
</v-btn>
<v-btn @click="resetData()" color="input" class="ma-1">
<v-icon :icon="mdiEraser"></v-icon>
Refresh Data
</v-btn>
</v-sheet>
</v-col>
</v-row>
Expand All @@ -46,17 +55,25 @@ import { Line } from 'vue-chartjs'
import 'chartjs-adapter-date-fns';
import { ref } from 'vue'
import { customCanvasBackgroundColor } from '@/_helpers/charts/plugins'
import { mdiDownloadBox, mdiFileDelimited, mdiCodeJson, mdiMagnifyMinusOutline } from '@mdi/js'
import { mdiDownloadBox, mdiFileDelimited, mdiCodeJson, mdiMagnifyMinusOutline, mdiEraser } from '@mdi/js'
import { downloadMultiNodesCsv, downloadMultiNodesJson } from '../_helpers/hydroCron';
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 +97,7 @@ const options = {
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
display: true,
position: 'bottom',
},
title: {
Expand Down Expand Up @@ -126,6 +143,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 +205,21 @@ 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()
}
const resetData = () => {
line.value.chart.data.datasets = chartData.value.datasets
line.value.chart.update()
}
</script>
96 changes: 96 additions & 0 deletions frontend/src/components/TimeRangeSlider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<template>
<v-form>
<v-container>
<v-range-slider v-model="sliderRange" :min="featuresStore.minTime" :max="featuresStore.maxTime"
class="align-center" hide-details @update:modelValue="updateDateRange">
<template v-slot:prepend>
<v-text-field v-model="dateRange[0]" density="compact" type="date" variant="outlined" hide-details single-line
@update:modelValue="updateSliderRange" :rules="[rules.min,]"></v-text-field>
</template>
<template v-slot:append>
<v-text-field v-model="dateRange[1]" density="compact" type="date" variant="outlined" hide-details single-line
@update:modelValue="updateSliderRange" :rules="[rules.max,]"></v-text-field>
</template>
</v-range-slider>
</v-container>
</v-form>
</template>

<script setup>
import { ref } from 'vue'
import { useFeaturesStore } from '../stores/features';
import { useChartsStore } from '@/stores/charts'
import { addMinutes, subMinutes } from "date-fns";
import { NODE_DATETIME_VARIATION } from '@/constants'
// define an update event that emits the new range
const emit = defineEmits(['update'])
const featuresStore = useFeaturesStore()
const chartStore = useChartsStore()
const convertSecondsToDateString = (seconds) => {
return new Date(seconds * 1000).toISOString().split('T')[0]
}
const convertDateStringToSeconds = (dateString) => {
return new Date(dateString).getTime() / 1000
}
// There are two inputs. User can select a range of dates (string) using the date picker, or a range of decimal seconds using the slider.
const sliderRange = ref(featuresStore.timeRange)
const dateRange = ref(featuresStore.timeRange.map((t) => convertSecondsToDateString(t)))
// When the date range changes, update the slider range.
const updateSliderRange = () => {
sliderRange.value = dateRange.value.map((dateString) => {
return convertDateStringToSeconds(dateString)
})
filterDatasetsToTimeRange()
}
// When the slider range changes, update the date range.
const updateDateRange = () => {
dateRange.value = sliderRange.value.map((seconds) => {
return convertSecondsToDateString(seconds)
})
filterDatasetsToTimeRange()
}
async function filterDatasetsToTimeRange() {
const startTime = subMinutes(dateRange.value[0], NODE_DATETIME_VARIATION)
const endTime = addMinutes(dateRange.value[1], NODE_DATETIME_VARIATION)
chartStore.filterDatasetsToTimeRange(chartStore.nodeChartData.datasets, startTime, endTime)
emit('update', sliderRange.value)
}
const rules = {
min: (v) => {
if (v === null || v === undefined || v === '') {
return 'Start date is required'
}
if (v > dateRange.value[1]) {
return 'Start date must be before end date'
}
const seconds = convertDateStringToSeconds(v)
if (seconds < featuresStore.minTime) {
return 'Start date must be after the first date in the dataset'
}
return true
},
max: (v) => {
if (v === null || v === undefined || v === '') {
return 'End date is required'
}
if (v < dateRange.value[0]) {
return 'End date must be after start date'
}
const seconds = convertDateStringToSeconds(v)
if (seconds > featuresStore.maxTime) {
return 'End date must be before the last date in the dataset'
}
return true
},
}
</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 5405d85

Please sign in to comment.