blob: 7fd3bd684fcffc8f9682ddab334318e858949eb0 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Scale, { ScaleGetTicksOpt } from './Scale';
import IntervalScale from './Interval';
import {
ScaleTick,
NullUndefined,
AxisBreakOption
} from '../util/types';
import {
logScalePowTick,
IntervalScaleGetLabelOpt,
logScaleLogTick,
ValueTransformLookupOpt,
} from './helper';
import { getBreaksUnsafe, getScaleBreakHelper, ParseAxisBreakOptionInwardTransformOut } from './break';
import { getMinorTicks } from './minorTicks';
import {
DecoratedScaleMapperMethods, decorateScaleMapper, enableScaleMapperFreeze, SCALE_EXTENT_KIND_EFFECTIVE,
SCALE_MAPPER_DEPTH_OUT_OF_BREAK,
ScaleMapperTransformOutOpt
} from './scaleMapper';
import { map } from 'zrender/src/core/util';
import { isValidBoundsForExtent } from '../util/model';
import { isNullableNumberFinite } from '../util/number';
type LogScaleSetting = {
logBase: number | NullUndefined;
breakOption: AxisBreakOption[] | NullUndefined;
};
const LOOKUP_IDX_EXTENT_START = 0;
const LOOKUP_IDX_EXTENT_END = 1;
const LOOKUP_IDX_BREAK_START = 2;
/**
* @final NEVER inherit me!
*/
class LogScale extends Scale<LogScale> {
static type = 'log';
readonly type = 'log' as const;
readonly base: number;
/**
* `powStub` is used to save original values, i.e., values before logarithm
* applied, such as raw extent and raw breaks.
* NOTE: Logarithm transform is probably not inversible by rounding error, which
* may cause min/max tick is displayed like `5.999999999999999`. The extent in
* powStub is used to get the original precise extent for this issue.
*
* [CAVEAT] `powStub` and `intervalStub` should be modified synchronously.
*/
readonly powStub: IntervalScale;
/**
* `intervalStub` provides linear tick arrangement (logarithm applied).
* @see {powStub}
*/
readonly intervalStub: IntervalScale;
private _lookup: ValueTransformLookupOpt['lookup'];
constructor(setting: LogScaleSetting) {
super();
this.parse = IntervalScale.parse;
this.base = setting.logBase || 10;
const lookupFrom: number[] = [];
const lookupTo: number[] = [];
const lookup = this._lookup = {from: lookupFrom, to: lookupTo};
lookupFrom[LOOKUP_IDX_EXTENT_START] =
lookupFrom[LOOKUP_IDX_EXTENT_END] =
lookupTo[LOOKUP_IDX_EXTENT_START] =
lookupTo[LOOKUP_IDX_EXTENT_END] = NaN;
decorateScaleMapper(this, LogScale.mapperMethods);
const scaleBreakHelper = getScaleBreakHelper();
const breakOption = setting.breakOption;
const out: ParseAxisBreakOptionInwardTransformOut = {lookup};
if (scaleBreakHelper) {
scaleBreakHelper.parseAxisBreakOptionInwardTransform(
breakOption, this, {noNegative: true}, LOOKUP_IDX_BREAK_START, out
);
}
this.powStub = new IntervalScale({breakParsed: out.original});
this.intervalStub = new IntervalScale({breakParsed: out.transformed});
enableScaleMapperFreeze(this, this.intervalStub);
}
getTicks(opt?: ScaleGetTicksOpt): ScaleTick[] {
const base = this.base;
const powStub = this.powStub;
const scaleBreakHelper = getScaleBreakHelper();
const intervalStub = this.intervalStub;
const intervalExtent = intervalStub.getExtent();
const powExtent = powStub.getExtent();
const powOpt: ValueTransformLookupOpt = {lookup: {from: intervalExtent, to: powExtent}};
return map(intervalStub.getTicks(opt || {}), function (tick) {
const val = tick.value;
let powVal = logScalePowTick(val, base, powOpt);
let vBreak;
if (scaleBreakHelper) {
const brkPowResult = scaleBreakHelper.getTicksBreakOutwardTransform(
this,
tick,
getBreaksUnsafe(powStub),
this._lookup,
);
if (brkPowResult) {
vBreak = brkPowResult.vBreak;
powVal = brkPowResult.tickVal;
}
}
return {
value: powVal,
break: vBreak,
};
}, this);
}
getMinorTicks(splitNumber: number): number[][] {
return getMinorTicks(
this,
splitNumber,
getBreaksUnsafe(this.powStub),
// NOTE: minor ticks are in the log scale value to visually hint users "logarithm".
this.intervalStub.getConfig().interval
);
}
getLabel(
data: ScaleTick,
opt?: IntervalScaleGetLabelOpt
) {
return this.intervalStub.getLabel(data, opt);
}
static mapperMethods: DecoratedScaleMapperMethods<LogScale> = {
needTransform() {
return true;
},
normalize(val) {
return this.intervalStub.normalize(logScaleLogTick(val, this.base));
},
scale(val) {
// PENDING: Input `intervalStub.getExtent()` and `powStub.getExtent()` may
// break monotonicity. Do not do it until real problems found.
return logScalePowTick(this.intervalStub.scale(val), this.base, null);
},
transformIn(val, opt) {
val = logScaleLogTick(val, this.base);
return (opt && opt.depth === SCALE_MAPPER_DEPTH_OUT_OF_BREAK)
? val
: this.intervalStub.transformIn(val, opt);
},
transformOut(val, opt) {
const depth = opt ? opt.depth : null;
tmpTransformOutOpt1.depth = depth;
tmpTransformOutOpt2.lookup = this._lookup;
return logScalePowTick(
(depth === SCALE_MAPPER_DEPTH_OUT_OF_BREAK)
? val
: this.intervalStub.transformOut(val, tmpTransformOutOpt1),
this.base,
tmpTransformOutOpt2
);
},
contain(val) {
return this.powStub.contain(val);
},
/**
* NOTICE: The caller should ensure `start` and `end` are both non-negative.
*/
setExtent(start, end) {
this.setExtent2(SCALE_EXTENT_KIND_EFFECTIVE, start, end);
},
setExtent2(kind, start, end) {
if (!isValidBoundsForExtent(start, end)
|| start <= 0 || end <= 0
) {
return;
}
let lookupTo = tmpNotUsedArr;
let lookupFrom = tmpNotUsedArr;
if (kind === SCALE_EXTENT_KIND_EFFECTIVE) {
const lookup = this._lookup;
lookupTo = lookup.to;
lookupFrom = lookup.from;
}
this.powStub.setExtent2(
kind,
(lookupTo[LOOKUP_IDX_EXTENT_START] = start),
(lookupTo[LOOKUP_IDX_EXTENT_END] = end)
);
const base = this.base;
this.intervalStub.setExtent2(
kind,
(lookupFrom[LOOKUP_IDX_EXTENT_START] = logScaleLogTick(start, base)),
(lookupFrom[LOOKUP_IDX_EXTENT_END] = logScaleLogTick(end, base))
);
},
getFilter() {
return {g: 0};
},
sanitize(value, dataExtent) {
// Conservative - if dataExtent is invalid, do not sanitize.
if (isValidBoundsForExtent(dataExtent[0], dataExtent[1])
&& isNullableNumberFinite(value)
&& value <= 0
) {
// `DataStore` has ensured that `dataExtent` is valid for LogScale.
value = dataExtent[0];
}
return value;
},
getDefaultStartValue() {
return 1;
},
getExtent() {
return this.powStub.getExtent();
},
getExtentUnsafe(kind, depth) {
return depth === null
? this.powStub.getExtentUnsafe(kind, null)
: this.intervalStub.getExtentUnsafe(kind, depth);
},
};
}
Scale.registerClass(LogScale);
const tmpTransformOutOpt1: ScaleMapperTransformOutOpt = {};
const tmpTransformOutOpt2: ValueTransformLookupOpt = {};
const tmpNotUsedArr: number[] = [];
export default LogScale;