import { Component, Input, OnInit } from '@angular/core'; import * as d3 from 'd3'; @Component({ selector: 'box-plot-with-line', template: `
`, styles: [ `.tooltip { position: absolute; text-align: center; width: 60px; height: 28px; padding: 2px; font: 12px sans-serif; background: lightsteelblue; border: 0px; border-radius: 8px; pointer-events: none; }` ] }) export class BoxPlotWithLineComponent implements OnInit { svg; width = 1200; height = 500; barWidth = 20; margin = { top: 20, right: 10, bottom: 20, left: 10 }; yMax; yMin; @Input() options: DataInput; constructor() { } ngOnInit() { this.width = this.width - this.margin.left - this.margin.right; this.height = this.height - this.margin.top - this.margin.bottom; this.getYMaxMinValue(); this.validateInput(); if (this.options.showBoxPlot) { this.drawBoxPlotChart(this.options.box_plot_data); } if (this.options.showLine) { this.drawLineChart(this.options.box_plot_data, this.options.currentLine, this.options.medianLine); } } validateInput = () => { // Validation for Box plot data if (this.options.showBoxPlot) { if (this.options.box_plot_data) { this.options.box_plot_data.forEach((data) => { let whiskers, quartile, key; [key, quartile, whiskers] = Object.keys(data); if (key !== 'key' || quartile !== 'quartile' || whiskers !== 'whiskers') { console.error(`key : ${key}, quartile : ${quartile}, whiskers : ${whiskers}`); } }); } else { console.log('No Data for Box plot'); } } } getYMaxMinValue = () => { const tempArray = []; this.options.box_plot_data.forEach((data) => { tempArray.push(...data['whiskers']); }); this.options.currentLine.forEach((obj: Array) => { tempArray.push(obj['y']); }); this.options.medianLine.forEach((obj: Array) => { tempArray.push(obj['y']); }); this.yMax = d3.max(tempArray); this.yMin = d3.min(tempArray); } drawBoxPlotChart = (BOX_PLOT_DATA) => { // Prepare the data for the box plots const boxPlotData = BOX_PLOT_DATA; const colorScale = d3.scaleOrdinal(d3.schemeCategory10) .domain(Object.keys(BOX_PLOT_DATA)); for (let i = 0; i < boxPlotData.length; i++) { boxPlotData[i]['color'] = colorScale(i + ''); } const xScale = d3.scalePoint() .domain(Object.keys(BOX_PLOT_DATA)) .rangeRound([0, this.width]) .padding(1); const yScale = d3.scaleLinear() .domain([this.yMax, this.yMin]) .range([0, this.height]); // Setup the svg and group we will draw the box plot in this.svg = d3.select('#chartDiv').append('svg').attr('viewBox', '0 0 1200 500') .append('g') .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')'); // Move the left axis over 25 pixels, and the top axis over 465 pixels const axisG = this.svg.append('g').attr('transform', 'translate(25,0)'); const axisTopG = this.svg.append('g').attr('transform', 'translate(35,460)'); // Setup the group the box plot elements will render in const g = this.svg.append('g') .attr('transform', 'translate(25,0)'); // Draw the box plot vertical lines const verticalLines = g.selectAll('.verticalLines') .data(boxPlotData) .enter() .append('line') .attr('x1', (datum) => { return xScale(datum.key) + this.barWidth / 2; } ) .attr('y1', (datum) => { const whisker = datum.whiskers[0]; return yScale(whisker); } ) .attr('x2', (datum) => { return xScale(datum.key) + this.barWidth / 2; } ) .attr('y2', (datum) => { const whisker = datum.whiskers[1]; return yScale(whisker); } ) // .attr('stroke', '#000') .attr('stroke', (datum) => datum.color) .attr('stroke-width', 1) .attr('fill', 'none'); const rectToolTipDiv = d3.select('#chartDiv').append('div') .attr('class', 'tooltip') .style('opacity', 0) .style('position', 'absolute'); // Draw the boxes of the box plot, filled in white and on top of vertical lines const rects = g.selectAll('rect') .data(boxPlotData) .enter() .append('rect') .attr('width', this.barWidth) .attr('height', function (datum) { const quartiles = datum.quartile; const height = yScale(quartiles[0]) - yScale(quartiles[2]); return height; } ) .attr('x', function (datum) { return xScale(datum.key); } ) .attr('y', function (datum) { return yScale(datum.quartile[2]); } ) .attr('fill', function (datum) { return datum.color; } ) .attr('stroke', (datum) => datum.color) .attr('stroke-width', 1) .on('mouseover', (d) => { let twentyFivePercent = d['quartile']['0']; let FiftyPercent = d['quartile']['1']; let seventyFivePercent = d['quartile']['2']; twentyFivePercent = Number(twentyFivePercent).toFixed(2); FiftyPercent = Number(FiftyPercent).toFixed(2); seventyFivePercent = Number(seventyFivePercent).toFixed(2); rectToolTipDiv.transition() .duration(500) .style('display', 'block') .style('opacity', .9); rectToolTipDiv.html( 'P75 = ' + seventyFivePercent + '
' + 'Avg = ' + FiftyPercent + '
' + 'P25 = ' + twentyFivePercent ) .style('padding', 10 + 'px') .style('font-weight', 700) .style('font-size', '12px') .style('font-family', '12px') .style('background-color', 'sans-serif') .style('border', '1px solid') .style('border-radius', '10px') .style('left', (d3.event.pageX) + 10 + 'px') .style('top', (d3.event.pageY - 28) + 'px'); }) .on('mouseout', (d) => { rectToolTipDiv.transition() .duration(500) .style('display', 'none'); }); // Now render all the horizontal lines at once - the whiskers and the median const horizontalLineConfigs = [ // Top whisker { x1: (datum) => xScale(datum.key), y1: (datum) => yScale(datum.whiskers[0]), x2: (datum) => xScale(datum.key) + this.barWidth, y2: (datum) => yScale(datum.whiskers[0]) }, // Median line { x1: (datum) => xScale(datum.key), y1: (datum) => yScale(datum.quartile[1]), x2: (datum) => xScale(datum.key) + this.barWidth, y2: (datum) => yScale(datum.quartile[1]) }, // Bottom whisker { x1: (datum) => xScale(datum.key), y1: (datum) => yScale(datum.whiskers[1]), x2: (datum) => xScale(datum.key) + this.barWidth, y2: (datum) => yScale(datum.whiskers[1]) } ]; for (let i = 0; i < horizontalLineConfigs.length; i++) { const lineConfig = horizontalLineConfigs[i]; // Draw the whiskers at the min for this series let horizontalLine; if (i === 1) { horizontalLine = g.selectAll('.whiskers') .data(boxPlotData) .enter() .append('line') .attr('x1', lineConfig.x1) .attr('y1', lineConfig.y1) .attr('x2', lineConfig.x2) .attr('y2', lineConfig.y2) .attr('stroke', '#000') .attr('stroke-width', 1) .attr('fill', 'none'); } else { horizontalLine = g.selectAll('.whiskers') .data(boxPlotData) .enter() .append('line') .attr('x1', lineConfig.x1) .attr('y1', lineConfig.y1) .attr('x2', lineConfig.x2) .attr('y2', lineConfig.y2) .attr('stroke', (datum) => datum.color) .attr('stroke-width', 1) .attr('fill', 'none'); } } // Setup a scale on the left const yAxis = d3.axisLeft(yScale); axisG.append('g') .call(yAxis) .selectAll('line') .attr('x2', () => this.width) .attr('stroke', '#e5e5e5') .attr('shape-rendering', 'crispEdges') .attr('fill', 'none'); // Setup a series axis on the top const xAxis = d3.axisBottom(xScale); axisTopG.append('g') .call(xAxis) .selectAll('line') .attr('y2', () => -this.height) .attr('stroke', '#e5e5e5') .attr('shape-rendering', 'crispEdges') .attr('fill', 'none'); axisTopG.select('path').attr('stroke', '#e5e5e5'); } drawLineChart = (BOX_PLOT_DATA, CURRENT_LINE, MEDIAN_LINE) => { const x = d3.scaleLinear().rangeRound([0, this.width]); const y = d3.scaleLinear().range([0, this.height]); const median = MEDIAN_LINE; const currentLine = CURRENT_LINE; // Define the line const valueline = d3.line() .x((d: ChartData) => x(d['x'])) .y((d: ChartData) => y(d['y'])); const xScale = d3.scalePoint() .domain(Object.keys(BOX_PLOT_DATA)) .rangeRound([0, this.width]) .padding(1); const yScale = d3.scaleLinear() .domain([this.yMax, this.yMin]) // input .range([0, this.height]); // output const plotLine = d3.line() .x((d) => { return xScale(d['x']); }) .y((d) => { return yScale(d['y']); }); for (let i = 0; i < currentLine.length - 1; i++) { const currentValue = currentLine[i + 1]['y']; const data = [currentLine[i], currentLine[i + 1]]; const path = this.svg.append('path') .data([data]) .attr('d', plotLine) .attr('stroke-width', '2') .attr('transform', 'translate(35,0)') .attr('fill', 'none'); const boxPlotObj = BOX_PLOT_DATA[i + 1]; if (boxPlotObj) { if (currentValue >= boxPlotObj['quartile'][0] && currentValue <= boxPlotObj['quartile'][2]) { path.attr('stroke', 'lawngreen'); } else if (currentValue >= boxPlotObj['whiskers'][0] && currentValue <= boxPlotObj['whiskers'][1]) { path.attr('stroke', 'yellow'); } else { path.attr('stroke', 'red'); } } } this.svg.append('path') .data([median]) .attr('d', plotLine) .attr('stroke', 'steelblue') .attr('stroke-width', '2') .attr('transform', 'translate(35,0)') .attr('fill', 'none'); const dotToolTip = d3.select('#chartDiv').append('div') .attr('class', 'tooltip') .style('opacity', 0) .style('position', 'absolute'); // 12. Appends a circle for each datapoint this.svg.selectAll('.dot') .data(currentLine) .enter().append('circle') // Uses the enter().append() method .attr('class', 'dot') // Assign a class for styling .attr('cx', (d, i) => xScale(i)) .attr('cy', (d) => yScale(d.y)) .attr('transform', 'translate(35,0)') .style('fill', 'green') .attr('r', 5) .on('mouseover', (d) => { dotToolTip.transition() .duration(500) .style('display', 'block') .style('opacity', .9); dotToolTip.html('(' + d['x'] + ', ' + d['y'] + ')') .style('padding', 10 + 'px') .style('font-weight', 700) .style('font-size', '12px') .style('font-family', '12px') .style('background-color', 'sans-serif') .style('border', '1px solid') .style('border-radius', '10px') .style('left', (d3.event.pageX) + 10 + 'px') .style('top', (d3.event.pageY - 28) + 'px'); }) .on('mouseout', (d) => { dotToolTip.transition() .duration(500) .style('display', 'none'); }); } } interface ChartData { x: number; y: number; } interface DataInput { box_plot_data: Array; showLine: Boolean; showBoxPlot: Boolean; medianLine: Array; currentLine: Array; colorArrayForBoxPlot: Array; colorArrayForLine: Array; }