Skip to content

Commit

Permalink
feat: avoid label collision
Browse files Browse the repository at this point in the history
  • Loading branch information
MAudelGisaia committed Feb 3, 2025
1 parent b187d72 commit 188fecf
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 32 deletions.
95 changes: 81 additions & 14 deletions src/histograms/AbstractHistogram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -65,6 +66,9 @@ export abstract class AbstractHistogram {
protected plottingCount = 0;
protected minusSign = 1;

protected _xlabelMeanWidth = 0;


public constructor() {
this.brushCornerTooltips = this.createEmptyBrushCornerTooltips();
}
Expand Down Expand Up @@ -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 + ')')
Expand All @@ -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');
Expand All @@ -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<HistogramData>, axes: ChartAxes | SwimlaneAxes, xDataDomain: ScaleBand<string>, 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')
Expand Down
1 change: 1 addition & 0 deletions src/histograms/HistogramParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down
3 changes: 3 additions & 0 deletions src/histograms/charts/AbstractChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HistogramData>) {
super.init();
this.dataDomain = inputData;
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions src/histograms/charts/ChartCurve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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)) {
Expand All @@ -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 => {
Expand Down
4 changes: 4 additions & 0 deletions test/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
body, html {
width: 100%;
}


.histogram{
background: rgba(0,0,255,0);
Expand Down
20 changes: 16 additions & 4 deletions test/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand All @@ -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,
Expand Down Expand Up @@ -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' },
Expand Down Expand Up @@ -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)
Expand All @@ -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');
Expand Down
28 changes: 14 additions & 14 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,31 @@
</head>

<body>
<div style="width: 1000px; height: 150px;">
<div id="containerBars" style="width: 1000px; height: 150px;">
<svg id="svgixBars" style="width: 1000px; height: 150px"></svg>
<div style="width: 100%; height: 150px;">
<div id="containerBars" style="width: 100%; height: 150px;">
<svg id="svgixBars" style="width: 100%; height: 150px"></svg>
</div>
<div id="containerCurve" style="width: 1000px; height: 150px;">
<svg id="svgixCurve" style="width: 1000px; height: 150px"></svg>
<div id="containerCurve" style="width: 100%; height: 150px;">
<svg id="svgixCurve" style="width: 100%; height: 150px"></svg>
</div>
<div id="containerArea" style="width: 1000px; height: 150px;">
<svg id="svgixArea" style="width: 1000px; height: 150px"></svg>
<div id="containerArea" style="width: 100%; height: 150px;">
<svg id="svgixArea" style="width: 100%; height: 150px"></svg>
</div>
<div id="containerHistTime" style="width: 1000px; height: 150px;">
<svg id="svgixHistTime" style="width: 1000px; height: 150px"></svg>
<div id="containerHistTime" style="width: 100%; height: 150px;">
<svg id="svgixHistTime" style="width: 100%; height: 150px"></svg>
</div>
<div id="containerTimeline" style="width: 1000px; height: 150px;">
<svg id="svgixTimeline" style="width: 1000px; height: 150px"></svg>
</div>
<div id="containerSwimlane" style="width: 1000px; height: 150px;">
<svg id="svgixSwimlane" style="width: 1000px; height: 150px"></svg>
<div id="containerSwimlane" style="width: 100%; height: 150px;">
<svg id="svgixSwimlane" style="width: 100%; height: 150px"></svg>
</div>
<div id="containerDonut" style="width: 1000px; height: 150px;">
<svg id="svgixDonut" style="width: 1000px; height: 150px"></svg>
<div id="containerDonut" style="width: 100%; height: 150px;">
<svg id="svgixDonut" style="width: 100%; height: 150px"></svg>
</div>
</div>

<script src='app.js'></script>
</body>

</html>
</html>

0 comments on commit 188fecf

Please sign in to comment.