Skip to content

Commit ffcc636

Browse files
committed
fix(alignTicks): Change alignTick strategy: (1) Previously some series data may be out of the calculated extent and can not be displayed. (2) Previously the precision is incorrect for small float number (fixed at 10 rather than based on the magnitude of the value). (3) Make the tick precision more acceptable when min/max of axis is fixed, and remove console warning, because whey can be specified when dataZoom dragging. (4) Clarify the related code for LogScale.
1 parent d168bf2 commit ffcc636

24 files changed

Lines changed: 1459 additions & 382 deletions

src/coord/axisAlignTicks.ts

Lines changed: 240 additions & 94 deletions
Large diffs are not rendered by default.

src/coord/axisCommonTypes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,10 @@ export interface ValueAxisBaseOption extends NumericAxisBaseOptionCommon {
210210

211211
/**
212212
* Optional value can be:
213-
* + `false`: always include value 0.
213+
* + `false`: always include value 0 if not conflict with `axis.min/max` setting.
214214
* + `true`: the axis may not contain zero position.
215215
*/
216-
scale?: boolean;
216+
scale?: boolean;
217217
}
218218
export interface LogAxisBaseOption extends NumericAxisBaseOptionCommon {
219219
type?: 'log';

src/coord/axisHelper.ts

Lines changed: 61 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,35 @@ import CartesianAxisModel from './cartesian/AxisModel';
4646
import SeriesData from '../data/SeriesData';
4747
import { getStackedDimension } from '../data/helper/dataStackHelper';
4848
import { Dictionary, DimensionName, ScaleTick } from '../util/types';
49-
import { ensureScaleRawExtentInfo } from './scaleRawExtentInfo';
49+
import { ensureScaleRawExtentInfo, ScaleRawExtentResult } from './scaleRawExtentInfo';
5050
import { parseTimeAxisLabelFormatter } from '../util/time';
5151
import { getScaleBreakHelper } from '../scale/break';
5252
import { error } from '../util/log';
53+
import { isIntervalScale, isTimeScale } from '../scale/helper';
5354

5455

5556
type BarWidthAndOffset = ReturnType<typeof makeColumnLayout>;
5657

5758
/**
58-
* Get axis scale extent before niced.
59+
* Prepare axis scale extent before niced.
5960
* Item of returned array can only be number (including Infinity and NaN).
6061
*
61-
* Caution:
62-
* Precondition of calling this method:
63-
* The scale extent has been initialized using series data extent via
64-
* `scale.setExtent` or `scale.unionExtentFromData`;
62+
* CAVEAT:
63+
* This function has side-effect.
64+
*
65+
* FIXME:
66+
* Refector to decouple `unionExtentFromData` and irregular value handling from `scale`.
67+
* Merge `unionAxisExtentFromData` and `unionExtentFromData`.
68+
* Refector `ensureScaleRawExtentInfo`.
6569
*/
66-
export function getScaleExtent(scale: Scale, model: AxisBaseModel) {
67-
const scaleType = scale.type;
68-
const rawExtentResult = ensureScaleRawExtentInfo(scale, model, scale.getExtent()).calculate();
70+
export function adoptScaleExtentOptionAndPrepare(
71+
scale: Scale,
72+
model: AxisBaseModel,
73+
// Typically: data extent from all series on this axis.
74+
// Can be obtained by `scale.unionExtentFromData(); scale.getExtent()`;
75+
dataExtent: number[]
76+
): ScaleRawExtentResult {
77+
const rawExtentResult = ensureScaleRawExtentInfo(scale, model, dataExtent).calculate();
6978

7079
scale.setBlank(rawExtentResult.isBlank);
7180

@@ -82,7 +91,7 @@ export function getScaleExtent(scale: Scale, model: AxisBaseModel) {
8291
// (4) Consider other chart types using `barGrid`?
8392
// See #6728, #4862, `test/bar-overflow-time-plot.html`
8493
const ecModel = model.ecModel;
85-
if (ecModel && (scaleType === 'time' /* || scaleType === 'interval' */)) {
94+
if (ecModel && (isTimeScale(scale) /* || scaleType === 'interval' */)) {
8695
const barSeriesModels = prepareLayoutBarSeries('bar', ecModel);
8796
let isBaseAxisAndHasBarSeries = false;
8897

@@ -102,13 +111,10 @@ export function getScaleExtent(scale: Scale, model: AxisBaseModel) {
102111
}
103112
}
104113

105-
return {
106-
extent: [min, max],
107-
// "fix" means "fixed", the value should not be
108-
// changed in the subsequent steps.
109-
fixMin: rawExtentResult.minFixed,
110-
fixMax: rawExtentResult.maxFixed
111-
};
114+
rawExtentResult.min = min;
115+
rawExtentResult.max = max;
116+
117+
return rawExtentResult;
112118
}
113119

114120
function adjustScaleForOverflow(
@@ -151,32 +157,25 @@ function adjustScaleForOverflow(
151157
return {min: min, max: max};
152158
}
153159

154-
// Precondition of calling this method:
155-
// The scale extent has been initialized using series data extent via
156-
// `scale.setExtent` or `scale.unionExtentFromData`;
157160
export function niceScaleExtent(
158161
scale: Scale,
159-
inModel: AxisBaseModel
160-
) {
162+
inModel: AxisBaseModel,
163+
// Typically: data extent from all series on this axis, which can be obtained by
164+
// `scale.unionExtentFromData(...); scale.getExtent();`.
165+
dataExtent: number[],
166+
): void {
161167
const model = inModel as AxisBaseModel<LogAxisBaseOption>;
162-
const extentInfo = getScaleExtent(scale, model);
163-
const extent = extentInfo.extent;
164-
const splitNumber = model.get('splitNumber');
165-
166-
if (scale instanceof LogScale) {
167-
scale.base = model.get('logBase');
168-
}
168+
const extentInfo = adoptScaleExtentOptionAndPrepare(scale, model, dataExtent);
169169

170-
const scaleType = scale.type;
171-
const interval = model.get('interval');
172-
const isIntervalOrTime = scaleType === 'interval' || scaleType === 'time';
170+
const isInterval = isIntervalScale(scale);
171+
const isIntervalOrTime = isInterval || isTimeScale(scale);
173172

174173
scale.setBreaksFromOption(retrieveAxisBreaksOption(model));
175-
scale.setExtent(extent[0], extent[1]);
174+
scale.setExtent(extentInfo.min, extentInfo.max);
176175
scale.calcNiceExtent({
177-
splitNumber: splitNumber,
178-
fixMin: extentInfo.fixMin,
179-
fixMax: extentInfo.fixMax,
176+
splitNumber: model.get('splitNumber'),
177+
fixMin: extentInfo.minFixed,
178+
fixMax: extentInfo.maxFixed,
180179
minInterval: isIntervalOrTime ? model.get('minInterval') : null,
181180
maxInterval: isIntervalOrTime ? model.get('maxInterval') : null
182181
});
@@ -185,36 +184,35 @@ export function niceScaleExtent(
185184
// is not good enough. He can specify the interval. It is often appeared
186185
// in angle axis with angle 0 - 360. Interval calculated in interval scale is hard
187186
// to be 60.
188-
// FIXME
189-
if (interval != null) {
190-
(scale as IntervalScale).setInterval && (scale as IntervalScale).setInterval(interval);
187+
// In `xxxAxis.type: 'log'`, ec option `xxxAxis.interval` requires a logarithm-applied
188+
// value rather than a value in the raw scale.
189+
const interval = model.get('interval');
190+
if (interval != null && (scale as IntervalScale).setInterval) {
191+
(scale as IntervalScale).setInterval({interval});
191192
}
192193
}
193194

194-
/**
195-
* @param axisType Default retrieve from model.type
196-
*/
197-
export function createScaleByModel(model: AxisBaseModel, axisType?: string): Scale {
198-
axisType = axisType || model.get('type');
199-
if (axisType) {
200-
switch (axisType) {
201-
// Buildin scale
202-
case 'category':
203-
return new OrdinalScale({
204-
ordinalMeta: model.getOrdinalMeta
205-
? model.getOrdinalMeta()
206-
: model.getCategories(),
207-
extent: [Infinity, -Infinity]
208-
});
209-
case 'time':
210-
return new TimeScale({
211-
locale: model.ecModel.getLocaleModel(),
212-
useUTC: model.ecModel.get('useUTC'),
213-
});
214-
default:
215-
// case 'value'/'interval', 'log', or others.
216-
return new (Scale.getClass(axisType) || IntervalScale)();
217-
}
195+
export function createScaleByModel(model: AxisBaseModel): Scale {
196+
const axisType = model.get('type');
197+
switch (axisType) {
198+
case 'category':
199+
return new OrdinalScale({
200+
ordinalMeta: model.getOrdinalMeta
201+
? model.getOrdinalMeta()
202+
: model.getCategories(),
203+
extent: [Infinity, -Infinity]
204+
});
205+
case 'time':
206+
return new TimeScale({
207+
locale: model.ecModel.getLocaleModel(),
208+
useUTC: model.ecModel.get('useUTC'),
209+
});
210+
case 'log':
211+
// See also #3749
212+
return new LogScale((model as AxisBaseModel<LogAxisBaseOption>).get('logBase'));
213+
default:
214+
// case 'value'/'interval', or others.
215+
return new (Scale.getClass(axisType) || IntervalScale)();
218216
}
219217
}
220218

@@ -303,7 +301,6 @@ export function getAxisRawValue<TIsCategory extends boolean>(axis: Axis, tick: S
303301

304302
/**
305303
* @param model axisLabelModel or axisTickModel
306-
* @return {number|String} Can be null|'auto'|number|function
307304
*/
308305
export function getOptionCategoryInterval(
309306
model: Model<AxisBaseOption['axisLabel']>

src/coord/axisModelCommonMixin.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ interface AxisModelCommonMixin<Opt extends AxisBaseOption> extends Pick<Model<Op
3131
class AxisModelCommonMixin<Opt extends AxisBaseOption> {
3232

3333
getNeedCrossZero(): boolean {
34-
const option = this.option as ValueAxisBaseOption;
35-
return !option.scale;
34+
return !(this.option as ValueAxisBaseOption).scale;
3635
}
3736

3837
/**

src/coord/cartesian/Grid.ts

Lines changed: 77 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
getDataDimensionsOnAxis,
3333
isNameLocationCenter,
3434
shouldAxisShow,
35+
retrieveAxisBreaksOption,
3536
} from '../../coord/axisHelper';
3637
import Cartesian2D, {cartesian2DDimensions} from './Cartesian2D';
3738
import Axis2D from './Axis2D';
@@ -124,50 +125,32 @@ class Grid implements CoordinateSystemMaster {
124125
this._updateScale(ecModel, this.model);
125126

126127
function updateAxisTicks(axes: Record<number, Axis2D>) {
127-
let alignTo: Axis2D;
128128
// Axis is added in order of axisIndex.
129129
const axesIndices = keys(axes);
130-
const len = axesIndices.length;
131-
if (!len) {
132-
return;
133-
}
134130
const axisNeedsAlign: Axis2D[] = [];
135-
// Process once and calculate the ticks for those don't use alignTicks.
136-
for (let i = len - 1; i >= 0; i--) {
137-
const idx = +axesIndices[i]; // Convert to number.
138-
const axis = axes[idx];
139-
const model = axis.model as AxisBaseModel<NumericAxisBaseOptionCommon>;
140-
const scale = axis.scale;
141-
if (// Only value and log axis without interval support alignTicks.
142-
isIntervalOrLogScale(scale)
143-
&& model.get('alignTicks')
144-
&& model.get('interval') == null
145-
) {
131+
132+
for (let i = axesIndices.length - 1; i >= 0; i--) { // Reverse order
133+
const axis = axes[+axesIndices[i]];
134+
if (axis.alignTo) {
146135
axisNeedsAlign.push(axis);
147136
}
148137
else {
149-
niceScaleExtent(scale, model);
150-
if (isIntervalOrLogScale(scale)) { // Can only align to interval or log axis.
151-
alignTo = axis;
152-
}
138+
niceScaleExtent(axis.scale, axis.model, axis.scale.getExtent());
153139
}
154140
};
155-
// All axes has set alignTicks. Pick the first one.
156-
// PENDING. Should we find the axis that both set interval, min, max and align to this one?
157-
if (axisNeedsAlign.length) {
158-
if (!alignTo) {
159-
alignTo = axisNeedsAlign.pop();
160-
niceScaleExtent(alignTo.scale, alignTo.model);
141+
each(axisNeedsAlign, axis => {
142+
if (incapableOfAlignNeedFallback(axis, axis.alignTo as Axis2D)) {
143+
niceScaleExtent(axis.scale, axis.model, axis.scale.getExtent());
161144
}
162-
163-
each(axisNeedsAlign, axis => {
145+
else {
164146
alignScaleTicks(
165147
axis.scale as IntervalScale | LogScale,
148+
axis.scale.getExtent(),
166149
axis.model,
167-
alignTo.scale as IntervalScale | LogScale
150+
axis.alignTo.scale as IntervalScale | LogScale
168151
);
169-
});
170-
}
152+
}
153+
});
171154
}
172155

173156
updateAxisTicks(axesMap.x);
@@ -450,6 +433,9 @@ class Grid implements CoordinateSystemMaster {
450433
});
451434
});
452435

436+
prepareAlignToInCoordSysCreate(axesMap.x);
437+
prepareAlignToInCoordSysCreate(axesMap.y);
438+
453439
function createAxisCreator(dimName: Cartesian2DDimensionName) {
454440
return function (axisModel: CartesianAxisModel, idx: number): void {
455441
if (!isAxisUsedInTheGrid(axisModel, gridModel)) {
@@ -698,6 +684,66 @@ function canOnZeroToAxis(axis: Axis2D): boolean {
698684
return axis && axis.type !== 'category' && axis.type !== 'time' && ifAxisCrossZero(axis);
699685
}
700686

687+
/**
688+
* [CAVEAT] This method is called before data processing stage.
689+
* Do not rely on any info that is determined afterward.
690+
*/
691+
function prepareAlignToInCoordSysCreate(axes: Record<number, Axis2D>): void {
692+
// Axis is added in order of axisIndex.
693+
const axesIndices = keys(axes);
694+
695+
let alignTo: Axis2D;
696+
const axisNeedsAlign: Axis2D[] = [];
697+
698+
for (let i = axesIndices.length - 1; i >= 0; i--) { // Reverse order
699+
const axis = axes[+axesIndices[i]];
700+
if (
701+
isIntervalOrLogScale(axis.scale)
702+
// NOTE: `scale.hasBreaks()` is not available at this moment. Check it later.
703+
&& retrieveAxisBreaksOption(axis.model) == null
704+
// NOTE: `scale.getTicks()` is not available at this moment. Check it later.
705+
) {
706+
// Request `alignTicks`.
707+
if ((axis.model as AxisBaseModel<NumericAxisBaseOptionCommon>).get('alignTicks')
708+
&& (axis.model as AxisBaseModel<NumericAxisBaseOptionCommon>).get('interval') == null
709+
) {
710+
axisNeedsAlign.push(axis);
711+
}
712+
else {
713+
// `alignTo` the last one that does not request `alignTicks`
714+
// (This rule is retained for backward compat).
715+
alignTo = axis;
716+
}
717+
}
718+
};
719+
// If all axes has set alignTicks, pick the first one as alignTo.
720+
// PENDING. Should we find the axis that both set interval, min, max and align to this one?
721+
// PENDING. Should we allow specifying alignTo via ec option?
722+
if (!alignTo) {
723+
alignTo = axisNeedsAlign.pop();
724+
}
725+
if (alignTo) {
726+
each(axisNeedsAlign, function (axis) {
727+
axis.alignTo = alignTo;
728+
});
729+
}
730+
}
731+
732+
/**
733+
* This is just a defence code. They are unlikely to be actually `true`,
734+
* since these cases have been addressed in `prepareAlignToInCoordSysCreate`.
735+
*
736+
* Can not be called BEFORE "nice" performed.
737+
*/
738+
function incapableOfAlignNeedFallback(targetAxis: Axis2D, alignTo: Axis2D): boolean {
739+
return targetAxis.scale.hasBreaks()
740+
|| alignTo.scale.hasBreaks()
741+
// Normally ticks length are more than 2 even when axis is blank.
742+
// But still guard for corner cases and possible changes.
743+
|| alignTo.scale.getTicks().length < 2;
744+
}
745+
746+
701747
function updateAxisTransform(axis: Axis2D, coordBase: number) {
702748
const axisExtent = axis.getExtent();
703749
const axisExtentSum = axisExtent[0] + axisExtent[1];

src/coord/cartesian/defaultAxisExtentFromData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ function shrinkAxisExtent(axisRecordMap: HashMap<AxisRecord>) {
241241
if (tarAxisExtent) {
242242
const rawExtentResult = axisRecord.rawExtentResult;
243243
const rawExtentInfo = axisRecord.rawExtentInfo;
244-
// Shink the original extent.
244+
// Shrink the original extent.
245245
if (!rawExtentResult.minFixed && tarAxisExtent[0] > rawExtentResult.min) {
246246
rawExtentInfo.modifyDataMinMax('min', tarAxisExtent[0]);
247247
}

0 commit comments

Comments
 (0)