From c425ba6eb36c40a82d264f16efc52bf98296a7e6 Mon Sep 17 00:00:00 2001 From: Lexachoc <20377719+Lexachoc@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:57:57 +0200 Subject: [PATCH 1/3] Add modebar button for toggling ternary spikelines --- src/components/modebar/buttons.js | 49 +++++++++++++++ src/components/modebar/manage.js | 7 +++ src/fonts/ploticon.js | 5 ++ test/jasmine/tests/modebar_test.js | 95 ++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 7914bc1c793..cb79e92e7e6 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -790,6 +790,55 @@ function setSpikelineVisibility(gd) { return aobj; } +// unlike cartesian toggleSpikelines above +// this toggles ternary spikelines by hiding and restoring their previous visibility states +modeBarButtons.toggleTernarySpikelines = { + name: 'toggleTernarySpikelines', + title: function(gd) { + return _(gd, 'Toggle Spike Lines'); + }, + icon: Icons.ternary_spikeline, + attr: 'ternary-spikelines', + toggle: true, + click: function(gd, ev) { + Registry.call('_guiRelayout', gd, toggleTernarySpikelineVisibility(gd, ev)); + } +}; + +function toggleTernarySpikelineVisibility(gd, ev) { + var button = ev.currentTarget; + var val = button._previousVal; + var fullLayout = gd._fullLayout; + var ternaryIds = fullLayout._subplots.ternary; + + var axes = ['aaxis', 'baxis', 'caxis']; + + var currentSpikes = {}; + var layoutUpdate = {}; + + if (val) { + layoutUpdate = val; + button._previousVal = null; + } else { + for (var i = 0; i < ternaryIds.length; i++) { + var ternaryId = ternaryIds[i]; + var ternaryLayout = fullLayout[ternaryId]; + + for (var j = 0; j < axes.length; j++) { + var axis = axes[j]; + var spikeAStr = ternaryId + '.' + axis + '.showspikes'; + + layoutUpdate[spikeAStr] = false; + currentSpikes[spikeAStr] = ternaryLayout[axis].showspikes; + } + } + + button._previousVal = currentSpikes; + } + + return layoutUpdate; +} + modeBarButtons.resetViewMap = { name: 'resetViewMap', _cat: 'resetView', diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 2ab6b934d52..35dac5e185c 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -180,6 +180,11 @@ function getButtonGroups(gd) { if(hasCartesian) { hoverGroup.push('toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'); } + + if(hasTernary) { + hoverGroup.push('toggleTernarySpikelines'); + } + if(hasNoHover(fullData) || hasUnifiedHoverLabel) { hoverGroup = []; } @@ -228,6 +233,8 @@ function getButtonGroups(gd) { } } else if(b === 'togglespikelines') { enableHover('toggleSpikelines'); + } else if(b === 'toggleternaryspikelines') { + enableHover('toggleTernarySpikelines'); } else if(b === 'togglehover') { enableHover('toggleHover'); } else if(b === 'hovercompare') { diff --git a/src/fonts/ploticon.js b/src/fonts/ploticon.js index bcc31471b8a..bda8fe491e0 100644 --- a/src/fonts/ploticon.js +++ b/src/fonts/ploticon.js @@ -169,6 +169,11 @@ module.exports = { path: 'M512 409c0-57-46-104-103-104-57 0-104 47-104 104 0 57 47 103 104 103 57 0 103-46 103-103z m-327-39l92 0 0 92-92 0z m-185 0l92 0 0 92-92 0z m370-186l92 0 0 93-92 0z m0-184l92 0 0 92-92 0z', transform: 'matrix(1.5 0 0 -1.5 0 850)' }, + ternary_spikeline: { + width: 1000, + height: 1000, + path: 'M725 500c0-69-56-125-125-125-69 0-125 56-125 125 0 69 56 125 125 125 69 0 125-56 125-125z M329 444l112 0 0 112-112 0z M104 444l112 0 0 112-112 0z M652 258l112 0 0 112-112 0z M764 63l112 0 0 112-112 0z M652 630l112 0 0 112-112 0z M764 825l112 0 0 112-112 0z' + }, pencil: { width: 1792, height: 1792, diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 64ce1f20be1..549fcc051a5 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -1520,6 +1520,101 @@ describe('ModeBar', function() { }); }); + describe('button toggleTernarySpikelines', function() { + it('should hide and restore ternary spikelines', function(done) { + var gd = createGraphDiv(); + + Plotly.newPlot(gd, [{ + type: 'scatterternary', + a: [1], + b: [2], + c: [3] + }], { + ternary: { + aaxis: {showspikes: true}, + baxis: {showspikes: false}, + caxis: {showspikes: true} + } + }, { + modeBarButtonsToAdd: ['toggleTernarySpikelines'] + }) + .then(function() { + var button = selectButton(gd._fullLayout._modeBar, 'toggleTernarySpikelines'); + + expect(gd._fullLayout.ternary.aaxis.showspikes).toBe(true); + expect(gd._fullLayout.ternary.baxis.showspikes).toBe(false); + expect(gd._fullLayout.ternary.caxis.showspikes).toBe(true); + + button.click(); + + expect(gd._fullLayout.ternary.aaxis.showspikes).toBe(false); + expect(gd._fullLayout.ternary.baxis.showspikes).toBe(false); + expect(gd._fullLayout.ternary.caxis.showspikes).toBe(false); + + button.click(); + + expect(gd._fullLayout.ternary.aaxis.showspikes).toBe(true); + expect(gd._fullLayout.ternary.baxis.showspikes).toBe(false); + expect(gd._fullLayout.ternary.caxis.showspikes).toBe(true); + }) + .then(done, done.fail); + }); + + it('should toggle spikelines on multiple ternary subplots', function(done) { + var gd = createGraphDiv(); + + Plotly.newPlot(gd, [{ + type: 'scatterternary', + subplot: 'ternary', + a: [1], + b: [2], + c: [3] + }, { + type: 'scatterternary', + subplot: 'ternary2', + a: [2], + b: [3], + c: [1] + }], { + ternary: { + domain: {x: [0, 0.45]}, + aaxis: {showspikes: true}, + baxis: {showspikes: false}, + caxis: {showspikes: true} + }, + ternary2: { + domain: {x: [0.55, 1]}, + aaxis: {showspikes: false}, + baxis: {showspikes: true}, + caxis: {showspikes: false} + } + }, { + modeBarButtonsToAdd: ['toggleTernarySpikelines'] + }) + .then(function() { + var button = selectButton(gd._fullLayout._modeBar, 'toggleTernarySpikelines'); + + button.click(); + + ['aaxis', 'baxis', 'caxis'].forEach(function(axis) { + expect(gd._fullLayout.ternary[axis].showspikes).toBe(false); + expect(gd._fullLayout.ternary2[axis].showspikes).toBe(false); + }); + + button.click(); + + expect(gd._fullLayout.ternary.aaxis.showspikes).toBe(true); + expect(gd._fullLayout.ternary.baxis.showspikes).toBe(false); + expect(gd._fullLayout.ternary.caxis.showspikes).toBe(true); + + expect(gd._fullLayout.ternary2.aaxis.showspikes).toBe(false); + expect(gd._fullLayout.ternary2.baxis.showspikes).toBe(true); + expect(gd._fullLayout.ternary2.caxis.showspikes).toBe(false); + }) + .then(done, done.fail); + }); + }); + describe('button toggleHover', function() { it('ternary case', function(done) { var gd = createGraphDiv(); From a2c13dccf7b8cde9fd5d98f063ebf14e1638baef Mon Sep 17 00:00:00 2001 From: Ya-Fan Chen <20377719+Lexachoc@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:09:05 +0200 Subject: [PATCH 2/3] add draftlog --- draftlogs/8131_add.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/8131_add.md diff --git a/draftlogs/8131_add.md b/draftlogs/8131_add.md new file mode 100644 index 00000000000..e4a3f221025 --- /dev/null +++ b/draftlogs/8131_add.md @@ -0,0 +1 @@ + - Add a modebar button (`toggleTernarySpikelines`) for toggling ternary spikelines [[#8131](https://github.com/plotly/plotly.js/pull/8131)] From e1676f309c2f2a7267b9928603b9507cf6dbf744 Mon Sep 17 00:00:00 2001 From: Lexachoc <20377719+Lexachoc@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:15:09 +0200 Subject: [PATCH 3/3] update schema --- src/types/generated/schema.d.ts | 2 +- test/plot-schema.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/generated/schema.d.ts b/src/types/generated/schema.d.ts index 8a855665e73..4afe3ae3dfb 100644 --- a/src/types/generated/schema.d.ts +++ b/src/types/generated/schema.d.ts @@ -16372,7 +16372,7 @@ export interface Layout { * @default 'h' */ orientation?: 'v' | 'h'; - /** Determines which predefined modebar buttons to remove. Similar to `config.modeBarButtonsToRemove` option. This may include *autoScale2d*, *autoscale*, *hoverCompareCartesian*, *hovercompare*, *lasso*, *lasso2d*, *orbitRotation*, *orbitrotation*, *pan*, *pan2d*, *pan3d*, *reset*, *resetCameraDefault3d*, *resetCameraLastSave3d*, *resetGeo*, *resetSankeyGroup*, *resetScale2d*, *resetViewMap*, *resetViews*, *resetcameradefault*, *resetcameralastsave*, *resetsankeygroup*, *resetscale*, *resetview*, *resetviews*, *select*, *select2d*, *sendChartToCloud*, *sendcharttocloud*, *tableRotation*, *tablerotation*, *toImage*, *toggleHover*, *toggleSpikelines*, *togglehover*, *togglespikelines*, *toimage*, *zoom*, *zoom2d*, *zoom3d*, *zoomIn2d*, *zoomInGeo*, *zoomInMap*, *zoomOut2d*, *zoomOutGeo*, *zoomOutMap*, *zoomin*, *zoomout*. */ + /** Determines which predefined modebar buttons to remove. Similar to `config.modeBarButtonsToRemove` option. This may include *autoScale2d*, *autoscale*, *hoverCompareCartesian*, *hovercompare*, *lasso*, *lasso2d*, *orbitRotation*, *orbitrotation*, *pan*, *pan2d*, *pan3d*, *reset*, *resetCameraDefault3d*, *resetCameraLastSave3d*, *resetGeo*, *resetSankeyGroup*, *resetScale2d*, *resetViewMap*, *resetViews*, *resetcameradefault*, *resetcameralastsave*, *resetsankeygroup*, *resetscale*, *resetview*, *resetviews*, *select*, *select2d*, *sendChartToCloud*, *sendcharttocloud*, *tableRotation*, *tablerotation*, *toImage*, *toggleHover*, *toggleSpikelines*, *toggleTernarySpikelines*, *togglehover*, *togglespikelines*, *toggleternaryspikelines*, *toimage*, *zoom*, *zoom2d*, *zoom3d*, *zoomIn2d*, *zoomInGeo*, *zoomInMap*, *zoomOut2d*, *zoomOutGeo*, *zoomOutMap*, *zoomin*, *zoomout*. */ remove?: string | string[]; /** Controls persistence of user-driven changes related to the modebar, including `hovermode`, `dragmode`, and `showspikes` at both the root level and inside subplots. Defaults to `layout.uirevision`. */ uirevision?: any; diff --git a/test/plot-schema.json b/test/plot-schema.json index 4d51312e82d..47b64e1b8f9 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -4242,7 +4242,7 @@ }, "remove": { "arrayOk": true, - "description": "Determines which predefined modebar buttons to remove. Similar to `config.modeBarButtonsToRemove` option. This may include *autoScale2d*, *autoscale*, *hoverCompareCartesian*, *hovercompare*, *lasso*, *lasso2d*, *orbitRotation*, *orbitrotation*, *pan*, *pan2d*, *pan3d*, *reset*, *resetCameraDefault3d*, *resetCameraLastSave3d*, *resetGeo*, *resetSankeyGroup*, *resetScale2d*, *resetViewMap*, *resetViews*, *resetcameradefault*, *resetcameralastsave*, *resetsankeygroup*, *resetscale*, *resetview*, *resetviews*, *select*, *select2d*, *sendChartToCloud*, *sendcharttocloud*, *tableRotation*, *tablerotation*, *toImage*, *toggleHover*, *toggleSpikelines*, *togglehover*, *togglespikelines*, *toimage*, *zoom*, *zoom2d*, *zoom3d*, *zoomIn2d*, *zoomInGeo*, *zoomInMap*, *zoomOut2d*, *zoomOutGeo*, *zoomOutMap*, *zoomin*, *zoomout*.", + "description": "Determines which predefined modebar buttons to remove. Similar to `config.modeBarButtonsToRemove` option. This may include *autoScale2d*, *autoscale*, *hoverCompareCartesian*, *hovercompare*, *lasso*, *lasso2d*, *orbitRotation*, *orbitrotation*, *pan*, *pan2d*, *pan3d*, *reset*, *resetCameraDefault3d*, *resetCameraLastSave3d*, *resetGeo*, *resetSankeyGroup*, *resetScale2d*, *resetViewMap*, *resetViews*, *resetcameradefault*, *resetcameralastsave*, *resetsankeygroup*, *resetscale*, *resetview*, *resetviews*, *select*, *select2d*, *sendChartToCloud*, *sendcharttocloud*, *tableRotation*, *tablerotation*, *toImage*, *toggleHover*, *toggleSpikelines*, *toggleTernarySpikelines*, *togglehover*, *togglespikelines*, *toggleternaryspikelines*, *toimage*, *zoom*, *zoom2d*, *zoom3d*, *zoomIn2d*, *zoomInGeo*, *zoomInMap*, *zoomOut2d*, *zoomOutGeo*, *zoomOutMap*, *zoomin*, *zoomout*.", "dflt": "", "editType": "modebar", "valType": "string"