From b8572b217960591ed82899d7886b30d16a9624a2 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 27 May 2026 17:58:11 -0600 Subject: [PATCH 1/4] Linting/formatting --- test/jasmine/tests/violin_test.js | 1033 ++++++++++++++++------------- 1 file changed, 581 insertions(+), 452 deletions(-) diff --git a/test/jasmine/tests/violin_test.js b/test/jasmine/tests/violin_test.js index 375aa92aa39..2e0589af7b0 100644 --- a/test/jasmine/tests/violin_test.js +++ b/test/jasmine/tests/violin_test.js @@ -15,7 +15,7 @@ var supplyAllDefaults = require('../assets/supply_defaults'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelContent = customAssertions.assertHoverLabelContent; -describe('Test violin defaults', function() { +describe('Test violin defaults', function () { var traceOut; function _supply(traceIn, layout) { @@ -23,7 +23,7 @@ describe('Test violin defaults', function() { Violin.supplyDefaults(traceIn, traceOut, '#444', layout || {}); } - it('should set visible to false when x and y are empty', function() { + it('should set visible to false when x and y are empty', function () { _supply({ x: [], y: [] @@ -32,8 +32,8 @@ describe('Test violin defaults', function() { expect(traceOut.visible).toBe(false); }); - it('should inherit layout.calendar', function() { - _supply({y: [1, 2, 3]}, {calendar: 'islamic'}); + it('should inherit layout.calendar', function () { + _supply({ y: [1, 2, 3] }, { calendar: 'islamic' }); // we always fill calendar attributes, because it's hard to tell if // we're on a date axis at this point. @@ -41,14 +41,17 @@ describe('Test violin defaults', function() { expect(traceOut.ycalendar).toBe('islamic'); }); - it('should take its own calendars', function() { - _supply({ - y: [1, 2, 3], - xcalendar: 'coptic', - ycalendar: 'ethiopian' - }, { - calendar: 'islamic' - }); + it('should take its own calendars', function () { + _supply( + { + y: [1, 2, 3], + xcalendar: 'coptic', + ycalendar: 'ethiopian' + }, + { + calendar: 'islamic' + } + ); // we always fill calendar attributes, because it's hard to tell if // we're on a date axis at this point. @@ -56,7 +59,7 @@ describe('Test violin defaults', function() { expect(traceOut.ycalendar).toBe('ethiopian'); }); - it('should not coerce point attributes when *points* is false', function() { + it('should not coerce point attributes when *points* is false', function () { _supply({ y: [1, 1, 2], points: false @@ -69,7 +72,7 @@ describe('Test violin defaults', function() { expect(traceOut.text).toBeUndefined(); }); - it('should default *points* to suspectedoutliers when marker.outliercolor is set & valid', function() { + it('should default *points* to suspectedoutliers when marker.outliercolor is set & valid', function () { _supply({ y: [1, 1, 2], marker: { outliercolor: 'blue' } @@ -78,10 +81,10 @@ describe('Test violin defaults', function() { expect(traceOut.points).toBe('suspectedoutliers'); }); - it('should default *points* to suspectedoutliers when marker.line.outliercolor is set & valid', function() { + it('should default *points* to suspectedoutliers when marker.line.outliercolor is set & valid', function () { _supply({ y: [1, 1, 2], - marker: { line: {outliercolor: 'blue'} } + marker: { line: { outliercolor: 'blue' } } }); expect(traceOut.points).toBe('suspectedoutliers'); @@ -89,7 +92,7 @@ describe('Test violin defaults', function() { expect(traceOut.text).toBeDefined(); }); - it('should default *spanmode* to manual when *span* is set to an array', function() { + it('should default *spanmode* to manual when *span* is set to an array', function () { _supply({ y: [1, 1, 2], span: [0, 1] @@ -105,7 +108,7 @@ describe('Test violin defaults', function() { expect(traceOut.spanmode).toBe('soft'); }); - it('should default *.visible attributes when one of their corresponding style attributes is set & valid', function() { + it('should default *.visible attributes when one of their corresponding style attributes is set & valid', function () { _supply({ y: [1, 2, 1], box: { width: 0.1 }, @@ -125,17 +128,17 @@ describe('Test violin defaults', function() { color: 'red' } }); - expect(traceOut.box).toEqual({visible: false}); - expect(traceOut.meanline).toEqual({visible: false}); + expect(traceOut.box).toEqual({ visible: false }); + expect(traceOut.meanline).toEqual({ visible: false }); }); - it('should use violin style settings to default inner style attribute', function() { + it('should use violin style settings to default inner style attribute', function () { _supply({ y: [1, 2, 1], fillcolor: 'red', - line: {color: 'blue', width: 10}, - box: {visible: true}, - meanline: {visible: true} + line: { color: 'blue', width: 10 }, + box: { visible: true }, + meanline: { visible: true } }); expect(traceOut.box.fillcolor).toBe('red'); expect(traceOut.box.line.color).toBe('blue'); @@ -144,7 +147,7 @@ describe('Test violin defaults', function() { expect(traceOut.meanline.width).toBe(10); }); - it('should not coerce *scalegroup* and *scalemode* when *width* is set', function() { + it('should not coerce *scalegroup* and *scalemode* when *width* is set', function () { _supply({ y: [1, 2, 1], width: 1 @@ -161,7 +164,7 @@ describe('Test violin defaults', function() { expect(traceOut.scalegroup).toBe(''); }); - it('should not coerce hovertemplate when *hoveron* does not contains *points* flag', function() { + it('should not coerce hovertemplate when *hoveron* does not contains *points* flag', function () { var ht = '--- %{y} ---'; _supply({ @@ -193,10 +196,10 @@ describe('Test violin defaults', function() { expect(traceOut.hovertemplate).toBe(ht, 'hoveron:violins+points'); }); - it('should not include alignementgroup/offsetgroup when violinmode is not *group*', function() { + it('should not include alignementgroup/offsetgroup when violinmode is not *group*', function () { var gd = { - data: [{type: 'violin', y: [1], alignmentgroup: 'a', offsetgroup: '1'}], - layout: {violinmode: 'group'} + data: [{ type: 'violin', y: [1], alignmentgroup: 'a', offsetgroup: '1' }], + layout: { violinmode: 'group' } }; supplyAllDefaults(gd); @@ -210,12 +213,12 @@ describe('Test violin defaults', function() { }); }); -describe('Test violin calc:', function() { +describe('Test violin calc:', function () { var cd, fullLayout; function _calc(attrs, layout) { var gd = { - data: [Lib.extendFlat({type: 'violin'}, attrs)], + data: [Lib.extendFlat({ type: 'violin' }, attrs)], layout: layout || {}, calcdata: [] }; @@ -226,86 +229,86 @@ describe('Test violin calc:', function() { return cd; } - it('should compute q1/q3 depending on *quartilemethod*', function() { + it('should compute q1/q3 depending on *quartilemethod*', function () { // samples from https://en.wikipedia.org/wiki/Quartile var specs = { // N is odd and is spanned by (4n+3) odd: { sample: [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49], methods: { - linear: {q1: 20.25, q3: 42.75}, - exclusive: {q1: 15, q3: 43}, - inclusive: {q1: 25.5, q3: 42.5} + linear: { q1: 20.25, q3: 42.75 }, + exclusive: { q1: 15, q3: 43 }, + inclusive: { q1: 25.5, q3: 42.5 } } }, // N is odd and is spanned by (4n+1) odd2: { sample: [6, 15, 36, 39, 40, 42, 43, 47, 49], methods: { - linear: {q1: 30.75, q3: 44}, - exclusive: {q1: 25.5, q3: 45}, - inclusive: {q1: 36, q3: 43} + linear: { q1: 30.75, q3: 44 }, + exclusive: { q1: 25.5, q3: 45 }, + inclusive: { q1: 36, q3: 43 } } }, // N is even even: { sample: [7, 15, 36, 39, 40, 41], methods: { - linear: {q1: 15, q3: 40}, - exclusive: {q1: 15, q3: 40}, - inclusive: {q1: 15, q3: 40} + linear: { q1: 15, q3: 40 }, + exclusive: { q1: 15, q3: 40 }, + inclusive: { q1: 15, q3: 40 } } }, // samples from http://jse.amstat.org/v14n3/langford.html s4: { sample: [1, 2, 3, 4], methods: { - linear: {q1: 1.5, q3: 3.5}, - exclusive: {q1: 1.5, q3: 3.5}, - inclusive: {q1: 1.5, q3: 3.5} + linear: { q1: 1.5, q3: 3.5 }, + exclusive: { q1: 1.5, q3: 3.5 }, + inclusive: { q1: 1.5, q3: 3.5 } } }, s5: { sample: [1, 2, 3, 4, 5], methods: { - linear: {q1: 1.75, q3: 4.25}, - exclusive: {q1: 1.5, q3: 4.5}, - inclusive: {q1: 2, q3: 4} + linear: { q1: 1.75, q3: 4.25 }, + exclusive: { q1: 1.5, q3: 4.5 }, + inclusive: { q1: 2, q3: 4 } } }, s6: { sample: [1, 2, 3, 4, 5, 6], methods: { - linear: {q1: 2, q3: 5}, - exclusive: {q1: 2, q3: 5}, - inclusive: {q1: 2, q3: 5} + linear: { q1: 2, q3: 5 }, + exclusive: { q1: 2, q3: 5 }, + inclusive: { q1: 2, q3: 5 } } }, s7: { sample: [1, 2, 3, 4, 5, 6, 7], methods: { - linear: {q1: 2.25, q3: 5.75}, - exclusive: {q1: 2, q3: 6}, - inclusive: {q1: 2.5, q3: 5.5} + linear: { q1: 2.25, q3: 5.75 }, + exclusive: { q1: 2, q3: 6 }, + inclusive: { q1: 2.5, q3: 5.5 } } } }; - for(var name in specs) { + for (var name in specs) { var spec = specs[name]; - for(var m in spec.methods) { - var cd = _calc({y: spec.sample, quartilemethod: m}); + for (var m in spec.methods) { + var cd = _calc({ y: spec.sample, quartilemethod: m }); expect(cd[0].q1).toBe(spec.methods[m].q1, ['q1', m, name].join(' | ')); expect(cd[0].q3).toBe(spec.methods[m].q3, ['q3', m, name].join(' | ')); } } }); - it('should compute bandwidth and span based on the sample and *spanmode*', function() { + it('should compute bandwidth and span based on the sample and *spanmode*', function () { var y = [1, 1, 2, 2, 3]; - _calc({y: y}); + _calc({ y: y }); expect(cd[0].bandwidth).toBeCloseTo(0.64); expect(cd[0].span).toBeCloseToArray([-0.28, 4.28]); @@ -334,7 +337,7 @@ describe('Test violin calc:', function() { expect(cd[0].span).toBeCloseToArray([0, 4.28], 'defaults to soft bound'); }); - it('should honor set bandwidth in span calculations', function() { + it('should honor set bandwidth in span calculations', function () { var y = [1, 1, 2, 2, 3]; var bw = 0.1; @@ -374,7 +377,7 @@ describe('Test violin calc:', function() { expect(cd[0].span).toBeCloseToArray([0, 3.2], 'defaults to soft bound'); }); - it('should fill in scale-group stats', function() { + it('should fill in scale-group stats', function () { _calc({ name: 'one', y: [0, 0, 0, 0, 10, 10, 10, 10] @@ -383,17 +386,21 @@ describe('Test violin calc:', function() { expect(fullLayout._violinScaleGroupStats.one.maxCount).toBe(8); }); - it('handle multi-box / single-value case', function() { + it('handle multi-box / single-value case', function () { _calc({ x: [1, 2, 3, 4, 5, 6], y: [1, 2, 3, 4, 5, 6] }); expect(cd.length).toBe(6, '# of violins'); - expect(cd.every(function(d) { return d.bandwidth; })).toBe(false, 'bandwidth'); + expect( + cd.every(function (d) { + return d.bandwidth; + }) + ).toBe(false, 'bandwidth'); }); - it('handle multi-value / single-but-unique-value case', function() { + it('handle multi-value / single-but-unique-value case', function () { _calc({ y: [1, 1, 1, 1, 1] }); @@ -402,7 +409,7 @@ describe('Test violin calc:', function() { expect(cd[0].bandwidth).toBe(0, 'bandwidth'); }); - it('should produce exactly n + 1 density samples for tiny or near-equal spans', function() { + it('should produce exactly n + 1 density samples for tiny or near-equal spans', function () { var cd = _calc({ type: 'violin', x: [0, 0], @@ -417,7 +424,7 @@ describe('Test violin calc:', function() { }); }); -describe('Test violin hover:', function() { +describe('Test violin hover:', function () { var gd; afterEach(destroyGraphDiv); @@ -426,368 +433,476 @@ describe('Test violin hover:', function() { gd = createGraphDiv(); var fig = Lib.extendDeep( - {width: 700, height: 500}, + { width: 700, height: 500 }, specs.mock || require('../../image/mocks/violin_grouped.json') ); - if(specs.patch) { + if (specs.patch) { fig = specs.patch(fig); } var pos = specs.pos || [200, 200]; - return Plotly.newPlot(gd, fig).then(function() { + return Plotly.newPlot(gd, fig).then(function () { mouseEvent('mousemove', pos[0], pos[1]); assertHoverLabelContent(specs); - if(specs.hoverLabelPos) { - d3SelectAll('g.hovertext').each(function(_, i) { + if (specs.hoverLabelPos) { + d3SelectAll('g.hovertext').each(function (_, i) { var bbox = this.getBoundingClientRect(); - expect([bbox.bottom, bbox.top]) - .toBeWithinArray(specs.hoverLabelPos[i], 10, 'bottom--top hover label ' + i); + expect([bbox.bottom, bbox.top]).toBeWithinArray( + specs.hoverLabelPos[i], + 10, + 'bottom--top hover label ' + i + ); }); } }); } - [{ - desc: 'base', - patch: function(fig) { - fig.layout.hovermode = 'x'; - return fig; + [ + { + desc: 'base', + patch: function (fig) { + fig.layout.hovermode = 'x'; + return fig; + }, + nums: [ + 'median: 0.55', + 'min: 0', + 'lower fence: 0', + 'q1: 0.3', + 'q3: 0.6', + 'upper fence: 0.7', + 'max: 0.7', + 'y: 0.9266848, kde: 0.182' + ], + name: ['radishes', '', '', '', '', '', '', ''], + axis: 'day 1' }, - nums: [ - 'median: 0.55', 'min: 0', 'lower fence: 0', 'q1: 0.3', 'q3: 0.6', 'upper fence: 0.7', 'max: 0.7', - 'y: 0.9266848, kde: 0.182' - ], - name: ['radishes', '', '', '', '', '', '', ''], - axis: 'day 1' - }, { - desc: 'with mean', - patch: function(fig) { - fig.data.forEach(function(trace) { - trace.meanline = {visible: true}; - }); - fig.layout.hovermode = 'x'; - return fig; + { + desc: 'with mean', + patch: function (fig) { + fig.data.forEach(function (trace) { + trace.meanline = { visible: true }; + }); + fig.layout.hovermode = 'x'; + return fig; + }, + nums: [ + 'median: 0.55', + 'min: 0', + 'lower fence: 0', + 'q1: 0.3', + 'q3: 0.6', + 'upper fence: 0.7', + 'max: 0.7', + 'mean: 0.45', + 'y: 0.9266848, kde: 0.182' + ], + name: ['radishes', '', '', '', '', '', '', '', ''], + axis: 'day 1' }, - nums: [ - 'median: 0.55', 'min: 0', 'lower fence: 0', 'q1: 0.3', 'q3: 0.6', 'upper fence: 0.7', 'max: 0.7', 'mean: 0.45', - 'y: 0.9266848, kde: 0.182' - ], - name: ['radishes', '', '', '', '', '', '', '', ''], - axis: 'day 1' - }, { - desc: 'with overlaid violins', - patch: function(fig) { - fig.layout.violinmode = 'overlay'; - fig.layout.hovermode = 'x'; - fig.layout.height = 700; - - return fig; + { + desc: 'with overlaid violins', + patch: function (fig) { + fig.layout.violinmode = 'overlay'; + fig.layout.hovermode = 'x'; + fig.layout.height = 700; + + return fig; + }, + nums: [ + 'median: 0.45', + 'median: 0.45', + 'median: 0.55', + 'min: 0', + 'min: 0.1', + 'min: 0.2', + 'lower fence: 0', + 'lower fence: 0.1', + 'lower fence: 0.2', + 'q1: 0.1', + 'q1: 0.2', + 'q1: 0.3', + 'q3: 0.6', + 'q3: 0.6', + 'q3: 0.6', + 'upper fence: 0.7', + 'upper fence: 0.9', + 'upper fence: 1', + 'max: 0.7', + 'max: 0.9', + 'max: 1', + 'y: 1.211363, kde: 0.119', + 'y: 1.211363, kde: 0.168' + ], + name: [ + 'carrots', + 'kale', + 'radishes', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ], + axis: 'day 1' }, - nums: [ - 'median: 0.45', 'median: 0.45', 'median: 0.55', - 'min: 0', 'min: 0.1', 'min: 0.2', - 'lower fence: 0', 'lower fence: 0.1', 'lower fence: 0.2', - 'q1: 0.1', 'q1: 0.2', 'q1: 0.3', - 'q3: 0.6', 'q3: 0.6', 'q3: 0.6', - 'upper fence: 0.7', 'upper fence: 0.9', 'upper fence: 1', - 'max: 0.7', 'max: 0.9', 'max: 1', - 'y: 1.211363, kde: 0.119', - 'y: 1.211363, kde: 0.168' - ], - name: [ - 'carrots', 'kale', 'radishes', - '', '', '', - '', '', '', - '', '', '', - '', '', '', - '', '', '', - '', '', '', - '', - '', - ], - axis: 'day 1' - }, { - desc: 'hoveron points | hovermode closest', - patch: function(fig) { - fig.data.forEach(function(trace) { - trace.points = 'all'; - trace.hoveron = 'points'; - }); - return fig; + { + desc: 'hoveron points | hovermode closest', + patch: function (fig) { + fig.data.forEach(function (trace) { + trace.points = 'all'; + trace.hoveron = 'points'; + }); + return fig; + }, + pos: [220, 200], + nums: '(day 1, 0.9)', + name: 'carrots' }, - pos: [220, 200], - nums: '(day 1, 0.9)', - name: 'carrots' - }, { - desc: 'hoveron points | hovermode x', - patch: function(fig) { - fig.data.forEach(function(trace) { - trace.points = 'all'; - trace.hoveron = 'points'; - }); - fig.layout.hovermode = 'x'; - return fig; + { + desc: 'hoveron points | hovermode x', + patch: function (fig) { + fig.data.forEach(function (trace) { + trace.points = 'all'; + trace.hoveron = 'points'; + }); + fig.layout.hovermode = 'x'; + return fig; + }, + pos: [220, 200], + nums: '0.9', + name: 'carrots', + axis: 'day 1' }, - pos: [220, 200], - nums: '0.9', - name: 'carrots', - axis: 'day 1' - }, { - desc: 'hoveron violins+points | hovermode x (hover on violin only - same result as base)', - patch: function(fig) { - fig.data.forEach(function(trace) { - trace.points = 'all'; - trace.hoveron = 'points+violins'; - }); - fig.layout.hovermode = 'x'; - return fig; + { + desc: 'hoveron violins+points | hovermode x (hover on violin only - same result as base)', + patch: function (fig) { + fig.data.forEach(function (trace) { + trace.points = 'all'; + trace.hoveron = 'points+violins'; + }); + fig.layout.hovermode = 'x'; + return fig; + }, + nums: ['median: 0.55', 'min: 0', 'lower fence: 0', 'q1: 0.3', 'q3: 0.6', 'upper fence: 0.7', 'max: 0.7'], + name: ['radishes', '', '', '', '', '', ''], + axis: 'day 1' }, - nums: ['median: 0.55', 'min: 0', 'lower fence: 0', 'q1: 0.3', 'q3: 0.6', 'upper fence: 0.7', 'max: 0.7'], - name: ['radishes', '', '', '', '', '', ''], - axis: 'day 1' - }, { - desc: 'hoveron violins+points | hovermode x (violin AND closest point)', - patch: function(fig) { - fig.data.forEach(function(trace) { - trace.points = 'all'; - trace.hoveron = 'points+violins'; - trace.pointpos = 0; - }); - fig.layout.hovermode = 'x'; - return fig; + { + desc: 'hoveron violins+points | hovermode x (violin AND closest point)', + patch: function (fig) { + fig.data.forEach(function (trace) { + trace.points = 'all'; + trace.hoveron = 'points+violins'; + trace.pointpos = 0; + }); + fig.layout.hovermode = 'x'; + return fig; + }, + pos: [207, 240], + nums: [ + '0.7', + 'median: 0.55', + 'min: 0', + 'lower fence: 0', + 'q1: 0.3', + 'q3: 0.6', + 'upper fence: 0.7', + 'max: 0.7' + ], + name: ['radishes', 'radishes', '', '', '', '', '', ''], + axis: 'day 1' }, - pos: [207, 240], - nums: ['0.7', 'median: 0.55', 'min: 0', 'lower fence: 0', 'q1: 0.3', 'q3: 0.6', 'upper fence: 0.7', 'max: 0.7'], - name: ['radishes', 'radishes', '', '', '', '', '', ''], - axis: 'day 1' - }, { - desc: 'text items on hover', - patch: function(fig) { - fig.data.forEach(function(trace) { - trace.points = 'all'; - trace.hoveron = 'points'; - trace.text = trace.y.map(function(v) { return 'look:' + v; }); - }); - return fig; + { + desc: 'text items on hover', + patch: function (fig) { + fig.data.forEach(function (trace) { + trace.points = 'all'; + trace.hoveron = 'points'; + trace.text = trace.y.map(function (v) { + return 'look:' + v; + }); + }); + return fig; + }, + pos: [180, 240], + nums: '(day 1, 0.7)\nlook:0.7', + name: 'radishes' }, - pos: [180, 240], - nums: '(day 1, 0.7)\nlook:0.7', - name: 'radishes' - }, { - desc: 'only text items on hover', - patch: function(fig) { - fig.data.forEach(function(trace) { - trace.points = 'all'; - trace.hoveron = 'points'; - trace.text = trace.y.map(function(v) { return 'look:' + v; }); - trace.hoverinfo = 'text'; - }); - return fig; + { + desc: 'only text items on hover', + patch: function (fig) { + fig.data.forEach(function (trace) { + trace.points = 'all'; + trace.hoveron = 'points'; + trace.text = trace.y.map(function (v) { + return 'look:' + v; + }); + trace.hoverinfo = 'text'; + }); + return fig; + }, + pos: [180, 240], + nums: 'look:0.7', + name: '' }, - pos: [180, 240], - nums: 'look:0.7', - name: '' - }, { - desc: 'only hovertext items on hover', - patch: function(fig) { - fig.data.forEach(function(trace) { - trace.points = 'all'; - trace.hoveron = 'points'; - trace.hovertext = trace.y.map(function(v) { return 'look:' + v; }); - trace.text = trace.y.map(function(v) { return 'NOT THIS:' + v; }); - trace.hoverinfo = 'text'; - }); - return fig; + { + desc: 'only hovertext items on hover', + patch: function (fig) { + fig.data.forEach(function (trace) { + trace.points = 'all'; + trace.hoveron = 'points'; + trace.hovertext = trace.y.map(function (v) { + return 'look:' + v; + }); + trace.text = trace.y.map(function (v) { + return 'NOT THIS:' + v; + }); + trace.hoverinfo = 'text'; + }); + return fig; + }, + pos: [180, 240], + nums: 'look:0.7', + name: '' }, - pos: [180, 240], - nums: 'look:0.7', - name: '' - }, { - desc: 'one-sided violin under hovermode closest', - // hoveron: 'kde+points' - // width: 400 - // height: 700 - mock: require('../../image/mocks/violin_side-by-side.json'), - pos: [250, 300], - nums: '(x: 42.43046, kde: 0.083, Saturday)', - name: '' - }, { - desc: 'one-sided violin under hovermode y', - // hoveron: 'kde+points' - // width: 400 - // height: 700 - mock: require('../../image/mocks/violin_side-by-side.json'), - patch: function(fig) { - fig.layout.hovermode = 'y'; - return fig; + { + desc: 'one-sided violin under hovermode closest', + // hoveron: 'kde+points' + // width: 400 + // height: 700 + mock: require('../../image/mocks/violin_side-by-side.json'), + pos: [250, 300], + nums: '(x: 42.43046, kde: 0.083, Saturday)', + name: '' }, - pos: [250, 300], - nums: 'x: 42.43046, kde: 0.083', - name: '', - axis: 'Saturday' - }, { - desc: 'one-sided violin under hovermode y (ridgeplot case)', - mock: require('../../image/mocks/violin_ridgeplot.json'), - patch: function(fig) { - fig.data.forEach(function(t) { t.hoveron = 'violins'; }); - fig.layout.hovermode = 'y'; - return fig; + { + desc: 'one-sided violin under hovermode y', + // hoveron: 'kde+points' + // width: 400 + // height: 700 + mock: require('../../image/mocks/violin_side-by-side.json'), + patch: function (fig) { + fig.layout.hovermode = 'y'; + return fig; + }, + pos: [250, 300], + nums: 'x: 42.43046, kde: 0.083', + name: '', + axis: 'Saturday' }, - nums: [ - 'max: 50.81', 'min: 3.07', 'median: 18.24', - 'q1: 13.8575', 'q3: 24.975', 'upper fence: 39.42', 'lower fence: 3.07' - ], - name: ['', '', '', '', '', '', ''], - axis: 'Sat', - hoverLabelPos: [ - [338, 270], - [377, 270], - [351, 270], - [363, 270], - [345, 270], - [385, 270], - [347, 270] - ] - }, { - desc: 'single horizontal violin', - mock: require('../../image/mocks/violin_non-linear.json'), - patch: function(fig) { - fig.layout.hovermode = 'y'; - return fig; + { + desc: 'one-sided violin under hovermode y (ridgeplot case)', + mock: require('../../image/mocks/violin_ridgeplot.json'), + patch: function (fig) { + fig.data.forEach(function (t) { + t.hoveron = 'violins'; + }); + fig.layout.hovermode = 'y'; + return fig; + }, + nums: [ + 'max: 50.81', + 'min: 3.07', + 'median: 18.24', + 'q1: 13.8575', + 'q3: 24.975', + 'upper fence: 39.42', + 'lower fence: 3.07' + ], + name: ['', '', '', '', '', '', ''], + axis: 'Sat', + hoverLabelPos: [ + [338, 270], + [377, 270], + [351, 270], + [363, 270], + [345, 270], + [385, 270], + [347, 270] + ] }, - pos: [310, 160], - nums: ['median: C', 'min: A', 'q1: B', 'q3: D', 'max: G', 'upper fence: D', 'lower fence: A', 'x: C, kde: 1.005'], - name: ['categories', '', '', '', '', '', '', ''], - axis: 'categories', - isRotated: true - }, { - desc: 'multiple horizontal violins', - mock: require('../../image/mocks/box_grouped_horz.json'), - patch: function(fig) { - fig.data.forEach(function(t) { - t.type = 'violin'; - t.hoveron = 'violins'; - }); - fig.layout.violinmode = 'group'; - fig.layout.hovermode = 'y'; - return fig; + { + desc: 'single horizontal violin', + mock: require('../../image/mocks/violin_non-linear.json'), + patch: function (fig) { + fig.layout.hovermode = 'y'; + return fig; + }, + pos: [310, 160], + nums: [ + 'median: C', + 'min: A', + 'q1: B', + 'q3: D', + 'max: G', + 'upper fence: D', + 'lower fence: A', + 'x: C, kde: 1.005' + ], + name: ['categories', '', '', '', '', '', '', ''], + axis: 'categories', + isRotated: true }, - nums: ['median: 0.4', 'min: 0.1', 'lower fence: 0.1', 'q1: 0.2', 'q3: 0.7', 'upper fence: 0.9', 'max: 0.9'], - name: ['kale', '', '', '', '', '', ''], - axis: 'day 2', - isRotated: true - }, { - desc: 'multiple horizontal violins (under hovermode:closest)', - mock: require('../../image/mocks/box_grouped_horz.json'), - patch: function(fig) { - fig.data.forEach(function(t) { - t.type = 'violin'; - t.hoveron = 'violins'; - }); - fig.layout.violinmode = 'group'; - return fig; + { + desc: 'multiple horizontal violins', + mock: require('../../image/mocks/box_grouped_horz.json'), + patch: function (fig) { + fig.data.forEach(function (t) { + t.type = 'violin'; + t.hoveron = 'violins'; + }); + fig.layout.violinmode = 'group'; + fig.layout.hovermode = 'y'; + return fig; + }, + nums: ['median: 0.4', 'min: 0.1', 'lower fence: 0.1', 'q1: 0.2', 'q3: 0.7', 'upper fence: 0.9', 'max: 0.9'], + name: ['kale', '', '', '', '', '', ''], + axis: 'day 2', + isRotated: true }, - pos: [200, 175], - nums: [ - '(median: 0.7, day 2)', '(min: 0.2, day 2)', '(lower fence: 0.2, day 2)', '(q1: 0.5, day 2)', - '(q3: 0.8, day 2)', '(upper fence: 0.9, day 2)', '(max: 0.9, day 2)' - ], - name: ['radishes', '', '', '', '', '', ''], - isRotated: true - }, { - desc: 'hovering over single pt on horizontal violin should not rotate labels', - mock: require('../../image/mocks/violin_old-faithful.json'), - patch: function(fig) { - fig.data[0].x = fig.data[0].y; - delete fig.data[0].y; - fig.layout = { - yaxis: {range: [-0.696, 0.5]} - }; - return fig; + { + desc: 'multiple horizontal violins (under hovermode:closest)', + mock: require('../../image/mocks/box_grouped_horz.json'), + patch: function (fig) { + fig.data.forEach(function (t) { + t.type = 'violin'; + t.hoveron = 'violins'; + }); + fig.layout.violinmode = 'group'; + return fig; + }, + pos: [200, 175], + nums: [ + '(median: 0.7, day 2)', + '(min: 0.2, day 2)', + '(lower fence: 0.2, day 2)', + '(q1: 0.5, day 2)', + '(q3: 0.8, day 2)', + '(upper fence: 0.9, day 2)', + '(max: 0.9, day 2)' + ], + name: ['radishes', '', '', '', '', '', ''], + isRotated: true }, - pos: [539, 293], - nums: '(96, Old Faithful)', - name: '', - isRotated: false - }, { - desc: 'orientation:h | hovermode:y', - mock: require('../../image/mocks/violin_grouped_horz-multicategory.json'), - patch: function(fig) { - // don't hover on kde, to avoid local vs CI discrepancies - fig.data.forEach(function(t) { - t.hoveron = 'violins'; - }); - fig.layout.hovermode = 'y'; - return fig; + { + desc: 'hovering over single pt on horizontal violin should not rotate labels', + mock: require('../../image/mocks/violin_old-faithful.json'), + patch: function (fig) { + fig.data[0].x = fig.data[0].y; + delete fig.data[0].y; + fig.layout = { + yaxis: { range: [-0.696, 0.5] } + }; + return fig; + }, + pos: [539, 293], + nums: '(96, Old Faithful)', + name: '', + isRotated: false }, - pos: [430, 130], - nums: ['upper fence: 0.9', 'lower fence: 0.1', 'max: 0.9', 'min: 0.1', 'q1: 0.2', 'q3: 0.7', 'median: 0.4'], - name: ['', '', '', '', '', '', 'kale'], - axis: '2018 - day 2', - }, { - desc: 'orientation:h | hovermode:closest', - mock: require('../../image/mocks/violin_grouped_horz-multicategory.json'), - patch: function(fig) { - // don't hover on kde, to avoid local vs CI discrepancies - fig.data.forEach(function(t) { - t.hoveron = 'violins'; - }); - return fig; + { + desc: 'orientation:h | hovermode:y', + mock: require('../../image/mocks/violin_grouped_horz-multicategory.json'), + patch: function (fig) { + // don't hover on kde, to avoid local vs CI discrepancies + fig.data.forEach(function (t) { + t.hoveron = 'violins'; + }); + fig.layout.hovermode = 'y'; + return fig; + }, + pos: [430, 130], + nums: ['upper fence: 0.9', 'lower fence: 0.1', 'max: 0.9', 'min: 0.1', 'q1: 0.2', 'q3: 0.7', 'median: 0.4'], + name: ['', '', '', '', '', '', 'kale'], + axis: '2018 - day 2' }, - pos: [430, 130], - nums: [ - '(upper fence: 0.9, 2018 - day 2)', '(lower fence: 0.1, 2018 - day 2)', - '(max: 0.9, 2018 - day 2)', '(min: 0.1, 2018 - day 2)', - '(q1: 0.2, 2018 - day 2)', '(q3: 0.7, 2018 - day 2)', - '(median: 0.4, 2018 - day 2)' - ], - name: ['', '', '', '', '', '', 'kale'] - }, { - desc: 'on points with numeric positions | orientation:h | hovermode:closest', - mock: { - data: [{ - type: 'violin', - points: 'all', - jitter: 0, - orientation: 'h', - y: [2, 2, 2, 2, 2], - x: [13.1, 14.2, 14, 13, 13.3] - }] + { + desc: 'orientation:h | hovermode:closest', + mock: require('../../image/mocks/violin_grouped_horz-multicategory.json'), + patch: function (fig) { + // don't hover on kde, to avoid local vs CI discrepancies + fig.data.forEach(function (t) { + t.hoveron = 'violins'; + }); + return fig; + }, + pos: [430, 130], + nums: [ + '(upper fence: 0.9, 2018 - day 2)', + '(lower fence: 0.1, 2018 - day 2)', + '(max: 0.9, 2018 - day 2)', + '(min: 0.1, 2018 - day 2)', + '(q1: 0.2, 2018 - day 2)', + '(q3: 0.7, 2018 - day 2)', + '(median: 0.4, 2018 - day 2)' + ], + name: ['', '', '', '', '', '', 'kale'] }, - pos: [417, 309], - nums: '(14, 2)', - name: '' - }, { - desc: 'with hovertemplate for points', - patch: function(fig) { - fig.data.forEach(function(trace) { - trace.points = 'all'; - trace.hoveron = 'points'; - trace.hovertemplate = 'Sample pt %{pointNumber}: %{y:.3f}'; - }); - return fig; + { + desc: 'on points with numeric positions | orientation:h | hovermode:closest', + mock: { + data: [ + { + type: 'violin', + points: 'all', + jitter: 0, + orientation: 'h', + y: [2, 2, 2, 2, 2], + x: [13.1, 14.2, 14, 13, 13.3] + } + ] + }, + pos: [417, 309], + nums: '(14, 2)', + name: '' }, - pos: [220, 200], - nums: 'Sample pt 3: 0.900', - name: '' - }] - .forEach(function(specs) { - it('should generate correct hover labels ' + specs.desc, function(done) { + { + desc: 'with hovertemplate for points', + patch: function (fig) { + fig.data.forEach(function (trace) { + trace.points = 'all'; + trace.hoveron = 'points'; + trace.hovertemplate = 'Sample pt %{pointNumber}: %{y:.3f}'; + }); + return fig; + }, + pos: [220, 200], + nums: 'Sample pt 3: 0.900', + name: '' + } + ].forEach(function (specs) { + it('should generate correct hover labels ' + specs.desc, function (done) { run(specs).then(done, done.fail); }); }); - describe('KDE lines inside violin under *kde* hoveron flag', function() { + describe('KDE lines inside violin under *kde* hoveron flag', function () { var fig; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); fig = Lib.extendDeep({}, require('../../image/mocks/violin_old-faithful.json'), { - layout: {width: 500, height: 500} + layout: { width: 500, height: 500 } }); fig.data[0].points = false; }); @@ -797,42 +912,45 @@ describe('Test violin hover:', function() { expect(line.size()).toBe(1, 'only one violin line at a time'); expect(line.attr('class').indexOf('violinline')).toBe(0, 'correct class name'); - expect([ - line.attr('x1'), line.attr('y1'), - line.attr('x2'), line.attr('y2') - ]).toBeCloseToArray(pos, 'line position'); + expect([line.attr('x1'), line.attr('y1'), line.attr('x2'), line.attr('y2')]).toBeCloseToArray( + pos, + 'line position' + ); } - it('should show in two-sided base case', function(done) { - Plotly.newPlot(gd, fig).then(function() { - mouseEvent('mousemove', 250, 250); - assertViolinHoverLine([299.35, 250, 200.65, 250]); - }) - .then(done, done.fail); + it('should show in two-sided base case', function (done) { + Plotly.newPlot(gd, fig) + .then(function () { + mouseEvent('mousemove', 250, 250); + assertViolinHoverLine([299.35, 250, 200.65, 250]); + }) + .then(done, done.fail); }); - it('should show in one-sided positive case', function(done) { + it('should show in one-sided positive case', function (done) { fig.data[0].side = 'positive'; - Plotly.newPlot(gd, fig).then(function() { - mouseEvent('mousemove', 300, 250); - assertViolinHoverLine([277.3609, 250, 80, 250]); - }) - .then(done, done.fail); + Plotly.newPlot(gd, fig) + .then(function () { + mouseEvent('mousemove', 300, 250); + assertViolinHoverLine([277.3609, 250, 80, 250]); + }) + .then(done, done.fail); }); - it('should show in one-sided negative case', function(done) { + it('should show in one-sided negative case', function (done) { fig.data[0].side = 'negative'; - Plotly.newPlot(gd, fig).then(function() { - mouseEvent('mousemove', 200, 250); - assertViolinHoverLine([222.6391, 250, 420, 250]); - }) - .then(done, done.fail); + Plotly.newPlot(gd, fig) + .then(function () { + mouseEvent('mousemove', 200, 250); + assertViolinHoverLine([222.6391, 250, 420, 250]); + }) + .then(done, done.fail); }); }); - it('labels should avoid overlaps', function(done) { + it('labels should avoid overlaps', function (done) { gd = createGraphDiv(); var fig = Lib.extendDeep({}, require('../../image/mocks/violin_zoomed-in.json')); @@ -840,50 +958,51 @@ describe('Test violin hover:', function() { fig.layout.height = 450; Plotly.newPlot(gd, fig) - .then(function() { - mouseEvent('mousemove', 350, 225); - - var actual = []; - d3SelectAll('g.hovertext').each(function() { - var bbox = this.getBoundingClientRect(); - var tx = d3Select(this).text(); - actual.push([tx, bbox]); - }); + .then(function () { + mouseEvent('mousemove', 350, 225); - actual = actual.sort(function(a, b) { return a[1].top - b[1].top; }); + var actual = []; + d3SelectAll('g.hovertext').each(function () { + var bbox = this.getBoundingClientRect(); + var tx = d3Select(this).text(); + actual.push([tx, bbox]); + }); + + actual = actual.sort(function (a, b) { + return a[1].top - b[1].top; + }); - expect(actual.length).toBe(9, '# of value hover labels'); + expect(actual.length).toBe(9, '# of value hover labels'); - for(var i = 0; i < actual.length - 1; i++) { - var a = actual[i]; - var b = actual[i + 1]; - if(b[1].top < a[1].bottom) { - fail('Labels ' + a[0] + ' and ' + b[1] + ' overlap.'); + for (var i = 0; i < actual.length - 1; i++) { + var a = actual[i]; + var b = actual[i + 1]; + if (b[1].top < a[1].bottom) { + fail('Labels ' + a[0] + ' and ' + b[1] + ' overlap.'); + } } - } - }) - .then(done, done.fail); + }) + .then(done, done.fail); }); }); -describe('Test violin restyle:', function() { +describe('Test violin restyle:', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); }); afterEach(destroyGraphDiv); - it('should be able to add/remove innner parts', function(done) { + it('should be able to add/remove innner parts', function (done) { var fig = Lib.extendDeep({}, require('../../image/mocks/violin_old-faithful.json')); // start with just 1 violin delete fig.data[0].meanline; delete fig.data[0].points; function _assertOne(msg, exp, trace3, k, query) { - expect(trace3.selectAll(query).size()) - .toBe(exp[k] || 0, k + ' - ' + msg); + expect(trace3.selectAll(query).size()).toBe(exp[k] || 0, k + ' - ' + msg); } function _assert(msg, exp) { @@ -896,29 +1015,39 @@ describe('Test violin restyle:', function() { } Plotly.newPlot(gd, fig) - .then(function() { - _assert('base', {violinCnt: 1}); - }) - .then(function() { return Plotly.restyle(gd, 'box.visible', true); }) - .then(function() { - _assert('with inner box', {violinCnt: 1, boxCnt: 1}); - }) - .then(function() { return Plotly.restyle(gd, 'meanline.visible', true); }) - .then(function() { - _assert('with inner box & meanline', {violinCnt: 1, boxCnt: 1, meanlineInBoxCnt: 1}); - }) - .then(function() { return Plotly.restyle(gd, 'box.visible', false); }) - .then(function() { - _assert('with meanline', {violinCnt: 1, meanlineOutOfBoxCnt: 1}); - }) - .then(function() { return Plotly.restyle(gd, 'points', 'all'); }) - .then(function() { - _assert('with meanline & pts', {violinCnt: 1, meanlineOutOfBoxCnt: 1, ptsCnt: 272}); - }) - .then(function() { return Plotly.restyle(gd, 'meanline.visible', false); }) - .then(function() { - _assert('with pts', {violinCnt: 1, ptsCnt: 272}); - }) - .then(done, done.fail); + .then(function () { + _assert('base', { violinCnt: 1 }); + }) + .then(function () { + return Plotly.restyle(gd, 'box.visible', true); + }) + .then(function () { + _assert('with inner box', { violinCnt: 1, boxCnt: 1 }); + }) + .then(function () { + return Plotly.restyle(gd, 'meanline.visible', true); + }) + .then(function () { + _assert('with inner box & meanline', { violinCnt: 1, boxCnt: 1, meanlineInBoxCnt: 1 }); + }) + .then(function () { + return Plotly.restyle(gd, 'box.visible', false); + }) + .then(function () { + _assert('with meanline', { violinCnt: 1, meanlineOutOfBoxCnt: 1 }); + }) + .then(function () { + return Plotly.restyle(gd, 'points', 'all'); + }) + .then(function () { + _assert('with meanline & pts', { violinCnt: 1, meanlineOutOfBoxCnt: 1, ptsCnt: 272 }); + }) + .then(function () { + return Plotly.restyle(gd, 'meanline.visible', false); + }) + .then(function () { + _assert('with pts', { violinCnt: 1, ptsCnt: 272 }); + }) + .then(done, done.fail); }); }); From f56d1e0d67da375c2652b0d5f5be2007b0144832 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 27 May 2026 17:59:37 -0600 Subject: [PATCH 2/4] Set _hasPreCompStats correctly for non-box (violin) traces --- src/traces/box/defaults.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index a7ea6ce01bb..29fcab945f4 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -97,6 +97,10 @@ function handleSampleDefaults(traceIn, traceOut, coerce, layout) { traceOut._hasPreCompStats = q1 && q1.length && median && median.length && q3 && q3.length; sLen = Math.min(Lib.minRowLength(q1), Lib.minRowLength(median), Lib.minRowLength(q3)); + } else { + // Non-box (violin) traces don't support pre-computed stats; set explicitly + // so Plotly.react's relinkPrivateKeys can't carry over a stale truthy value. + traceOut._hasPreCompStats = false; } var yDims = getDims(y); From 86dd1a56bc5b89552e9a970500d8a3900d81ab90 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 27 May 2026 18:08:00 -0600 Subject: [PATCH 3/4] Add test --- test/jasmine/tests/violin_test.js | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/jasmine/tests/violin_test.js b/test/jasmine/tests/violin_test.js index 2e0589af7b0..d52fad49d38 100644 --- a/test/jasmine/tests/violin_test.js +++ b/test/jasmine/tests/violin_test.js @@ -1051,3 +1051,38 @@ describe('Test violin restyle:', function () { .then(done, done.fail); }); }); + +describe('Test violin react:', () => { + let gd; + + beforeEach(() => (gd = createGraphDiv())); + + afterEach(destroyGraphDiv); + + // Regression test for https://github.com/plotly/plotly.js/issues/7791 + it('should not inherit _hasPreCompStats when swapping a pre-comp-stats box to a violin', async () => { + const box = { + type: 'box', + q1: [0.2], + median: [0.5], + q3: [0.8], + lowerfence: [-2.5], + upperfence: [2.7], + boxpoints: false + }; + const violin = { + type: 'violin', + y: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + box: { visible: true }, + meanline: { visible: true } + }; + + await Plotly.newPlot(gd, [box]); + expect(gd._fullData[0].type).toBe('box'); + expect(gd._fullData[0]._hasPreCompStats).toBeTruthy(); + + await Plotly.react(gd, [violin]); + expect(gd._fullData[0].type).toBe('violin'); + expect(gd._fullData[0]._hasPreCompStats).toBe(false); + }); +}); From 2875585e135fad4e5082ae3cdcb43e42978ad2db Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 27 May 2026 19:07:00 -0600 Subject: [PATCH 4/4] Add draftlog --- draftlogs/7811_fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/7811_fix.md diff --git a/draftlogs/7811_fix.md b/draftlogs/7811_fix.md new file mode 100644 index 00000000000..00fd7d413e8 --- /dev/null +++ b/draftlogs/7811_fix.md @@ -0,0 +1 @@ +- Update box plot defaults to handle situation where `Plotly.react` is called to switch from box to violin plot [[#7811](https://github.com/plotly/plotly.js/pull/7811)]