Skip to content

[Feature] Monotone cubic Hermite smoothing for cartesian line series #21793

Open

Description

@awtaylor

What problem does this feature solve?

Smoothed line and area series are easy to misread. With smooth: true, the constrained Bézier can bow past the samples and invent a peak or a valley that is not in the data. smoothMonotone: 'x' or 'y' avoids that overshoot, but it does so by making the tangent horizontal or vertical at every sample, so each knot looks like a saddle or a plateau. That behavior was an intentional tradeoff (#7158). It is still surprising on ordinary time series (#21532): a sensor that rises and then levels off is drawn as if it turned over at every measurement.

There is no option today for a curve that passes through the samples, keeps a continuous slope, and does not add extrema between samples. That is the usual monotone cubic interpolant in scientific tools (MATLAB pchip, SciPy PchipInterpolator, R splinefun(..., method = "monoH.FC")). Dashboards and monitoring charts are the common case: category or time on one axis, a value on the other, including stacked areas.

This could be opt-in. The default smoother, and the existing 'x', 'y', and 'none' modes, could happily stay as they are.

What does the proposed API look like?

Extend series.line.smoothMonotone with two values. No new option name, so the axis and the algorithm stay in the option that already means "how should smoothing respect monotonicity".

option = {
    xAxis: {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    },
    yAxis: {},
    series: [{
        type: 'line',
        data: [12, 18, 18, 30, 22, 16, 28],
        smooth: true,
        smoothMonotone: 'monotoneX',
        areaStyle: {}
    }]
};
  • monotoneX: y is a function of x. This is the time-series case in [Bug] smoothMonotone generates saddle points #21532.
  • monotoneY: x is a function of y.
  • 'x', 'y', 'none', and the default null remain unchanged. In particular, 'x' and 'y' keep the flat tangents.
  • smooth: false still a polyline. In this mode only, smooth: true means full-strength Hermite tangents. A number is clamped to (0, 1] and multiplies those tangents, which stays monotone. Numbers above 1 are clamped to 1. In the existing Bézier modes, smooth: true would still means tension 0.5.
  • Stacked areas use the same mode for the baseline. Each series' own smooth value is the tangent scale for its boundary.
  • Cartesian line series only. On a polar series the value is ignored and the current Bézier smooth is used, because the guarantee is about screen x or y and would be false after a polar projection.
  • Where the chosen axis is not strictly monotone (a repeated x, or a value axis that turns backward), that piece uses the existing non-monotone Bézier. A strictly decreasing run is handled, so an inverse axis still works.
  • connectNulls keeps its current meaning: nulls break the line, or are skipped and the remaining samples are interpolated.

monotoneX names the axis. It is not a claim that the curve matches d3 curveMonotoneX (that implementation is Steffen's method). Other names could work too e.g., smoothInterpolation: 'monotone' if that fits the option style better.

Method

One cubic Bézier per segment, so the current path, hit testing, and area fill stay in place. The knots are the samples. Interior slopes are the Fritsch–Butland weighted harmonic mean (1984, doi:10.1137/0905021, which lies in the Fritsch–Carlson monotone region (1980, doi:10.1137/0717021). End slopes are the one-sided three-point estimate, then clamped into that region (Moler, Numerical Computing with MATLAB, 2004). The code would be original, with those citations, and not be a port of SciPy, d3, or Chart.js.

Other monotone or shape-preserving methods were considered and are not proposed as extra options. The three local cubic rules below are the same feature with different slope formulas; they differ visibly only when neighboring secants are very different. Shipping a menu of them would freeze that choice in the API. The slope step would be one function, so the formula can be swapped in review without an option change.

  • Steffen (1990) is also local and monotone. It is d3 curveMonotoneX / curveMonotoneY and Vega-Lite interpolate: "monotone". On equal spacing the slope is held down to the smaller neighboring secant, so a sharp turn looks a little tighter than Fritsch–Butland. This is the alternative I would switch to if maintainers prefer the d3/Vega look.
  • Arithmetic mean of the secants, then project onto the 1980 disk is what Chart.js uses for cubicInterpolationMode: 'monotone'. It is the roundest of the three and still monotone. Fritsch–Butland is the direct construction, so that projection is only needed at the endpoints.
  • Akima (1970) and modified Akima are local cubics that are not monotone. They can still overshoot.
  • Hyman (1983) and Huynh (1993) limit the slopes of a global cubic. One edited point can move the whole line, which fits shock-capturing better than an interactive chart with null breaks and large series. The quintic extension is not a cubic Bézier.
  • Delbourgo and Gregory's rational curves, and Schumaker's shape-preserving quadratics, have a real tension parameter, and a segment is not one cubic Bézier. ECharts strokes with bezierCurveTo. Scaling the Fritsch–Butland slopes by smooth in (0, 1] is the tension control that stays inside that path model.

The option spelling or the slope formula could be adjusted as the reviewers see fit. I would like to implement this and open a pull request that leaves the default and the existing smoothMonotone values behaviorally unchanged (with option docs in apache/echarts-doc). I would follow up there once the name is settled.

Activity

  1. added
    enThis issue is in English
    pendingWe are not sure about whether this is a bug/new feature.
    on Oct 19, 2026
  2. awtaylor commented on Oct 19, 2026

    @awtaylor
    Author

    I've opened a pull request with an opt-in implementation:

    It leaves the default smoother and the existing smoothMonotone values unchanged. I'm happy to rename monotoneX / monotoneY, or to switch the tangent formula to Steffen, if that is preferred.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enThis issue is in Englishnew-featurependingWe are not sure about whether this is a bug/new feature.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions