<commit_before>import {Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild} from '@angular/core';
import {select} from "d3-selection";
import {ScaleBand, scaleBand, ScaleLinear, scaleLinear} from "d3-scale";
import {ChartCommons} from "../../../../enums/chart-commons.enum";
import {max} from "d3-array";
import {AggregationChartDataService} from "../../services/aggregation-chart-data.service";
import {BarchartDataService} from "../../services/barchart-data.service";
import {ResultSelectionStore} from "../../../result-selection/services/result-selection.store";
import {AggregationChartDataByMeasurand} from "../../models/aggregation-chart-data.model";

@Component({
  selector: 'osm-aggregation-chart',
  templateUrl: './aggregation-chart.component.html',
  styleUrls: ['./aggregation-chart.component.scss']
})
export class AggregationChartComponent implements OnChanges {

  @ViewChild('svg') svgElement: ElementRef;
  @Input() barchartAverageData;
  @Input() barchartMedianData;

  hasFilterRules: boolean = false;
  percentileValue: number = 50;

  svgWidth: number;
  svgHeight: number;
  private xScale: ScaleLinear<number, number>;
  private yScale: ScaleBand<string>;

  private sideLabelWidth: number;
  private barsWidth: number;
  private barsHeight: number;

  private headerHeight: number;
  private barScorePosY: number;
  private barScoreHeight: number;
  private legendPosY: number;
  private legendHeight: number;

  private dataForScoreBar:{min: number, max: number, barsToRender: Array<any>};
  private measurandDataMap: AggregationChartDataByMeasurand = {};
  private filterRules = {};
  private dataForHeader: string = '';
  private sideLabels:string[] = [];
  private anyHighlighted:boolean = false;
  private anySelected:boolean = false;
  private clickedMeasurand:string = '';

  constructor(private aggregationChartDataService: AggregationChartDataService, private barchartDataService: BarchartDataService, private resultSelectionStore: ResultSelectionStore) {
  }

  redraw() {
    if (this.barchartAverageData.length< 1 ||this.barchartMedianData.length< 1) {
      return;
    }

    this.aggregationChartDataService.setData();
    this.filterRules = this.aggregationChartDataService.filterRules;
    this.hasFilterRules = Object.keys(this.filterRules).length > 0;
    this.measurandDataMap = this.aggregationChartDataService.allMeasurandDataMap;
    this.dataForScoreBar = this.aggregationChartDataService.dataForScoreBar;
    this.dataForHeader = this.aggregationChartDataService.dataForHeader;
    this.sideLabels = this.aggregationChartDataService.uniqueSideLabels;
    if(this.clickedMeasurand.length > 0 && Object.keys(this.measurandDataMap).includes(this.clickedMeasurand)){
      this.measurandDataMap[this.clickedMeasurand].selected = true;
    }else{
      this.anySelected = false;
    }

    this.svgWidth = this.svgElement.nativeElement.getBoundingClientRect().width;
    this.svgHeight = this.svgElement.nativeElement.parentElement.offsetHeight;

    this.sideLabelWidth = max(this.getTextWidths(this.svgElement.nativeElement, this.sideLabels));
    this.barsWidth = this.svgWidth - 2 * ChartCommons.COMPONENT_MARGIN - this.sideLabelWidth;
    this.barsHeight = this.calculateChartBarsHeight();

    this.headerHeight = ChartCommons.CHART_HEADER_HEIGHT + ChartCommons.COMPONENT_MARGIN;
    this.barScorePosY = this.barsHeight + ChartCommons.COMPONENT_MARGIN;
    this.barScoreHeight = ChartCommons.BAR_BAND + ChartCommons.COMPONENT_MARGIN;
    this.legendPosY = this.barScorePosY + this.barScoreHeight + ChartCommons.COMPONENT_MARGIN;
    this.legendHeight = this.estimateHeight(this.svgElement.nativeElement) + ChartCommons.COMPONENT_MARGIN;

    this.svgHeight = this.legendPosY + this.legendHeight + this.headerHeight;
    this.svgElement.nativeElement.setAttribute('height', this.svgHeight);

    this.xScale = scaleLinear()
      .domain([0, max(this.aggregationChartDataService.series.map(it => it.value))])
      .range([0, this.barsWidth]);

    this.yScale = scaleBand()
      .domain(this.sideLabels)
      .range([0, this.barsHeight]);

    this.render();
  }

  render() {
    const svgElement: SVGElement = this.svgElement.nativeElement;

    this.renderHeader(svgElement);
    this.renderSideLabels(svgElement);
    this.renderBarsContent(svgElement);
  }

  private renderHeader(svgElement: SVGElement) {
    const header = select(svgElement).selectAll('.header-group').data([this.dataForHeader]);
    header.join(
      enter => enter
        .append('g')
        .attr('class', 'header-group'),
      update => update,
      exit => exit.remove()
    );

    const headerText = select('.header-group').selectAll('.header-text').data([this.dataForHeader]);
    headerText.join(
      enter => enter
        .append('text')
        .attr('class', 'header-text')
        .attr('text-anchor', 'middle')
        .attr('dominant-baseline', 'alphabetic')
        .style('opacity', 0),
      update => update,
      exit => exit
        .style('opacity', 0)
        .transition()
        .duration(ChartCommons.TRANSITION_DURATION)
        .remove()
    )
      .attr('x', this.svgWidth / 2)
      .attr('y', ChartCommons.CHART_HEADER_HEIGHT)
      .transition()
      .duration(ChartCommons.TRANSITION_DURATION)
      .text(datum => datum)
      .style('opacity', 1);
  }

