diff --git a/build.gradle b/build.gradle index ca6b6da643..e39c0a7fca 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,7 @@ buildscript { } } -version "5.3.2" +version "5.3.3" group "OpenSpeedMonitor" apply plugin: "eclipse" diff --git a/frontend/src/app/enums/chart-commons.enum.ts b/frontend/src/app/enums/chart-commons.enum.ts index d3f29e91d1..e27ac3583e 100644 --- a/frontend/src/app/enums/chart-commons.enum.ts +++ b/frontend/src/app/enums/chart-commons.enum.ts @@ -6,5 +6,5 @@ export enum ChartCommons { CHART_HEADER_HEIGHT = 40, COLOR_PREVIEW_SIZE = 10, COLOR_PREVIEW_MARGIN = 5, - LABEL_HEIGHT = 30 + LABEL_HEIGHT = 18 } 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 1c996b630a..6f84a12c9a 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 @@ -67,3 +67,11 @@ osm-time-series-line-chart { .summary-label { font-weight: normal; } + +.legend-entry { + cursor: pointer; +} + +.legend-text { + font-size: 12px; +} 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 a049ee4822..3caf23c59d 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 @@ -38,7 +38,7 @@ import { import 'd3-transition'; -import {brushX as d3BrushX} from 'd3-brush'; +import {BrushBehavior, 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'; @@ -63,13 +63,17 @@ export class LineChartService { // D3 margin conventions // > With this convention, all subsequent code can ignore margins. // see: https://bl.ocks.org/mbostock/3019563 - private _margin = {top: 40, right: 70, bottom: 40, left: 60}; - private _width = 600 - this._margin.left - this._margin.right; - private _height = 550 - this._margin.top - this._margin.bottom; - private _labelGroupHeight; - private _legendGroupTop = this._margin.top + this._height + 50; - private legendDataMap = {}; - private brush; + private _margin: any = {top: 40, right: 70, bottom: 40, left: 60}; + private _width: number = 600 - this._margin.left - this._margin.right; + private _height: number = 550 - this._margin.top - this._margin.bottom; + private _labelGroupHeight: number; + private _legendGroupTop: number = this._margin.top + this._height + 50; + private _legendGroupHeight: number; + private _legendGroupColumnWidth: number; + private _legendGroupColumns: number; + private legendDataMap: Object = {}; + private brush: BrushBehavior<{}>; + private focusedLegendEntry: string; // Map that holds all points clustered by their x-axis values private _xAxisCluster: any = {}; @@ -78,7 +82,6 @@ export class LineChartService { private _mouseEventCatcher: D3Selection; private _markerTooltip: D3Selection; - constructor(private translationService: TranslateService) { } @@ -98,19 +101,17 @@ export class LineChartService { * Draws a line chart for the given data into the given svg */ public drawLineChart(incomingData: EventResultDataDTO): void { - - let data: TimeSeries[] = this.prepareData(incomingData); - - if (data.length == 0) { + if (incomingData.series.length == 0) { return; } + let data: TimeSeries[] = this.prepareData(incomingData); let chart: D3Selection = d3Select('g#time-series-chart-drawing-area'); let xScale: D3ScaleTime = this.getXScale(data); let yScale: D3ScaleLinear = this.getYScale(data); - this._labelGroupHeight = data.length * ChartCommons.LABEL_HEIGHT; +this.calculateLegendDimensions(); d3Select('osm-time-series-line-chart').transition().duration(500).style('visibility', 'visible'); - d3Select('svg#time-series-chart').transition().duration(500).attr('height', this._height + this._labelGroupHeight + this._margin.top + this._margin.bottom); + d3Select('svg#time-series-chart').transition().duration(500).attr('height', this._height + this._legendGroupHeight + this._margin.top + this._margin.bottom); 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]]); @@ -135,6 +136,10 @@ export class LineChartService { * Set the data for the legend after the incoming data is received */ public setLegendData(incomingData: EventResultDataDTO) { + if (incomingData.series.length == 0) { + return; + } + let labelDataMap = {}; incomingData.series.forEach((data: EventResultSeriesDTO) => { if (incomingData.summaryLabels.length > 0 && incomingData.summaryLabels[0].key != "measurand") { @@ -209,6 +214,7 @@ export class LineChartService { .attr('transform', `translate(${this._margin.left}, ${this._margin.top})`); // translates the origin to the top left corner (default behavior of D3) svg.append('g') + .attr('id', 'time-series-chart-legend') .attr('class', 'legend-group') .attr('transform', `translate(${this._margin.left}, ${this._legendGroupTop})`); @@ -314,6 +320,34 @@ export class LineChartService { .nice(); } + private calculateLegendDimensions(): void { + let maximumLabelWidth: number = 1; + let labels = Object.keys(this.legendDataMap); + + d3Select('g#time-series-chart-legend') + .append('g') + .attr('id', 'renderToCalculateMaxWidth') + .selectAll('.renderToCalculateMaxWidth') + .data(labels) + .enter() + .append('text') + .attr('class', 'legend-text') + .text(datum => this.legendDataMap[datum].text) + .each((datum, index, groups) => { + Array.from(groups).forEach((text) => { + if (text) { + maximumLabelWidth = Math.max(maximumLabelWidth, text.getBoundingClientRect().width) + } + }); + }); + + d3Select('g#renderToCalculateMaxWidth').remove(); + + this._legendGroupColumnWidth = maximumLabelWidth + ChartCommons.COLOR_PREVIEW_SIZE + 30; + this._legendGroupColumns = Math.floor(this._width / this._legendGroupColumnWidth); + this._legendGroupHeight = Math.ceil(labels.length / this._legendGroupColumns) * ChartCommons.LABEL_HEIGHT + 30; + } + private getMaxValue(data: TimeSeries[]): number { return d3Max(data, (dataItem: TimeSeries) => { return d3Max(dataItem.values, (point: TimeSeriesPoint) => { @@ -825,29 +859,36 @@ export class LineChartService { .style('opacity', 0) .remove() ) - .attr("transform", (datum, index) => this.position(index)) + .attr("transform", (datum, index) => this.getPosition(index)) .on('click', (datum) => this.onMouseClick(datum, incomingData)); } - private position(index: number): string { - const columns = 3; - const columnWidth = 550; - const xMargin = 10; - const yMargin = 10; - - const x = index % columns * columnWidth + xMargin; - const y = Math.floor(index / columns) * ChartCommons.LABEL_HEIGHT + yMargin; + private getPosition(index: number): string { + const x = index % this._legendGroupColumns * this._legendGroupColumnWidth; + const y = Math.floor(index / this._legendGroupColumns) * ChartCommons.LABEL_HEIGHT + 12; return "translate(" + x + "," + y + ")"; } private onMouseClick(labelKey: string, incomingData: EventResultDataDTO): void { - if (d3Event.metaKey) { - this.legendDataMap[labelKey].show ? this.legendDataMap[labelKey].show = false : this.legendDataMap[labelKey].show = true; + if (d3Event.metaKey || d3Event.ctrlKey) { + this.legendDataMap[labelKey].show = !this.legendDataMap[labelKey].show; } else { - Object.keys(this.legendDataMap).forEach((legend) => { - this.legendDataMap[legend].show = legend === labelKey; - }); + if (labelKey == this.focusedLegendEntry) { + Object.keys(this.legendDataMap).forEach((legend) => { + this.legendDataMap[legend].show = true; + }); + this.focusedLegendEntry = ""; + } else { + Object.keys(this.legendDataMap).forEach((legend) => { + if (legend === labelKey) { + this.legendDataMap[legend].show = true; + this.focusedLegendEntry = legend; + } else { + this.legendDataMap[legend].show = false; + } + }); + } } this.drawLineChart(incomingData); } diff --git a/grails-app/controllers/de/iteratec/osm/csi/CsiBenchmarkController.groovy b/grails-app/controllers/de/iteratec/osm/csi/CsiBenchmarkController.groovy index 67a7ba6a5b..5a536f78bf 100644 --- a/grails-app/controllers/de/iteratec/osm/csi/CsiBenchmarkController.groovy +++ b/grails-app/controllers/de/iteratec/osm/csi/CsiBenchmarkController.groovy @@ -53,7 +53,7 @@ class CsiBenchmarkController extends ExceptionHandlerController { } String selectedCsiType = params.csiType == "visuallyComplete" ? 'csByWptVisuallyCompleteInPercent' : 'csByWptDocCompleteInPercent' - List allJobGroups = JobGroup.findAllByNameInList(cmd.jobGroups) + List allJobGroups = JobGroup.findAllByIdInList(cmd.jobGroups) CsiAggregationInterval interval = CsiAggregationInterval.findByIntervalInMinutes(CsiAggregationInterval.DAILY) diff --git a/grails-app/controllers/de/iteratec/osm/result/PageComparisonController.groovy b/grails-app/controllers/de/iteratec/osm/result/PageComparisonController.groovy index 8f84327982..8178727dcb 100644 --- a/grails-app/controllers/de/iteratec/osm/result/PageComparisonController.groovy +++ b/grails-app/controllers/de/iteratec/osm/result/PageComparisonController.groovy @@ -64,14 +64,14 @@ class PageComparisonController extends ExceptionHandlerController { BarchartDatum comparativeSeries = mapToSeriesFor(aggregation.comperativeAggregation) return new BarchartSeries( stacked: false, - dimensionalUnit: aggregation.baseAggregation.selectedMeasurand.getMeasurandGroup().unit.label, + dimensionalUnit: aggregation.baseAggregation.unit, data: [baseSeries, comparativeSeries] ) } private BarchartDatum mapToSeriesFor(BarchartAggregation aggregation) { return new BarchartDatum( - measurand: i18nService.msg("de.iteratec.isr.measurand.${aggregation.selectedMeasurand.name}", aggregation.selectedMeasurand.name), + measurand: i18nService.msg("de.iteratec.isr.measurand.${aggregation.measurandName}", aggregation.measurandName), value: aggregation.value, aggregationValue: aggregation.aggregationValue, grouping: "${aggregation.jobGroup.name} | ${aggregation.page.name}") diff --git a/grails-app/views/csiBenchmark/show.gsp b/grails-app/views/csiBenchmark/show.gsp index e8c72d6116..ffa6e9a8c8 100644 --- a/grails-app/views/csiBenchmark/show.gsp +++ b/grails-app/views/csiBenchmark/show.gsp @@ -106,8 +106,8 @@ data: { from: selectedTimeFrame[0].toISOString(), to: selectedTimeFrame[1].toISOString(), - selectedJobGroups: JSON.stringify($.map($("#folderSelectHtmlId option:selected"), function (e) { - return $(e).text() + jobGroups: JSON.stringify($.map($("#folderSelectHtmlId option:selected"), function (e) { + return parseInt(e.value); })), csiType: $('input[name=csiTypeRadios]:checked').val() },