Mercurial > gemma
view client/src/components/TimeSlider.vue @ 5069:7ef8d3dab29b time-sliding
timeslider: resizable
author | Thomas Junk <thomas.junk@intevation.de> |
---|---|
date | Wed, 11 Mar 2020 16:26:31 +0100 |
parents | 99ac62a56dd2 |
children | a0b0e0f916f5 |
line wrap: on
line source
<template> <div id="slider" :class="[ 'd-flex box ui-element rounded bg-white flex-row', { expanded: showTimeSlider } ]" :style="reposition" > <div id="timeselection" class="d-flex mt-1 mr-1"> <input class="form-control-sm mr-1" type="date" v-model="dateSelection" min="2015-01-01" :max="new Date().toISOString().split('T')[0]" /> <input type="time" min="00:00" max="23:59" v-model="timeSelection" class="form-control-sm" /> </div> <div id="sliderContainer" class="d-flex sliderContainer" style="width: 98%;" ></div> <div id="closebutton" @click="close" class="d-flex box-control mr-0" style="width: 2%;" > <font-awesome-icon icon="times"></font-awesome-icon> </div> </div> </template> <style lang="scss" scoped> #slider { position: absolute; bottom: 0; min-width: 100vw; } #slider.expanded { max-height: 100%; max-width: 100%; margin: 0; } </style> <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. * * SPDX-License-Identifier: AGPL-3.0-or-later * License-Filename: LICENSES/AGPL-3.0.txt * * Copyright (C) 2020 by via donau * – Österreichische Wasserstraßen-Gesellschaft mbH * Software engineering by Intevation GmbH * * Author(s): * Fadi Abbud <fadiabbud@intevation.de> */ import { mapState } from "vuex"; import * as d3 from "d3"; import app from "@/main"; import { localeDateString } from "@/lib/datelocalization"; import { format, setHours, setMinutes } from "date-fns"; import debounce from "debounce"; let zoom = null; let xScale = null; let xAxis = null; export default { name: "timeslider", data() { return { isSelectedTimeHourly: false, resizeListenerFunction: null }; }, watch: { ongoingRefresh() { if (this.ongoingRefresh) return; this.$store.commit("application/setSelectedTime", new Date()); this.$nextTick(this.rescaleSlider(1)); } }, computed: { ...mapState("application", ["showTimeSlider", "paneSetup"]), ...mapState("map", ["ongoingRefresh"]), reposition() { // reposition time slider in case of opened diagram if (["DEFAULT", "COMPARESURVEYS"].indexOf(this.paneSetup) === -1) { const height = document.getElementById("main").clientHeight + 1; return `bottom: ${height}px`; } else { return ""; } }, dateSelection: { get() { const date = this.$store.state.application.selectedTime; return format(date, "YYYY-MM-DD"); }, set(value) { if (!value) return; let date = new Date(value); const [hours, minutes] = this.timeSelection.split(":"); date = setHours(date, hours); date = setMinutes(date, minutes); this.$store.commit("application/setSelectedTime", date); this.rescaleSlider(50); } }, timeSelection: { get() { const time = this.$store.state.application.selectedTime; return format(time, "HH:mm"); }, set(value) { if (!value) return; let date = this.selectedTime; date = setHours(date, value.split(":")[0]); date = setMinutes(date, value.split(":")[1]); this.$store.commit("application/setSelectedTime", date); this.rescaleSlider(800); } }, selectedTime: { get() { return this.$store.state.application.selectedTime; }, set(value) { if (!this.isSelectedTimeHourly) { value = setHours(value, 12); value = setMinutes(value, 0); } this.$store.commit("application/setSelectedTime", value); } } }, methods: { close() { this.$store.commit("application/showTimeSlider", false); }, rescaleSlider(scaleFactor) { const tx = -scaleFactor * this.getScale()(d3.isoParse(this.selectedTime.toISOString())) + document.getElementById("sliderContainer").clientWidth / 2; var t = d3.zoomIdentity.translate(tx, 0).scale(scaleFactor); this.getScale().domain(t.rescaleX(this.getScale())); d3.select(".zoom").call(zoom.transform, t); }, createSlider() { const element = document.getElementById("sliderContainer"); const svgWidth = element ? element.clientWidth : 0, svgHeight = 40, marginTop = 20, marginLeft = 0; d3.timeFormatDefaultLocale(localeDateString); xScale = this.getScale(); xAxis = this.getAxes(); let svg = d3 .select(".sliderContainer") .append("svg") .attr("width", svgWidth) .attr("height", svgHeight); zoom = d3 .zoom() .scaleExtent([0.8, 102000]) .translateExtent([[0, 0], [svgWidth, svgHeight]]) .extent([[0, 0], [(svgWidth, svgHeight)]]) .on("zoom", this.zoomed); svg .append("g") .attr("class", "axis--x") .attr("transform", `translate(${marginLeft}, ${marginTop})`) .call(xAxis); // create rectanlge on the slider area to capture mouse events const eventRect = svg .append("rect") .attr("id", "zoom") .attr("class", "zoom") .attr("width", svgWidth) .attr("height", svgHeight) .attr("fill", "white") .attr("opacity", 0.2) .on("mouseover", () => { svg.select(".zoom").attr("cursor", "move"); }); eventRect.call(zoom).on("click", this.onClick); const toIsoDate = d => { return d.toISOString(); }; let drag = d3 .drag() .on("start", () => { d3.select(".line") .raise() .classed("active", true); }) .on("drag", this.onDrag) .on("end", () => { d3.select(".line").classed("active", false); }); // Create cursor to indicate to the selected time svg .append("rect") .attr("class", "line") .attr("id", "scrubber") .attr("x", xAxis.scale()(d3.isoParse(toIsoDate(this.selectedTime)))) .attr("y", 0) .attr("width", 2) .attr("height", svgHeight) .attr("stroke", "#17a2b8") .attr("stroke-width", 2) .attr("opacity", 0.6) .on("mouseover", () => { svg.select(".line").attr("cursor", "e-resize"); }) .call(drag); }, getScale() { return d3 .scaleTime() .range([0, document.getElementById("sliderContainer").clientWidth || 0]) .domain([d3.isoParse(new Date("2015-01-01")), d3.isoParse(new Date())]); }, getAxes() { const axesFormat = date => { return (d3.timeSecond(date) < date ? d3.timeFormat(".%L") : d3.timeMinute(date) < date ? d3.timeFormat(":%S") : d3.timeHour(date) < date ? d3.timeFormat("%H:%M") : d3.timeDay(date) < date ? d3.timeFormat("%H:%M") : d3.timeMonth(date) < date ? d3.timeWeek(date) < date ? d3.timeFormat(app.$gettext("%a %d")) : d3.timeFormat(app.$gettext("%b %d")) : d3.timeYear(date) < date ? d3.timeFormat("%B") : d3.timeFormat("%Y"))(date); }; return d3 .axisBottom(xScale) .ticks(12) .tickFormat(axesFormat); }, zoomed() { let newX = d3.event.transform.rescaleX(xScale); const currentScaleFactor = d3.event.transform.k; const isHourly = currentScaleFactor > 400; if (this.isSelectedTimeHourly != isHourly) this.isSelectedTimeHourly = isHourly; xAxis.scale(newX); d3.select(".axis--x").call(xAxis); d3.select(".line").attr("x", newX(d3.isoParse(this.selectedTime))); }, onClick() { // Extract the click location let point = d3.mouse(document.getElementById("zoom")), p = { x: point[0], y: point[1] }; d3.select(".line").attr("x", p.x); this.selectedTime = d3.isoParse(xAxis.scale().invert(p.x + 2)); }, onDrag() { this.selectedTime = d3.isoParse(xAxis.scale().invert(d3.event.x + 2)); d3.select(".line").attr("x", d3.event.x); }, rescaleTimeSlider() { const bodyWidth = document.querySelector("body").clientWidth; const timeSelectionWidth = document.querySelector("#timeselection") .clientWidth; const closeButton = document.querySelector("#closebutton").clientWidth; const svgWidth = bodyWidth - timeSelectionWidth - closeButton; document .querySelector(".sliderContainer svg") .setAttribute("width", svgWidth); xScale.range([0, svgWidth]); xAxis.scale(xScale); d3.select(".axis--x").call(xAxis); d3.select(".line").attr( "x", xAxis.scale()(d3.isoParse(this.selectedTime)) ); } }, created() { this.resizeListenerFunction = debounce(this.rescaleTimeSlider, 100); window.addEventListener("resize", this.resizeListenerFunction); }, destroyed() { window.removeEventListener("resize", this.resizeListenerFunction); }, mounted() { setTimeout(this.createSlider, 150); } }; </script>