  private renderSideLabels(svgElement: SVGElement) {
    const sideLabelGroup = select(svgElement).selectAll('.side-labels-group').data(this.sideLabels);
    sideLabelGroup.join(
      enter => enter
        .append('g')
        .attr('class', 'side-labels-group'),
      update => update,
      exit => exit.remove()
    )
      .attr('transform', `translate(0, ${this.headerHeight})`);

    const sideLabels = select('.side-labels-group').selectAll('.side-label').data(this.sideLabels);
    sideLabels.join(
      enter => enter
        .append('text')
        .attr('class', 'side-label')
        .attr('dominant-baseline', 'middle')
        .attr('x', 0)
        .style('opacity', 0),
      update => update,
      exit => exit
        .transition()
        .duration(ChartCommons.TRANSITION_DURATION)
        .style('opacity', 0)
        .remove()
    )
      .text(datum => datum)
      .transition()
      .duration(ChartCommons.TRANSITION_DURATION)
      .style('opacity', 1)
      .attr('y', datum => this.yScale(datum) + this.yScale.bandwidth() / 2)
  }

  private renderBarsContent(svgElement: SVGElement) {
    const contentGroup = select(svgElement).selectAll('.bars-content-group').data([1]);
    contentGroup.join(
      enter => enter
        .append('g')
        .attr('class', 'bars-content-group'),
      update => update,
      exit => exit.remove()
    )
      .attr('transform', `translate(${this.sideLabelWidth + ChartCommons.COMPONENT_MARGIN}, ${ChartCommons.CHART_HEADER_HEIGHT + ChartCommons.COMPONENT_MARGIN})`);

    const contentGroupSelector: string = '.bars-content-group';

    this.renderBarGroup(contentGroupSelector);
    this.renderChartScoreGroup(contentGroupSelector);
    this.renderLegendGroup(contentGroupSelector);
  }

  private renderBarGroup(contentGroupSelector: string) {
    const barGroup = select(contentGroupSelector).selectAll('.chart-bar-group').data([1]);
    barGroup.join(
      enter => enter
        .append('g')
        .attr('class', 'chart-bar-group'),
      update => update,
      exit => exit.remove()
    );

    const chartBars = select('.chart-bar-group').selectAll('.chart-bars').data(Object.keys(this.measurandDataMap));
    const barOffset = this.aggregationChartDataService.stackBars ? 0 : ChartCommons.BAR_BAND;

    chartBars.join(
      enter => enter
        .append('g')
        .attr('class', 'chart-bars'),
      update => update,
      exit => exit
        .transition()
        .duration(ChartCommons.TRANSITION_DURATION)
        .attr('transform', 'translate(0, 0)')
        .style('opacity', 0)
        .remove()
    )
      .transition()
      .duration(ChartCommons.TRANSITION_DURATION)
      .attr('transform', (datum, index) => `translate(0, ${index * barOffset})`)
      .each((datum, index, groups) => this.renderBar(select(groups[index]), datum));
  }

  private renderBar(chartBarSelection, measurand: string) {
    const bar = chartBarSelection.selectAll('.bar').data(this.measurandDataMap[measurand].series);
    bar.join(
      enter => {
        const barElement = enter
          .append('g')
          .attr('class', 'bar')
          .style('opacity', () => {return ((this.anyHighlighted && !this.measurandDataMap[measurand].highlighted) || (this.anySelected && !this.measurandDataMap[measurand].selected)) ? 0.2 : 1});
        barElement
          .append('rect')
          .attr('class', 'bar-rect')
          .attr('x', 0)
          .attr('y', 0)
          .attr('height', ChartCommons.BAR_BAND)
          .attr('fill', this.measurandDataMap[measurand].color)
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .attr('x', datum => this.barStart(this.xScale, datum.value))
          .attr('y', datum => this.yScale(datum.sideLabel))
          .attr('width', datum => this.barWidth(this.xScale, datum.value));
        barElement
          .append('text')
          .attr('class', 'bar-value')
          .attr('dominant-baseline', 'middle')
          .style('fill', 'white')
          .style('font-weight', 'bold')
          .style('opacity', 0)
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .text(datum => `${this.formatBarValue(datum.value)} ${datum.unit}`)
          .attr('x', datum => (datum.value < 0) ? (this.barStart(this.xScale, datum.value) + 10) : (this.barEnd(this.xScale, datum.value) - 10))
          .attr('y', datum => (this.yScale(datum.sideLabel) + ChartCommons.BAR_BAND / 2))
          .attr('text-anchor', datum => (datum.value < 0) ? 'start' : 'end')
          .style('opacity', (datum, index, groups) => ((groups[index].getComputedTextLength() + 2 * 10) > this.barWidth(this.xScale, datum.value)) ? 0 : 1);
        return barElement;
      },
      update => {
        update
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .style('opacity', () => {return ((this.anyHighlighted && !this.measurandDataMap[measurand].highlighted) || (this.anySelected && !this.measurandDataMap[measurand].selected)) ? 0.2 : 1});

        update.select('.bar-rect')
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .attr('x', datum => this.barStart(this.xScale, datum.value))
          .attr('y', datum => this.yScale(datum.sideLabel))
          .attr('width', datum => this.barWidth(this.xScale, datum.value))
          .attr('fill', this.measurandDataMap[measurand].color);
        update.select('.bar-value')
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .text(datum => `${this.formatBarValue(datum.value)} ${datum.unit}`)
          .attr('x', datum => (datum.value < 0) ? (this.barStart(this.xScale, datum.value) + 10) : (this.barEnd(this.xScale, datum.value) - 10))
          .attr('y', datum => (this.yScale(datum.sideLabel) + ChartCommons.BAR_BAND / 2))
          .attr('text-anchor', datum => (datum.value < 0) ? 'start' : 'end')
          .style('opacity', (datum, index, groups) => ((groups[index].getComputedTextLength() + 2 * 10) > this.barWidth(this.xScale, datum.value)) ? 0 : 1);
        return update;
      },
      exit => exit
        .transition()
        .duration(ChartCommons.TRANSITION_DURATION)
        .style('opacity', 0)
        .remove()
    )
      .on('click', () => this.onMouseClick(measurand));
  }

