Skip to content

Commit 2583060

Browse files
committed
fix(morph): always use polygon-resample for cross-type morph + radialBar linecap
The 'commands' algorithm produced visible "wings/flips" mid-frame for any pair whose source and target have very different anchor-point counts (bar rect ↔ pie wedge / radial arc). The 'polygons' algorithm doesn't have this failure mode, so the user-facing algorithm choice is dropped — all cross-type morphs now use polygon resample + rotation-search alignment. Within-type morphs (bar updates with new data) continue to use the per-command lerp via Animations.morphSVG's default path; morphPaths stays in PathMorphing.js for that use. Also adds `stroke.lineCap: 'butt'` to radialBar() defaults. bar() defaults set lineCap='square'; without this override, a chart that started as bar carried 'square' across to radialBar after a morph, making each value arc's stroke extend half a stroke-width *past* the arc endpoint via the square cap. The "leftmost edge extends suddenly" visible at the end of every bar/pie → radialBar morph was that cap becoming visible at the swap moment (donut-segment polygon during morph has stroke-width 0, so its linecap is invisible; the stroked open arc after the swap has stroke-width=21 and the square cap shows). Removes: - `chart.animations.chartTypeMorph.algorithm` config + matching type - `MorphTypeChange.getAlgorithm()` - the algorithm radio toggle from the chart-type-morph demo Renderer call sites in Pie/Radial/Animations now pass the literal 'polygons' when a cross-type morph is active.
1 parent 9348ac2 commit 2583060

29 files changed

Lines changed: 141 additions & 262 deletions

