From 188fecfde5815d2aed15ea21987d497cb16d9e9e Mon Sep 17 00:00:00 2001 From: MAudelGisaia Date: Thu, 30 Jan 2025 15:12:11 +0100 Subject: [PATCH] feat: avoid label collision --- src/histograms/AbstractHistogram.ts | 95 ++++++++++++++++++++++---- src/histograms/HistogramParams.ts | 1 + src/histograms/charts/AbstractChart.ts | 3 + src/histograms/charts/ChartCurve.ts | 4 ++ test/app.css | 4 ++ test/app.js | 20 ++++-- test/index.html | 28 ++++---- 7 files changed, 123 insertions(+), 32 deletions(-) diff --git a/src/histograms/AbstractHistogram.ts b/src/histograms/AbstractHistogram.ts index c582f32..d560eee 100644 --- a/src/histograms/AbstractHistogram.ts +++ b/src/histograms/AbstractHistogram.ts @@ -24,6 +24,7 @@ import { import { HistogramParams } from './HistogramParams'; import { scaleUtc, scaleLinear, scaleTime, ScaleTime, ScaleLinear, ScaleBand } from 'd3-scale'; import { min, max } from 'd3-array'; +import { Selection } from 'd3-selection'; export abstract class AbstractHistogram { @@ -65,6 +66,9 @@ export abstract class AbstractHistogram { protected plottingCount = 0; protected minusSign = 1; + protected _xlabelMeanWidth = 0; + + public constructor() { this.brushCornerTooltips = this.createEmptyBrushCornerTooltips(); } @@ -225,20 +229,7 @@ export abstract class AbstractHistogram { // leftOffset is the width of Y labels, so x axes are translated by leftOffset // Y axis is translated to the left of 1px so that the chart doesn't hide it // Therefore, we substruct 1px (leftOffset - 1) so that the first tick of xAxis will coincide with y axis - let horizontalOffset = this.chartDimensions.height; - if (isChartAxes(chartAxes)) { - if (!this.histogramParams.yAxisFromZero) { - const minMax = chartAxes.yDomain.domain(); - if (minMax[0] >= 0) { - horizontalOffset = chartAxes.yDomain(minMax[0]); - } else { - horizontalOffset = chartAxes.yDomain(minMax[1]); - } - } else { - horizontalOffset = chartAxes.yDomain(0); - - } - } + const horizontalOffset = this.getHorizontalOffset(chartAxes); this.xAxis = this.allAxesContext.append('g') .attr('class', 'histogram__only-axis') .attr('transform', 'translate(' + (leftOffset - 1) + ',' + horizontalOffset + ')') @@ -251,6 +242,7 @@ export abstract class AbstractHistogram { .attr('class', 'histogram__labels-axis') .attr('transform', 'translate(' + (leftOffset - 1) + ',' + this.chartDimensions.height * this.histogramParams.xAxisPosition + ')') .call(chartAxes.xLabelsAxis); + this.xTicksAxis.selectAll('path').attr('class', 'histogram__axis'); this.xAxis.selectAll('path').attr('class', 'histogram__axis'); this.xTicksAxis.selectAll('line').attr('class', 'histogram__ticks'); @@ -263,6 +255,81 @@ export abstract class AbstractHistogram { } } + public getHorizontalOffset(chartAxes){ + let h = this.chartDimensions.height; + if (isChartAxes(chartAxes)) { + if (!this.histogramParams.yAxisFromZero) { + const minMax = chartAxes.yDomain.domain(); + if (minMax[0] >= 0) { + h = chartAxes.yDomain(minMax[0]); + } else { + h = chartAxes.yDomain(minMax[1]); + } + } else { + h = chartAxes.yDomain(0); + } + } + return h; + } + + public updateNumberOfLabelDisplayedIfOverlap(chartAxes: ChartAxes | SwimlaneAxes, leftOffset = 0){ + const horizontalOffset = this.getHorizontalOffset(chartAxes); + let sumWidth = 0; + const virtualLabels = this.chartDimensions.svg.append('g'); + const labels = virtualLabels.append('g') + .attr('class', 'histogram__labels-axis') + .attr('transform', 'translate(' + (leftOffset - 1) + ',' + this.chartDimensions.height * this.histogramParams.xAxisPosition + ')') + .call(chartAxes.xLabelsAxis).selectAll('text'); + + let hasOverlap = false; + for (let i = 0; i < labels.size(); i++) { + const next = i + 1; + if(labels.data()[next]){ + const c = this.getDimension(labels.nodes()[i]); + const n = this.getDimension(labels.nodes()[next]); + if(!this.getOverlapFromX(c,n)) { + hasOverlap = true; + } + sumWidth += c.width; + } + } + + if(!this._xlabelMeanWidth) { + this._xlabelMeanWidth = sumWidth / this.histogramParams.xLabels; + } + + virtualLabels.remove(); + if(!hasOverlap) { + return 0; + } + + const labelCount = min([ + this.histogramParams.xLabels, + Math.floor(this.histogramParams.chartWidth / (this._xlabelMeanWidth + horizontalOffset))] + ); + + chartAxes.xLabelsAxis.ticks(labelCount); + chartAxes.xTicksAxis.ticks(labelCount * 4); + } + + public getDimension(node): DOMRect { + if (typeof node.getBoundingClientRect === 'function') { + return node.getBoundingClientRect(); + } else if (node instanceof SVGGraphicsElement) { // check if node is svg element + return node.getBBox(); + } + } + + public getOverlapFromX (l, r) { + const a = {left: 0, right: 0}; + const b = {left: 0, right: 0}; + a.left = l.x - this.histogramParams.overlapXTolerance; + a.right = l.x + l.width + this.histogramParams.overlapXTolerance; + b.left = r.x - this.histogramParams.overlapXTolerance; + b.right = r.x + r.width + this.histogramParams.overlapXTolerance; + return a.left >= b.right || a.right <= b.left; + } + protected plotBars(data: Array, axes: ChartAxes | SwimlaneAxes, xDataDomain: ScaleBand, barWeight?: number): void { const barWidth = barWeight ? axes.stepWidth * barWeight : axes.stepWidth * this.histogramParams.barWeight; this.barsContext = this.context.append('g').attr('class', 'histogram__bars').selectAll('.bar') diff --git a/src/histograms/HistogramParams.ts b/src/histograms/HistogramParams.ts index e0f54bc..57db80e 100644 --- a/src/histograms/HistogramParams.ts +++ b/src/histograms/HistogramParams.ts @@ -70,6 +70,7 @@ export class HistogramParams { public ticksDateFormat: string = null; public xAxisPosition: Position = Position.bottom; public descriptionPosition: Position = Position.bottom; + public overlapXTolerance = 2; /** Desctiption */ public chartTitle = ''; diff --git a/src/histograms/charts/AbstractChart.ts b/src/histograms/charts/AbstractChart.ts index cf7e923..e2be103 100644 --- a/src/histograms/charts/AbstractChart.ts +++ b/src/histograms/charts/AbstractChart.ts @@ -69,6 +69,7 @@ export abstract class AbstractChart extends AbstractHistogram { /** Maximum number of buckets that a chart can have */ private MAX_BUCKET_NUMBER = 1000; + public plot(inputData: Array) { super.init(); this.dataDomain = inputData; @@ -84,6 +85,7 @@ export abstract class AbstractChart extends AbstractHistogram { this.customizeData(data); const extendedData = this.extendData(data); this.createChartAxes(extendedData); + this.updateNumberOfLabelDisplayedIfOverlap(this.chartAxes, 0); this.drawChartAxes(this.chartAxes, 0); this.plotChart(data); this.showTooltips(data); @@ -405,6 +407,7 @@ export abstract class AbstractChart extends AbstractHistogram { const labelPadding = (this.histogramParams.xAxisPosition === Position.bottom) ? 9 : -15; this.chartAxes.xLabelsAxis = axisBottom(this.chartAxes.xDomain).tickSize(0) .tickPadding(labelPadding).ticks(this.histogramParams.xLabels); + this.applyFormatOnXticks(data); if (this.histogramParams.dataType === DataType.time) { if (this.histogramParams.ticksDateFormat) { diff --git a/src/histograms/charts/ChartCurve.ts b/src/histograms/charts/ChartCurve.ts index be779de..86b6646 100644 --- a/src/histograms/charts/ChartCurve.ts +++ b/src/histograms/charts/ChartCurve.ts @@ -107,12 +107,14 @@ export class ChartCurve extends AbstractChart { this.histogramParams.margin.right = 10; } this.initializeChartDimensions(); + const extendedData = this.extendData(data); if (chartIdToData.size === 1) { // We add just one Y axis on the left // No normalization this.createChartXAxes(extendedData); this.createChartYLeftAxes(data); + this.updateNumberOfLabelDisplayedIfOverlap(this.chartAxes, 0); this.drawChartAxes(this.chartAxes, 0); this.drawYAxis(this.chartAxes, chartIdsToSides, Array.from(chartIds)[0]); this.createClipperContext(); @@ -129,6 +131,7 @@ export class ChartCurve extends AbstractChart { this.createChartYRightAxes(dataArray[1]); this.createChartYLeftAxes(dataArray[0]); } + this.updateNumberOfLabelDisplayedIfOverlap(this.chartAxes, 0); this.drawChartAxes(this.chartAxes, 0); const chartIdsArray = Array.from(chartIds); if (!!this.histogramParams.mainChartId && chartIdToData.has(this.histogramParams.mainChartId)) { @@ -153,6 +156,7 @@ export class ChartCurve extends AbstractChart { // We normalize the data this.createChartXAxes(extendedData); this.createChartNormalizeLeftAxes(); + this.updateNumberOfLabelDisplayedIfOverlap(this.chartAxes, 0); this.drawChartAxes(this.chartAxes, 0); this.createClipperContext(); dataArray.forEach(chartData => { diff --git a/test/app.css b/test/app.css index 8b06d4a..f5459a0 100644 --- a/test/app.css +++ b/test/app.css @@ -16,6 +16,10 @@ * specific language governing permissions and limitations * under the License. */ +body, html { + width: 100%; +} + .histogram{ background: rgba(0,0,255,0); diff --git a/test/app.js b/test/app.js index 6fdc76c..489a007 100644 --- a/test/app.js +++ b/test/app.js @@ -22,9 +22,9 @@ import { Dimensions, Granularity, Margins, Timeline } from '../dist/index.js' const inputCharts = { - "xTicks": 9, + "xTicks": 60, "yTicks": 2, - "xLabels": 9, + "xLabels": 50, "yLabels": 2, "xUnit": "", "yUnit": "", @@ -39,7 +39,7 @@ const inputCharts = { "ticksDateFormat": "%b %d %Y %H:%M", "chartType": "bars", "chartHeight": 150, - "chartWidth": 500, + "chartWidth": null, "yAxisStartsFromZero": true, "descriptionPosition": "top", "showXTicks": true, @@ -76,6 +76,8 @@ const defaultHistogramData = [ { value: 100, key: 12000, chartId: '1' }, { value: 222, key: 13000, chartId: '1' }, { value: 120, key: 14000, chartId: '1' }, + { value: 123, key: 15000, chartId: '1' }, + { value: 123, key: 16000, chartId: '1' }, { value: 212 + 200, key: 5000, chartId: '2' }, @@ -124,9 +126,12 @@ const timeHistogramBars = new ChartBars(); displayHistogram(timeHistogramBars, 'containerHistTime', timeHistogramData, false, DataType.time) function displayHistogram(histogram, containerName, data, selectionOverflow = false, dataType = DataType.numeric) { - histogram.histogramParams = histogramParams; + histogram.histogramParams = {...histogramParams}; histogram.histogramParams.dataType = dataType; histogram.histogramParams.multiselectable = true; + histogram.histogramParams.chartWidth = null; + + console.log(histogram.histogramParams) // histogram.histogramParams.selectionType = 'slider'; histogram.histogramParams.intervalSelectedMap = new Map(); histogram.histogramParams.histogramContainer = document.getElementById(containerName) @@ -141,6 +146,13 @@ function displayHistogram(histogram, containerName, data, selectionOverflow = fa }); } +window.addEventListener('resize', () => { + histogramBars.resize(document.getElementById('containerBars')); + histogramCurve.resize(document.getElementById('containerCurve')); + histogramArea.resize(document.getElementById('containerArea')); + timeHistogramBars.resize(document.getElementById('containerHistTime')); +}); + /** Timeline */ const svg = document.getElementById('containerTimeline').querySelector('svg'); diff --git a/test/index.html b/test/index.html index ed3f983..1571067 100644 --- a/test/index.html +++ b/test/index.html @@ -10,31 +10,31 @@ -
-
- +
+
+
-
- +
+
-
- +
+
-
- +
+
-
- +
+
-
- +
+
- \ No newline at end of file +