  private renderChartScoreGroup(contentGroupSelector: string) {
    const scoreGroup = select(contentGroupSelector).selectAll('.chart-score-group').data([1]);
    scoreGroup.join(
      enter => enter
        .append('g')
        .attr('class', 'chart-score-group'),
      update => update,
      exit => exit.remove()
    )
      .attr('transform', `translate(0, ${this.barScorePosY})`);

    const scoreBars = select('.chart-score-group').selectAll('.score-bar').data(this.dataForScoreBar.barsToRender);
    const scaleForScoreBar = scaleLinear().rangeRound([0, this.barsWidth]).domain([this.dataForScoreBar.min, this.dataForScoreBar.max]);

    scoreBars.join(
      enter => {
        const scoreBarElement = enter
          .append('g')
          .attr('class', 'score-bar');
        scoreBarElement
          .append('rect')
          .attr('class', 'score-rect')
          .attr('height', ChartCommons.BAR_BAND)
          .attr('width', 0)
          .attr('x', 0)
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .attr('fill', datum => datum.fill)
          .attr('width', datum => scaleForScoreBar(datum.end) - scaleForScoreBar(datum.start))
          .attr('x', datum => scaleForScoreBar(datum.start));
        scoreBarElement
          .append('text')
          .attr('class', 'score-value')
          .attr('dominant-baseline', 'middle')
          .attr('text-anchor', "middle")
          .attr('x', 0)
          .attr('y', ChartCommons.BAR_BAND / 2)
          .style('opacity', 0)
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .text(datum => datum.label)
          .style('opacity', (datum, index, groups) => groups[index].getComputedTextLength() + 20 > (scaleForScoreBar(datum.end) - scaleForScoreBar(datum.start)) / 2 ? 0 : 1)
          .attr('x', datum => (scaleForScoreBar(datum.end) + scaleForScoreBar(datum.start)) / 2);
        return scoreBarElement;
      },
      update => {
        update.select('.score-rect')
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .attr('fill', datum => datum.fill)
          .attr('width', datum => scaleForScoreBar(datum.end) - scaleForScoreBar(datum.start))
          .attr('x', datum => scaleForScoreBar(datum.start));
        update.select<SVGTextElement>('.score-value')
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .text(datum => datum.label)
          .style('opacity', (datum, index, groups) => groups[index].getComputedTextLength() + 20 > (scaleForScoreBar(datum.end) - scaleForScoreBar(datum.start)) / 2 ? 0 : 1)
          .attr('x', datum => (scaleForScoreBar(datum.end) + scaleForScoreBar(datum.start)) / 2);
        return update;
      },
      exit => exit
        .transition()
        .duration(ChartCommons.TRANSITION_DURATION)
        .style('opacity', 0)
        .remove()
    );
  }

  private renderLegendGroup(contentGroupSelector: string) {
    const legendGroup = select(contentGroupSelector).selectAll('.chart-legend-group').data([1]);
    legendGroup.join(
      enter => enter
        .append('g')
        .attr('class', 'chart-legend-group')
        .style('cursor', 'pointer'),
      update => update,
      exit => exit.remove()
    )
      .attr('transform', `translate(0, ${this.legendPosY})`);

    const legendEntry = select('.chart-legend-group').selectAll('.legend-entry').data(Object.keys(this.measurandDataMap));
    const maxEntryGroupSize = this.calculateMaxEntryGroupWidth(this.svgElement.nativeElement);
    const maxEntriesInRow = Math.floor(this.svgWidth / maxEntryGroupSize);

    legendEntry.join(
      enter => {
        const legendElement = enter
          .append('g')
          .attr('class', 'legend-entry')
          .style('opacity', (datum)=>{return ((this.anyHighlighted && !this.measurandDataMap[datum].highlighted) || (this.anySelected && !this.measurandDataMap[datum].selected)) ? 0.2 : 1});
        legendElement
          .append('rect')
          .attr('class', 'legend-rect')
          .attr('height', ChartCommons.COLOR_PREVIEW_SIZE)
          .attr('width', ChartCommons.COLOR_PREVIEW_SIZE)
          .attr("rx", 2)
          .attr("ry", 2)
          .attr('fill', datum => this.measurandDataMap[datum].color);
        legendElement
          .append('text')
          .attr('class', 'legend-text')
          .attr('x', ChartCommons.COLOR_PREVIEW_SIZE + ChartCommons.COLOR_PREVIEW_MARGIN)
          .attr('y', ChartCommons.COLOR_PREVIEW_SIZE)
          .text(datum => this.measurandDataMap[datum].label);
        return legendElement;
      },
      update => {
        update
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .style('opacity', (datum)=>{return ((this.anyHighlighted && !this.measurandDataMap[datum].highlighted) || (this.anySelected && !this.measurandDataMap[datum].selected)) ? 0.2 : 1});

        update.select('.legend-rect')
          .attr('fill', datum => this.measurandDataMap[datum].color);
        update.select('.legend-text')
          .text(datum => this.measurandDataMap[datum].label);
        return update;
      },
        exit => exit
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .style('opacity', 0)
          .remove()
    )
      .attr('transform', (datum, index) => `translate(${maxEntryGroupSize * (index % maxEntriesInRow)}, 0)`)
      .on('mouseover', (datum) => this.onMouseOver(datum))
      .on('mouseout', (datum) => this.onMouseOut(datum))
      .on('click', (datum) => this.onMouseClick(datum));
  }


