Open pavelbruyako opened 2 years ago
Okay, I examined apexchart's inner code and came up with the following solution.
I wrote a directive, that calculates tickAmount
based on chart's inner properties and sets it on every resize of chart's parent. There is the code:
import { Directive, ElementRef, Input, OnInit, OnDestroy, NgZone, ContentChild, AfterContentInit } from '@angular/core';
import { ChartComponent } from 'ng-apexcharts';
@Directive({
selector: `[adjustedAxesOfChildApexChart]`,
})
// This directive should be applied to sizeable(!) parent of horizontal bar apexchart
export class AdjustedAxesOfChildApexChartDirective implements OnInit, AfterContentInit, OnDestroy {
@ContentChild(ChartComponent)
chart: any;
observer: ResizeObserver;
readonly minDistanceBetweenLabels = 10; // px
readonly maxDistinguishableIncrement = 6; // if adjacent labels' difference is more than that, then we don't care if ticks are a little bit off
readonly minIntegerLabel = 3.9; // apexhart starts producing labels with /10 presision if max label is < than minIntegerLabel
constructor(private host: ElementRef, private readonly zone: NgZone) {}
ngOnInit() {
this.observer = new ResizeObserver(() => {
this.zone.run(() => this.setTickAmount());
});
this.observer.observe(this.host.nativeElement);
}
ngAfterContentInit() {
this.setTickAmount();
}
setTickAmount() {
// this field can be uninitialised at the very first run
if (!this.chart?.chartObj?.w?.globals) {
return;
}
const chartGlobalOptions = this.chart.chartObj.w.globals;
const xAxisLabels = chartGlobalOptions.yAxisScale[0].result;
const firstLabel = xAxisLabels[0];
const lastLabel = xAxisLabels[xAxisLabels.length - 1];
// I don't care if this is not precise since I do Math.floor which gets us result bigger or equal than the actual size
const firstLabelWidth =
chartGlobalOptions.xAxisLabelsWidth / Math.max(Math.floor(`${lastLabel}`.length / `${firstLabel}`.length), 1);
// this formula is based on apexchart code of getting x-values for labels (node_modules\apexcharts\src\modules\axes\YAxis.js drawYaxisInversed)
const availableXAxisWidth =
chartGlobalOptions.gridWidth + // gridWidth is an actual grid width (xaxis length)
chartGlobalOptions.padHorizontal -
firstLabelWidth - // we need to subtract first label width because it can be the same length as the rest of them. Btw, this case with very long first label has a bug: label starts earlier than left border. No fix for that)
this.chart.chartObj.w.config.xaxis.labels.offsetX;
const maxTickAmount = Math.floor(
availableXAxisWidth / (chartGlobalOptions.xAxisLabelsWidth + this.minDistanceBetweenLabels)
);
this.chart?.updateOptions({
xaxis: {
tickAmount: this.getOptimalTickAmount(
Math.round(lastLabel < this.minIntegerLabel ? lastLabel * 10 : lastLabel), // for actual labels apexchart rounds the actual numbers, so must I
maxTickAmount
),
},
});
}
getOptimalTickAmount(lastLabel: number, maxTickAmount: number) {
if (lastLabel / maxTickAmount > this.maxDistinguishableIncrement) {
return maxTickAmount;
}
// if increment is indistinguishable with some tickAmount, then there is no reason in taking anything less than that
const minTickAmount = Math.min(Math.max(Math.floor(lastLabel / this.maxDistinguishableIncrement), 1), maxTickAmount);
// common algorithm of searching for divisors with some restrictions
let lastDivisor = 1;
for (let divisor = 1; divisor * divisor <= lastLabel && divisor <= maxTickAmount; ++divisor) {
if (lastLabel % divisor === 0) {
lastDivisor = divisor;
const pairedDivisor = lastLabel / divisor;
if (minTickAmount <= pairedDivisor && pairedDivisor <= maxTickAmount) {
return pairedDivisor;
}
}
}
return lastDivisor === 1 || lastDivisor < minTickAmount ? minTickAmount : lastDivisor;
}
ngOnDestroy() {
this.observer.unobserve(this.host.nativeElement);
}
}
As the comment states, this directive should be applied to apexchart's sized parent, who can be observed with ResizeObserver
. I had no luck applying it to an apexchart itself. So, the usage is:
<div adjustedAxesOfChildApexChart>
<apx-chart
[series]="series"
[yaxis]="yaxis"
[xaxis]="xaxis"
[colors]="colors"
[fill]="fill"
[chart]="chart"
[grid]="grid"
[tooltip]="tooltip"
[legend]="legend"
[dataLabels]="dataLabels"
[stroke]="stroke"
[plotOptions]="plotOptions"
[states]="states"
></apx-chart>
</div>
This workaround is not perfect, since there are still a bug with yaxis.max
field being partially disregarded sometimes when there is .tickAmount
set. I didn't go the whole way down through the apexchart's code to find out why that happens and I'll be grateful if someone can figure out that))
There is also a big problem with floating point numbers (example in the description). The algorithm of calculating yAxisScale
should address them properly.
Description
I work with a horizontal bar chart.
When I set
the number of labels increases for some reason and:
Expected Behavior
Actual Behavior
Screenshots
Example with overlapping labels:
Example with repeating labels:
Reproduction Link
https://codesandbox.io/s/apx-bar-basic-forked-zp5xw5?file=/src/app/app.component.ts
Additional question
Can you please specify all the fields which get swapped between xaxis and yaxis when plotOptions.bar.horizontal = true?