blob: 20a61bb7f2968127b8499fce6e76f5c1f38086e4 [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, each } from 'zrender/src/core/util';
import { NullUndefined } from '../util/types';
import type Axis from './Axis';
import { isOrdinalScale } from '../scale/helper';
import { isNullableNumberFinite, mathAbs, mathMax } from '../util/number';
import {
AxisStatKey, getAxisStat, getAxisStatBySeries,
LINEAR_POSITIVE_MIN_GAP_SINGLE_VALID_VALUE,
} from './axisStatistics';
import { getScaleLinearSpanForMapping } from '../scale/scaleMapper';
import type SeriesModel from '../model/Series';
// Arbitrary, leave some space to avoid overflowing when dataZoom moving.
const FALLBACK_BAND_WIDTH_RATIO = 0.8;
export type AxisBandWidthResult = {
// bandWidth in pixel.
// Never be null/undefined.
// May be NaN if no meaningfull value. But it's unlikely to be NaN, since edge cases
// are handled internally whenever possible.
w: number;
// bandWidth in data space.
// Never be null/undefined.
// May be NaN if no meaningfull value, typically when no valid series data item.
w2: number;
};
type CalculateBandWidthOpt = {
// Only used on non-'category' axes. Calculate `bandWidth` based on statistics.
// Require `requireAxisStatistics` to be called.
fromStat?: {
// Either `key` or `sers` is required.
// - `key`: Calculate `bandWidth` based on a series collection defined by the `AxisStatKey`.
key?: AxisStatKey;
// - `sers`: Query all keys by the given series collection and union the corresponding bandWidth.
// PENDING: If multiple axis statistics can be queried by `series`, currently we only support
// to return a maximum `bandWidth`, which is suitable for cases like "axis pointer shadow".
sers?: (SeriesModel | NullUndefined)[] | NullUndefined;
};
// It also act as a fallback for NaN/null/undefined result.
min?: number;
};
/**
* NOTICE:
* - Require the axis pixel extent and the scale extent as inputs. But they
* can be not precise for approximation.
* - Can only be called after "data processing" stage.
*
* PENDING:
* Currently `bandWidth` can not be specified by users explicitly. But if we
* allow that in future, these issues must be considered:
* - Can only allow specifying a band width in data scale rather than pixel.
* - LogScale needs to be considered - band width can only be specified on linear
* (but before break) scale, similar to `axis.interval`.
*
* A band is required on:
* - series group band width in bar/boxplot/candlestick/...;
* - tooltip axisPointer type "shadow";
* - etc.
*/
export function calcBandWidth(
axis: Axis,
opt?: CalculateBandWidthOpt | NullUndefined
): AxisBandWidthResult {
opt = opt || {};
const out: AxisBandWidthResult = {w: NaN, w2: NaN};
const scale = axis.scale;
const fromStat = opt.fromStat;
const min = opt.min;
// [BAND_WIDTH_USED_SCALE_LINEAR_SPAN]
// - Band width should always respect to the currently specified extent, and `SCALE_EXTENT_KIND_MAPPING`
// should be used if specified.
// Otherwise, the result may incorrect, especially when data count is small.
// For example, when "containShape" is calculating, no `SCALE_EXTENT_KIND_MAPPING` is set, so here only
// `SCALE_EXTENT_KIND_EFFECTIVE` is returned, say, `[3, 5]`, based on which a `SCALE_EXTENT_KIND_MAPPING`
// is calculated, say `[2.5, 5.5]` (expanded by `0.5`). Then when rendering, that `SCALE_EXTENT_KIND_MAPPING`
// is returned here.
// See AXIS_CONTAIN_SHAPE_COMMON_STRATEGY for more details.
// - The span should be in the linear space (typically, the innermost space).
// - We use the scale extent after being zoommed and `intervalScaleEnsureValidExtent`-ish applied and
// "nice"/"align" applied, because:
// - For OrdinalScale, fine;
// - For numeric scale, `scaleLinearSpan` is normally not used for a consistent result when `dataZoom`
// is applied, but used when none or single data item case.
let scaleLinearSpan = getScaleLinearSpanForMapping(scale);
if (!isNullableNumberFinite(scaleLinearSpan)) { // scale may be `[Infinity, -Infinity]`.
scaleLinearSpan = NaN;
}
const axisExtent = axis.getExtent();
// Always use a new pxSpan because it may be changed in `grid` contain label calculation.
const pxSpan = mathAbs(axisExtent[1] - axisExtent[0]);
if (isOrdinalScale(scale)) {
calcBandWidthForCategoryAxis(out, axis, scaleLinearSpan, pxSpan);
}
else if (fromStat) {
calcBandWidthForNumericAxis(out, axis, scaleLinearSpan, pxSpan, fromStat);
}
else if (min == null) {
if (__DEV__) {
assert(false);
}
}
if (min != null) {
out.w = isNullableNumberFinite(out.w)
? mathMax(min, out.w) : min;
}
return out;
}
function calcBandWidthForCategoryAxis(
out: AxisBandWidthResult,
axis: Axis,
scaleLinearSpan: number,
pxSpan: number,
): void {
const onBand = axis.onBand;
let len = scaleLinearSpan + (onBand ? 1 : 0);
// Fix #2728, avoid NaN when only one data.
len === 0 && (len = 1);
out.w = pxSpan / len;
// NOTE:
// - When `scaleLinearSpan === 0`, no need to expand extent.
// - `onBand: true` (`boundaryGap: true`) does not need to support `containShape`,
// thereby no `invRatio`.
if (!onBand && scaleLinearSpan && pxSpan) {
out.w2 = out.w * scaleLinearSpan / pxSpan;
}
}
function calcBandWidthForNumericAxis(
out: AxisBandWidthResult,
axis: Axis,
scaleLinearSpan: number,
pxSpan: number,
fromStat: CalculateBandWidthOpt['fromStat'],
): void {
if (__DEV__) {
assert(fromStat);
}
let onlySingular = false;
let bandWidthInData = -Infinity;
each(
fromStat.key
? [getAxisStat(axis, fromStat.key)]
: getAxisStatBySeries(axis, fromStat.sers || []),
function (stat) {
const liPosMinGap = stat.liPosMinGap;
// NOTE: `liPosMinGap == null` may indicate that `requireAxisStatistics`
// is not used by any series on this axis. We should not make `bandWidth`
// for this case.
if (liPosMinGap != null) {
if (liPosMinGap > 0) {
if (liPosMinGap > bandWidthInData) {
bandWidthInData = liPosMinGap;
}
onlySingular = false;
}
else if (liPosMinGap === LINEAR_POSITIVE_MIN_GAP_SINGLE_VALID_VALUE) {
onlySingular = true;
}
}
}
);
if (isNullableNumberFinite(scaleLinearSpan) && scaleLinearSpan > 0
&& isNullableNumberFinite(bandWidthInData)
) {
out.w = pxSpan / scaleLinearSpan * bandWidthInData;
out.w2 = bandWidthInData;
}
else if (onlySingular) {
// This is the special handing for single value case, where min gap can not
// be calculated, but `w` and `w2` (for "containShape") are still needed.
out.w = pxSpan * FALLBACK_BAND_WIDTH_RATIO;
out.w2 = out.w * scaleLinearSpan / pxSpan;
// Consider an axis has both candlestick and bar series, where candlestick has multiple data
// but the bar series has no data. In this case, that bar series should be ignored; otherwise,
// the axis will be significantly expanded by "containShape" but no bar shape displayed.
}
}