  formatBarValue(value: string): string {
    const precision = this.dataForScoreBar.max >= 1000 || this.dataForScoreBar.min <= -1000 ? 0 : 2;
    return parseFloat(value).toFixed(precision).toString();
  }

  estimateHeight(svgForEstimation: SVGElement): number {
    const maxEntryGroupSize = this.calculateMaxEntryGroupWidth(svgForEstimation);
    const maxEntriesInRow = Math.floor(this.svgWidth / maxEntryGroupSize);
    return Math.floor(this.aggregationChartDataService.series.length / maxEntriesInRow) * 20;
  }

  calculateMaxEntryGroupWidth(svgForEstimation: SVGElement): number {
    let dataMap = this.measurandDataMap;
    const labels = Object.keys(dataMap).map(measurand => dataMap[measurand].label);
    const labelWidths = this.getTextWidths(svgForEstimation, labels);
    return max(labelWidths) + 10 + 20 + 5;
  }

  getTextWidths(svgForEstimation: SVGElement, texts: string[]): number[] {
    const widths = [];
    select(svgForEstimation).selectAll('.invisible-text-to-measure')
      .data(texts)
      .enter()
      .append("text")
      .attr("opacity", 0)
      .text((d) =>  d.toString())
      .each(function () {
        widths.push(this.getComputedTextLength());
        this.remove();
      });
    return widths;
  }

  barWidth(xScale, value) {
    return value === null ? 0 : (this.barEnd(xScale, value) - this.barStart(xScale, value));
  }

  barEnd(xScale, value) {
    return (value < 0) ? xScale(0) : xScale(value);
  }

  barStart(xScale, value) {
    return (value < 0) ? xScale(value) : xScale(0);
  }

  calculateChartBarsHeight(): number {
    const barBand = ChartCommons.BAR_BAND;
    const barGap = ChartCommons.BAR_GAP;
    const numberOfMeasurands = Object.keys(this.measurandDataMap).length;
    let numberOfPages = 0;
    Object.keys(this.measurandDataMap).forEach((k) => {
      numberOfPages = (this.measurandDataMap[k].series.length > numberOfPages) ? this.measurandDataMap[k].series.length : numberOfPages;
    });
    const numberOfBars = numberOfPages * (this.aggregationChartDataService.stackBars ? 1 : numberOfMeasurands);
    const gapSize = barGap * ((this.aggregationChartDataService.stackBars || numberOfMeasurands < 2) ? 1 : 2);
    return ((numberOfPages - 1) * gapSize) + numberOfBars * barBand;
  }

  private onMouseOver(measurand: string): void {
    if(this.anySelected===false) {
      this.anyHighlighted = true;
      this.measurandDataMap[measurand].highlighted = true;
      this.renderBarGroup('.bars-content-group');
      this.renderLegendGroup('.legend-content-group');
    }
  }

  private onMouseOut(measurand: string): void {
    if(this.anyHighlighted === true) {
      this.anyHighlighted = false;
      this.measurandDataMap[measurand].highlighted = false;
      this.renderBarGroup('.bars-content-group');
      this.renderLegendGroup('.legend-content-group');
    }
  }

  private onMouseClick(measurand: string): void {
    if (this.anySelected === false) {
      this.measurandDataMap[measurand].selected = true;
      this.clickedMeasurand = measurand;
      this.anySelected = true;
    } else if(this.anySelected === true && this.clickedMeasurand !== measurand){
      this.measurandDataMap[this.clickedMeasurand].selected = false;
      this.measurandDataMap[measurand].selected = true;
      this.clickedMeasurand = measurand;
    }
    else{
      this.anySelected = false;
      this.measurandDataMap[measurand].selected = false;
      this.clickedMeasurand = '';
    }
    this.renderBarGroup('.bars-content-group');
    this.renderLegendGroup('.legend-content-group');
  }

  changeStackBars(status: string): void {
    this.aggregationChartDataService.stackBars = (status === 'true');
  }

  reloadPercentile(): void {
    if(this.percentileValue === null || this.percentileValue < 1){
      this.percentileValue = 1;
    }else if(this.percentileValue > 100){
      this.percentileValue = 100;
    }
    this.aggregationChartDataService.reloadPercentile(this.percentileValue, this.resultSelectionStore.resultSelectionCommand, this.resultSelectionStore.remainingResultSelection);
  }

  selectFilter(filter: string): void{
    this.aggregationChartDataService.selectedFilter = filter;
    this.redraw();
  }