dist/apexcharts.common.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/apexcharts.esm.js

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3019,6 +3019,16 @@ class Defaults {
30193019
show: false
30203020
}
30213021
},
3022+
stroke: {
3023+
// Radial value arcs are stroked open arcs; square/round caps would
3024+
// extend the stroke half a stroke-width past each endpoint, making
3025+
// the "starting edge" visibly stick out past the geometric arc.
3026+
// Butt cap is the only one that aligns with the arc's true angular
3027+
// span. Without this, a chart that previously was a bar (whose
3028+
// defaults set lineCap='square') would carry that cap across into
3029+
// the radial render after a type morph.
3030+
lineCap: "butt"
3031+
},
30223032
fill: {
30233033
gradient: {
30243034
shade: "dark",
@@ -3362,21 +3372,16 @@ class Options {
33623372
},
33633373
chartTypeMorph: {
33643374
// Cross-type morph (updateOptions changing chart.type). Bridges
3365-
// the destroy+recreate flicker by capturing old paths and morphing
3366-
// them into the new chart-type's paths via the existing PathMorphing
3367-
// engine. Supported pairs include bar ↔ pie/donut/radialBar/polarArea/
3368-
// funnel/pyramid (plus the trivial pie↔donut↔polarArea cases).
3369-
// Falls back to instant snap when types or data shape are incompatible.
3375+
// the destroy+recreate flicker by sampling source + target paths
3376+
// into N evenly-spaced perimeter points and tweening point-by-point
3377+
// with rotation-search alignment, so the transition is always smooth
3378+
// and non-self-intersecting even between very different shapes (bar
3379+
// rect ↔ pie wedge / radial arc). Supported pairs include bar ↔
3380+
// pie / donut / radialBar / polarArea / funnel / pyramid (plus the
3381+
// trivial pie ↔ donut ↔ polarArea cases). Falls back to instant
3382+
// snap when types or data shape are incompatible.
33703383
enabled: true,
3371-
speed: 600,
3372-
// 'commands' (default) — per-SVG-command lerp. Preserves curves;
3373-
// may produce "wings/flips" mid-frame for shapes with very
3374-
// different anchor counts (bar rect ↔ pie wedge).
3375-
// 'polygons' — resamples both paths into N evenly-spaced points
3376-
// and tweens point-by-point with rotation-search alignment.
3377-
// Always smooth + non-self-intersecting; every frame is a
3378-
// closed N-segment polyline (curves lost during the tween).
3379-
algorithm: "commands"
3384+
speed: 600
33803385
},
33813386
// Honor the OS-level prefers-reduced-motion setting. When true (default)
33823387
// and the user has the accessibility preference enabled, all initial-mount
@@ -6500,7 +6505,7 @@ class Animations {
65006505
* @param {number} delay
65016506
*/
65026507
morphSVG(el, realIndex, j, fill, pathFrom, pathTo, speed, delay) {
6503-
var _a;
6508+
var _a, _b;
65046509
const w = this.w;
65056510
if (!pathFrom) {
65066511
pathFrom = el.attr("pathFrom");
@@ -6523,8 +6528,8 @@ class Animations {
65236528
if (!w.globals.shouldAnimate) {
65246529
speed = 1;
65256530
}
6526-
const morphMod = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange;
6527-
const morphAlgo = morphMod && morphMod.isActive() ? morphMod.getAlgorithm() : "commands";
6531+
const crossTypeMorph = ((_b = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange) == null ? void 0 : _b.isActive()) === true;
6532+
const morphAlgo = crossTypeMorph ? "polygons" : "commands";
65286533
el.plot(pathFrom).animate(1, delay).plot(pathFrom).animate(speed, delay).plot(pathTo, morphAlgo).after(() => {
65296534
if (Utils$1.isNumber(j)) {
65306535
if (j === w.seriesData.series[w.globals.maxValsInArrayIndex].length - 2 && w.globals.shouldAnimate) {
@@ -27631,19 +27636,6 @@ class MorphTypeChange {
2763127636
const animCfg = this.w.config.chart.animations;
2763227637
return animCfg.chartTypeMorph && animCfg.chartTypeMorph.speed || animCfg.speed || 600;
2763327638
}
27634-
/**
27635-
* Which morph interpolator to use for this transition.
27636-
* 'commands' (default) — per-SVG-command lerp; preserves curves but can
27637-
* "wing/flip" when shapes have different anchor-point counts.
27638-
* 'polygons' — N-point perimeter resample with rotation-search alignment;
27639-
* always smooth + non-self-intersecting, but every frame is a polyline.
27640-
* @returns {'commands' | 'polygons'}
27641-
*/
27642-
getAlgorithm() {
27643-
const animCfg = this.w.config.chart.animations;
27644-
const algo = animCfg.chartTypeMorph && animCfg.chartTypeMorph.algorithm;
27645-
return algo === "polygons" ? "polygons" : "commands";
27646-
}
2764727639
/**
2764827640
* Fade newly-mounted axes / grid / legend / titles from opacity 0 → 1 in
2764927641
* parallel with the morph. Without this the chart's chrome would pop in
@@ -32282,9 +32274,8 @@ class Pie {
3228232274
size: this.sliceSizes[i]
3228332275
});
3228432276
const morphSpeed = this.ctx.morphTypeChange.getSpeed();
32285-
const morphAlgo = this.ctx.morphTypeChange.getAlgorithm();
3228632277
elPath.node.setAttribute("data:pathOrig", targetD);
32287-
elPath.animate(morphSpeed).plot(targetD, morphAlgo).attr({ "stroke-width": this.strokeWidth });
32278+
elPath.animate(morphSpeed).plot(targetD, "polygons").attr({ "stroke-width": this.strokeWidth });
3228832279
} else if (this.dynamicAnim && w.globals.dataChanged) {
3228932280
this.animatePaths(elPath, {
3229032281
size: this.sliceSizes[i],
@@ -33677,7 +33668,6 @@ class Radial extends Pie {
3367733668
this.animBeginArr.push(this.animDur);
3367833669
if (morphActive && morphFrom) {
3367933670
const morphSpeed = this.ctx.morphTypeChange.getSpeed();
33680-
const morphAlgo = this.ctx.morphTypeChange.getAlgorithm();
3368133671
const actualArcD = this.getPiePath({
3368233672
me: this,
3368333673
startAngle,
@@ -33693,7 +33683,7 @@ class Radial extends Pie {
3369333683
startAngle,
3369433684
startAngle + angle
3369533685
);
33696-
elPath.animate(morphSpeed).plot(targetD, morphAlgo).after(
33686+
elPath.animate(morphSpeed).plot(targetD, "polygons").after(
3369733687
/** @this {any} */
3369833688
function() {
3369933689
this.attr({
@@ -33705,7 +33695,7 @@ class Radial extends Pie {
3370533695
}
3370633696
);
3370733697
} else {
33708-
elPath.animate(morphSpeed).plot(actualArcD, morphAlgo).attr({ "stroke-width": strokeWidth });
33698+
elPath.animate(morphSpeed).plot(actualArcD, "polygons").attr({ "stroke-width": strokeWidth });
3370933699
}
3371033700
} else {
3371133701
this.animatePaths(elPath, {

dist/apexcharts.js

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3023,6 +3023,16 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
30233023
show: false
30243024
}
30253025
},
3026+
stroke: {
3027+
// Radial value arcs are stroked open arcs; square/round caps would
3028+
// extend the stroke half a stroke-width past each endpoint, making
3029+
// the "starting edge" visibly stick out past the geometric arc.
3030+
// Butt cap is the only one that aligns with the arc's true angular
3031+
// span. Without this, a chart that previously was a bar (whose
3032+
// defaults set lineCap='square') would carry that cap across into
3033+
// the radial render after a type morph.
3034+
lineCap: "butt"
3035+
},
30263036
fill: {
30273037
gradient: {
30283038
shade: "dark",
@@ -3366,21 +3376,16 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
33663376
},
33673377
chartTypeMorph: {
33683378
// Cross-type morph (updateOptions changing chart.type). Bridges
3369-
// the destroy+recreate flicker by capturing old paths and morphing
3370-
// them into the new chart-type's paths via the existing PathMorphing
3371-
// engine. Supported pairs include bar ↔ pie/donut/radialBar/polarArea/
3372-
// funnel/pyramid (plus the trivial pie↔donut↔polarArea cases).
3373-
// Falls back to instant snap when types or data shape are incompatible.
3379+
// the destroy+recreate flicker by sampling source + target paths
3380+
// into N evenly-spaced perimeter points and tweening point-by-point
3381+
// with rotation-search alignment, so the transition is always smooth
3382+
// and non-self-intersecting even between very different shapes (bar
3383+
// rect ↔ pie wedge / radial arc). Supported pairs include bar ↔
3384+
// pie / donut / radialBar / polarArea / funnel / pyramid (plus the
3385+
// trivial pie ↔ donut ↔ polarArea cases). Falls back to instant
3386+
// snap when types or data shape are incompatible.
33743387
enabled: true,
3375-
speed: 600,
3376-
// 'commands' (default) — per-SVG-command lerp. Preserves curves;
3377-
// may produce "wings/flips" mid-frame for shapes with very
3378-
// different anchor counts (bar rect ↔ pie wedge).
3379-
// 'polygons' — resamples both paths into N evenly-spaced points
3380-
// and tweens point-by-point with rotation-search alignment.
3381-
// Always smooth + non-self-intersecting; every frame is a
3382-
// closed N-segment polyline (curves lost during the tween).
3383-
algorithm: "commands"
3388+
speed: 600
33843389
},
33853390
// Honor the OS-level prefers-reduced-motion setting. When true (default)
33863391
// and the user has the accessibility preference enabled, all initial-mount
@@ -6504,7 +6509,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
65046509
* @param {number} delay
65056510
*/
65066511
morphSVG(el, realIndex, j, fill, pathFrom, pathTo, speed, delay) {
6507-
var _a;
6512+
var _a, _b;
65086513
const w = this.w;
65096514
if (!pathFrom) {
65106515
pathFrom = el.attr("pathFrom");
@@ -6527,8 +6532,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
65276532
if (!w.globals.shouldAnimate) {
65286533
speed = 1;
65296534
}
6530-
const morphMod = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange;
6531-
const morphAlgo = morphMod && morphMod.isActive() ? morphMod.getAlgorithm() : "commands";
6535+
const crossTypeMorph = ((_b = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange) == null ? void 0 : _b.isActive()) === true;
6536+
const morphAlgo = crossTypeMorph ? "polygons" : "commands";
65326537
el.plot(pathFrom).animate(1, delay).plot(pathFrom).animate(speed, delay).plot(pathTo, morphAlgo).after(() => {
65336538
if (Utils$1.isNumber(j)) {
65346539
if (j === w.seriesData.series[w.globals.maxValsInArrayIndex].length - 2 && w.globals.shouldAnimate) {
@@ -27635,19 +27640,6 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
2763527640
const animCfg = this.w.config.chart.animations;
2763627641
return animCfg.chartTypeMorph && animCfg.chartTypeMorph.speed || animCfg.speed || 600;
2763727642
}
27638-
/**
27639-
* Which morph interpolator to use for this transition.
27640-
* 'commands' (default) — per-SVG-command lerp; preserves curves but can
27641-
* "wing/flip" when shapes have different anchor-point counts.
27642-
* 'polygons' — N-point perimeter resample with rotation-search alignment;
27643-
* always smooth + non-self-intersecting, but every frame is a polyline.
27644-
* @returns {'commands' | 'polygons'}
27645-
*/
27646-
getAlgorithm() {
27647-
const animCfg = this.w.config.chart.animations;
27648-
const algo = animCfg.chartTypeMorph && animCfg.chartTypeMorph.algorithm;
27649-
return algo === "polygons" ? "polygons" : "commands";
27650-
}
2765127643
/**
2765227644
* Fade newly-mounted axes / grid / legend / titles from opacity 0 → 1 in
2765327645
* parallel with the morph. Without this the chart's chrome would pop in
@@ -32286,9 +32278,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3228632278
size: this.sliceSizes[i]
3228732279
});
3228832280
const morphSpeed = this.ctx.morphTypeChange.getSpeed();
32289-
const morphAlgo = this.ctx.morphTypeChange.getAlgorithm();
3229032281
elPath.node.setAttribute("data:pathOrig", targetD);
32291-
elPath.animate(morphSpeed).plot(targetD, morphAlgo).attr({ "stroke-width": this.strokeWidth });
32282+
elPath.animate(morphSpeed).plot(targetD, "polygons").attr({ "stroke-width": this.strokeWidth });
3229232283
} else if (this.dynamicAnim && w.globals.dataChanged) {
3229332284
this.animatePaths(elPath, {
3229432285
size: this.sliceSizes[i],
@@ -33681,7 +33672,6 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3368133672
this.animBeginArr.push(this.animDur);
3368233673
if (morphActive && morphFrom) {
3368333674
const morphSpeed = this.ctx.morphTypeChange.getSpeed();
33684-
const morphAlgo = this.ctx.morphTypeChange.getAlgorithm();
3368533675
const actualArcD = this.getPiePath({
3368633676
me: this,
3368733677
startAngle,
@@ -33697,7 +33687,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3369733687
startAngle,
3369833688
startAngle + angle
3369933689
);
33700-
elPath.animate(morphSpeed).plot(targetD, morphAlgo).after(
33690+
elPath.animate(morphSpeed).plot(targetD, "polygons").after(
3370133691
/** @this {any} */
3370233692
function() {
3370333693
this.attr({
@@ -33709,7 +33699,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3370933699
}
3371033700
);
3371133701
} else {
33712-
elPath.animate(morphSpeed).plot(actualArcD, morphAlgo).attr({ "stroke-width": strokeWidth });
33702+
elPath.animate(morphSpeed).plot(actualArcD, "polygons").attr({ "stroke-width": strokeWidth });
3371333703
}
3371433704
} else {
3371533705
this.animatePaths(elPath, {

dist/apexcharts.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/apexcharts.ssr.common.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/apexcharts.ssr.esm.js

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3039,6 +3039,16 @@ class Defaults {
30393039
show: false
30403040
}
30413041
},
3042+
stroke: {
3043+
// Radial value arcs are stroked open arcs; square/round caps would
3044+
// extend the stroke half a stroke-width past each endpoint, making
3045+
// the "starting edge" visibly stick out past the geometric arc.
3046+
// Butt cap is the only one that aligns with the arc's true angular
3047+
// span. Without this, a chart that previously was a bar (whose
3048+
// defaults set lineCap='square') would carry that cap across into
3049+
// the radial render after a type morph.
3050+
lineCap: "butt"
3051+
},
30423052
fill: {
30433053
gradient: {
30443054
shade: "dark",
@@ -3382,21 +3392,16 @@ class Options {
33823392
},
33833393
chartTypeMorph: {
33843394
// Cross-type morph (updateOptions changing chart.type). Bridges
3385-
// the destroy+recreate flicker by capturing old paths and morphing
3386-
// them into the new chart-type's paths via the existing PathMorphing
3387-
// engine. Supported pairs include bar ↔ pie/donut/radialBar/polarArea/
3388-
// funnel/pyramid (plus the trivial pie↔donut↔polarArea cases).
3389-
// Falls back to instant snap when types or data shape are incompatible.
3395+
// the destroy+recreate flicker by sampling source + target paths
3396+
// into N evenly-spaced perimeter points and tweening point-by-point
3397+
// with rotation-search alignment, so the transition is always smooth
3398+
// and non-self-intersecting even between very different shapes (bar
3399+
// rect ↔ pie wedge / radial arc). Supported pairs include bar ↔
3400+
// pie / donut / radialBar / polarArea / funnel / pyramid (plus the
3401+
// trivial pie ↔ donut ↔ polarArea cases). Falls back to instant
3402+
// snap when types or data shape are incompatible.
33903403
enabled: true,
3391-
speed: 600,
3392-
// 'commands' (default) — per-SVG-command lerp. Preserves curves;
3393-
// may produce "wings/flips" mid-frame for shapes with very
3394-
// different anchor counts (bar rect ↔ pie wedge).
3395-
// 'polygons' — resamples both paths into N evenly-spaced points
3396-
// and tweens point-by-point with rotation-search alignment.
3397-
// Always smooth + non-self-intersecting; every frame is a
3398-
// closed N-segment polyline (curves lost during the tween).
3399-
algorithm: "commands"
3404+
speed: 600
34003405
},
34013406
// Honor the OS-level prefers-reduced-motion setting. When true (default)
34023407
// and the user has the accessibility preference enabled, all initial-mount
@@ -6520,7 +6525,7 @@ class Animations {
65206525
* @param {number} delay
65216526
*/
65226527
morphSVG(el, realIndex, j, fill, pathFrom, pathTo, speed, delay) {
6523-
var _a;
6528+
var _a, _b;
65246529
const w = this.w;
65256530
if (!pathFrom) {
65266531
pathFrom = el.attr("pathFrom");
@@ -6543,8 +6548,8 @@ class Animations {
65436548
if (!w.globals.shouldAnimate) {
65446549
speed = 1;
65456550
}
6546-
const morphMod = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange;
6547-
const morphAlgo = morphMod && morphMod.isActive() ? morphMod.getAlgorithm() : "commands";
6551+
const crossTypeMorph = ((_b = (_a = this.ctx) == null ? void 0 : _a.morphTypeChange) == null ? void 0 : _b.isActive()) === true;
6552+
const morphAlgo = crossTypeMorph ? "polygons" : "commands";
65486553
el.plot(pathFrom).animate(1, delay).plot(pathFrom).animate(speed, delay).plot(pathTo, morphAlgo).after(() => {
65496554
if (Utils$1.isNumber(j)) {
65506555
if (j === w.seriesData.series[w.globals.maxValsInArrayIndex].length - 2 && w.globals.shouldAnimate) {
@@ -27651,19 +27656,6 @@ class MorphTypeChange {
2765127656
const animCfg = this.w.config.chart.animations;
2765227657
return animCfg.chartTypeMorph && animCfg.chartTypeMorph.speed || animCfg.speed || 600;
2765327658
}
27654-
/**
27655-
* Which morph interpolator to use for this transition.
27656-
* 'commands' (default) — per-SVG-command lerp; preserves curves but can
27657-
* "wing/flip" when shapes have different anchor-point counts.
27658-
* 'polygons' — N-point perimeter resample with rotation-search alignment;
27659-
* always smooth + non-self-intersecting, but every frame is a polyline.
27660-
* @returns {'commands' | 'polygons'}
27661-
*/
27662-
getAlgorithm() {
27663-
const animCfg = this.w.config.chart.animations;
27664-
const algo = animCfg.chartTypeMorph && animCfg.chartTypeMorph.algorithm;
27665-
return algo === "polygons" ? "polygons" : "commands";
27666-
}
2766727659
/**
2766827660
* Fade newly-mounted axes / grid / legend / titles from opacity 0 → 1 in
2766927661
* parallel with the morph. Without this the chart's chrome would pop in
@@ -32302,9 +32294,8 @@ class Pie {
3230232294
size: this.sliceSizes[i]
3230332295
});
3230432296
const morphSpeed = this.ctx.morphTypeChange.getSpeed();
32305-
const morphAlgo = this.ctx.morphTypeChange.getAlgorithm();
3230632297
elPath.node.setAttribute("data:pathOrig", targetD);
32307-
elPath.animate(morphSpeed).plot(targetD, morphAlgo).attr({ "stroke-width": this.strokeWidth });
32298+
elPath.animate(morphSpeed).plot(targetD, "polygons").attr({ "stroke-width": this.strokeWidth });
3230832299
} else if (this.dynamicAnim && w.globals.dataChanged) {
3230932300
this.animatePaths(elPath, {
3231032301
size: this.sliceSizes[i],
@@ -33697,7 +33688,6 @@ class Radial extends Pie {
3369733688
this.animBeginArr.push(this.animDur);
3369833689
if (morphActive && morphFrom) {
3369933690
const morphSpeed = this.ctx.morphTypeChange.getSpeed();
33700-
const morphAlgo = this.ctx.morphTypeChange.getAlgorithm();
3370133691
const actualArcD = this.getPiePath({
3370233692
me: this,
3370333693
startAngle,
@@ -33713,7 +33703,7 @@ class Radial extends Pie {
3371333703
startAngle,
3371433704
startAngle + angle
3371533705
);
33716-
elPath.animate(morphSpeed).plot(targetD, morphAlgo).after(
33706+
elPath.animate(morphSpeed).plot(targetD, "polygons").after(
3371733707
/** @this {any} */
3371833708
function() {
3371933709
this.attr({
@@ -33725,7 +33715,7 @@ class Radial extends Pie {
3372533715
}
3372633716
);
3372733717
} else {
33728-
elPath.animate(morphSpeed).plot(actualArcD, morphAlgo).attr({ "stroke-width": strokeWidth });
33718+
elPath.animate(morphSpeed).plot(actualArcD, "polygons").attr({ "stroke-width": strokeWidth });
3372933719
}
3373033720
} else {
3373133721
this.animatePaths(elPath, {

dist/core.common.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)