feat(scale): extend log axis to support negative and zero values#21646
feat(scale): extend log axis to support negative and zero values#21646netscout-mthorn wants to merge 6 commits into
Conversation
|
Thanks for your contribution! Please DO NOT commit the files in dist, i18n, and ssr/client/dist folders in a non-release pull request. These folders are for release use only. Document changes are required in this PR. Please also make a PR to apache/echarts-doc for document changes and update the issue id in the PR description. When the doc PR is merged, the maintainers will remove the |
Naming feedback requested:
|
| Library | Transform | Parameter name | Default |
|---|---|---|---|
| matplotlib | AsinhScale |
linear_width |
1.0 |
| matplotlib | SymmetricalLogScale |
linthresh |
2 |
| d3-scale | scaleSymlog |
constant |
1 (exposed via .constant(c) chainable method) |
| ECharts (proposed) | both | logLinearWidth |
1 |
matplotlib uses different parameter names for the two transforms (linear_width
vs linthresh), reflecting the different mathematical character of each:
linear_width(asinh): a continuous scale parameter with no hard boundarylinthresh(symlog): a hard threshold at which the piecewise formula transitions
I chose a single unified name logLinearWidth because:
- ECharts exposes a single
logMappingoption for both transforms, so a
single companion parameter is consistent. logLinearWidthis descriptive for both: it is the width (scale) of the
linear region in both cases.- It follows the
logBasenaming convention already in ECharts.
Alternatives considered
| Name | Notes |
|---|---|
logLinearWidth |
Chosen. Descriptive, consistent with logBase naming. |
logLinthresh |
Borrows from matplotlib symlog naming; less accurate for asinh (no hard threshold). |
logThreshold |
Accurate for symlog; misleading for asinh (no threshold). |
logLinearScale |
Could be confused with "linear scale" as a transform type. |
logLinearRange |
Reasonable; slightly less precise than "width". |
logC |
Matches d3's internal naming but opaque without context. |
Naming convention research
I surveyed existing ECharts mode-selection options before choosing logMapping
and logLinearWidth:
sampling: 'none' | 'average' | 'min' | 'max' | ...enables an alternative
data-reduction method;'none'is the explicit default.graph.layout: 'none' | 'force' | 'circular'selects a graph layout algorithm.smooth: boolean | number,boundaryGap: boolean | [string, string]are mixed
union types with non-string sentinels.
logMapping follows the sampling/layout pattern directly: a string union with
'none' as the explicit default, each value naming an alternative mode. I considered
scaleMapping, mappingMethod, and mapping (all suggested by reviewer 100pah in
#20872) but prefer logMapping for consistency with the existing logBase option name.
Question for reviewers
Is logLinearWidth acceptable? Is there a naming convention in ECharts that
I should follow instead? I am also open to separate parameter names for
asinh and symlog if the maintainers prefer consistency with matplotlib
(e.g. logLinthresh for symlog and logLinearWidth for asinh), though a
single unified parameter seems simpler.
Also: is logMapping the preferred option name, or would one of the names
suggested in #20872 (scaleMapping, mappingMethod, mapping) be preferred?
Add logMappingCalcNiceTicks to axisNiceTicks.ts, which generates round tick candidates (0, ±a0, ±b*a0, ...) in raw-value space and stores them on LogScale._mappedLogTicks. The standard IntervalScale-based tick path cannot be used because asinh/symlog candidates are non-uniformly spaced in transformed space. Wire the new function into calcNiceForIntervalOrLogScale via an early branch before the existing logScaleCalcNiceTicks call. Add an alignTicks guard in axisAlignTicks.ts so that mapped-log scales fall back to independent nice-tick calculation rather than the loopIncreaseInterval path, which assumes integer spacing in log space. Add unit tests covering tick candidates, normalize/scale round-trips, and the alignTicks guard for both asinh and symlog.
Adds test/log-mapping.html with 8 chart scenarios covering the new logMapping: 'asinh' and logMapping: 'symlog' axis options: 1. Standard log baseline (regression guard) 2. asinh, positive-only (visual comparison with standard log) 3. asinh, mixed [-1000, 1000] — primary use case, zero-crossing data 4. asinh, all-negative extent 5. asinh with logBase: 2 (ticks at powers of 2) 6. asinh scatter with P&L-style mixed-sign data 7. symlog, mixed [-1000, 1000] (side-by-side with scenario 3) 8. symlog with explicit logLinearWidth: 1 (linear segment visible) Each chart title lists what to verify, serving as an inline visual checklist for reviewers.
logMappingCalcNiceTicks now accepts an optional splitNumber (default 5). When the number of power-of-base candidates exceeds the requested tick count, the function computes a stride k and uses base^k as the effective step between ticks. This keeps tick density manageable for small bases like 2, where the raw candidate count can be very large over wide data ranges. The splitNumber is threaded from the axis model through calcNiceForIntervalOrLogScale, consistent with how interval and standard log scales already work.
de95bde to
930c8fa
Compare

Brief Information
This pull request is in the type of:
What does this PR do?
Adds a
logMapping: 'asinh' | 'symlog'option to thelogaxis type, enabling logarithmicaxes that correctly handle zero and negative values via symmetric transform functions.
Fixed issues
Details
Before: What was the problem?
The
logaxis type requires all data values to be strictly positive. Zero and negativevalues are filtered out by
getFilter()and rejected bysetExtent(). There is nosupported way to display mixed-sign data or data containing zero on a logarithmic axis.
Previous attempts (#16547, #20872) addressed this by auto-detecting negative data or
implementing a "negative log" transform, but reviewer
100pahidentified theseapproaches as fragile and mathematically unsound for the general case
(see the reviewer feedback in #20872).
After: How does it behave after the fixing?
A new axis option
logMappingis added to thelogaxis type:When
logMapping: 'asinh'is set:asinh(x / a0) * a0instead oflog_b(x), wherea0is controlled by
logLinearWidth(default1, matching d3/matplotlib conventions).This function is defined for all real numbers, passes smoothly through zero, and
behaves like
logfor large|x|.f(-x) = -f(x), so negative data produces a mirroredlog-like axis.
0, +-a0, +-b*a0, +-b^2*a0, ...(wherea0 = logLinearWidth,b = logBase) appear naturally.splitNumberoption (default 5) is respected. When the number ofpower-of-base tick candidates exceeds the requested count, the function
raises the effective base to
b^k(skipping intermediate powers) to keeptick density manageable. This is especially important for small bases like 2,
where the raw candidate count can be very large over wide data ranges.
When
logMapping: 'symlog'is set:sign(x) * ln(1 + |x|/C), whereCis the linearthreshold controlled by
logLinearWidth(same option, default1). Thisis the symlog transform familiar to matplotlib and d3 users.
'asinh': zero and negatives accepted,odd-symmetric axis, tick marks at
0, +-C, +-b*C, ...When
logMappingis absent or'none', behaviour is identical to before. Thestandard log transform is used and all existing constraints apply. There are no
breaking changes.
Document Info
One of the following should be checked.
Misc
Security Checking
ZRender Changes
Related test cases or examples to use the new APIs
test/log-mapping.html: visual test covering positive-only, mixed-sign, all-negative,logBase:2, and symlog comparison scenarios
test/ut/spec/scale/log.test.ts: Jest unit tests covering standard log(regression),
logMapping: 'asinh', andlogMapping: 'symlog'behaviourMerging options
Other information
Design note:
asinhandsymlogBoth transforms are provided as equals. Neither is preferred over the other in this PR.
The right choice depends on the user's data and familiarity with each transform.
asinh(asinh(x / a0) * a0):scaleMapper.tssource as an anticipated extension.Math.asinh/Math.sinhare ES2015 built-ins with no polyfill needed.symlog(sign(x) * ln(1 + |x|/C)):SymmetricalLogScaleand d3'sscaleSymlog.C(logLinearWidth) has a more concrete geometric meaningthan
a0for users who want explicit control over the transition point.Math.log1p/Math.expm1are ES2015 built-ins with no polyfill needed.Both reviewer
100pah(in the review of #20872) and matplotlib name both transformsas valid solutions to the negative/zero log problem. Both use the same
logLinearWidthparameter and the same tick generation strategy.
Design note:
logMapping: 'none'as the explicit defaultThe type is
'none' | 'asinh' | 'symlog'rather than just'asinh' | 'symlog' | undefined. This followsthe ECharts convention for mode-selection options:
samplinguses'none'for itsdefault, as does
graph.layout.Relationship to #20872
This PR supersedes #20872. It takes a different approach (opt-in
logMappingoptionwith
asinhandsymlogtransforms) rather than thenegative logmethod implementedthere, in direct response to
100pah's review feedback on that PR.Design note:
splitNumberand adaptive tick strideStandard log axes derive tick count from
splitNumbervia the interval scalestub. For asinh/symlog axes, ticks are non-uniformly spaced in transformed space,
so
logMappingCalcNiceTickswas initially generating all power-of-base candidateswithout limit. This commit adds
splitNumbersupport by computing a strideksuch that ticks are placed at
0, +-a0, +-a0*b^k, +-a0*b^(2k), ..., keeping the countwithin bounds while preserving "nice" power-of-base values.
Files changed
src/coord/axisCommonTypes.tssrc/coord/axisDefault.tssrc/coord/axisHelper.tssrc/coord/axisNiceTicks.tssrc/coord/axisAlignTicks.tssrc/scale/Log.tssrc/scale/helper.tstest/ut/spec/scale/log.test.ts(new)test/log-mapping.html(new)