  ngOnChanges(changes: SimpleChanges): void {
    this.redraw();
  }

}
<commit_msg>Make aggregationChartDataService public to access data from within template<commit_after>import {Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild} from '@angular/core';
import {select} from "d3-selection";
import {ScaleBand, scaleBand, ScaleLinear, scaleLinear} from "d3-scale";
import {ChartCommons} from "../../../../enums/chart-commons.enum";
import {max} from "d3-array";
import {AggregationChartDataService} from "../../services/aggregation-chart-data.service";
import {BarchartDataService} from "../../services/barchart-data.service";
import {ResultSelectionStore} from "../../../result-selection/services/result-selection.store";
import {AggregationChartDataByMeasurand} from "../../models/aggregation-chart-data.model";

@Component({
  selector: 'osm-aggregation-chart',
  templateUrl: './aggregation-chart.component.html',
  styleUrls: ['./aggregation-chart.component.scss']
})
export class AggregationChartComponent implements OnChanges {

  @ViewChild('svg') svgElement: ElementRef;
  @Input() barchartAverageData;
  @Input() barchartMedianData;

  hasFilterRules: boolean = false;
  percentileValue: number = 50;

  svgWidth: number;
  svgHeight: number;
  private xScale: ScaleLinear<number, number>;
  private yScale: ScaleBand<string>;

  private sideLabelWidth: number;
  private barsWidth: number;
  private barsHeight: number;

  private headerHeight: number;
  private barScorePosY: number;
  private barScoreHeight: number;
  private legendPosY: number;
  private legendHeight: number;

  private dataForScoreBar:{min: number, max: number, barsToRender: Array<any>};
  private measurandDataMap: AggregationChartDataByMeasurand = {};
  private filterRules = {};
  private dataForHeader: string = '';
  private sideLabels:string[] = [];
  private anyHighlighted:boolean = false;
  private anySelected:boolean = false;
  private clickedMeasurand:string = '';

  constructor(public aggregationChartDataService: AggregationChartDataService, private barchartDataService: BarchartDataService, private resultSelectionStore: ResultSelectionStore) {
  }

  redraw() {
    if (this.barchartAverageData.length< 1 ||this.barchartMedianData.length< 1) {
      return;
    }

    this.aggregationChartDataService.setData();
    this.filterRules = this.aggregationChartDataService.filterRules;
    this.hasFilterRules = Object.keys(this.filterRules).length > 0;
    this.measurandDataMap = this.aggregationChartDataService.allMeasurandDataMap;
    this.dataForScoreBar = this.aggregationChartDataService.dataForScoreBar;
    this.dataForHeader = this.aggregationChartDataService.dataForHeader;
    this.sideLabels = this.aggregationChartDataService.uniqueSideLabels;
    if(this.clickedMeasurand.length > 0 && Object.keys(this.measurandDataMap).includes(this.clickedMeasurand)){
      this.measurandDataMap[this.clickedMeasurand].selected = true;
    }else{
      this.anySelected = false;
    }

    this.svgWidth = this.svgElement.nativeElement.getBoundingClientRect().width;
    this.svgHeight = this.svgElement.nativeElement.parentElement.offsetHeight;

    this.sideLabelWidth = max(this.getTextWidths(this.svgElement.nativeElement, this.sideLabels));
    this.barsWidth = this.svgWidth - 2 * ChartCommons.COMPONENT_MARGIN - this.sideLabelWidth;
    this.barsHeight = this.calculateChartBarsHeight();

    this.headerHeight = ChartCommons.CHART_HEADER_HEIGHT + ChartCommons.COMPONENT_MARGIN;
    this.barScorePosY = this.barsHeight + ChartCommons.COMPONENT_MARGIN;
    this.barScoreHeight = ChartCommons.BAR_BAND + ChartCommons.COMPONENT_MARGIN;
    this.legendPosY = this.barScorePosY + this.barScoreHeight + ChartCommons.COMPONENT_MARGIN;
    this.legendHeight = this.estimateHeight(this.svgElement.nativeElement) + ChartCommons.COMPONENT_MARGIN;

    this.svgHeight = this.legendPosY + this.legendHeight + this.headerHeight;
    this.svgElement.nativeElement.setAttribute('height', this.svgHeight);

    this.xScale = scaleLinear()
      .domain([0, max(this.aggregationChartDataService.series.map(it => it.value))])
      .range([0, this.barsWidth]);

    this.yScale = scaleBand()
      .domain(this.sideLabels)
      .range([0, this.barsHeight]);

    this.render();
  }

  render() {
    const svgElement: SVGElement = this.svgElement.nativeElement;

    this.renderHeader(svgElement);
    this.renderSideLabels(svgElement);
    this.renderBarsContent(svgElement);
  }

  private renderHeader(svgElement: SVGElement) {
    const header = select(svgElement).selectAll('.header-group').data([this.dataForHeader]);
    header.join(
      enter => enter
        .append('g')
        .attr('class', 'header-group'),
      update => update,
      exit => exit.remove()
    );

    const headerText = select('.header-group').selectAll('.header-text').data([this.dataForHeader]);
    headerText.join(
      enter => enter
        .append('text')
        .attr('class', 'header-text')
        .attr('text-anchor', 'middle')
        .attr('dominant-baseline', 'alphabetic')
        .style('opacity', 0),
      update => update,
      exit => exit
        .style('opacity', 0)
        .transition()
        .duration(ChartCommons.TRANSITION_DURATION)
        .remove()
    )
      .attr('x', this.svgWidth / 2)
      .attr('y', ChartCommons.CHART_HEADER_HEIGHT)
      .transition()
      .duration(ChartCommons.TRANSITION_DURATION)
      .text(datum => datum)
      .style('opacity', 1);
  }

