blob: cc7c9f137083c4019315fb4008135917c533ebd0 [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 { assert, clone, each, filter, find, isString, map, trim } from 'zrender/src/core/util';
import {
NullUndefined, ParsedAxisBreak, ParsedAxisBreakList, AxisBreakOption,
AxisBreakOptionIdentifierInAxis, ScaleTick, VisualAxisBreak,
} from '../util/types';
import { error } from '../util/log';
import type Scale from './Scale';
import {
AxisBreakParsingResult, registerScaleBreakHelperImpl, ParamPruneByBreak, BreakScaleMapper, ParseBreakOptionOpt
} from './break';
import { mathMax, mathMin, mathRound } from '../util/number';
import { AxisLabelFormatterExtraParams } from '../coord/axisCommonTypes';
import {
DecoratedScaleMapperMethods,
decorateScaleMapper, enableScaleMapperFreeze, initLinearScaleMapper,
SCALE_EXTENT_KIND_EFFECTIVE,
SCALE_MAPPER_DEPTH_OUT_OF_BREAK,
ScaleMapper,
ScaleMapperTransformOutOpt
} from './scaleMapper';
import { isValidBoundsForExtent } from '../util/model';
import { ValueTransformLookupOpt } from './helper';
/**
* @caution
* Must not export anything except `installScaleBreakHelper`
*/
interface BreakScaleMapperImpl extends BreakScaleMapper {
// Linear space, the elapsed result.
_linear: ScaleMapper;
}
class BreakScaleMapperImpl {
// [CAVEAT]: Should set only by the constructor!
readonly breaks: ParsedAxisBreakList;
// Only used to save the original extent to avoid rounding error.
private _outOfBrk: ScaleMapper;
constructor(
breakParsed: AxisBreakParsingResult | NullUndefined,
initialExtent: number[] | NullUndefined
) {
decorateScaleMapper(this, BreakScaleMapperImpl.decoratedMethods);
this._outOfBrk = initLinearScaleMapper(null, initialExtent);
const mapper = this._linear = initLinearScaleMapper(null, initialExtent);
enableScaleMapperFreeze(this, mapper);
this.breaks = breakParsed && breakParsed.breaks || [];
}
hasBreaks(): boolean {
return !!this.breaks.length;
}
/**
* When iteratively generating ticks by nice interval, currently the `interval`, which is
* calculated by break-elapsed extent span, is probably very small comparing to the original
* extent, leading to a large number of iteration and tick generation, even over `safeLimit`.
* Thus stepping over breaks is necessary in that loop.
*
* "Nice" should be ensured on ticks when step over the breaks. Thus this method returns
* a integer multiple of the "nice tick interval".
*
* This method does little work; it is just for unifying and restricting the behavior.
*/
calcNiceTickMultiple(
tickVal: number,
estimateNiceMultiple: (tickVal: number, brkEnd: number) => number
): number {
for (let idx = 0; idx < this.breaks.length; idx++) {
const brk = this.breaks[idx];
if (brk.vmin < tickVal && tickVal < brk.vmax) {
const multiple = estimateNiceMultiple(tickVal, brk.vmax);
if (__DEV__) {
// If not, it may cause dead loop or not nice tick.
assert(multiple >= 0 && mathRound(multiple) === multiple);
}
return multiple;
}
}
return 0;
}
static decoratedMethods: DecoratedScaleMapperMethods<BreakScaleMapperImpl> = {
needTransform() {
return !this.breaks.length;
},
getExtent() {
return this._outOfBrk.getExtent();
},
getExtentUnsafe(kind, depth) {
return (depth == null
|| depth === SCALE_MAPPER_DEPTH_OUT_OF_BREAK
)
? this._outOfBrk.getExtentUnsafe(kind, null)
: this._linear.getExtentUnsafe(kind, null);
},
setExtent(start, end) {
this.setExtent2(SCALE_EXTENT_KIND_EFFECTIVE, start, end);
},
setExtent2(kind, start, end) {
if (isValidBoundsForExtent(start, end)) {
if (kind === SCALE_EXTENT_KIND_EFFECTIVE) {
updateAxisBreakGapReal(this, [start, end]);
}
this._outOfBrk.setExtent2(kind, start, end);
this._linear.setExtent2(
kind,
this.transformIn(start, null),
this.transformIn(end, null)
);
}
},
normalize(val) {
return this._linear.normalize(this.transformIn(val, null));
},
scale(val) {
return this.transformOut(this._linear.scale(val), null);
},
contain(val) {
return this._outOfBrk.contain(val);
},
/**
* a.k.a., "elapse"
* Suppose:
* AXIS_BREAK_LAST_BREAK_END_BASE: 0
* AXIS_BREAK_ELAPSED_BASE: 0
* breaks: [
* {start: -400, end: -300, gap: 27},
* {start: -100, end: 100, gap: 10},
* {start: 200, end: 400, gap: 300},
* ]
* The mapping will be:
* | |
* 400 + -> + 237
* | | | | (gap: 300)
* 200 + -> + -63
* | |
* 100 + -> + -163
* | | | | (gap: 10)
* -100 + -> + -173
* | |
* -300 + -> + -373
* | | | | (gap: 27)
* -400 + -> + -400
* | |
* origianl elapsed
*
* Note:
* `transformIn` and `transformOut` has nothing to do with "scale extent" - out of extent is supported.
*/
transformIn(val, opt) {
if (opt && opt.depth === SCALE_MAPPER_DEPTH_OUT_OF_BREAK) {
return val;
}
// If the value is in the break, return the normalized value in the break
let elapsedVal = AXIS_BREAK_ELAPSED_BASE;
let lastBreakEnd = AXIS_BREAK_LAST_BREAK_END_BASE;
let stillOver = true;
for (let i = 0; i < this.breaks.length; i++) {
const brk = this.breaks[i];
if (val <= brk.vmax) {
if (val > brk.vmin) {
elapsedVal += brk.vmin - lastBreakEnd
+ (val - brk.vmin) / (brk.vmax - brk.vmin) * brk.gapReal;
}
else {
elapsedVal += val - lastBreakEnd;
}
lastBreakEnd = brk.vmax;
stillOver = false;
break;
}
elapsedVal += brk.vmin - lastBreakEnd + brk.gapReal;
lastBreakEnd = brk.vmax;
}
if (stillOver) {
elapsedVal += val - lastBreakEnd;
}
return elapsedVal;
},
/**
* @see transformIn
* a.k.a., "unelapse"
*/
transformOut(elapsedVal, opt) {
if (opt && opt.depth === SCALE_MAPPER_DEPTH_OUT_OF_BREAK) {
return elapsedVal;
}
let lastElapsedEnd = AXIS_BREAK_ELAPSED_BASE;
let lastBreakEnd = AXIS_BREAK_LAST_BREAK_END_BASE;
let stillOver = true;
let unelapsedVal = 0;
for (let i = 0; i < this.breaks.length; i++) {
const brk = this.breaks[i];
const elapsedStart = lastElapsedEnd + brk.vmin - lastBreakEnd;
const elapsedEnd = elapsedStart + brk.gapReal;
if (elapsedVal <= elapsedEnd) {
if (elapsedVal > elapsedStart) {
unelapsedVal = brk.vmin
+ (elapsedVal - elapsedStart) / (elapsedEnd - elapsedStart) * (brk.vmax - brk.vmin);
}
else {
unelapsedVal = lastBreakEnd + elapsedVal - lastElapsedEnd;
}
lastBreakEnd = brk.vmax;
stillOver = false;
break;
}
lastElapsedEnd = elapsedEnd;
lastBreakEnd = brk.vmax;
}
if (stillOver) {
unelapsedVal = lastBreakEnd + elapsedVal - lastElapsedEnd;
}
return unelapsedVal;
},
};
};
function createBreakScaleMapper(
breakParsed: AxisBreakParsingResult | NullUndefined,
initialExtent: number[] | NullUndefined
): BreakScaleMapper {
return new BreakScaleMapperImpl(breakParsed, initialExtent);
}
// Both can start with any finite value, and are not necessarily equal. But they need to
// be the same in `axisBreakElapse` and `axisBreakUnelapse` respectively.
const AXIS_BREAK_ELAPSED_BASE = 0;
const AXIS_BREAK_LAST_BREAK_END_BASE = 0;
/**
* `gapReal` in brkMapper.breaks will be calculated.
*/
function updateAxisBreakGapReal(
brkMapper: BreakScaleMapper,
scaleExtent: number[]
): void {
// Considered the effect:
// - Use dataZoom to move some of the breaks outside the extent.
// - Some scenarios that `series.clip: false`.
//
// How to calculate `prctBrksGapRealSum`:
// Based on the formula:
// xxx.span = brk.vmax - brk.vmin
// xxx.tpPrct.val / xxx.tpAbs.val means ParsedAxisBreak['gapParsed']['val']
// .S/.E means a break that is semi in scaleExtent[0] or scaleExtent[1]
// valP = (
// + (fullyInExtBrksSum.tpAbs.gapReal - fullyInExtBrksSum.tpAbs.span)
// + (semiInExtBrk.S.tpAbs.gapReal - semiInExtBrk.S.tpAbs.span) * semiInExtBrk.S.tpAbs.inExtFrac
// + (semiInExtBrk.E.tpAbs.gapReal - semiInExtBrk.E.tpAbs.span) * semiInExtBrk.E.tpAbs.inExtFrac
// )
// valQ = (
// - fullyInExtBrksSum.tpPrct.span
// - semiInExtBrk.S.tpPrct.span * semiInExtBrk.S.tpPrct.inExtFrac
// - semiInExtBrk.E.tpPrct.span * semiInExtBrk.E.tpPrct.inExtFrac
// )
// gapPrctSum = sum(xxx.tpPrct.val)
// gapPrctSum = prctBrksGapRealSum / (
// + (scaleExtent[1] - scaleExtent[0]) + valP + valQ
// + fullyInExtBrksSum.tpPrct.gapReal
// + semiInExtBrk.S.tpPrct.gapReal * semiInExtBrk.S.tpPrct.inExtFrac
// + semiInExtBrk.E.tpPrct.gapReal * semiInExtBrk.E.tpPrct.inExtFrac
// )
// Assume:
// xxx.tpPrct.gapReal = xxx.tpPrct.val / gapPrctSum * prctBrksGapRealSum
// (NOTE: This is not accurate when semi-in-extent break exist because its
// proportion is not linear, but this assumption approximately works.)
// Derived as follows:
// prctBrksGapRealSum = gapPrctSum * ( (scaleExtent[1] - scaleExtent[0]) + valP + valQ )
// / (1
// - fullyInExtBrksSum.tpPrct.val
// - semiInExtBrk.S.tpPrct.val * semiInExtBrk.S.tpPrct.inExtFrac
// - semiInExtBrk.E.tpPrct.val * semiInExtBrk.E.tpPrct.inExtFrac
// )
let gapPrctSum = 0;
const fullyInExtBrksSum = {
tpAbs: {span: 0, val: 0},
tpPrct: {span: 0, val: 0},
};
const init = () => ({has: false, span: NaN, inExtFrac: NaN, val: NaN});
const semiInExtBrk = {
S: {tpAbs: init(), tpPrct: init()},
E: {tpAbs: init(), tpPrct: init()},
};
each(brkMapper.breaks, brk => {
const gapParsed = brk.gapParsed;
if (gapParsed.type === 'tpPrct') {
gapPrctSum += gapParsed.val;
}
const clampedBrk = clampBreakByExtent(brk, scaleExtent);
if (clampedBrk) {
const vminClamped = clampedBrk.vmin !== brk.vmin;
const vmaxClamped = clampedBrk.vmax !== brk.vmax;
const clampedSpan = clampedBrk.vmax - clampedBrk.vmin;
if (vminClamped && vmaxClamped) {
// Do nothing, which simply makes the result `gapReal` cover the entire scaleExtent.
// This transform is not consistent with the other cases but practically works.
}
else if (vminClamped || vmaxClamped) {
const sOrE = vminClamped ? 'S' : 'E';
semiInExtBrk[sOrE][gapParsed.type].has = true;
semiInExtBrk[sOrE][gapParsed.type].span = clampedSpan;
semiInExtBrk[sOrE][gapParsed.type].inExtFrac = clampedSpan / (brk.vmax - brk.vmin);
semiInExtBrk[sOrE][gapParsed.type].val = gapParsed.val;
}
else {
fullyInExtBrksSum[gapParsed.type].span += clampedSpan;
fullyInExtBrksSum[gapParsed.type].val += gapParsed.val;
}
}
});
const prctBrksGapRealSum = gapPrctSum
* (0
+ (scaleExtent[1] - scaleExtent[0])
+ (fullyInExtBrksSum.tpAbs.val - fullyInExtBrksSum.tpAbs.span)
+ (semiInExtBrk.S.tpAbs.has
? (semiInExtBrk.S.tpAbs.val - semiInExtBrk.S.tpAbs.span) * semiInExtBrk.S.tpAbs.inExtFrac : 0
)
+ (semiInExtBrk.E.tpAbs.has
? (semiInExtBrk.E.tpAbs.val - semiInExtBrk.E.tpAbs.span) * semiInExtBrk.E.tpAbs.inExtFrac : 0
)
- fullyInExtBrksSum.tpPrct.span
- (semiInExtBrk.S.tpPrct.has ? semiInExtBrk.S.tpPrct.span * semiInExtBrk.S.tpPrct.inExtFrac : 0)
- (semiInExtBrk.E.tpPrct.has ? semiInExtBrk.E.tpPrct.span * semiInExtBrk.E.tpPrct.inExtFrac : 0)
) / (1
- fullyInExtBrksSum.tpPrct.val
- (semiInExtBrk.S.tpPrct.has ? semiInExtBrk.S.tpPrct.val * semiInExtBrk.S.tpPrct.inExtFrac : 0)
- (semiInExtBrk.E.tpPrct.has ? semiInExtBrk.E.tpPrct.val * semiInExtBrk.E.tpPrct.inExtFrac : 0)
);
each(brkMapper.breaks, brk => {
const gapParsed = brk.gapParsed;
if (gapParsed.type === 'tpPrct') {
brk.gapReal = gapPrctSum !== 0
// prctBrksGapRealSum is supposed to be non-negative but add a safe guard
? mathMax(prctBrksGapRealSum, 0) * gapParsed.val / gapPrctSum : 0;
}
if (gapParsed.type === 'tpAbs') {
brk.gapReal = gapParsed.val;
}
if (brk.gapReal == null) {
brk.gapReal = 0;
}
});
}
function pruneTicksByBreak<TItem extends ScaleTick | number>(
pruneByBreak: ParamPruneByBreak,
ticks: TItem[],
breaks: ParsedAxisBreakList,
getValue: (item: TItem) => number,
interval: number,
scaleExtent: number[],
): void {
if (pruneByBreak === 'no') {
return;
}
each(breaks, brk => {
// break.vmin/vmax that out of extent must not impact the visible of
// normal ticks and labels.
const clampedBrk = clampBreakByExtent(brk, scaleExtent);
if (!clampedBrk) {
return;
}
// Remove some normal ticks to avoid zigzag shapes overlapping with split lines
// and to avoid break labels overlapping with normal tick labels (thouth it can
// also be avoided by `axisLabel.hideOverlap`).
// It's OK to O(n^2) since the number of `ticks` are small.
for (let j = ticks.length - 1; j >= 0; j--) {
const tick = ticks[j];
const val = getValue(tick);
// 1. Ensure there is no ticks inside `break.vmin` and `break.vmax`.
// 2. Use an empirically gap value here. Theoritically `zigzagAmplitude` is
// supposed to be involved to provide better precision but it will brings
// more complexity. The empirically gap value is conservative because break
// labels and normal tick lables are prone to overlapping.
const gap = interval * 3 / 4;
if (val > clampedBrk.vmin - gap
&& val < clampedBrk.vmax + gap
&& (
pruneByBreak !== 'preserve_extent_bound'
|| (
val !== scaleExtent[0] && val !== scaleExtent[1]
)
)
) {
ticks.splice(j, 1);
}
}
});
}
function addBreaksToTicks(
// The input ticks should be in accending order.
ticks: ScaleTick[],
breaks: ParsedAxisBreakList,
scaleExtent: number[],
// Keep the break ends at the same level to avoid an awkward appearance.
getTimeProps?: (clampedBrk: ParsedAxisBreak) => ScaleTick['time'],
): void {
each(breaks, brk => {
const clampedBrk = clampBreakByExtent(brk, scaleExtent);
if (!clampedBrk) {
return;
}
// - When neight `break.vmin` nor `break.vmax` is in scale extent,
// break label should not be displayed and we do not add them to the result.
// - When only one of `break.vmin` and `break.vmax` is inside the extent and the
// other is outsite, we comply with the extent and display only part of the breaks area,
// because the extent might be determined by user settings (such as `axis.min/max`)
ticks.push({
value: clampedBrk.vmin,
break: {
type: 'vmin',
parsedBreak: clampedBrk,
},
time: getTimeProps ? getTimeProps(clampedBrk) : undefined,
});
// When gap is 0, start tick overlap with end tick, but we still count both of them. Break
// area shape can address that overlapping. `axisLabel` need draw both start and end separately,
// otherwise it brings complexity to the logic of label overlapping resolving (e.g., when label
// rotated), and introduces inconsistency to users in `axisLabel.formatter` between gap is 0 or not.
ticks.push({
value: clampedBrk.vmax,
break: {
type: 'vmax',
parsedBreak: clampedBrk,
},
time: getTimeProps ? getTimeProps(clampedBrk) : undefined,
});
});
if (breaks.length) {
ticks.sort((a, b) => a.value - b.value);
}
}
/**
* If break and extent does not intersect, return null/undefined.
* If the intersection is only a point at scaleExtent[0] or scaleExtent[1], return null/undefined.
*/
function clampBreakByExtent(
brk: ParsedAxisBreak,
scaleExtent: number[]
): NullUndefined | ParsedAxisBreak {
const vmin = mathMax(brk.vmin, scaleExtent[0]);
const vmax = mathMin(brk.vmax, scaleExtent[1]);
return (
vmin < vmax
|| (vmin === vmax && vmin > scaleExtent[0] && vmin < scaleExtent[1])
)
? {
vmin,
vmax,
breakOption: brk.breakOption,
gapParsed: brk.gapParsed,
gapReal: brk.gapReal,
}
: null;
}
function parseAxisBreakOption(
// raw user input breaks, retrieved from axis model.
breakOptionList: AxisBreakOption[] | NullUndefined,
scale: {parse: Scale['parse']},
opt?: ParseBreakOptionOpt
): AxisBreakParsingResult {
const parsedBreaks: ParsedAxisBreakList = [];
if (!breakOptionList) {
return {breaks: parsedBreaks};
}
function validatePercent(normalizedPercent: number, msg: string): boolean {
if (normalizedPercent >= 0 && normalizedPercent < 1 - 1e-5) { // Avoid division error.
return true;
}
if (__DEV__) {
error(`${msg} must be >= 0 and < 1, rather than ${normalizedPercent} .`);
}
return false;
}
each(breakOptionList, brkOption => {
if (!brkOption || brkOption.start == null || brkOption.end == null) {
if (__DEV__) {
error('The input axis breaks start/end should not be empty.');
}
return;
}
if (brkOption.isExpanded) {
return;
}
const parsedBrk: ParsedAxisBreak = {
breakOption: clone(brkOption),
vmin: scale.parse(brkOption.start),
vmax: scale.parse(brkOption.end),
gapParsed: {type: 'tpAbs', val: 0},
gapReal: null
};
if (brkOption.gap != null) {
let isPrct = false;
if (isString(brkOption.gap)) {
const trimmedGap = trim(brkOption.gap);
if (trimmedGap.match(/%$/)) {
let normalizedPercent = parseFloat(trimmedGap) / 100;
if (!validatePercent(normalizedPercent, 'Percent gap')) {
normalizedPercent = 0;
}
parsedBrk.gapParsed.type = 'tpPrct';
parsedBrk.gapParsed.val = normalizedPercent;
isPrct = true;
}
}
if (!isPrct) {
let absolute = scale.parse(brkOption.gap);
if (!isFinite(absolute) || absolute < 0) {
if (__DEV__) {
error(`Axis breaks gap must positive finite rather than (${brkOption.gap}).`);
}
absolute = 0;
}
parsedBrk.gapParsed.type = 'tpAbs';
parsedBrk.gapParsed.val = absolute;
}
}
if (parsedBrk.vmin === parsedBrk.vmax) {
parsedBrk.gapParsed.type = 'tpAbs';
parsedBrk.gapParsed.val = 0;
}
if (opt && opt.noNegative) {
each(['vmin', 'vmax'] as const, se => {
if (parsedBrk[se] < 0) {
if (__DEV__) {
error(`Axis break.${se} must not be negative.`);
}
parsedBrk[se] = 0;
}
});
}
// Ascending numerical order is the prerequisite of the calculation in Scale#normalize.
// User are allowed to input desending vmin/vmax for simplifying the usage.
if (parsedBrk.vmin > parsedBrk.vmax) {
const tmp = parsedBrk.vmax;
parsedBrk.vmax = parsedBrk.vmin;
parsedBrk.vmin = tmp;
}
parsedBreaks.push(parsedBrk);
});
// Ascending numerical order is the prerequisite of the calculation in Scale#normalize.
parsedBreaks.sort((item1, item2) => item1.vmin - item2.vmin);
// Make sure that the intervals in breaks are not overlap.
let lastEnd = -Infinity;
each(parsedBreaks, (brk, idx) => {
if (lastEnd > brk.vmin) {
if (__DEV__) {
error('Axis breaks must not overlap.');
}
parsedBreaks[idx] = null;
}
lastEnd = brk.vmax;
});
return {
breaks: filter(parsedBreaks, brk => !!brk),
};
}
function identifyAxisBreak(
brk: AxisBreakOption,
identifier: AxisBreakOptionIdentifierInAxis
): boolean {
return serializeAxisBreakIdentifier(identifier) === serializeAxisBreakIdentifier(brk);
}
function serializeAxisBreakIdentifier(identifier: AxisBreakOptionIdentifierInAxis): string {
// We use user input start/end to identify break. Considered cases like `start: new Date(xxx)`,
// Theoretically `Scale#parse` should be used here, but not used currently to reduce dependencies,
// since simply converting to string happens to be correct.
return identifier.start + '_\0_' + identifier.end;
}
/**
* - A break pair represents `[vmin, vmax]`,
* - Only both vmin and vmax item exist, they are counted as a pair.
*/
function retrieveAxisBreakPairs<TItem, TReturnIdx extends boolean>(
itemList: TItem[],
getVisualAxisBreak: (item: TItem) => VisualAxisBreak | NullUndefined,
returnIdx: TReturnIdx
): (
TReturnIdx extends false ? TItem[][] : number[][]
) {
const idxPairList: number[][] = [];
each(itemList, (el, idx) => {
const vBreak = getVisualAxisBreak(el);
if (vBreak && vBreak.type === 'vmin') {
idxPairList.push([idx]);
}
});
each(itemList, (el, idx) => {
const vBreak = getVisualAxisBreak(el);
if (vBreak && vBreak.type === 'vmax') {
const idxPair = find(
idxPairList,
// parsedBreak may be changed, can only use breakOption to match them.
pr => identifyAxisBreak(
getVisualAxisBreak(itemList[pr[0]]).parsedBreak.breakOption,
vBreak.parsedBreak.breakOption
)
);
idxPair && idxPair.push(idx);
}
});
const result = [] as (TReturnIdx extends false ? TItem[][] : number[][]);
each(idxPairList, idxPair => {
if (idxPair.length === 2) {
result.push(returnIdx
? (idxPair as any)
: ([itemList[idxPair[0]] as any, itemList[idxPair[1]] as any])
);
}
});
return result;
}
function getTicksBreakOutwardTransform(
scale: ScaleMapper,
tick: ScaleTick,
outermostBreaks: ParsedAxisBreakList,
lookup: ValueTransformLookupOpt['lookup']
): {
tickVal: number;
vBreak: VisualAxisBreak | NullUndefined;
// Return: If no break, return null/undefined.
} | NullUndefined {
if (!tick.break) {
return;
}
const brk = tick.break.parsedBreak;
const originalBrkItem = find(outermostBreaks, brk => identifyAxisBreak(
brk.breakOption, tick.break.parsedBreak.breakOption
));
// NOTE: `tick.break` may have been clamped by scale extent.
const opt: ScaleMapperTransformOutOpt = {lookup, depth: SCALE_MAPPER_DEPTH_OUT_OF_BREAK};
const vmin = scale.transformOut(brk.vmin, opt);
const vmax = scale.transformOut(brk.vmax, opt);
const parsedBreak = {
vmin,
vmax,
breakOption: brk.breakOption, // It is not changed by extent clamping.
gapParsed: clone(originalBrkItem.gapParsed),
gapReal: brk.gapReal,
};
return {
tickVal: parsedBreak[tick.break.type],
vBreak: {type: tick.break.type, parsedBreak},
};
}
function parseAxisBreakOptionInwardTransform(
breakOptionList: AxisBreakOption[] | NullUndefined,
scale: Scale,
parseOpt: ParseBreakOptionOpt,
lookupStartIdx: number,
out: {
lookup: ValueTransformLookupOpt['lookup'];
original?: AxisBreakParsingResult;
transformed?: AxisBreakParsingResult;
}
): void {
out.original = parseAxisBreakOption(breakOptionList, scale, parseOpt);
const transformed = out.transformed = parseAxisBreakOption(breakOptionList, scale, parseOpt);
const lookup = out.lookup;
transformed.breaks = map(transformed.breaks, (brk, idx) => {
const transOpt = {depth: SCALE_MAPPER_DEPTH_OUT_OF_BREAK} as const;
const vmin = scale.transformIn(brk.vmin, transOpt);
const vmax = scale.transformIn(brk.vmax, transOpt);
const gapParsed = {
type: brk.gapParsed.type,
val: brk.gapParsed.type === 'tpAbs'
? scale.transformIn(brk.vmin + brk.gapParsed.val, transOpt) - vmin
: brk.gapParsed.val,
};
lookup.from[lookupStartIdx + idx] = vmin;
lookup.to[lookupStartIdx + idx] = brk.vmin;
lookup.from[lookupStartIdx + idx + 1] = vmax;
lookup.to[lookupStartIdx + idx + 1] = brk.vmax;
return {
vmin,
vmax,
gapParsed,
gapReal: brk.gapReal,
breakOption: brk.breakOption
};
});
}
const BREAK_MIN_MAX_TO_PARAM = {vmin: 'start', vmax: 'end'} as const;
function makeAxisLabelFormatterParamBreak(
extraParam: AxisLabelFormatterExtraParams | NullUndefined,
vBreak: VisualAxisBreak | NullUndefined
): AxisLabelFormatterExtraParams | NullUndefined {
if (vBreak) {
extraParam = extraParam || ({} as AxisLabelFormatterExtraParams);
extraParam.break = {
type: BREAK_MIN_MAX_TO_PARAM[vBreak.type],
start: vBreak.parsedBreak.vmin,
end: vBreak.parsedBreak.vmax,
};
}
return extraParam;
}
export function installScaleBreakHelper(): void {
registerScaleBreakHelperImpl({
createBreakScaleMapper,
pruneTicksByBreak,
addBreaksToTicks,
parseAxisBreakOption,
identifyAxisBreak,
serializeAxisBreakIdentifier,
retrieveAxisBreakPairs,
getTicksBreakOutwardTransform,
parseAxisBreakOptionInwardTransform,
makeAxisLabelFormatterParamBreak,
});
}