Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions src/coord/axisAlignTicks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ export function scaleCalcAlign(
// (2) `SCALE_EXTENT_KIND_MAPPING` is not considered yet.

const isTargetLogScale = isLogScale(targetScale);

// alignTicks is not supported for mapped log scales (asinh/symlog).
// loopIncreaseInterval multiplies by targetLogScaleBase, which assumes
// integer spacing in log space. That assumption does not hold for these
// transforms, so each axis calculates its own nice ticks independently.
if (isTargetLogScale && (targetScale as LogScale).logMapping) {
return;
}

const alignToScaleLinear = isLogScale(alignToScale) ? alignToScale.intervalStub : alignToScale;
const targetIntervalStub = isTargetLogScale ? targetScale.intervalStub : targetScale;

Expand Down
2 changes: 2 additions & 0 deletions src/coord/axisCommonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ export interface LogAxisBaseOption extends NumericAxisBaseOptionCommon {
type?: 'log';
axisLabel?: AxisLabelOption<'log'>;
logBase?: number;
logMapping?: 'none' | 'asinh' | 'symlog';
logLinearWidth?: number;
}
export interface TimeAxisBaseOption extends NumericAxisBaseOptionCommon {
type?: 'time';
Expand Down
4 changes: 3 additions & 1 deletion src/coord/axisDefault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,9 @@ const timeAxis: AxisBaseOption = zrUtil.merge({
}, valueAxis);

const logAxis: AxisBaseOption = zrUtil.defaults({
logBase: 10
logBase: 10,
logMapping: 'none',
logLinearWidth: 1,
}, valueAxis);


Expand Down
4 changes: 3 additions & 1 deletion src/coord/axisHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function createScaleByModel(
// Expect `Pick<AxisBaseOptionCommon, 'type'>`,
// but be lenient for user's invalid input.
{type?: string}
& Pick<LogAxisBaseOption, 'logBase'>
& Pick<LogAxisBaseOption, 'logBase' | 'logMapping' | 'logLinearWidth'>
& Pick<AxisBaseOptionCommon, 'breaks'>
>
& Partial<Pick<
Expand Down Expand Up @@ -120,6 +120,8 @@ export function createScaleByModel(
// See also #3749
return new LogScale({
logBase: model.get('logBase'),
logMapping: model.get('logMapping'),
logLinearWidth: model.get('logLinearWidth'),
breakOption,
});
case 'value':
Expand Down
94 changes: 93 additions & 1 deletion src/coord/axisNiceTicks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
* under the License.
*/

import { assert, noop } from 'zrender/src/core/util';
import { assert, map, noop } from 'zrender/src/core/util';
import {
ensureValidSplitNumber, getIntervalPrecision,
intervalScaleEnsureValidExtent,
isIntervalScale, isLogScale, isTimeScale,
asinhScaleForwardTick,
symlogScaleForwardTick,
} from '../scale/helper';
import IntervalScale, { IntervalScaleConfig } from '../scale/Interval';
import { mathCeil, mathFloor, mathMax, nice, quantity, round } from '../util/number';
Expand Down Expand Up @@ -55,6 +57,12 @@ function calcNiceForIntervalOrLogScale(
const isTargetLogScale = isLogScale(scale);
const intervalStub = isTargetLogScale ? scale.intervalStub : scale;

// For mapped-log axes (asinh/symlog), use the raw-space tick strategy.
if (isTargetLogScale && (scale as LogScale).logMapping) {
logMappingCalcNiceTicks(scale as LogScale, opt.splitNumber);
return;
}

const fixMinMax = opt.fixMinMax || [];
const oldOutermostExtent = isTargetLogScale ? scale.getExtent() : null;
const oldIntervalExtent = intervalStub.getExtent();
Expand Down Expand Up @@ -197,6 +205,90 @@ function logScaleCalcNiceTicks(
// ------ END: LogScale Nice ------


// ------ START: logMapping Nice ------

/**
* Tick strategy for `logMapping: 'asinh' | 'symlog'` axes.
*
* Standard log ticking assumes integer intervals in transformed space (integer
* log_b values = powers of b in raw space). For asinh/symlog the candidates
* `0, ±a0, ±b*a0, ...` are non-uniformly spaced in transformed space, so a
* single integer interval does not work.
*
* Strategy: choose candidates in raw-value space, store as `_mappedLogTicks`,
* then set `intervalStub` extent to the transformed range so that
* `normalize`/`scale` pixel mapping remains correct.
*/
export function logMappingCalcNiceTicks(
scale: LogScale, splitNumber?: number | NullUndefined
): void {
const base = scale.base;
const a0 = scale.linearWidth || 1;
const [rawMin, rawMax] = scale.getExtent();
const maxTicks = ensureValidSplitNumber(splitNumber, 5) + 1;

const forward = scale.logMapping === 'asinh'
? (v: number) => asinhScaleForwardTick(v, a0)
: (v: number) => symlogScaleForwardTick(v, a0);

// Count how many powers of `base` span the extent so we can decide
// whether to step by base^1, base^2, … to stay within `splitNumber`.
const absMax = Math.max(Math.abs(rawMin), Math.abs(rawMax));
const totalSteps = absMax > a0 ? Math.ceil(Math.log(absMax / a0) / Math.log(base)) : 0;
// Account for both positive and negative sides plus zero.
const hasNeg = rawMin < 0;
const hasPos = rawMax > 0;
const sidesMultiplier = (hasNeg && hasPos) ? 2 : 1;
const estimatedTicks = totalSteps * sidesMultiplier + 1; // +1 for zero

// Raise the effective base so tick count stays within splitNumber.
let stride = 1;
if (estimatedTicks > maxTicks && totalSteps > 0) {
stride = Math.ceil(totalSteps * sidesMultiplier / (maxTicks - 1));
}
const effectiveBase = Math.pow(base, stride);

// Candidates: 0, ±a0, ±a0·effectiveBase, ±a0·effectiveBase², ...
const candidates: number[] = [0];
let v = a0;
while (v <= absMax * 1.0001) {
candidates.push(v, -v);
v *= effectiveBase;
}

// Filter to data extent and sort ascending.
// Candidates are distinct by construction (0 once, then ±v pairs with v > 0),
// so no deduplication is needed.
const filtered: number[] = [];
candidates.sort((a, b) => a - b);
for (let i = 0; i < candidates.length; i++) {
const c = candidates[i];
if (c >= rawMin && c <= rawMax) {
filtered.push(c);
}
}
const ticks = map(filtered, value => ({ value }));

// Degenerate extent: ensure at least one tick.
if (ticks.length === 0) {
ticks.push({ value: (rawMin + rawMax) / 2 });
}

scale._mappedLogTicks = ticks;

// Align intervalStub extent to the transformed data range so that
// `normalize`/`scale` (pixel mapping) covers the full data extent.
// Use the data min/max rather than the tick min/max, because the
// outermost ticks may fall inside the data range when thinning skips
// intermediate powers.
const intervalStub = scale.intervalStub;
intervalStub.setExtent(forward(rawMin), forward(rawMax));
intervalStub.setConfig({ interval: 1 });
}

// ------ END: logMapping Nice ------


// ------ START: scaleCalcNice Entry ------

export type ScaleCalcNiceMethod = (
Expand Down
Loading