From 4f105a0ce6b93796b1a329f345a5aa493c01ca7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Wei=C3=9F?= Date: Wed, 16 Oct 2019 09:36:09 +0200 Subject: [PATCH 01/11] Added REST endpoint to get events by date range and job group --- .../de/iteratec/osm/UrlMappings.groovy | 4 +++ .../osm/report/chart/EventController.groovy | 20 +++++++++++--- .../osm/report/chart/EventService.groovy | 23 +++++++++++++++- .../osm/report/chart/events/EventDTO.groovy | 7 +++++ .../chart/events/GetEventsCommand.groovy | 27 +++++++++++++++++++ 5 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 src/main/groovy/de/iteratec/osm/report/chart/events/EventDTO.groovy create mode 100644 src/main/groovy/de/iteratec/osm/report/chart/events/GetEventsCommand.groovy diff --git a/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy b/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy index b88ab13b98..cba1fcd9e2 100755 --- a/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy +++ b/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy @@ -135,6 +135,10 @@ class UrlMappings { // GeneralMeasurementApiController ////////////////////////////////////////// + "/rest/events" { + controller = "Event" + action = [GET: "getEvents"] + } "/rest/event/create" { controller = "GeneralMeasurementApi" action = [POST: "securedViaApiKeyCreateEvent"] diff --git a/grails-app/controllers/de/iteratec/osm/report/chart/EventController.groovy b/grails-app/controllers/de/iteratec/osm/report/chart/EventController.groovy index 85d448bf86..dce2a4acaa 100644 --- a/grails-app/controllers/de/iteratec/osm/report/chart/EventController.groovy +++ b/grails-app/controllers/de/iteratec/osm/report/chart/EventController.groovy @@ -17,14 +17,14 @@ package de.iteratec.osm.report.chart + +import de.iteratec.osm.report.chart.events.EventDTO + +import de.iteratec.osm.report.chart.events.GetEventsCommand import de.iteratec.osm.util.ControllerUtils -import grails.converters.JSON -import org.joda.time.DateTime import org.springframework.http.HttpStatus import org.springframework.web.servlet.support.RequestContextUtils -import javax.servlet.http.HttpServletResponse - /** * EventController * A controller class handles incoming web requests and performs actions such as redirects, rendering views and so on. @@ -124,6 +124,18 @@ class EventController { } } + def getEvents(GetEventsCommand cmd) { + if (cmd.hasErrors()) { + ControllerUtils.sendSimpleResponseAsStream(response, HttpStatus.BAD_REQUEST, + "Invalid parameters: " + cmd.getErrors().fieldErrors.collect { it.field }.join(", ")) + + return + } + + List eventList = eventService.getEventsByDateRangeAndJobGroups(cmd) + ControllerUtils.sendObjectAsJSON(response, eventList) + } + /** * Combines time and date within the param list, where time ist 'time' and date is 'eventDate' * @param params diff --git a/grails-app/services/de/iteratec/osm/report/chart/EventService.groovy b/grails-app/services/de/iteratec/osm/report/chart/EventService.groovy index 9467f11363..f35c624ad8 100644 --- a/grails-app/services/de/iteratec/osm/report/chart/EventService.groovy +++ b/grails-app/services/de/iteratec/osm/report/chart/EventService.groovy @@ -17,8 +17,10 @@ package de.iteratec.osm.report.chart +import de.iteratec.osm.report.chart.events.EventDTO + +import de.iteratec.osm.report.chart.events.GetEventsCommand import grails.gorm.transactions.Transactional -import org.hibernate.criterion.CriteriaSpecification import org.hibernate.sql.JoinType import org.springframework.dao.DataIntegrityViolationException @@ -95,6 +97,25 @@ class EventService { delegateMap.action.success.call(eventInstance) } + List getEventsByDateRangeAndJobGroups(GetEventsCommand cmd) { + Date from = cmd.from.toDate() + Date to = cmd.to.toDate() + List jobGroupIds = cmd.jobGroups.collect { it.toLong() } + def allEvents = retrieveEventsByDateRangeAndVisibilityAndJobGroup(from, to, jobGroupIds) + + List eventList = new ArrayList<>() + allEvents.forEach { event -> + EventDTO eventDTO = new EventDTO() + eventDTO.eventDate = event.eventDate + eventDTO.shortName = event.shortName + eventDTO.description = event.description + + eventList.add(eventDTO) + } + + return eventList + } + List findAllEventsBetweenDate(Date resetFromDate, Date resetToDate){ Event.findAllByEventDateBetween(resetFromDate, resetToDate) } diff --git a/src/main/groovy/de/iteratec/osm/report/chart/events/EventDTO.groovy b/src/main/groovy/de/iteratec/osm/report/chart/events/EventDTO.groovy new file mode 100644 index 0000000000..d756f8cfac --- /dev/null +++ b/src/main/groovy/de/iteratec/osm/report/chart/events/EventDTO.groovy @@ -0,0 +1,7 @@ +package de.iteratec.osm.report.chart.events + +class EventDTO { + Date eventDate = null + String shortName = "" + String description = "" +} diff --git a/src/main/groovy/de/iteratec/osm/report/chart/events/GetEventsCommand.groovy b/src/main/groovy/de/iteratec/osm/report/chart/events/GetEventsCommand.groovy new file mode 100644 index 0000000000..f87af4b873 --- /dev/null +++ b/src/main/groovy/de/iteratec/osm/report/chart/events/GetEventsCommand.groovy @@ -0,0 +1,27 @@ +package de.iteratec.osm.report.chart.events + +import grails.databinding.BindUsing +import grails.validation.Validateable +import groovy.json.JsonSlurper +import org.joda.time.DateTime + +class GetEventsCommand implements Validateable { + DateTime from + DateTime to + + @BindUsing({ obj, source -> + return new JsonSlurper().parseText(source['jobGroups']) + }) + List jobGroups = [] + + static constraints = { + from(nullable: false) + to(nullable: false) + jobGroups(nullable: false, validator: { List jobGroups, GetEventsCommand cmd -> + if (jobGroups.isEmpty()) { + return false + } + }) + } + +} From e1c21ff0a6ba9268e28d5285b73baa40618bc810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Wei=C3=9F?= Date: Fri, 18 Oct 2019 15:02:50 +0200 Subject: [PATCH 02/11] Get events for selected data --- frontend/src/app/enums/url.enum.ts | 1 + .../services/linechart-data.service.ts | 15 +++++++++++++++ .../time-series/time-series.component.html | 2 +- .../modules/time-series/time-series.component.ts | 12 ++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/enums/url.enum.ts b/frontend/src/app/enums/url.enum.ts index 0912e6b0d1..70e3a482d4 100644 --- a/frontend/src/app/enums/url.enum.ts +++ b/frontend/src/app/enums/url.enum.ts @@ -10,5 +10,6 @@ export enum URL { RESULT_COUNT = '/resultSelection/getResultCount', AGGREGATION_BARCHART_DATA = "/aggregation/getBarchartData", EVENT_RESULT_DASHBOARD_LINECHART_DATA = "/eventResultDashboard/getLinechartData", + EVENTS = "/rest/events", DISTRIBUTION_VIOLINCHART_DATA = "/distributionChart/getViolinchartData" } diff --git a/frontend/src/app/modules/time-series/services/linechart-data.service.ts b/frontend/src/app/modules/time-series/services/linechart-data.service.ts index d8c7a8219f..d4cfee0075 100644 --- a/frontend/src/app/modules/time-series/services/linechart-data.service.ts +++ b/frontend/src/app/modules/time-series/services/linechart-data.service.ts @@ -22,6 +22,21 @@ export class LinechartDataService { ) } + fetchEvents(resultSeclectionCommand: ResultSelectionCommand, url: string): Observable { + const from: Date = resultSeclectionCommand.from; + const to: Date = resultSeclectionCommand.to; + const jobGroupIds: number[] = resultSeclectionCommand.jobGroupIds; + + let params = new HttpParams(); + params = params.append('from', from.toISOString()); + params = params.append('to', to.toISOString()); + params = params.append('jobGroups', JSON.stringify(jobGroupIds)); + + return this.http.get(url, {params: params}).pipe( + this.handleError() + ) + } + private buildCommand(resultSelectionCommand: ResultSelectionCommand, remainingResultSelection: RemainingResultSelection): GetEventResultDataCommand { return new GetEventResultDataCommand({ preconfiguredDashboard: null, diff --git a/frontend/src/app/modules/time-series/time-series.component.html b/frontend/src/app/modules/time-series/time-series.component.html index 2c3656c425..c88eb964e7 100644 --- a/frontend/src/app/modules/time-series/time-series.component.html +++ b/frontend/src/app/modules/time-series/time-series.component.html @@ -8,7 +8,7 @@
- +
diff --git a/frontend/src/app/modules/time-series/time-series.component.ts b/frontend/src/app/modules/time-series/time-series.component.ts index 97e7e836e7..8949b2098f 100644 --- a/frontend/src/app/modules/time-series/time-series.component.ts +++ b/frontend/src/app/modules/time-series/time-series.component.ts @@ -26,4 +26,16 @@ export class TimeSeriesComponent implements OnInit { URL.EVENT_RESULT_DASHBOARD_LINECHART_DATA ).subscribe(next => this.results$.next(next)); } + + getEvents() { + this.linechartDataService.fetchEvents( + this.resultSelectionStore.resultSelectionCommand, + URL.EVENTS + ).subscribe(next => console.log(next)); + } + + getData() { + this.getTimeSeriesChartData(); + this.getEvents(); + } } From 41ed927514691e0f17df8b80eaea172d60615dd2 Mon Sep 17 00:00:00 2001 From: Benny Li Date: Fri, 1 Nov 2019 10:55:52 +0100 Subject: [PATCH 03/11] Updated rxjs to 6.5 --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 041275e314..afa5cb4c25 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,7 @@ "d3": "^5.11.0", "ng-pick-datetime": "^7.0.0", "ngx-smart-modal": "^7.1.1", - "rxjs": "^6.4.0", + "rxjs": "^6.5.0", "spin.js": "^4.0.0", "zone.js": "^0.9.0" }, From a881a177a1b2897ff4cbd80c6254e8b96f0694ea Mon Sep 17 00:00:00 2001 From: Benny Li Date: Fri, 1 Nov 2019 13:45:09 +0100 Subject: [PATCH 04/11] Show event marker in the TimeSeriesChart * Markers are always shown * Dots are relatively big (need some styling) * Tooltips on mouse over marker circle --- frontend/package-lock.json | 16 ++-- .../time-series-line-chart.component.scss | 37 +++++++-- .../models/event-result-data.model.ts | 4 + .../modules/time-series/models/event.model.ts | 5 ++ .../services/line-chart.service.ts | 82 +++++++++++++++++++ .../time-series/time-series.component.ts | 21 +++-- 6 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 frontend/src/app/modules/time-series/models/event.model.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fd4037e0c2..38bd03cec7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -338,7 +338,7 @@ }, "load-json-file": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "dev": true, "requires": { @@ -391,7 +391,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -1587,7 +1587,7 @@ }, "util": { "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -3570,7 +3570,7 @@ }, "engine.io-client": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "resolved": "http://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", "dev": true, "requires": { @@ -8511,9 +8511,9 @@ "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" }, "rxjs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", - "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", + "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", "requires": { "tslib": "^1.9.0" } @@ -9005,7 +9005,7 @@ }, "socket.io-parser": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "resolved": "http://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", "dev": true, "requires": { diff --git a/frontend/src/app/modules/time-series/components/time-series-line-chart/time-series-line-chart.component.scss b/frontend/src/app/modules/time-series/components/time-series-line-chart/time-series-line-chart.component.scss index c5ad4863de..626da8e8b9 100644 --- a/frontend/src/app/modules/time-series/components/time-series-line-chart/time-series-line-chart.component.scss +++ b/frontend/src/app/modules/time-series/components/time-series-line-chart/time-series-line-chart.component.scss @@ -2,17 +2,42 @@ osm-time-series-line-chart { visibility: hidden; -} + + #event-marker-tooltip { + position: absolute; + text-align: left; + font: 10pt sans-serif; + color: white; + background: rgba(0, 0, 0, 0.7); + border: 1px solid rgba(0, 0, 0, 0.4); + border-radius: 3px; + pointer-events: none; + } + + #time-series-chart-drawing-area { + .axis { + .domain { + display: none; + } -#time-series-chart-drawing-area { - .axis { - .domain { - display: none; + line { + stroke: $color-contour-medium; + } + + text { + fill: $color-text-light; + } + + .description { + font-size: 14px; + fill: $color-text-light; + } } - line { + .event-marker-line { stroke: $color-contour-medium; } + text { fill: $color-text-light; } diff --git a/frontend/src/app/modules/time-series/models/event-result-data.model.ts b/frontend/src/app/modules/time-series/models/event-result-data.model.ts index 473173fe5f..f79cf59f47 100644 --- a/frontend/src/app/modules/time-series/models/event-result-data.model.ts +++ b/frontend/src/app/modules/time-series/models/event-result-data.model.ts @@ -1,13 +1,17 @@ import {EventResultSeriesDTO} from './event-result-series.model'; +import {EventDTO} from './event.model'; export interface EventResultDataDTO { series: EventResultSeriesDTO[]; + events: EventDTO[]; } export class EventResultData implements EventResultDataDTO { series: EventResultSeriesDTO[]; + events: EventDTO[]; constructor() { this.series = []; + this.events = []; } } diff --git a/frontend/src/app/modules/time-series/models/event.model.ts b/frontend/src/app/modules/time-series/models/event.model.ts new file mode 100644 index 0000000000..4c387af6f8 --- /dev/null +++ b/frontend/src/app/modules/time-series/models/event.model.ts @@ -0,0 +1,5 @@ +export class EventDTO { + eventDate: Date; + description: string; + shortName: string; +} diff --git a/frontend/src/app/modules/time-series/services/line-chart.service.ts b/frontend/src/app/modules/time-series/services/line-chart.service.ts index bbe4fc6cca..18a711535e 100644 --- a/frontend/src/app/modules/time-series/services/line-chart.service.ts +++ b/frontend/src/app/modules/time-series/services/line-chart.service.ts @@ -43,6 +43,7 @@ import {brushX as d3BrushX} from 'd3-brush'; import {EventResultDataDTO} from 'src/app/modules/time-series/models/event-result-data.model'; import {EventResultSeriesDTO} from 'src/app/modules/time-series/models/event-result-series.model'; import {EventResultPointDTO} from 'src/app/modules/time-series/models/event-result-point.model'; +import {EventDTO} from 'src/app/modules/time-series/models/event.model'; import {TimeSeries} from 'src/app/modules/time-series/models/time-series.model'; import {TimeSeriesPoint} from 'src/app/modules/time-series/models/time-series-point.model'; import {parseDate} from 'src/app/utils/date.util'; @@ -77,6 +78,9 @@ export class LineChartService { this.addXAxisToChart(chart, xScale); this.addYAxisToChart(chart, yScale); + + this.addEventMarkerGroupToChart(chart); + this.addEventMarkerTooltipBoxToChart(chart); } /** @@ -103,9 +107,87 @@ export class LineChartService { d3Select('.y-axis').transition().call(this.updateYAxis, yScale, this._width, this._margin); this.brush = d3BrushX().extent([[0,0], [this._width, this._height]]); this.addBrush(chart, xScale, yScale, data); + this.addEventMarkerToChart(chart, xScale, incomingData.events); this.addDataLinesToChart(chart, xScale, yScale, data); } + private addEventMarkerGroupToChart(chart: D3Selection) { + chart.append('g').attr('id', 'event-marker-group'); + } + + private addEventMarkerTooltipBoxToChart(chart: D3Selection) { + d3Select('#time-series-chart').select(function () { + return (this).parentNode; + }).append('div') + .attr('id', 'event-marker-tooltip') + .style('opacity', '0.9'); + } + + private addEventMarkerToChart(chart: D3Selection, xScale: D3ScaleTime, events: import("../models/event.model").EventDTO[]) { + console.log(events); + let eventMarkerLine = d3Select('#event-marker-group').selectAll('line').data(events); + let eventMarkerCircle = d3Select('#event-marker-group').selectAll('circle').data(events); + + eventMarkerLine.join( + enter => enter.append('line').attr('class', 'event-marker-line') + ) + .attr('x1', (event: EventDTO) => { return xScale(parseDate(event.eventDate)); }) + .attr('y1', 0) + .attr('x2', (event: EventDTO) => { return xScale(parseDate(event.eventDate)); }) + .attr('y2', this._height); + + eventMarkerCircle.join( + enter => enter + .append('circle') + .attr('class', 'event-marker-dot') + .on('mouseover', (event: EventDTO, index: number, nodes: []) => this.showEventMarkerTooltip(event, index, nodes)) + .on('mouseout', () => d3Select('#event-marker-tooltip').style('opacity', 0)) + ) + .attr('cx', (event: EventDTO) => { return xScale(parseDate(event.eventDate)); }) + .attr('cy', this._height) + .attr('r', 8); + } + + private showEventMarkerTooltip(event: EventDTO, index: number, nodes: []): D3Selection { + let eventMarkerTooltipBox = d3Select('#event-marker-tooltip'); + eventMarkerTooltipBox.style('opacity', '0.9'); + eventMarkerTooltipBox.html(this.createEventMarkerTooltipContent(event).outerHTML); + + let circle = d3Select(nodes[index]); + const top = parseFloat(circle.attr('cy')) + this._margin.top; + + const tooltipWidth: number = (eventMarkerTooltipBox.node()).getBoundingClientRect().width; + const xPos = parseFloat(circle.attr('cx')); + const left = (tooltipWidth + xPos > this._width) ? xPos - tooltipWidth + this._margin.right + 10 : xPos + this._margin.left + 50; + eventMarkerTooltipBox.style('top', top + 'px'); + eventMarkerTooltipBox.style('left', left + 'px'); + + return nodes[index]; + } + + private createEventMarkerTooltipContent(event: EventDTO): HTMLDivElement { + const container: HTMLDivElement = document.createElement('div'); + container.className = 'gridContainer'; + + const dateItem = document.createElement('div'); + dateItem.append(event.eventDate.toLocaleString()); + container.append(dateItem); + + const shortNameItem = document.createElement('div'); + const shortNameParagraph = document.createElement('p'); + shortNameParagraph.style.fontWeight = 'bold'; + shortNameParagraph.append(event.shortName + ':'); + shortNameItem.append(shortNameParagraph); + container.append(shortNameItem); + + const descriptionItem = document.createElement('div'); + const descriptionParagraph = document.createElement('p'); + descriptionParagraph.append(event.description); + descriptionItem.append(descriptionParagraph); + container.append(descriptionItem); + + return container; + } /** * Prepares the incoming data for drawing with D3.js diff --git a/frontend/src/app/modules/time-series/time-series.component.ts b/frontend/src/app/modules/time-series/time-series.component.ts index 8949b2098f..9eb9331728 100644 --- a/frontend/src/app/modules/time-series/time-series.component.ts +++ b/frontend/src/app/modules/time-series/time-series.component.ts @@ -3,7 +3,8 @@ import {URL} from "../../enums/url.enum"; import {LinechartDataService} from "./services/linechart-data.service"; import {ResultSelectionStore} from "../result-selection/services/result-selection.store"; import {EventResultData, EventResultDataDTO} from './models/event-result-data.model'; -import {BehaviorSubject} from 'rxjs'; +import {EventDTO} from './models/event.model'; +import {BehaviorSubject, forkJoin} from 'rxjs'; @Component({ selector: 'osm-time-series', @@ -20,22 +21,28 @@ export class TimeSeriesComponent implements OnInit { } getTimeSeriesChartData() { - this.linechartDataService.fetchEventResultData( + return this.linechartDataService.fetchEventResultData( this.resultSelectionStore.resultSelectionCommand, this.resultSelectionStore.remainingResultSelection, URL.EVENT_RESULT_DASHBOARD_LINECHART_DATA - ).subscribe(next => this.results$.next(next)); + ); } getEvents() { - this.linechartDataService.fetchEvents( + return this.linechartDataService.fetchEvents( this.resultSelectionStore.resultSelectionCommand, URL.EVENTS - ).subscribe(next => console.log(next)); + ); } getData() { - this.getTimeSeriesChartData(); - this.getEvents(); + forkJoin({ + eventResultData: this.getTimeSeriesChartData(), + events: this.getEvents() + }) + .subscribe((next) => { + next.eventResultData.events = next.events; + this.results$.next(next.eventResultData); + }); } } From 4c13a60bbe58a8042c7beb0aae557c29b732710f Mon Sep 17 00:00:00 2001 From: Benny Li Date: Fri, 1 Nov 2019 20:52:06 +0100 Subject: [PATCH 05/11] Fix event marker on zoom --- .../time-series/services/line-chart.service.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/modules/time-series/services/line-chart.service.ts b/frontend/src/app/modules/time-series/services/line-chart.service.ts index 18a711535e..5eb169c814 100644 --- a/frontend/src/app/modules/time-series/services/line-chart.service.ts +++ b/frontend/src/app/modules/time-series/services/line-chart.service.ts @@ -106,7 +106,7 @@ export class LineChartService { d3Select('.x-axis').transition().call(this.updateXAxis, xScale); d3Select('.y-axis').transition().call(this.updateYAxis, yScale, this._width, this._margin); this.brush = d3BrushX().extent([[0,0], [this._width, this._height]]); - this.addBrush(chart, xScale, yScale, data); + this.addBrush(chart, xScale, yScale, data, incomingData.events); this.addEventMarkerToChart(chart, xScale, incomingData.events); this.addDataLinesToChart(chart, xScale, yScale, data); } @@ -123,8 +123,7 @@ export class LineChartService { .style('opacity', '0.9'); } - private addEventMarkerToChart(chart: D3Selection, xScale: D3ScaleTime, events: import("../models/event.model").EventDTO[]) { - console.log(events); + private addEventMarkerToChart(chart: D3Selection, xScale: D3ScaleTime, events: EventDTO[]) { let eventMarkerLine = d3Select('#event-marker-group').selectAll('line').data(events); let eventMarkerCircle = d3Select('#event-marker-group').selectAll('circle').data(events); @@ -440,22 +439,24 @@ export class LineChartService { private addBrush(chart: D3Selection, xScale: D3ScaleTime, yScale: D3ScaleLinear, - data: TimeSeries[]){ + data: TimeSeries[], + events: EventDTO[]){ chart.selectAll('.brush') .remove(); - this.brush.on('end', () => this.updateChart(chart, xScale, yScale)); + this.brush.on('end', () => this.updateChart(chart, xScale, yScale, events)); chart.append('g') .attr('class', 'brush') .data([1]) .call(this.brush) .on('dblclick', () => { xScale.domain([this.getMinDate(data), this.getMaxDate(data)]); - this.resetChart(xScale, yScale); + this.resetChart(chart, xScale, yScale, events); }); } - private resetChart(xScale: D3ScaleTime, yScale: D3ScaleLinear){ + private resetChart(chart: D3Selection, xScale: D3ScaleTime, yScale: D3ScaleLinear, events: EventDTO[]){ d3Select('.x-axis').transition().call(this.updateXAxis, xScale); + this.addEventMarkerToChart(chart, xScale, events); d3Select('g#time-series-chart-drawing-area').selectAll('.line') .each((data, index, nodes) => { d3Select(nodes[index]) @@ -463,7 +464,7 @@ export class LineChartService { }) } - private updateChart( selection: any, xScale: D3ScaleTime, yScale: D3ScaleLinear) { + private updateChart( selection: any, xScale: D3ScaleTime, yScale: D3ScaleLinear, events: EventDTO[]) { //selected boundaries let extent = d3Event.selection; // If no selection, back to initial coordinate. Otherwise, update X axis domain @@ -475,6 +476,7 @@ export class LineChartService { xScale.domain([ minDate, maxDate ]); selection.select(".brush").call(this.brush.move, null); // This remove the grey brush area d3Select('.x-axis').transition().call(this.updateXAxis, xScale); + this.addEventMarkerToChart(selection, xScale, events); selection.selectAll('.line').each((_, index, nodes) => { d3Select(nodes[index]) .transition() From b64ffd7210020949c873a3e551f7a42814c70394 Mon Sep 17 00:00:00 2001 From: mekiert Date: Tue, 4 Feb 2020 11:24:30 +0100 Subject: [PATCH 06/11] #306 Display correct event date in event tooltip --- .../src/app/modules/time-series/services/line-chart.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/modules/time-series/services/line-chart.service.ts b/frontend/src/app/modules/time-series/services/line-chart.service.ts index 5eb169c814..5a94847604 100644 --- a/frontend/src/app/modules/time-series/services/line-chart.service.ts +++ b/frontend/src/app/modules/time-series/services/line-chart.service.ts @@ -169,7 +169,7 @@ export class LineChartService { container.className = 'gridContainer'; const dateItem = document.createElement('div'); - dateItem.append(event.eventDate.toLocaleString()); + dateItem.append(new Date(event.eventDate).toLocaleString()); container.append(dateItem); const shortNameItem = document.createElement('div'); From 0446124b8b4a2951be7a9d078e02ad50e30f29a6 Mon Sep 17 00:00:00 2001 From: mekiert Date: Thu, 6 Feb 2020 14:43:08 +0100 Subject: [PATCH 07/11] #306 Working events markers in line chart --- .../modules/time-series/models/event.model.ts | 13 ++++ .../services/line-chart.service.ts | 78 +++++++++++++------ 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/frontend/src/app/modules/time-series/models/event.model.ts b/frontend/src/app/modules/time-series/models/event.model.ts index 4c387af6f8..9de08bf259 100644 --- a/frontend/src/app/modules/time-series/models/event.model.ts +++ b/frontend/src/app/modules/time-series/models/event.model.ts @@ -3,3 +3,16 @@ export class EventDTO { description: string; shortName: string; } + +export class TimeEvent extends EventDTO { + eventDate: Date; + description: string; + shortName: string; + + constructor(eventDate: Date, description: string, shortName: string) { + super(); + this.eventDate = eventDate; + this.description = description; + this.shortName = shortName; + } +} diff --git a/frontend/src/app/modules/time-series/services/line-chart.service.ts b/frontend/src/app/modules/time-series/services/line-chart.service.ts index 5a94847604..86bfdcf600 100644 --- a/frontend/src/app/modules/time-series/services/line-chart.service.ts +++ b/frontend/src/app/modules/time-series/services/line-chart.service.ts @@ -43,7 +43,7 @@ import {brushX as d3BrushX} from 'd3-brush'; import {EventResultDataDTO} from 'src/app/modules/time-series/models/event-result-data.model'; import {EventResultSeriesDTO} from 'src/app/modules/time-series/models/event-result-series.model'; import {EventResultPointDTO} from 'src/app/modules/time-series/models/event-result-point.model'; -import {EventDTO} from 'src/app/modules/time-series/models/event.model'; +import {EventDTO, TimeEvent} from 'src/app/modules/time-series/models/event.model'; import {TimeSeries} from 'src/app/modules/time-series/models/time-series.model'; import {TimeSeriesPoint} from 'src/app/modules/time-series/models/time-series-point.model'; import {parseDate} from 'src/app/utils/date.util'; @@ -87,9 +87,14 @@ export class LineChartService { * Draws a line chart for the given data into the given svg */ public drawLineChart(incomingData: EventResultDataDTO): void { + const prepareEventsData = (events: EventDTO[]) => { + return events.map(eventDto => { + return new TimeEvent(new Date(eventDto.eventDate), eventDto.description, eventDto.shortName); + }) + }; let data: TimeSeries[] = this.prepareData(incomingData); - //console.log(incomingData); console.log(data); + const eventsData: TimeEvent[] = prepareEventsData(incomingData.events); if (data.length == 0) { console.log("No data > No chart !"); @@ -106,8 +111,8 @@ export class LineChartService { d3Select('.x-axis').transition().call(this.updateXAxis, xScale); d3Select('.y-axis').transition().call(this.updateYAxis, yScale, this._width, this._margin); this.brush = d3BrushX().extent([[0,0], [this._width, this._height]]); - this.addBrush(chart, xScale, yScale, data, incomingData.events); - this.addEventMarkerToChart(chart, xScale, incomingData.events); + this.addBrush(chart, xScale, yScale, data, eventsData); + this.addEventMarkerToChart(chart, xScale, eventsData); this.addDataLinesToChart(chart, xScale, yScale, data); } @@ -124,27 +129,50 @@ export class LineChartService { } private addEventMarkerToChart(chart: D3Selection, xScale: D3ScaleTime, events: EventDTO[]) { - let eventMarkerLine = d3Select('#event-marker-group').selectAll('line').data(events); - let eventMarkerCircle = d3Select('#event-marker-group').selectAll('circle').data(events); + const changeLineVisibility = (event: EventDTO, index: number, nodes) => { + const eventMarkerLineSelection = d3Select(nodes[index].parentNode).select(".event-marker-line"); + if(eventMarkerLineSelection.style('opacity') == '1') { + eventMarkerLineSelection.style('opacity', '0'); + } else { + eventMarkerLineSelection.style('opacity', '1'); + } + }; + + const eventMarkerGroup = d3Select('#event-marker-group') + .selectAll('g') + .data(events, (d: EventDTO) => { + // fixme This key is not unique. EventDTO has no unique date. + return d.eventDate.toString() + }) + .join( + enter => { + const eventMarker = enter + .append('g') + .attr('class', 'event-marker'); + eventMarker + .append('line') + .attr('class', 'event-marker-line') + .style('opacity', '0') + .attr('y1', 0) + .attr('y2', this._height); + eventMarker + .append('circle') + .attr('class', 'event-marker-dot') + .style('cursor', 'pointer') + .on('mouseover', (event: EventDTO, index: number, nodes: []) => this.showEventMarkerTooltip(event, index, nodes)) + .on('mouseout', () => d3Select('#event-marker-tooltip').style('opacity', 0)) + .on('click', (data: EventDTO, index: number, nodes: []) => changeLineVisibility(data, index, nodes)) + .attr('cy', this._height) + .attr('r', 8); + return eventMarker; + } + ); - eventMarkerLine.join( - enter => enter.append('line').attr('class', 'event-marker-line') - ) - .attr('x1', (event: EventDTO) => { return xScale(parseDate(event.eventDate)); }) - .attr('y1', 0) - .attr('x2', (event: EventDTO) => { return xScale(parseDate(event.eventDate)); }) - .attr('y2', this._height); - - eventMarkerCircle.join( - enter => enter - .append('circle') - .attr('class', 'event-marker-dot') - .on('mouseover', (event: EventDTO, index: number, nodes: []) => this.showEventMarkerTooltip(event, index, nodes)) - .on('mouseout', () => d3Select('#event-marker-tooltip').style('opacity', 0)) - ) - .attr('cx', (event: EventDTO) => { return xScale(parseDate(event.eventDate)); }) - .attr('cy', this._height) - .attr('r', 8); + eventMarkerGroup.selectAll('.event-marker-line') + .attr('x1', (event: EventDTO) => xScale(event.eventDate)) + .attr('x2', (event: EventDTO) => xScale(event.eventDate)); + eventMarkerGroup.selectAll('.event-marker-dot') + .attr('cx', (event: EventDTO) => xScale(parseDate(event.eventDate))); } private showEventMarkerTooltip(event: EventDTO, index: number, nodes: []): D3Selection { @@ -169,7 +197,7 @@ export class LineChartService { container.className = 'gridContainer'; const dateItem = document.createElement('div'); - dateItem.append(new Date(event.eventDate).toLocaleString()); + dateItem.append(event.eventDate.toLocaleString()); container.append(dateItem); const shortNameItem = document.createElement('div'); From 97deb5d631ea79d4e7338d2ccc90c52c9f8d6807 Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Wed, 24 Jun 2020 17:34:40 +0200 Subject: [PATCH 08/11] Merge --- frontend/package-lock.json | 10 +- .../time-series-chart.component.scss | 11 ++ .../time-series-chart.component.ts | 11 +- .../time-series-line-chart.component.scss | 0 .../modules/time-series/models/event.model.ts | 5 +- ...ice.ts => line-chart-dom-event.service.ts} | 61 ++++++--- .../line-chart-legend.service.ts | 6 +- .../line-chart-time-event.service.ts | 127 ++++++++++++++++++ .../services/line-chart.service.ts | 45 ++++--- 9 files changed, 223 insertions(+), 53 deletions(-) delete mode 100644 frontend/src/app/modules/time-series/components/time-series-line-chart/time-series-line-chart.component.scss rename frontend/src/app/modules/time-series/services/chart-services/{line-chart-event.service.ts => line-chart-dom-event.service.ts} (90%) create mode 100644 frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fff13abedd..023b606196 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -338,7 +338,7 @@ }, "load-json-file": { "version": "2.0.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "dev": true, "requires": { @@ -391,7 +391,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -1587,7 +1587,7 @@ }, "util": { "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -3570,7 +3570,7 @@ }, "engine.io-client": { "version": "3.2.1", - "resolved": "http://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", "dev": true, "requires": { @@ -9005,7 +9005,7 @@ }, "socket.io-parser": { "version": "3.2.0", - "resolved": "http://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", "dev": true, "requires": { diff --git a/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss b/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss index 95007d4c78..f3af972e11 100644 --- a/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss +++ b/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss @@ -31,6 +31,17 @@ osm-time-series-line-chart { } } + #event-marker-tooltip { + position: absolute; + text-align: left; + font: 10pt sans-serif; + color: white; + background: rgba(0, 0, 0, 0.7); + border: 1px solid rgba(0, 0, 0, 0.4); + border-radius: 3px; + pointer-events: none; + } + #time-series-chart-drawing-area { .axis { .domain { diff --git a/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.ts b/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.ts index 9eba709d58..9335942c35 100644 --- a/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.ts +++ b/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.ts @@ -15,6 +15,7 @@ import {LineChartService} from '../../services/line-chart.service'; import {NgxSmartModalService} from 'ngx-smart-modal'; import {SpinnerService} from '../../../shared/services/spinner.service'; import {TranslateService} from '@ngx-translate/core'; +import {TimeEvent} from "../../models/event.model"; @Component({ @@ -89,8 +90,9 @@ export class TimeSeriesChartComponent implements AfterContentInit, OnChanges { } const timeSeries = this.lineChartService.prepareData(this.timeSeriesResults, this.selectedTrimValues); - this.lineChartService.prepareLegend(this.timeSeriesResults); - this.lineChartService.drawLineChart(timeSeries, this.timeSeriesResults.measurandGroups, + const eventData: TimeEvent[] = this.lineChartService.prepareEventsData(this.timeSeriesResults.events); + this.lineChartService.prepareLegendData(this.timeSeriesResults); + this.lineChartService.drawLineChart(timeSeries, eventData, this.timeSeriesResults.measurandGroups, this.timeSeriesResults.summaryLabels, this.timeSeriesResults.numberOfTimeSeries, this.selectedTrimValues); this.spinnerService.hideSpinner('time-series-line-chart-spinner'); @@ -103,9 +105,10 @@ export class TimeSeriesChartComponent implements AfterContentInit, OnChanges { } const timeSeries = this.lineChartService.prepareData(this.timeSeriesResults, this.selectedTrimValues); - this.lineChartService.drawLineChart(timeSeries, this.timeSeriesResults.measurandGroups, + const eventData: TimeEvent[] = this.lineChartService.prepareEventsData(this.timeSeriesResults.events); + this.lineChartService.drawLineChart(timeSeries, eventData, this.timeSeriesResults.measurandGroups, this.timeSeriesResults.summaryLabels, this.timeSeriesResults.numberOfTimeSeries, this.selectedTrimValues); - this.lineChartService.restoreZoom(timeSeries, this.selectedTrimValues); + this.lineChartService.restoreZoom(timeSeries, this.selectedTrimValues, eventData); this.spinnerService.hideSpinner('time-series-line-chart-spinner'); } diff --git a/frontend/src/app/modules/time-series/components/time-series-line-chart/time-series-line-chart.component.scss b/frontend/src/app/modules/time-series/components/time-series-line-chart/time-series-line-chart.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/src/app/modules/time-series/models/event.model.ts b/frontend/src/app/modules/time-series/models/event.model.ts index 9de08bf259..3f21fd481f 100644 --- a/frontend/src/app/modules/time-series/models/event.model.ts +++ b/frontend/src/app/modules/time-series/models/event.model.ts @@ -1,16 +1,15 @@ -export class EventDTO { +export interface EventDTO { eventDate: Date; description: string; shortName: string; } -export class TimeEvent extends EventDTO { +export class TimeEvent implements EventDTO { eventDate: Date; description: string; shortName: string; constructor(eventDate: Date, description: string, shortName: string) { - super(); this.eventDate = eventDate; this.description = description; this.shortName = shortName; diff --git a/frontend/src/app/modules/time-series/services/chart-services/line-chart-event.service.ts b/frontend/src/app/modules/time-series/services/chart-services/line-chart-dom-event.service.ts similarity index 90% rename from frontend/src/app/modules/time-series/services/chart-services/line-chart-event.service.ts rename to frontend/src/app/modules/time-series/services/chart-services/line-chart-dom-event.service.ts index f714952475..81dca5d30b 100644 --- a/frontend/src/app/modules/time-series/services/chart-services/line-chart-event.service.ts +++ b/frontend/src/app/modules/time-series/services/chart-services/line-chart-dom-event.service.ts @@ -19,11 +19,13 @@ import {LineChartDrawService} from './line-chart-draw.service'; import {LineChartScaleService} from './line-chart-scale.service'; import {PointsSelection} from '../../models/points-selection.model'; import {TranslateService} from '@ngx-translate/core'; +import {TimeEvent} from '../../models/event.model'; +import {LineChartTimeEventService} from './line-chart-time-event.service'; @Injectable({ providedIn: 'root' }) -export class LineChartEventService { +export class LineChartDomEventService { private _DOT_HIGHLIGHT_RADIUS = 5; private _contextMenuBackground: D3Selection; @@ -38,7 +40,8 @@ export class LineChartEventService { constructor(private urlBuilderService: UrlBuilderService, private translationService: TranslateService, private lineChartDrawService: LineChartDrawService, - private lineChartScaleService: LineChartScaleService) { + private lineChartScaleService: LineChartScaleService, + private lineChartTimeEventService: LineChartTimeEventService) { } private _pointSelectionErrorHandler: Function; @@ -53,7 +56,7 @@ export class LineChartEventService { return this._pointsSelection; } - private contextMenu: ContextMenuPosition[] = [ + private readonly contextMenu: ContextMenuPosition[] = [ { title: 'summary', icon: 'fas fa-file-alt', @@ -156,7 +159,7 @@ export class LineChartEventService { }, ]; - private backgroundContextMenu: ContextMenuPosition[] = [ + private readonly backgroundContextMenu: ContextMenuPosition[] = [ { title: 'compareFilmstrips', icon: 'fas fa-columns', @@ -241,22 +244,26 @@ export class LineChartEventService { width: number, height: number, yAxisWidth: number, + margin: { [key: string]: number }, xScale: D3ScaleTime, data: { [key: string]: TimeSeries[] }, dataTrimValues: { [key: string]: { [key: string]: number } }, - legendDataMap: { [key: string]: { [key: string]: (boolean | string) } }): void { + legendDataMap: { [key: string]: { [key: string]: (boolean | string) } }, + events: TimeEvent[]): void { // remove old brush d3Select('.brush').remove(); this.brush = d3BrushX() .extent([[0, 0], [width, height]]) - .on('end', () => this.zoomInTheChart(chartContentContainer, width, height, yAxisWidth, xScale, data, dataTrimValues, legendDataMap)); + .on('end', () => + this.zoomInTheChart(chartContentContainer, width, height, yAxisWidth, margin, xScale, data, dataTrimValues, legendDataMap, events)); chartContentContainer .append('g') .attr('class', 'brush') .call(this.brush); d3Select('.overlay') - .on('dblclick', () => this.resetChart(chartContentContainer, width, height, yAxisWidth, xScale, data, dataTrimValues, legendDataMap)) + .on('dblclick', () => + this.resetChart(chartContentContainer, width, height, yAxisWidth, margin, xScale, data, dataTrimValues, legendDataMap, events)) .on('contextmenu', (d, i, e) => this.showContextMenu(this.backgroundContextMenu)(d, i, e)); } @@ -264,12 +271,14 @@ export class LineChartEventService { width: number, height: number, yAxisWidth: number, + margin: { [key: string]: number }, xScale: D3ScaleTime, data: { [key: string]: TimeSeries[] }, dataTrimValues: { [key: string]: { [key: string]: number } }, - legendDataMap: { [key: string]: { [key: string]: (boolean | string) } }): void { + legendDataMap: { [key: string]: { [key: string]: (boolean | string) } }, + events: TimeEvent[]): void { if (this.brushMinDate !== null && this.brushMaxDate !== null) { - this.updateChart(chartContentContainer, width, height, yAxisWidth, xScale, data, dataTrimValues, legendDataMap); + this.updateChart(chartContentContainer, width, height, yAxisWidth, margin, xScale, data, dataTrimValues, legendDataMap, events); } } @@ -323,13 +332,15 @@ export class LineChartEventService { } private zoomInTheChart(chartContentContainer: D3Selection, - width: number, - height: number, - yAxisWidth: number, - xScale: D3ScaleTime, - data: { [key: string]: TimeSeries[] }, - dataTrimValues: { [key: string]: { [key: string]: number } }, - legendDataMap: { [key: string]: { [key: string]: (boolean | string) } }): void { + width: number, + height: number, + yAxisWidth: number, + margin: { [key: string]: number }, + xScale: D3ScaleTime, + data: { [key: string]: TimeSeries[] }, + dataTrimValues: { [key: string]: { [key: string]: number } }, + legendDataMap: { [key: string]: { [key: string]: (boolean | string) } }, + events: TimeEvent[]): void { const extent = d3Event.selection; if (!extent) { return; @@ -339,17 +350,19 @@ export class LineChartEventService { d3Select('.brush').call(this.brush.move, null); this.brushMinDate = xScale.invert(extent[0]); this.brushMaxDate = xScale.invert(extent[1]); - this.updateChart(chartContentContainer, width, height, yAxisWidth, xScale, data, dataTrimValues, legendDataMap); + this.updateChart(chartContentContainer, width, height, yAxisWidth, margin, xScale, data, dataTrimValues, legendDataMap, events); } private resetChart(chartContentContainer: D3Selection, width: number, height: number, yAxisWidth: number, + margin: { [key: string]: number }, xScale: D3ScaleTime, data: { [key: string]: TimeSeries[] }, dataTrimValues: { [key: string]: { [key: string]: number } }, - legendDataMap: { [key: string]: { [key: string]: (boolean | string) } }): void { + legendDataMap: { [key: string]: { [key: string]: (boolean | string) } }, + events: TimeEvent[]): void { if (this.brushMinDate === null || this.brushMaxDate === null) { return; } @@ -359,6 +372,7 @@ export class LineChartEventService { xScale.domain([this.lineChartScaleService.getMinDate(data), this.lineChartScaleService.getMaxDate(data)]); d3Select('.x-axis').transition().call((transition: D3Transition) => this.lineChartDrawService.updateXAxis(transition, xScale)); + this.lineChartTimeEventService.addEventMarkerToChart(chartContentContainer, xScale, events, width, height, margin); const yNewScales = this.lineChartScaleService.getYScales(data, height, dataTrimValues); this.lineChartDrawService.updateYAxes(yNewScales, width, yAxisWidth); Object.keys(yNewScales).forEach((key: string, index: number) => { @@ -372,19 +386,22 @@ export class LineChartEventService { width: number, height: number, yAxisWidth: number, + margin: { [key: string]: number }, xScale: D3ScaleTime, data: { [key: string]: TimeSeries[] }, dataTrimValues: { [key: string]: { [key: string]: number } }, - legendDataMap: { [key: string]: { [key: string]: (boolean | string) } }): void { + legendDataMap: { [key: string]: { [key: string]: (boolean | string) } }, + events: TimeEvent[]): void { xScale.domain([this.brushMinDate, this.brushMaxDate]); d3Select('.x-axis').transition().call((transition: D3Transition) => - this.lineChartDrawService.updateXAxis(transition, xScale)); + this.lineChartDrawService.updateXAxis(transition, xScale)); + this.lineChartTimeEventService.addEventMarkerToChart(chartContentContainer, xScale, events, width, height, margin); const yNewScales = this.lineChartScaleService.getYScalesInTimeRange(data, height, dataTrimValues, this.brushMinDate, this.brushMaxDate); this.lineChartDrawService.updateYAxes(yNewScales, width, yAxisWidth); Object.keys(yNewScales).forEach((key: string, index: number) => { - this.lineChartDrawService.addDataLinesToChart( - chartContentContainer, this._pointsSelection, xScale, yNewScales[key], data[key], legendDataMap, index); + this.lineChartDrawService.addDataLinesToChart( + chartContentContainer, this._pointsSelection, xScale, yNewScales[key], data[key], legendDataMap, index); }); } diff --git a/frontend/src/app/modules/time-series/services/chart-services/line-chart-legend.service.ts b/frontend/src/app/modules/time-series/services/chart-services/line-chart-legend.service.ts index 60d61b3680..2d60fff5e8 100644 --- a/frontend/src/app/modules/time-series/services/chart-services/line-chart-legend.service.ts +++ b/frontend/src/app/modules/time-series/services/chart-services/line-chart-legend.service.ts @@ -16,7 +16,7 @@ import {EventResultSeriesDTO} from '../../models/event-result-series.model'; import {EventResultDataDTO} from '../../models/event-result-data.model'; import {TranslateService} from '@ngx-translate/core'; import {LineChartDrawService} from './line-chart-draw.service'; -import {LineChartEventService} from './line-chart-event.service'; +import {LineChartDomEventService} from './line-chart-dom-event.service'; @Injectable({ providedIn: 'root' @@ -30,7 +30,7 @@ export class LineChartLegendService { constructor(private translationService: TranslateService, private lineChartDrawService: LineChartDrawService, - private lineChartEventService: LineChartEventService) { + private lineChartDomEventService: LineChartDomEventService) { } get legendDataMap(): { [p: string]: { [p: string]: boolean | string } } { @@ -249,7 +249,7 @@ export class LineChartLegendService { Object.keys(yScales).forEach((key: string, index: number) => { this.lineChartDrawService.addDataLinesToChart( - chartContentContainer, this.lineChartEventService.pointsSelection, xScale, yScales[key], data[key], this._legendDataMap, index); + chartContentContainer, this.lineChartDomEventService.pointsSelection, xScale, yScales[key], data[key], this._legendDataMap, index); }); // redraw legend diff --git a/frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts b/frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts new file mode 100644 index 0000000000..dde3a367ad --- /dev/null +++ b/frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts @@ -0,0 +1,127 @@ +import {Injectable} from '@angular/core'; +import {EventDTO, TimeEvent} from '../../models/event.model'; +import { + BaseType as D3BaseType, + ContainerElement as D3ContainerElement, + select as d3Select, + Selection as D3Selection +} from 'd3-selection'; +import {ScaleTime as D3ScaleTime} from 'd3-scale'; +import {parseDate} from '../../../../utils/date.util'; + +@Injectable({ + providedIn: 'root' +}) +export class LineChartTimeEventService { + + constructor() { + } + + addEventMarkerGroupToChart(chart: D3Selection) { + chart.append('g').attr('id', 'event-marker-group'); + } + + addEventMarkerTooltipBoxToSvgParent() { + d3Select('#time-series-chart').select(function () { + return (this).parentNode; + }).append('div') + .attr('id', 'event-marker-tooltip') + .style('opacity', '0.9'); + } + + addEventMarkerToChart(chart: D3Selection, + xScale: D3ScaleTime, + events: EventDTO[], + width: number, + height: number, + margin: { [key: string]: number }): void { + const eventMarkerGroup = d3Select('#event-marker-group') + .selectAll('g') + .data(events, (d: EventDTO) => { + // fixme This key is not unique. EventDTO has no unique date. + return d.eventDate.toString(); + }) + .join( + enter => { + const eventMarker = enter + .append('g') + .attr('class', 'event-marker'); + eventMarker + .append('line') + .attr('class', 'event-marker-line') + .style('opacity', '0') + .attr('y1', 0) + .attr('y2', height); + eventMarker + .append('circle') + .attr('class', 'event-marker-dot') + .style('cursor', 'pointer') + .on('mouseover', (event: EventDTO, index: number, nodes: []) => this.showEventMarkerTooltip(event, index, nodes, width, margin)) + .on('mouseout', () => d3Select('#event-marker-tooltip').style('opacity', 0)) + .on('click', (_, index: number, nodes: []) => this.changeLineVisibility(index, nodes)) + .attr('cy', height) + .attr('r', 8); + return eventMarker; + } + ); + + eventMarkerGroup.selectAll('.event-marker-line') + .attr('x1', (event: EventDTO) => xScale(event.eventDate)) + .attr('x2', (event: EventDTO) => xScale(event.eventDate)); + eventMarkerGroup.selectAll('.event-marker-dot') + .attr('cx', (event: EventDTO) => xScale(parseDate(event.eventDate))); + } + + private showEventMarkerTooltip(event: EventDTO, + index: number, nodes: [], + width: number, + margin: { [key: string]: number }): D3Selection { + const eventMarkerTooltipBox = d3Select('#event-marker-tooltip'); + eventMarkerTooltipBox.style('opacity', '0.9'); + eventMarkerTooltipBox.html(this.createEventMarkerTooltipContent(event).outerHTML); + + const circle = d3Select(nodes[index]); + const top = parseFloat(circle.attr('cy')) + margin.top; + + const tooltipWidth: number = (eventMarkerTooltipBox.node()).getBoundingClientRect().width; + const xPos = parseFloat(circle.attr('cx')); + const left = (tooltipWidth + xPos > width) ? xPos - tooltipWidth + margin.right + 10 : xPos + margin.left + 50; + eventMarkerTooltipBox.style('top', top + 'px'); + eventMarkerTooltipBox.style('left', left + 'px'); + + return nodes[index]; + } + + private changeLineVisibility(index: number, nodes) { + const eventMarkerLineSelection = d3Select(nodes[index].parentNode).select('.event-marker-line'); + if (eventMarkerLineSelection.style('opacity') === '1') { + eventMarkerLineSelection.style('opacity', '0'); + } else { + eventMarkerLineSelection.style('opacity', '1'); + } + } + + private createEventMarkerTooltipContent(event: EventDTO): HTMLDivElement { + const container: HTMLDivElement = document.createElement('div'); + container.className = 'gridContainer'; + + const dateItem = document.createElement('div'); + dateItem.append(event.eventDate.toLocaleString()); + container.append(dateItem); + + const shortNameItem = document.createElement('div'); + const shortNameParagraph = document.createElement('p'); + shortNameParagraph.style.fontWeight = 'bold'; + shortNameParagraph.append(event.shortName + ':'); + shortNameItem.append(shortNameParagraph); + container.append(shortNameItem); + + const descriptionItem = document.createElement('div'); + const descriptionParagraph = document.createElement('p'); + descriptionParagraph.append(event.description); + descriptionItem.append(descriptionParagraph); + container.append(descriptionItem); + + return container; + } +} diff --git a/frontend/src/app/modules/time-series/services/line-chart.service.ts b/frontend/src/app/modules/time-series/services/line-chart.service.ts index d9b292dd7c..c91407d38b 100644 --- a/frontend/src/app/modules/time-series/services/line-chart.service.ts +++ b/frontend/src/app/modules/time-series/services/line-chart.service.ts @@ -24,9 +24,10 @@ import {parseDate} from 'src/app/utils/date.util'; import {UrlBuilderService} from './url-builder.service'; import {LineChartScaleService} from './chart-services/line-chart-scale.service'; import {LineChartDrawService} from './chart-services/line-chart-draw.service'; -import {LineChartEventService} from './chart-services/line-chart-event.service'; +import {LineChartDomEventService} from './chart-services/line-chart-dom-event.service'; import {LineChartLegendService} from './chart-services/line-chart-legend.service'; import {SummaryLabel} from '../models/summary-label.model'; +import {LineChartTimeEventService} from './chart-services/line-chart-time-event.service'; /** * Generate line charts with ease and fun 😎 @@ -56,8 +57,9 @@ export class LineChartService { private urlBuilderService: UrlBuilderService, private lineChartScaleService: LineChartScaleService, private lineChartDrawService: LineChartDrawService, - private lineChartEventService: LineChartEventService, - private lineChartLegendService: LineChartLegendService) { + private lineChartDomEventService: LineChartDomEventService, + private lineChartLegendService: LineChartLegendService, + private lineChartTimeEventService: LineChartTimeEventService) { } private _dataTrimLabels: { [key: string]: string } = {}; @@ -73,14 +75,16 @@ export class LineChartService { } initChart(svgElement: ElementRef, pointSelectionErrorHandler: Function): void { - this.lineChartEventService.pointSelectionErrorHandler = pointSelectionErrorHandler; + this.lineChartDomEventService.pointSelectionErrorHandler = pointSelectionErrorHandler; const data: { [key: string]: TimeSeries[] } = {}; const chart: D3Selection = this.createChart(svgElement); this._xScale = this.lineChartScaleService.getXScale(data, this._width); this.addXAxisToChart(chart); - this.lineChartEventService.prepareMouseEventCatcher(chart, this._width, this._height, this._margin.top, this._margin.left); + this.lineChartTimeEventService.addEventMarkerGroupToChart(chart); + this.lineChartTimeEventService.addEventMarkerTooltipBoxToSvgParent(); + this.lineChartDomEventService.prepareMouseEventCatcher(chart, this._width, this._height, this._margin.top, this._margin.left); } /** @@ -124,16 +128,23 @@ export class LineChartService { return data; } - prepareLegend(incomingData: EventResultDataDTO): void { + prepareLegendData(incomingData: EventResultDataDTO): void { this.lineChartLegendService.setLegendData(incomingData); } + prepareEventsData(events: EventDTO[]): TimeEvent[] { + return events.map(eventDto => { + return new TimeEvent(new Date(eventDto.eventDate), eventDto.description, eventDto.shortName); + }); + } + drawLineChart(timeSeries: { [key: string]: TimeSeries[] }, + eventData: TimeEvent[], measurandGroups: { [key: string]: string }, summaryLabels: SummaryLabel[], timeSeriesAmount: number, dataTrimValues: { [key: string]: { [key: string]: number } }): void { - this.lineChartEventService.prepareCleanState(); + this.lineChartDomEventService.prepareCleanState(); this.adjustChartDimensions(measurandGroups, summaryLabels); const chart: D3Selection = d3Select('g#time-series-chart-drawing-area'); @@ -157,20 +168,21 @@ export class LineChartService { this.addYAxisUnits(measurandGroups, width); const chartContentContainer = chart.select('.chart-content'); - this.lineChartEventService.createContextMenu(); - this.lineChartEventService.addBrush(chartContentContainer, this._width, this._height, this.Y_AXIS_WIDTH, this._xScale, - timeSeries, dataTrimValues, this.lineChartLegendService.legendDataMap); + this.lineChartDomEventService.createContextMenu(); + this.lineChartDomEventService.addBrush(chartContentContainer, this._width, this._height, this.Y_AXIS_WIDTH, this._margin, + this._xScale, timeSeries, dataTrimValues, this.lineChartLegendService.legendDataMap, eventData); + this.lineChartTimeEventService.addEventMarkerToChart(chart, this._xScale, eventData, this._width, this._height, this._margin); this.lineChartLegendService.addLegendsToChart(chartContentContainer, this._xScale, yScales, timeSeries, timeSeriesAmount); this.lineChartLegendService.setSummaryLabel(chart, summaryLabels, this._width); Object.keys(yScales).forEach((key: string, index: number) => { - this.lineChartDrawService.addDataLinesToChart(chartContentContainer, this.lineChartEventService.pointsSelection, + this.lineChartDrawService.addDataLinesToChart(chartContentContainer, this.lineChartDomEventService.pointsSelection, this._xScale, yScales[key], timeSeries[key], this.lineChartLegendService.legendDataMap, index); }); - this.lineChartEventService.addMouseMarkerToChart(chartContentContainer); - this.lineChartDrawService.drawAllSelectedPoints(this.lineChartEventService.pointsSelection); + this.lineChartDomEventService.addMouseMarkerToChart(chartContentContainer); + this.lineChartDrawService.drawAllSelectedPoints(this.lineChartDomEventService.pointsSelection); this.setDataTrimLabels(measurandGroups); @@ -180,10 +192,11 @@ export class LineChartService { } restoreZoom(timeSeriesData: { [key: string]: TimeSeries[] }, - dataTrimValues: { [key: string]: { [key: string]: number } }): void { + dataTrimValues: { [key: string]: { [key: string]: number } }, + events: TimeEvent[]): void { const chartContentContainer = d3Select('g#time-series-chart-drawing-area .chart-content'); - this.lineChartEventService.restoreSelectedZoom(chartContentContainer, this._width, this._height, - this.Y_AXIS_WIDTH, this._xScale, timeSeriesData, dataTrimValues, this.lineChartLegendService.legendDataMap); + this.lineChartDomEventService.restoreSelectedZoom(chartContentContainer, this._width, this._height, + this.Y_AXIS_WIDTH, this._margin, this._xScale, timeSeriesData, dataTrimValues, this.lineChartLegendService.legendDataMap, events); } startResize(svgElement: ElementRef): void { From a30ce3b3b3f1d7bbd9445158257c3f4bb2ddda39 Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Thu, 25 Jun 2020 18:10:35 +0200 Subject: [PATCH 09/11] Added event time line --- .../time-series-chart.component.scss | 42 ++++++- .../time-series-chart.component.ts | 9 +- .../modules/time-series/models/event.model.ts | 5 +- .../line-chart-dom-event.service.ts | 10 +- .../line-chart-legend.service.ts | 2 +- .../line-chart-time-event.service.ts | 108 +++++++++++++----- .../services/line-chart-data.service.ts | 2 +- .../services/line-chart.service.ts | 17 +-- .../de/iteratec/osm/report/chart/Event.groovy | 1 + grails-app/i18n/messages.properties | 1 + grails-app/i18n/messages_de.properties | 1 + .../osm/report/chart/EventService.groovy | 1 + .../osm/report/chart/events/EventDTO.groovy | 1 + 13 files changed, 155 insertions(+), 45 deletions(-) diff --git a/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss b/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss index f3af972e11..0c4e644f9d 100644 --- a/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss +++ b/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss @@ -75,13 +75,53 @@ osm-time-series-line-chart { .marker-line { stroke: $color-contour-medium; stroke-width: 1px; - opacity: 0.7; + shape-rendering: crispEdges; } circle.dot { stroke-width: 2px; fill: white; } + + circle.event-marker-dot { + fill: $color-contour-light; + opacity: 0.9; + } + + circle.selected-event-marker-dot { + fill: $color-contour-medium; + } + + .event-marker-line { + stroke: $color-contour-medium; + stroke-width: 1px; + shape-rendering: crispEdges; + } + + .unselected-event-marker-line { + opacity: 0; + } + + .selected-event-marker-line { + opacity: 1; + } + + circle.event-marker-dot:hover { + fill: $color-contour-medium; + opacity: 0.7; + } + + .event-time-line { + stroke: $color-contour-light; + stroke-width: 1px; + shape-rendering: crispEdges; + } + + .event-time-line-label { + font-size: 10px; + fill: $color-text-light; + opacity: 0.7; + } } } diff --git a/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.ts b/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.ts index 9335942c35..868b5a9607 100644 --- a/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.ts +++ b/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.ts @@ -15,7 +15,8 @@ import {LineChartService} from '../../services/line-chart.service'; import {NgxSmartModalService} from 'ngx-smart-modal'; import {SpinnerService} from '../../../shared/services/spinner.service'; import {TranslateService} from '@ngx-translate/core'; -import {TimeEvent} from "../../models/event.model"; +import {TimeEvent} from '../../models/event.model'; +import {TimeSeries} from '../../models/time-series.model'; @Component({ @@ -89,8 +90,9 @@ export class TimeSeriesChartComponent implements AfterContentInit, OnChanges { return; } - const timeSeries = this.lineChartService.prepareData(this.timeSeriesResults, this.selectedTrimValues); + const timeSeries: { [key: string]: TimeSeries[] } = this.lineChartService.prepareData(this.timeSeriesResults, this.selectedTrimValues); const eventData: TimeEvent[] = this.lineChartService.prepareEventsData(this.timeSeriesResults.events); + this.lineChartService.prepareLegendData(this.timeSeriesResults); this.lineChartService.drawLineChart(timeSeries, eventData, this.timeSeriesResults.measurandGroups, this.timeSeriesResults.summaryLabels, this.timeSeriesResults.numberOfTimeSeries, this.selectedTrimValues); @@ -104,8 +106,9 @@ export class TimeSeriesChartComponent implements AfterContentInit, OnChanges { return; } - const timeSeries = this.lineChartService.prepareData(this.timeSeriesResults, this.selectedTrimValues); + const timeSeries: { [key: string]: TimeSeries[] } = this.lineChartService.prepareData(this.timeSeriesResults, this.selectedTrimValues); const eventData: TimeEvent[] = this.lineChartService.prepareEventsData(this.timeSeriesResults.events); + this.lineChartService.drawLineChart(timeSeries, eventData, this.timeSeriesResults.measurandGroups, this.timeSeriesResults.summaryLabels, this.timeSeriesResults.numberOfTimeSeries, this.selectedTrimValues); this.lineChartService.restoreZoom(timeSeries, this.selectedTrimValues, eventData); diff --git a/frontend/src/app/modules/time-series/models/event.model.ts b/frontend/src/app/modules/time-series/models/event.model.ts index 3f21fd481f..3933c67bc6 100644 --- a/frontend/src/app/modules/time-series/models/event.model.ts +++ b/frontend/src/app/modules/time-series/models/event.model.ts @@ -1,15 +1,18 @@ export interface EventDTO { + id: number; eventDate: Date; description: string; shortName: string; } export class TimeEvent implements EventDTO { + id: number; eventDate: Date; description: string; shortName: string; - constructor(eventDate: Date, description: string, shortName: string) { + constructor(id: number, eventDate: Date, description: string, shortName: string) { + this.id = id; this.eventDate = eventDate; this.description = description; this.shortName = shortName; diff --git a/frontend/src/app/modules/time-series/services/chart-services/line-chart-dom-event.service.ts b/frontend/src/app/modules/time-series/services/chart-services/line-chart-dom-event.service.ts index 81dca5d30b..3bbd3556e3 100644 --- a/frontend/src/app/modules/time-series/services/chart-services/line-chart-dom-event.service.ts +++ b/frontend/src/app/modules/time-series/services/chart-services/line-chart-dom-event.service.ts @@ -299,7 +299,7 @@ export class LineChartDomEventService { .select((_, index: number, elem) => (elem[index]).parentNode) .append('div') .attr('id', 'marker-tooltip') - .style('opacity', '0.9'); + .style('opacity', '1'); } private moveMarker(node: D3ContainerElement, width: number, containerHeight: number, marginTop: number, marginLeft: number): void { @@ -372,7 +372,7 @@ export class LineChartDomEventService { xScale.domain([this.lineChartScaleService.getMinDate(data), this.lineChartScaleService.getMaxDate(data)]); d3Select('.x-axis').transition().call((transition: D3Transition) => this.lineChartDrawService.updateXAxis(transition, xScale)); - this.lineChartTimeEventService.addEventMarkerToChart(chartContentContainer, xScale, events, width, height, margin); + this.lineChartTimeEventService.addEventTimeLineAndMarkersToChart(chartContentContainer, xScale, events, width, height, margin); const yNewScales = this.lineChartScaleService.getYScales(data, height, dataTrimValues); this.lineChartDrawService.updateYAxes(yNewScales, width, yAxisWidth); Object.keys(yNewScales).forEach((key: string, index: number) => { @@ -396,7 +396,7 @@ export class LineChartDomEventService { d3Select('.x-axis').transition().call((transition: D3Transition) => this.lineChartDrawService.updateXAxis(transition, xScale)); - this.lineChartTimeEventService.addEventMarkerToChart(chartContentContainer, xScale, events, width, height, margin); + this.lineChartTimeEventService.addEventTimeLineAndMarkersToChart(chartContentContainer, xScale, events, width, height, margin); const yNewScales = this.lineChartScaleService.getYScalesInTimeRange(data, height, dataTrimValues, this.brushMinDate, this.brushMaxDate); this.lineChartDrawService.updateYAxes(yNewScales, width, yAxisWidth); Object.keys(yNewScales).forEach((key: string, index: number) => { @@ -455,7 +455,7 @@ export class LineChartDomEventService { } private showMarker(): void { - d3Select('.marker-line').style('opacity', 1); + d3Select('.marker-line').style('opacity', 0.5); d3Select('#marker-tooltip').style('opacity', 1); } @@ -664,6 +664,8 @@ export class LineChartDomEventService { value.append(lineColorDot); if (currentPoint.value !== undefined && currentPoint.value !== null) { value.append(currentPoint.value.toString()); + } else { + value.append(' -'); } const row: HTMLTableRowElement = document.createElement('tr'); diff --git a/frontend/src/app/modules/time-series/services/chart-services/line-chart-legend.service.ts b/frontend/src/app/modules/time-series/services/chart-services/line-chart-legend.service.ts index 2d60fff5e8..951aec28a4 100644 --- a/frontend/src/app/modules/time-series/services/chart-services/line-chart-legend.service.ts +++ b/frontend/src/app/modules/time-series/services/chart-services/line-chart-legend.service.ts @@ -93,7 +93,7 @@ export class LineChartLegendService { if (this._legendGroupColumns < 1) { this._legendGroupColumns = 1; } - return Math.ceil(labels.length / this._legendGroupColumns) * ChartCommons.LABEL_HEIGHT + 30; + return Math.ceil(labels.length / this._legendGroupColumns) * ChartCommons.LABEL_HEIGHT; } addLegendsToChart(chartContentContainer: D3Selection, diff --git a/frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts b/frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts index dde3a367ad..b1c08c399e 100644 --- a/frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts +++ b/frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts @@ -8,38 +8,66 @@ import { } from 'd3-selection'; import {ScaleTime as D3ScaleTime} from 'd3-scale'; import {parseDate} from '../../../../utils/date.util'; +import {TranslateService} from '@ngx-translate/core'; @Injectable({ providedIn: 'root' }) export class LineChartTimeEventService { - constructor() { + private readonly EVENT_LINE_OFFSET = 100; + private readonly EVENT_MARKER_RADIUS = 8; + + private selectedEventMarkerIds: number[] = []; + + constructor(private translateService: TranslateService) { } addEventMarkerGroupToChart(chart: D3Selection) { - chart.append('g').attr('id', 'event-marker-group'); + chart + .append('g') + .attr('id', 'event-group'); } addEventMarkerTooltipBoxToSvgParent() { - d3Select('#time-series-chart').select(function () { - return (this).parentNode; - }).append('div') + d3Select('#time-series-chart') + .select(function () { + return (this).parentNode; + }) + .append('div') .attr('id', 'event-marker-tooltip') .style('opacity', '0.9'); } - addEventMarkerToChart(chart: D3Selection, - xScale: D3ScaleTime, - events: EventDTO[], - width: number, - height: number, - margin: { [key: string]: number }): void { - const eventMarkerGroup = d3Select('#event-marker-group') - .selectAll('g') - .data(events, (d: EventDTO) => { - // fixme This key is not unique. EventDTO has no unique date. - return d.eventDate.toString(); + addEventTimeLineAndMarkersToChart(chart: D3Selection, + xScale: D3ScaleTime, + events: EventDTO[], + width: number, + height: number, + margin: { [key: string]: number }): void { + const eventGroup = d3Select('#event-group'); + eventGroup.selectAll('*').remove(); + + const eventTimeLineGroup = eventGroup + .append('g') + .attr('id', 'event-time-line-group'); + eventTimeLineGroup + .append('text') + .attr('class', 'event-time-line-label') + .attr('x', 0) + .attr('y', height + this.EVENT_LINE_OFFSET - 2.5 * this.EVENT_MARKER_RADIUS) + .text(this.translateService.instant('frontend.de.iteratec.osm.timeSeries.chart.eventTimeLine.label')); + eventTimeLineGroup.append('line') + .attr('class', 'event-time-line') + .attr('x1', 0) + .attr('x2', width) + .attr('y1', height + this.EVENT_LINE_OFFSET) + .attr('y2', height + this.EVENT_LINE_OFFSET); + + const eventMarkerGroup = eventGroup + .selectAll('g#event-marker-group') + .data(events, (event: EventDTO) => { + return event.id.toString(); }) .join( enter => { @@ -48,8 +76,7 @@ export class LineChartTimeEventService { .attr('class', 'event-marker'); eventMarker .append('line') - .attr('class', 'event-marker-line') - .style('opacity', '0') + .attr('class', 'event-marker-line unselected-event-marker-line') .attr('y1', 0) .attr('y2', height); eventMarker @@ -58,9 +85,9 @@ export class LineChartTimeEventService { .style('cursor', 'pointer') .on('mouseover', (event: EventDTO, index: number, nodes: []) => this.showEventMarkerTooltip(event, index, nodes, width, margin)) .on('mouseout', () => d3Select('#event-marker-tooltip').style('opacity', 0)) - .on('click', (_, index: number, nodes: []) => this.changeLineVisibility(index, nodes)) - .attr('cy', height) - .attr('r', 8); + .on('click', (event: EventDTO, index: number, nodes: []) => this.setEventMarkerSelection(event.id, index, nodes)) + .attr('cy', height + this.EVENT_LINE_OFFSET) + .attr('r', this.EVENT_MARKER_RADIUS); return eventMarker; } ); @@ -70,10 +97,19 @@ export class LineChartTimeEventService { .attr('x2', (event: EventDTO) => xScale(event.eventDate)); eventMarkerGroup.selectAll('.event-marker-dot') .attr('cx', (event: EventDTO) => xScale(parseDate(event.eventDate))); + + if (this.selectedEventMarkerIds.length > 0) { + this.restoreEventMarkerSelection(eventMarkerGroup); + } + } + + clearEventMarkerSelection(): void { + this.selectedEventMarkerIds = []; } private showEventMarkerTooltip(event: EventDTO, - index: number, nodes: [], + index: number, + nodes: [], width: number, margin: { [key: string]: number }): D3Selection { const eventMarkerTooltipBox = d3Select('#event-marker-tooltip'); @@ -92,12 +128,19 @@ export class LineChartTimeEventService { return nodes[index]; } - private changeLineVisibility(index: number, nodes) { + private setEventMarkerSelection(eventId: number, index: number, nodes) { const eventMarkerLineSelection = d3Select(nodes[index].parentNode).select('.event-marker-line'); - if (eventMarkerLineSelection.style('opacity') === '1') { - eventMarkerLineSelection.style('opacity', '0'); + const eventMarkerDotSelection = d3Select(nodes[index]); + const arrayIndex = this.selectedEventMarkerIds.indexOf(eventId); + + if (arrayIndex !== -1) { + eventMarkerLineSelection.attr('class', 'event-marker-line unselected-event-marker-line'); + eventMarkerDotSelection.attr('class', 'event-marker-dot'); + this.selectedEventMarkerIds.splice(arrayIndex, 1); } else { - eventMarkerLineSelection.style('opacity', '1'); + eventMarkerLineSelection.attr('class', 'event-marker-line selected-event-marker-line'); + eventMarkerDotSelection.attr('class', 'selected-event-marker-dot'); + this.selectedEventMarkerIds.push(eventId); } } @@ -124,4 +167,17 @@ export class LineChartTimeEventService { return container; } + + private restoreEventMarkerSelection(eventMarkerGroup: D3Selection): void { + eventMarkerGroup.selectAll('.event-marker-line') + .attr('class', (event: EventDTO) => + this.selectedEventMarkerIds.includes(event.id) ? + 'event-marker-line selected-event-marker-line' : + 'event-marker-line unselected-event-marker-line' + ); + eventMarkerGroup.selectAll('.event-marker-dot') + .attr('class', (event: EventDTO) => + this.selectedEventMarkerIds.includes(event.id) ? 'selected-event-marker-dot' : 'event-marker-dot'); + + } } diff --git a/frontend/src/app/modules/time-series/services/line-chart-data.service.ts b/frontend/src/app/modules/time-series/services/line-chart-data.service.ts index b5164250d5..575c7d7c24 100644 --- a/frontend/src/app/modules/time-series/services/line-chart-data.service.ts +++ b/frontend/src/app/modules/time-series/services/line-chart-data.service.ts @@ -37,7 +37,7 @@ export class LineChartDataService { return this.http.get(url, {params: params}).pipe( this.handleError() - ) + ); } private buildCommand(resultSelectionCommand: ResultSelectionCommand, diff --git a/frontend/src/app/modules/time-series/services/line-chart.service.ts b/frontend/src/app/modules/time-series/services/line-chart.service.ts index c91407d38b..4dea0adfd2 100644 --- a/frontend/src/app/modules/time-series/services/line-chart.service.ts +++ b/frontend/src/app/modules/time-series/services/line-chart.service.ts @@ -39,8 +39,9 @@ export class LineChartService { // D3 margin conventions // > With this convention, all subsequent code can ignore margins. // see: https://bl.ocks.org/mbostock/3019563 - private MARGIN: { [key: string]: number } = {top: 60, right: 60, bottom: 40, left: 75}; + private MARGIN: { [key: string]: number } = {top: 60, right: 60, bottom: 20, left: 75}; private Y_AXIS_WIDTH = 65; + private readonly LEGEND_GROUP_OFFSET = 130; private _margin: { [key: string]: number } = { top: this.MARGIN.top, @@ -50,7 +51,6 @@ export class LineChartService { }; private _width: number = 600 - this._margin.left - this._margin.right; private _height: number = 550 - this._margin.top - this._margin.bottom; - private _legendGroupTop: number = this._margin.top + this._height + 50; private _xScale: D3ScaleTime; constructor(private translationService: TranslateService, @@ -133,8 +133,9 @@ export class LineChartService { } prepareEventsData(events: EventDTO[]): TimeEvent[] { + this.lineChartTimeEventService.clearEventMarkerSelection(); return events.map(eventDto => { - return new TimeEvent(new Date(eventDto.eventDate), eventDto.description, eventDto.shortName); + return new TimeEvent(eventDto.id, new Date(eventDto.eventDate), eventDto.description, eventDto.shortName); }); } @@ -159,7 +160,7 @@ export class LineChartService { d3Select('svg#time-series-chart') .transition() .duration(500) - .attr('height', this._height + legendGroupHeight + this._margin.top + this._margin.bottom); + .attr('height', this._margin.top + this._height + this.LEGEND_GROUP_OFFSET + legendGroupHeight + this._margin.bottom); d3Select('.x-axis').transition().call((transition: D3Transition) => { this.lineChartDrawService.updateXAxis(transition, this._xScale); @@ -171,7 +172,8 @@ export class LineChartService { this.lineChartDomEventService.createContextMenu(); this.lineChartDomEventService.addBrush(chartContentContainer, this._width, this._height, this.Y_AXIS_WIDTH, this._margin, this._xScale, timeSeries, dataTrimValues, this.lineChartLegendService.legendDataMap, eventData); - this.lineChartTimeEventService.addEventMarkerToChart(chart, this._xScale, eventData, this._width, this._height, this._margin); + this.lineChartTimeEventService.addEventTimeLineAndMarkersToChart(chart, this._xScale, eventData, width, + this._height, this._margin); this.lineChartLegendService.addLegendsToChart(chartContentContainer, this._xScale, yScales, timeSeries, timeSeriesAmount); this.lineChartLegendService.setSummaryLabel(chart, summaryLabels, this._width); @@ -234,7 +236,7 @@ export class LineChartService { svg.append('g') .attr('id', 'time-series-chart-legend') .attr('class', 'legend-group') - .attr('transform', `translate(${this._margin.left}, ${this._legendGroupTop})`); + .attr('transform', `translate(${this._margin.left}, ${this._margin.top + this._height + this.LEGEND_GROUP_OFFSET})`); return chart; } @@ -271,7 +273,6 @@ export class LineChartService { } else { this._margin.top = this.MARGIN.top + 20; } - this._legendGroupTop = this._margin.top + this._height + 50; d3Select('#time-series-chart') .attr('width', this._width + this._margin.left + this._margin.right); @@ -283,7 +284,7 @@ export class LineChartService { .attr('transform', `translate(${this._margin.left}, ${this._margin.top})`); d3Select('#time-series-chart-legend') - .attr('transform', `translate(${this._margin.left}, ${this._legendGroupTop})`); + .attr('transform', `translate(${this._margin.left}, ${this._margin.top + this._height + this.LEGEND_GROUP_OFFSET})`); } private addYAxisUnits(measurandGroups: { [key: string]: string }, width: number): void { diff --git a/grails-app/domain/de/iteratec/osm/report/chart/Event.groovy b/grails-app/domain/de/iteratec/osm/report/chart/Event.groovy index 04f56fa2e7..255c8c717a 100644 --- a/grails-app/domain/de/iteratec/osm/report/chart/Event.groovy +++ b/grails-app/domain/de/iteratec/osm/report/chart/Event.groovy @@ -27,6 +27,7 @@ import grails.gorm.annotation.Entity @Entity class Event { + long id Date eventDate String shortName String description diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index ce15ce5239..4e794a8b4b 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -1313,6 +1313,7 @@ frontend.de.iteratec.osm.timeSeries.chart.label.timestamp=Timestamp: frontend.de.iteratec.osm.timeSeries.chart.label.testAgent=Test agent: frontend.de.iteratec.osm.timeSeries.chart.settings.minimum=min frontend.de.iteratec.osm.timeSeries.chart.settings.maximum=max +frontend.de.iteratec.osm.timeSeries.chart.eventTimeLine.label=Events: frontend.de.iteratec.osm.distribution.chart.settings.maximum=Maximum value frontend.de.iteratec.osm.distribution.chart.settings.filter=Filter frontend.de.iteratec.osm.distribution.chart.settings.sort=Sort diff --git a/grails-app/i18n/messages_de.properties b/grails-app/i18n/messages_de.properties index 47d1773b2d..2f1deb8308 100644 --- a/grails-app/i18n/messages_de.properties +++ b/grails-app/i18n/messages_de.properties @@ -1287,6 +1287,7 @@ frontend.de.iteratec.osm.timeSeries.chart.label.timestamp=Zeitstempel: frontend.de.iteratec.osm.timeSeries.chart.label.testAgent=Testagent: frontend.de.iteratec.osm.timeSeries.chart.settings.minimum=min frontend.de.iteratec.osm.timeSeries.chart.settings.maximum=max +frontend.de.iteratec.osm.timeSeries.chart.eventTimeLine.label=Ereignisse: frontend.de.iteratec.osm.distribution.chart.settings.maximum=Maximalwert frontend.de.iteratec.osm.distribution.chart.settings.filter=Filtern frontend.de.iteratec.osm.distribution.chart.settings.sort=Sortierung diff --git a/grails-app/services/de/iteratec/osm/report/chart/EventService.groovy b/grails-app/services/de/iteratec/osm/report/chart/EventService.groovy index f35c624ad8..cb93dfc76b 100644 --- a/grails-app/services/de/iteratec/osm/report/chart/EventService.groovy +++ b/grails-app/services/de/iteratec/osm/report/chart/EventService.groovy @@ -106,6 +106,7 @@ class EventService { List eventList = new ArrayList<>() allEvents.forEach { event -> EventDTO eventDTO = new EventDTO() + eventDTO.id = event.id eventDTO.eventDate = event.eventDate eventDTO.shortName = event.shortName eventDTO.description = event.description diff --git a/src/main/groovy/de/iteratec/osm/report/chart/events/EventDTO.groovy b/src/main/groovy/de/iteratec/osm/report/chart/events/EventDTO.groovy index d756f8cfac..584b9cef10 100644 --- a/src/main/groovy/de/iteratec/osm/report/chart/events/EventDTO.groovy +++ b/src/main/groovy/de/iteratec/osm/report/chart/events/EventDTO.groovy @@ -1,6 +1,7 @@ package de.iteratec.osm.report.chart.events class EventDTO { + long id Date eventDate = null String shortName = "" String description = "" From 44fffb0bb1eee5bd1c6b6e7657b1bdf807d417f1 Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Fri, 26 Jun 2020 18:15:07 +0200 Subject: [PATCH 10/11] Beautify tooltip --- .../time-series-chart.component.scss | 23 +++++++++++++++++++ .../line-chart-time-event.service.ts | 14 +++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss b/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss index 0c4e644f9d..48d07832c8 100644 --- a/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss +++ b/frontend/src/app/modules/time-series/components/time-series-chart/time-series-chart.component.scss @@ -40,6 +40,7 @@ osm-time-series-line-chart { border: 1px solid rgba(0, 0, 0, 0.4); border-radius: 3px; pointer-events: none; + max-width: 250px; } #time-series-chart-drawing-area { @@ -211,3 +212,25 @@ osm-time-series-line-chart { border-right: 0; } } + +.grid-container { + display: grid; +} + +.event-marker-tooltip-element { + padding: 0.2em 0.75em 0.2em 0.75em; +} + +.event-marker-tooltip-date { + margin-bottom: 10px; +} + +.event-marker-tooltip-title { + font-weight: bold; + margin: 0; +} + +.event-marker-tooltip-text { + margin: 0; + text-align: justify; +} diff --git a/frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts b/frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts index b1c08c399e..7a14d1eeb1 100644 --- a/frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts +++ b/frontend/src/app/modules/time-series/services/chart-services/line-chart-time-event.service.ts @@ -117,11 +117,11 @@ export class LineChartTimeEventService { eventMarkerTooltipBox.html(this.createEventMarkerTooltipContent(event).outerHTML); const circle = d3Select(nodes[index]); - const top = parseFloat(circle.attr('cy')) + margin.top; + const top = parseFloat(circle.attr('cy')) + 5; const tooltipWidth: number = (eventMarkerTooltipBox.node()).getBoundingClientRect().width; const xPos = parseFloat(circle.attr('cx')); - const left = (tooltipWidth + xPos > width) ? xPos - tooltipWidth + margin.right + 10 : xPos + margin.left + 50; + const left = (tooltipWidth + xPos > width) ? xPos - tooltipWidth + margin.right - 10 : xPos + margin.left + 25; eventMarkerTooltipBox.style('top', top + 'px'); eventMarkerTooltipBox.style('left', left + 'px'); @@ -146,21 +146,25 @@ export class LineChartTimeEventService { private createEventMarkerTooltipContent(event: EventDTO): HTMLDivElement { const container: HTMLDivElement = document.createElement('div'); - container.className = 'gridContainer'; + container.className = 'grid-container'; const dateItem = document.createElement('div'); + dateItem.className = 'event-marker-tooltip-element event-marker-tooltip-date'; dateItem.append(event.eventDate.toLocaleString()); container.append(dateItem); const shortNameItem = document.createElement('div'); + shortNameItem.className = 'event-marker-tooltip-element'; const shortNameParagraph = document.createElement('p'); - shortNameParagraph.style.fontWeight = 'bold'; - shortNameParagraph.append(event.shortName + ':'); + shortNameParagraph.className = 'event-marker-tooltip-title'; + shortNameParagraph.append(`${event.shortName}:`); shortNameItem.append(shortNameParagraph); container.append(shortNameItem); const descriptionItem = document.createElement('div'); + descriptionItem.className = 'event-marker-tooltip-element'; const descriptionParagraph = document.createElement('p'); + descriptionParagraph.className = 'event-marker-tooltip-text'; descriptionParagraph.append(event.description); descriptionItem.append(descriptionParagraph); container.append(descriptionItem); From d1c41d550853888c509e34a1089c02a23c46a7d4 Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Mon, 29 Jun 2020 18:58:37 +0200 Subject: [PATCH 11/11] Change id type of event entity --- grails-app/domain/de/iteratec/osm/report/chart/Event.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grails-app/domain/de/iteratec/osm/report/chart/Event.groovy b/grails-app/domain/de/iteratec/osm/report/chart/Event.groovy index 255c8c717a..3ed0fac9ec 100644 --- a/grails-app/domain/de/iteratec/osm/report/chart/Event.groovy +++ b/grails-app/domain/de/iteratec/osm/report/chart/Event.groovy @@ -27,7 +27,7 @@ import grails.gorm.annotation.Entity @Entity class Event { - long id + Long id Date eventDate String shortName String description