  private renderSideLabels(svgElement: SVGElement) {
    const sideLabelGroup = select(svgElement).selectAll('.side-labels-group').data(this.sideLabels);
    sideLabelGroup.join(
      enter => enter
        .append('g')
        .attr('class', 'side-labels-group'),
      update => update,
      exit => exit.remove()
    )
      .attr('transform', `translate(0, ${this.headerHeight})`);

    const sideLabels = select('.side-labels-group').selectAll('.side-label').data(this.sideLabels);
    sideLabels.join(
      enter => enter
        .append('text')
        .attr('class', 'side-label')
        .attr('dominant-baseline', 'middle')
        .attr('x', 0)
        .style('opacity', 0),
      update => update,
      exit => exit
        .transition()
        .duration(ChartCommons.TRANSITION_DURATION)
        .style('opacity', 0)
        .remove()
    )
      .text(datum => datum)
      .transition()
      .duration(ChartCommons.TRANSITION_DURATION)
      .style('opacity', 1)
      .attr('y', datum => this.yScale(datum) + this.yScale.bandwidth() / 2)
  }

  private renderBarsContent(svgElement: SVGElement) {
    const contentGroup = select(svgElement).selectAll('.bars-content-group').data([1]);
    contentGroup.join(
      enter => enter
        .append('g')
        .attr('class', 'bars-content-group'),
      update => update,
      exit => exit.remove()
    )
      .attr('transform', `translate(${this.sideLabelWidth + ChartCommons.COMPONENT_MARGIN}, ${ChartCommons.CHART_HEADER_HEIGHT + ChartCommons.COMPONENT_MARGIN})`);

    const contentGroupSelector: string = '.bars-content-group';

    this.renderBarGroup(contentGroupSelector);
    this.renderChartScoreGroup(contentGroupSelector);
    this.renderLegendGroup(contentGroupSelector);
  }

  private renderBarGroup(contentGroupSelector: string) {
    const barGroup = select(contentGroupSelector).selectAll('.chart-bar-group').data([1]);
    barGroup.join(
      enter => enter
        .append('g')
        .attr('class', 'chart-bar-group'),
      update => update,
      exit => exit.remove()
    );

    const chartBars = select('.chart-bar-group').selectAll('.chart-bars').data(Object.keys(this.measurandDataMap));
    const barOffset = this.aggregationChartDataService.stackBars ? 0 : ChartCommons.BAR_BAND;

    chartBars.join(
      enter => enter
        .append('g')
        .attr('class', 'chart-bars'),
      update => update,
      exit => exit
        .transition()
        .duration(ChartCommons.TRANSITION_DURATION)
        .attr('transform', 'translate(0, 0)')
        .style('opacity', 0)
        .remove()
    )
      .transition()
      .duration(ChartCommons.TRANSITION_DURATION)
      .attr('transform', (datum, index) => `translate(0, ${index * barOffset})`)
      .each((datum, index, groups) => this.renderBar(select(groups[index]), datum));
  }

  private renderBar(chartBarSelection, measurand: string) {
    const bar = chartBarSelection.selectAll('.bar').data(this.measurandDataMap[measurand].series);
    bar.join(
      enter => {
        const barElement = enter
          .append('g')
          .attr('class', 'bar')
          .style('opacity', () => {return ((this.anyHighlighted && !this.measurandDataMap[measurand].highlighted) || (this.anySelected && !this.measurandDataMap[measurand].selected)) ? 0.2 : 1});
        barElement
          .append('rect')
          .attr('class', 'bar-rect')
          .attr('x', 0)
          .attr('y', 0)
          .attr('height', ChartCommons.BAR_BAND)
          .attr('fill', this.measurandDataMap[measurand].color)
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .attr('x', datum => this.barStart(this.xScale, datum.value))
          .attr('y', datum => this.yScale(datum.sideLabel))
          .attr('width', datum => this.barWidth(this.xScale, datum.value));
        barElement
          .append('text')
          .attr('class', 'bar-value')
          .attr('dominant-baseline', 'middle')
          .style('fill', 'white')
          .style('font-weight', 'bold')
          .style('opacity', 0)
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .text(datum => `${this.formatBarValue(datum.value)} ${datum.unit}`)
          .attr('x', datum => (datum.value < 0) ? (this.barStart(this.xScale, datum.value) + 10) : (this.barEnd(this.xScale, datum.value) - 10))
          .attr('y', datum => (this.yScale(datum.sideLabel) + ChartCommons.BAR_BAND / 2))
          .attr('text-anchor', datum => (datum.value < 0) ? 'start' : 'end')
          .style('opacity', (datum, index, groups) => ((groups[index].getComputedTextLength() + 2 * 10) > this.barWidth(this.xScale, datum.value)) ? 0 : 1);
        return barElement;
      },
      update => {
        update
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .style('opacity', () => {return ((this.anyHighlighted && !this.measurandDataMap[measurand].highlighted) || (this.anySelected && !this.measurandDataMap[measurand].selected)) ? 0.2 : 1});

        update.select('.bar-rect')
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .attr('x', datum => this.barStart(this.xScale, datum.value))
          .attr('y', datum => this.yScale(datum.sideLabel))
          .attr('width', datum => this.barWidth(this.xScale, datum.value))
          .attr('fill', this.measurandDataMap[measurand].color);
        update.select('.bar-value')
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .text(datum => `${this.formatBarValue(datum.value)} ${datum.unit}`)
          .attr('x', datum => (datum.value < 0) ? (this.barStart(this.xScale, datum.value) + 10) : (this.barEnd(this.xScale, datum.value) - 10))
          .attr('y', datum => (this.yScale(datum.sideLabel) + ChartCommons.BAR_BAND / 2))
          .attr('text-anchor', datum => (datum.value < 0) ? 'start' : 'end')
          .style('opacity', (datum, index, groups) => ((groups[index].getComputedTextLength() + 2 * 10) > this.barWidth(this.xScale, datum.value)) ? 0 : 1);
        return update;
      },
      exit => exit
        .transition()
        .duration(ChartCommons.TRANSITION_DURATION)
        .style('opacity', 0)
        .remove()
    )
      .on('click', () => this.onMouseClick(measurand));
  }

