Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AG-10444 Increase axis ticks on zoom #3329

Merged
merged 10 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 31 additions & 12 deletions packages/ag-charts-community/src/chart/axis/axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { AxisLabel } from './axisLabel';
import { AxisLine } from './axisLine';
import { AxisTick, type TickInterval } from './axisTick';
import { AxisTitle } from './axisTitle';
import type { AxisLineDatum } from './axisUtil';
import { type AxisLineDatum, NiceMode } from './axisUtil';

export interface LabelNodeDatum {
tickId: string;
Expand Down Expand Up @@ -474,29 +474,47 @@ export abstract class Axis<
this.animatable = animatable;
}

_niceDomainRange: number = NaN;
_scaleNiceDomainRange: number = NaN;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe call it _scaleNiceDomainRangeExtent, not to be confused with a range, which is what we conventionally call an array of min, max?

calculateLayout(initialPrimaryTickCount?: number) {
const { scale, label, visibleRange } = this;
const { scale, label, visibleRange, nice } = this;

const { rotation, parallelFlipRotation, regularFlipRotation } = this.calculateRotations();
const sideFlag = this.label.getSideFlag();

this.updateScale();
const { niceDomain, primaryTickCount, ticks, visibleTicks, fractionDigits, bbox } = this.calculateTickLayout(
this.dataDomain.domain,
visibleRange,
initialPrimaryTickCount
);

const range = findRangeExtent(this.range);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rename to rangeExtent?


const domain = this.dataDomain.domain;
let tickLayoutDomain: D[] | undefined;
if (visibleRange[0] === 0 && visibleRange[1] === 1) {
this.scale.domain = niceDomain;
} else if (this._niceDomainRange !== range) {
this.scale.domain = this.calculateTickLayout(this.dataDomain.domain, [0, 1]).niceDomain;
tickLayoutDomain = undefined;
} else if (!nice) {
tickLayoutDomain = domain;
} else if (this._scaleNiceDomainRange === range) {
tickLayoutDomain = this.scale.domain;
} else {
tickLayoutDomain = this.calculateTickLayout(domain, NiceMode.TickAndDomain, [0, 1]).niceDomain;
}

let niceMode: NiceMode;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes an issue where ticks would be in the wrong place. You do want to use a nice domain, but the nice domain of the zoomed out series. This means when generating the ticks, you'll still have nice ticks, but the domain won't be adjusted

if (!nice) {
niceMode = NiceMode.Off;
} else if (tickLayoutDomain == null) {
niceMode = NiceMode.TickAndDomain;
} else {
niceMode = NiceMode.TicksOnly;
}
const { niceDomain, primaryTickCount, ticks, visibleTicks, fractionDigits, bbox } = this.calculateTickLayout(
tickLayoutDomain ?? domain,
niceMode,
visibleRange,
initialPrimaryTickCount
);

this.scale.domain = niceDomain;

this._niceDomainRange = range;
this._scaleNiceDomainRange = nice ? range : NaN;

const specifier = label.format;
this.labelFormatter =
Expand Down Expand Up @@ -531,6 +549,7 @@ export abstract class Axis<

abstract calculateTickLayout(
domain: D[],
niceMode: NiceMode,
visibleRange: [number, number],
primaryTickCount?: number
): {
Expand Down
61 changes: 44 additions & 17 deletions packages/ag-charts-community/src/chart/axis/axisTickGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { TextSizeProperties } from '../../scene/shape/text';
import { type PlacedLabelDatum, axisLabelsOverlap } from '../../scene/util/labelPlacement';
import { normalizeAngle360, toRadians } from '../../util/angle';
import { arraysEqual } from '../../util/array';
import { countFractionDigits, findMinMax, findRangeExtent, round } from '../../util/number';
import { countFractionDigits, findMinMax, findRangeExtent } from '../../util/number';
import { calculateNiceSecondaryAxis } from '../../util/secondaryAxisTicks';
import { createIdsGenerator } from '../../util/tempUtils';
import { CachedTextMeasurerPool, TextUtils } from '../../util/textMeasurer';
Expand All @@ -16,6 +16,7 @@ import { calculateLabelRotation, createLabelData, getLabelSpacing, getTextAlign,
import type { AxisInterval } from './axisInterval';
import type { TickInterval } from './axisTick';
import type { TickDatum } from './axisUtil';
import { NiceMode } from './axisUtil';

export interface TickData<D = any> {
rawTicks: D[];
Expand All @@ -30,6 +31,7 @@ export interface TickGenerationParams<D = any> {
domain: D[];
primaryTickCount: number | undefined;
visibleRange: [number, number];
niceMode: NiceMode;
parallelFlipRotation: number;
regularFlipRotation: number;
labelX: number;
Expand Down Expand Up @@ -81,9 +83,9 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {
constructor(private readonly axis: IAxis<S, D>) {}

private estimateTickCount(visibleRange: [number, number], minSpacing: number, maxSpacing: number) {
const rangeWithBleed = round(findRangeExtent(this.axis.range) / findRangeExtent(visibleRange), 2);
return estimateTickCount(
rangeWithBleed,
findRangeExtent(this.axis.range),
findRangeExtent(visibleRange),
minSpacing,
maxSpacing,
ContinuousScale.defaultTickCount,
Expand All @@ -102,6 +104,7 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {
domain,
primaryTickCount,
visibleRange,
niceMode,
parallelFlipRotation,
regularFlipRotation,
labelX,
Expand Down Expand Up @@ -163,7 +166,7 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {
autoRotation = 0;
textAlign = getTextAlign(parallel, configuredRotation, 0, sideFlag, regularFlipFlag);

const tickStrategies = this.getTickStrategies({ domain, secondaryAxis, index });
const tickStrategies = this.getTickStrategies({ domain, niceMode, secondaryAxis, index });

for (const strategy of tickStrategies) {
({ tickData, index, autoRotation, terminate } = strategy({
Expand Down Expand Up @@ -198,10 +201,12 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {

private getTickStrategies({
domain,
niceMode,
index: iteration,
secondaryAxis,
}: {
domain: D[];
niceMode: NiceMode;
index: number;
secondaryAxis: boolean;
}): TickStrategy[] {
Expand Down Expand Up @@ -231,7 +236,16 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {
visibleRange,
terminate,
}: TickStrategyParams) =>
this.createTickData(domain, tickGenerationType, index, tickData, terminate, primaryTickCount, visibleRange);
this.createTickData(
domain,
niceMode,
visibleRange,
primaryTickCount,
tickGenerationType,
index,
tickData,
terminate
);

strategies.push(tickGenerationStrategy);

Expand All @@ -245,12 +259,13 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {
}: TickStrategyParams) =>
this.createTickData(
domain,
niceMode,
visibleRange,
primaryTickCount,
TickGenerationType.FILTER,
index,
tickData,
terminate,
primaryTickCount,
visibleRange
terminate
);
strategies.push(tickFilterStrategy);
}
Expand All @@ -270,12 +285,13 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {

private createTickData(
domain: D[],
niceMode: NiceMode,
visibleRange: [number, number],
primaryTickCount: number | undefined,
tickGenerationType: TickGenerationType,
index: number,
tickData: TickData,
terminate: boolean,
primaryTickCount: number | undefined,
visibleRange: [number, number]
terminate: boolean
): TickStrategyResult {
const { scale, interval } = this.axis;
const { step, values, minSpacing, maxSpacing } = interval;
Expand All @@ -297,6 +313,8 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {

tickData = this.getTicks({
domain,
niceMode,
visibleRange,
tickGenerationType,
previousTicks,
minTickCount,
Expand All @@ -317,6 +335,8 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {

private getTicks({
domain,
niceMode,
visibleRange,
tickGenerationType,
previousTicks,
tickCount,
Expand All @@ -325,6 +345,8 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {
primaryTickCount,
}: {
domain: D[];
niceMode: NiceMode;
visibleRange: [number, number];
tickGenerationType: TickGenerationType;
previousTicks: TickDatum[];
tickCount: number;
Expand All @@ -333,18 +355,23 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {
primaryTickCount?: number;
}): TickData {
const { axis } = this;
const { nice, range, scale, visibleRange, interval } = axis;
const { range, scale, interval } = axis;
const idGenerator = createIdsGenerator();

const tickParams: ScaleTickParams<any> = {
nice,
const domainParams: ScaleTickParams<any> = {
nice: niceMode === NiceMode.TickAndDomain,
interval: interval.step,
tickCount,
minTickCount,
maxTickCount,
};

let niceDomain = nice ? scale.niceDomain(tickParams, domain) : domain;
const tickParams = {
...domainParams,
nice: niceMode === NiceMode.TickAndDomain || niceMode === NiceMode.TicksOnly,
};

let niceDomain = niceMode === NiceMode.TickAndDomain ? scale.niceDomain(domainParams, domain) : domain;

let rawTicks: any[];

Expand Down Expand Up @@ -373,14 +400,14 @@ export class AxisTickGenerator<S extends Scale<D, number, TickInterval<S>>, D> {
niceDomain = secondaryAxisTicks.domain.map((d) => scaleStopTsComplaining.toDomain(d));
} else {
// AG-10654 Just use normal ticks for categorical axes.
rawTicks = scaleStopTsComplaining.ticks?.(tickParams, niceDomain, visibleRange) ?? [];
rawTicks = scaleStopTsComplaining.ticks(tickParams, niceDomain) ?? [];
}
break;
case TickGenerationType.FILTER:
rawTicks = this.filterTicks(previousTicks, tickCount);
break;
default:
rawTicks = scale.ticks(tickParams, niceDomain, visibleRange) ?? [];
rawTicks = scale.ticks(tickParams, niceDomain) ?? [];
break;
}

Expand Down
13 changes: 7 additions & 6 deletions packages/ag-charts-community/src/chart/axis/axisTicks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Selection } from '../../scene/selection';
import { Text } from '../../scene/shape/text';
import { formatValue } from '../../util/format.util';
import { createId } from '../../util/id';
import { countFractionDigits, findMinMax, findRangeExtent, round } from '../../util/number';
import { countFractionDigits, findMinMax, findRangeExtent } from '../../util/number';
import { createIdsGenerator } from '../../util/tempUtils';
import { CachedTextMeasurerPool } from '../../util/textMeasurer';
import { estimateTickCount } from '../../util/ticks';
Expand Down Expand Up @@ -50,7 +50,7 @@ interface LabelNodeDatum {

export class AxisTicks {
static readonly DefaultTickCount = 5;
static readonly DefaultMinSpacing = 50;
static readonly DefaultMinSpacing = 10;

readonly id = createId(this);

Expand Down Expand Up @@ -146,17 +146,17 @@ export class AxisTicks {

private generateTicks() {
const { minSpacing, maxSpacing } = this.interval;
const extentWithBleed = round(findRangeExtent(this.scale.range), 2);
const { maxTickCount, minTickCount, tickCount } = estimateTickCount(
extentWithBleed,
findRangeExtent(this.scale.range),
1,
minSpacing,
maxSpacing,
AxisTicks.DefaultTickCount,
AxisTicks.DefaultMinSpacing
);

const tickData = this.getTicksData({
nice: false,
nice: true,
interval: this.interval.step,
tickCount,
minTickCount,
Expand All @@ -182,7 +182,8 @@ export class AxisTicks {

private getTicksData(tickParams: ScaleTickParams<any>) {
const ticks: TickDatum[] = [];
const rawTicks = this.scale.ticks(tickParams);
const niceDomain = tickParams.nice ? this.scale.niceDomain(tickParams) : this.scale.domain;
const rawTicks = this.scale.ticks(tickParams, niceDomain);
const fractionDigits = rawTicks.reduce((max, tick) => Math.max(max, countFractionDigits(tick)), 0);
const idGenerator = createIdsGenerator();

Expand Down
6 changes: 6 additions & 0 deletions packages/ag-charts-community/src/chart/axis/axisUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import type { RotatableText, TransformableText } from '../../scene/shape/text';
import type { TranslatableType } from '../../scene/transformable';
import { findMinMax } from '../../util/number';

export enum NiceMode {
TickAndDomain,
TicksOnly,
Off,
}

export interface TickDatum {
tickLabel: string;
tick: any;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { ChartAxisDirection } from '../chartAxisDirection';
import type { AnimationManager } from '../interaction/animationManager';
import { Axis, AxisGroupZIndexMap, type LabelNodeDatum, TranslatableLine } from './axis';
import { AxisTickGenerator, type TickGenerationResult } from './axisTickGenerator';
import type { AxisLabelDatum, TickDatum } from './axisUtil';
import type { AxisLabelDatum, NiceMode, TickDatum } from './axisUtil';
import {
prepareAxisAnimationContext,
prepareAxisAnimationFunctions,
Expand Down Expand Up @@ -161,6 +161,7 @@ export abstract class CartesianAxis<S extends Scale<D, number, any> = Scale<any,

override calculateTickLayout(
domain: D[],
niceMode: NiceMode,
visibleRange: [number, number],
initialPrimaryTickCount?: number
): {
Expand All @@ -178,6 +179,7 @@ export abstract class CartesianAxis<S extends Scale<D, number, any> = Scale<any,

const tickGenerationResult = this.tickGenerator.generateTicks({
domain,
niceMode,
visibleRange,
primaryTickCount: initialPrimaryTickCount,
parallelFlipRotation,
Expand Down
9 changes: 4 additions & 5 deletions packages/ag-charts-community/src/scale/ordinalTimeScale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ export class OrdinalTimeScale extends BandScale<Date, TimeInterval | number> {

override ticks(
{ interval, maxTickCount }: ScaleTickParams<TimeInterval | number>,
domain: Date[] = this.domain,
visibleRange: [number, number] = [0, 1]
domain: Date[] = this.domain
): Date[] {
if (!domain.length) {
return [];
Expand All @@ -79,7 +78,7 @@ export class OrdinalTimeScale extends BandScale<Date, TimeInterval | number> {
const stop = Math.max(t0, t1);

if (interval == null) {
return this.getDefaultTicks(maxTickCount, isReversed, visibleRange);
return this.getDefaultTicks(maxTickCount, isReversed);
}

const [r0, r1] = this.range;
Expand All @@ -96,10 +95,10 @@ export class OrdinalTimeScale extends BandScale<Date, TimeInterval | number> {
});
}

private getDefaultTicks(maxTickCount: number, isReversed: boolean, visibleRange: [number, number]) {
private getDefaultTicks(maxTickCount: number, isReversed: boolean) {
const { domain } = this;
const ticks: Date[] = [];
const tickEvery = Math.ceil((domain.length * (visibleRange[1] - visibleRange[0])) / maxTickCount);
const tickEvery = Math.ceil(domain.length / maxTickCount);
const tickOffset = Math.floor(tickEvery / 2);

for (let index = 0; index < domain.length; index += 1) {
Expand Down
2 changes: 1 addition & 1 deletion packages/ag-charts-community/src/scale/scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface Scale<D, R, I = number> {
toDomain(value: number): D | undefined;
convert(value: D, clamp?: boolean): R;
invert(value: R, exact?: boolean): D | undefined;
ticks(ticks: ScaleTickParams<I>, domain?: D[], visibleRange?: [number, number]): D[] | undefined;
ticks(ticks: ScaleTickParams<I>, domain?: D[]): D[] | undefined;
niceDomain(ticks: ScaleTickParams<I>, domain?: D[]): D[];
tickFormatter(params: ScaleFormatParams<D>): ((x: any) => string) | undefined;
datumFormatter(params: ScaleFormatParams<D>): ((x: any) => string) | undefined;
Expand Down
Loading
Loading