  private renderChartScoreGroup(contentGroupSelector: string) {
    const scoreGroup = select(contentGroupSelector).selectAll('.chart-score-group').data([1]);
    scoreGroup.join(
      enter => enter
        .append('g')
        .attr('class', 'chart-score-group'),
      update => update,
      exit => exit.remove()
    )
      .attr('transform', `translate(0, ${this.barScorePosY})`);

    const scoreBars = select('.chart-score-group').selectAll('.score-bar').data(this.dataForScoreBar.barsToRender);
    const scaleForScoreBar = scaleLinear().rangeRound([0, this.barsWidth]).domain([this.dataForScoreBar.min, this.dataForScoreBar.max]);

    scoreBars.join(
      enter => {
        const scoreBarElement = enter
          .append('g')
          .attr('class', 'score-bar');
        scoreBarElement
          .append('rect')
          .attr('class', 'score-rect')
          .attr('height', ChartCommons.BAR_BAND)
          .attr('width', 0)
          .attr('x', 0)
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .attr('fill', datum => datum.fill)
          .attr('width', datum => scaleForScoreBar(datum.end) - scaleForScoreBar(datum.start))
          .attr('x', datum => scaleForScoreBar(datum.start));
        scoreBarElement
          .append('text')
          .attr('class', 'score-value')
          .attr('dominant-baseline', 'middle')
          .attr('text-anchor', "middle")
          .attr('x', 0)
          .attr('y', ChartCommons.BAR_BAND / 2)
          .style('opacity', 0)
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .text(datum => datum.label)
          .style('opacity', (datum, index, groups) => groups[index].getComputedTextLength() + 20 > (scaleForScoreBar(datum.end) - scaleForScoreBar(datum.start)) / 2 ? 0 : 1)
          .attr('x', datum => (scaleForScoreBar(datum.end) + scaleForScoreBar(datum.start)) / 2);
        return scoreBarElement;
      },
      update => {
        update.select('.score-rect')
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .attr('fill', datum => datum.fill)
          .attr('width', datum => scaleForScoreBar(datum.end) - scaleForScoreBar(datum.start))
          .attr('x', datum => scaleForScoreBar(datum.start));
        update.select<SVGTextElement>('.score-value')
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .text(datum => datum.label)
          .style('opacity', (datum, index, groups) => groups[index].getComputedTextLength() + 20 > (scaleForScoreBar(datum.end) - scaleForScoreBar(datum.start)) / 2 ? 0 : 1)
          .attr('x', datum => (scaleForScoreBar(datum.end) + scaleForScoreBar(datum.start)) / 2);
        return update;
      },
      exit => exit
        .transition()
        .duration(ChartCommons.TRANSITION_DURATION)
        .style('opacity', 0)
        .remove()
    );
  }

  private renderLegendGroup(contentGroupSelector: string) {
    const legendGroup = select(contentGroupSelector).selectAll('.chart-legend-group').data([1]);
    legendGroup.join(
      enter => enter
        .append('g')
        .attr('class', 'chart-legend-group')
        .style('cursor', 'pointer'),
      update => update,
      exit => exit.remove()
    )
      .attr('transform', `translate(0, ${this.legendPosY})`);

    const legendEntry = select('.chart-legend-group').selectAll('.legend-entry').data(Object.keys(this.measurandDataMap));
    const maxEntryGroupSize = this.calculateMaxEntryGroupWidth(this.svgElement.nativeElement);
    const maxEntriesInRow = Math.floor(this.svgWidth / maxEntryGroupSize);

    legendEntry.join(
      enter => {
        const legendElement = enter
          .append('g')
          .attr('class', 'legend-entry')
          .style('opacity', (datum)=>{return ((this.anyHighlighted && !this.measurandDataMap[datum].highlighted) || (this.anySelected && !this.measurandDataMap[datum].selected)) ? 0.2 : 1});
        legendElement
          .append('rect')
          .attr('class', 'legend-rect')
          .attr('height', ChartCommons.COLOR_PREVIEW_SIZE)
          .attr('width', ChartCommons.COLOR_PREVIEW_SIZE)
          .attr("rx", 2)
          .attr("ry", 2)
          .attr('fill', datum => this.measurandDataMap[datum].color);
        legendElement
          .append('text')
          .attr('class', 'legend-text')
          .attr('x', ChartCommons.COLOR_PREVIEW_SIZE + ChartCommons.COLOR_PREVIEW_MARGIN)
          .attr('y', ChartCommons.COLOR_PREVIEW_SIZE)
          .text(datum => this.measurandDataMap[datum].label);
        return legendElement;
      },
      update => {
        update
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .style('opacity', (datum)=>{return ((this.anyHighlighted && !this.measurandDataMap[datum].highlighted) || (this.anySelected && !this.measurandDataMap[datum].selected)) ? 0.2 : 1});

        update.select('.legend-rect')
          .attr('fill', datum => this.measurandDataMap[datum].color);
        update.select('.legend-text')
          .text(datum => this.measurandDataMap[datum].label);
        return update;
      },
        exit => exit
          .transition()
          .duration(ChartCommons.TRANSITION_DURATION)
          .style('opacity', 0)
          .remove()
    )
      .attr('transform', (datum, index) => `translate(${maxEntryGroupSize * (index % maxEntriesInRow)}, 0)`)
      .on('mouseover', (datum) => this.onMouseOver(datum))
      .on('mouseout', (datum) => this.onMouseOut(datum))
      .on('click', (datum) => this.onMouseClick(datum));
  }


  formatBarValue(value: string): string {
    const precision = this.dataForScoreBar.max >= 1000 || this.dataForScoreBar.min <= -1000 ? 0 : 2;
    return parseFloat(value).toFixed(precision).toString();
  }

  estimateHeight(svgForEstimation: SVGElement): number {
    const maxEntryGroupSize = this.calculateMaxEntryGroupWidth(svgForEstimation);
    const maxEntriesInRow = Math.floor(this.svgWidth / maxEntryGroupSize);
    return Math.floor(this.aggregationChartDataService.series.length / maxEntriesInRow) * 20;
  }

  calculateMaxEntryGroupWidth(svgForEstimation: SVGElement): number {
    let dataMap = this.measurandDataMap;
    const labels = Object.keys(dataMap).map(measurand => dataMap[measurand].label);
    const labelWidths = this.getTextWidths(svgForEstimation, labels);
    return max(labelWidths) + 10 + 20 + 5;
  }

  getTextWidths(svgForEstimation: SVGElement, texts: string[]): number[] {
    const widths = [];
    select(svgForEstimation).selectAll('.invisible-text-to-measure')
      .data(texts)
      .enter()
      .append("text")
      .attr("opacity", 0)
      .text((d) =>  d.toString())
      .each(function () {
        widths.push(this.getComputedTextLength());
        this.remove();
      });
    return widths;
  }

  barWidth(xScale, value) {
    return value === null ? 0 : (this.barEnd(xScale, value) - this.barStart(xScale, value));
  }

  barEnd(xScale, value) {
    return (value < 0) ? xScale(0) : xScale(value);
  }

  barStart(xScale, value) {
    return (value < 0) ? xScale(value) : xScale(0);
  }

  calculateChartBarsHeight(): number {
    const barBand = ChartCommons.BAR_BAND;
    const barGap = ChartCommons.BAR_GAP;
    const numberOfMeasurands = Object.keys(this.measurandDataMap).length;
    let numberOfPages = 0;
    Object.keys(this.measurandDataMap).forEach((k) => {
      numberOfPages = (this.measurandDataMap[k].series.length > numberOfPages) ? this.measurandDataMap[k].series.length : numberOfPages;
    });
    const numberOfBars = numberOfPages * (this.aggregationChartDataService.stackBars ? 1 : numberOfMeasurands);
    const gapSize = barGap * ((this.aggregationChartDataService.stackBars || numberOfMeasurands < 2) ? 1 : 2);
    return ((numberOfPages - 1) * gapSize) + numberOfBars * barBand;
  }

  private onMouseOver(measurand: string): void {
    if(this.anySelected===false) {
      this.anyHighlighted = true;
      this.measurandDataMap[measurand].highlighted = true;
      this.renderBarGroup('.bars-content-group');
      this.renderLegendGroup('.legend-content-group');
    }
  }

  private onMouseOut(measurand: string): void {
    if(this.anyHighlighted === true) {
      this.anyHighlighted = false;
      this.measurandDataMap[measurand].highlighted = false;
      this.renderBarGroup('.bars-content-group');
      this.renderLegendGroup('.legend-content-group');
    }
  }

  private onMouseClick(measurand: string): void {
    if (this.anySelected === false) {
      this.measurandDataMap[measurand].selected = true;
      this.clickedMeasurand = measurand;
      this.anySelected = true;
    } else if(this.anySelected === true && this.clickedMeasurand !== measurand){
      this.measurandDataMap[this.clickedMeasurand].selected = false;
      this.measurandDataMap[measurand].selected = true;
      this.clickedMeasurand = measurand;
    }
    else{
      this.anySelected = false;
      this.measurandDataMap[measurand].selected = false;
      this.clickedMeasurand = '';
    }
    this.renderBarGroup('.bars-content-group');
    this.renderLegendGroup('.legend-content-group');
  }

  changeStackBars(status: string): void {
    this.aggregationChartDataService.stackBars = (status === 'true');
  }

  reloadPercentile(): void {
    if(this.percentileValue === null || this.percentileValue < 1){
      this.percentileValue = 1;
    }else if(this.percentileValue > 100){
      this.percentileValue = 100;
    }
    this.aggregationChartDataService.reloadPercentile(this.percentileValue, this.resultSelectionStore.resultSelectionCommand, this.resultSelectionStore.remainingResultSelection);
  }

  selectFilter(filter: string): void{
    this.aggregationChartDataService.selectedFilter = filter;
    this.redraw();
  }

  ngOnChanges(changes: SimpleChanges): void {
    this.redraw();
  }

}

{'fraction_non_alphanumeric': 0.14937183313666968, 'fraction_numerical': 0.006200689479651095, 'mean_word_length': 3.1950888090847327, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 201, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2021080', 'n_tokens_mistral': 13857, 'n_tokens_neox': 13361, 'n_words': 2132}