Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option for rounded corners on bar charts #6761

Merged
merged 81 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
024f05f
add basic cornerradius for bar charts
emilykl Oct 18, 2023
31c3ac2
update plot-schema
emilykl Oct 18, 2023
b0c6258
add layout.barcornerradius property
emilykl Oct 31, 2023
d71fe49
handle negative bars
emilykl Nov 2, 2023
bb78c78
formatting
emilykl Nov 2, 2023
5dfd686
round corners of legend icon if bar is rounded
emilykl Nov 8, 2023
64912d4
change the way layout.barcornerradius is handled
emilykl Nov 8, 2023
cc1e6ff
rename mock
emilykl Nov 8, 2023
dc2c54c
handle reversed x and y axes
emilykl Nov 9, 2023
3b725fa
cleanup and simplify bar path logic for rounded corners
emilykl Nov 22, 2023
870d4c9
don't round corners of bars which are not top of stack
emilykl Nov 22, 2023
aa8ba1e
fix outmost bar logic
emilykl Nov 27, 2023
5845e0e
extend rounding to lower bars
emilykl Dec 15, 2023
85f47c6
remove 0 default for bar.marker.cornerradius
emilykl Dec 30, 2023
578712a
only set helper values for rounded corners if needed
emilykl Dec 30, 2023
8f77cde
better pct string handling
emilykl Dec 30, 2023
45689ae
remove cornerradius attr from barpolar and funnel
emilykl Dec 30, 2023
137e185
fix rounding for grouped barmode
emilykl Dec 30, 2023
795235b
handle stacked traces with different cornerradius
emilykl Jan 2, 2024
dd717b6
handle label text position inside bars
emilykl Jan 5, 2024
35e0c8f
fix function name
emilykl Jan 6, 2024
7aa7496
test label position for cornerradius in mock
emilykl Jan 6, 2024
250bf31
adjust cornerradius standardization for stacked bars
emilykl Jan 6, 2024
495d950
update plot-schema
emilykl Jan 6, 2024
be0ddff
don't coerce marker.cornerradius
emilykl Jan 6, 2024
819e8bf
add draftlog
emilykl Jan 6, 2024
ed69422
syntax
emilykl Jan 6, 2024
4a44291
syntax
emilykl Jan 6, 2024
65d3b55
add image baseline
emilykl Jan 6, 2024
e8cdde1
update draftlog
emilykl Jan 6, 2024
9c4227e
fix rounding for relative barmode
emilykl Jan 6, 2024
dabcaf0
fix NaN bug
emilykl Jan 6, 2024
701fd8c
fix bug with stacking in group mode
emilykl Jan 6, 2024
29589e8
coerce marker.cornerradius for histograms
emilykl Jan 8, 2024
d0b9a90
fix histogram2d
emilykl Jan 8, 2024
3e5a957
fix behavior for negative bars in stacked barmode
emilykl Jan 8, 2024
582ce13
fix behavior for negative bars in stacked barmode (2)
emilykl Jan 8, 2024
a238c87
Cast radius parameter to number
emilykl Jan 9, 2024
ac329f7
validate cornerradius value in supply defaults
emilykl Jan 9, 2024
8a607e6
export validateCornerradius
emilykl Jan 9, 2024
338b4e6
Change cornerradius editType to calc
emilykl Jan 9, 2024
60c918e
Change barcornerradius editType to calc
emilykl Jan 9, 2024
9dd0896
better handling of text placement on rounded horizontal bars
emilykl Jan 9, 2024
1628b5c
avoid using Math.sign
emilykl Jan 10, 2024
bfbd397
lint
emilykl Jan 10, 2024
71e4896
update plot-schema
emilykl Jan 10, 2024
4e794d0
bugfix
emilykl Jan 11, 2024
dedce2a
bugfix
emilykl Jan 11, 2024
b137b25
store cornerradius in t instead of trace; general cleanup
emilykl Jan 11, 2024
fdd6d03
small cleanup and add comments
emilykl Jan 17, 2024
8fcc3a5
Merge branch 'master' into rounded-bars
emilykl Jan 17, 2024
71b53c3
update baseline
emilykl Jan 17, 2024
90cf5c3
add text to zz-bar-rounded-corners
archmoj Jan 19, 2024
510c66b
rounded-bar display_height_zero line_width
archmoj Jan 19, 2024
5599ea6
round bar_gantt-chart
archmoj Jan 19, 2024
8dffbbe
round hist_cum_stacked
archmoj Jan 19, 2024
c165f6a
round histogram_colorscale
archmoj Jan 19, 2024
ee2451a
round histogram-offsetgroups
archmoj Jan 19, 2024
256a87e
round period_positioning9
archmoj Jan 19, 2024
b122521
round bar_stackto100_negative
archmoj Jan 19, 2024
015b881
round bar_attrs_relative
archmoj Jan 19, 2024
8b57fb5
round worldcup
archmoj Jan 19, 2024
ec44d87
bugfix
emilykl Jan 19, 2024
2913c6e
text position DRAFT
emilykl Jan 24, 2024
9e3c249
update baselines
archmoj Jan 24, 2024
f986349
fix text padding for rounded bars
emilykl Jan 26, 2024
99352d0
update baselines
archmoj Jan 26, 2024
6f685c0
fix text scale and pad for rounded bars
emilykl Jan 26, 2024
f093ee7
fix text position for stacked rounded bars
emilykl Jan 30, 2024
e3cd5a5
simplify logic in cross_trace_calc
emilykl Jan 30, 2024
98356b4
update baselines
emilykl Jan 30, 2024
a2befea
fix variable declaration
emilykl Jan 30, 2024
781fbe2
remove lxFunc and lyFunc
emilykl Jan 31, 2024
97f0b0e
use absolute arcs for bar path
emilykl Feb 1, 2024
c52890c
simplify text placement logic
emilykl Feb 1, 2024
10e38f4
Update docstring
emilykl Feb 1, 2024
f7d4b4a
Update docstring
emilykl Feb 1, 2024
4335ba0
more permissive text inside/outside logic for rounded bars
emilykl Feb 1, 2024
e9043e9
Merge branch 'rounded-bars' of https://github.com/plotly/plotly.js in…
emilykl Feb 1, 2024
4624132
updated baselines
archmoj Feb 1, 2024
8c45d15
update schema
archmoj Feb 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/6761_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add `layout.barcornerradius` and `trace.marker.cornerradius` properties to support rounding the corners of bar traces [[#6761](https://github.com/plotly/plotly.js/pull/6761)], with thanks to [Displayr](https://www.displayr.com) for sponsoring development!
7 changes: 6 additions & 1 deletion src/components/legend/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,14 +334,19 @@ module.exports = function style(s, gd, legend) {
var marker = trace.marker || {};
var markerLine = marker.line || {};

// If bar has rounded corners, round corners of legend icon
var pathStr = marker.cornerradius ?
'M6,3a3,3,0,0,1-3,3H-3a3,3,0,0,1-3-3V-3a3,3,0,0,1,3-3H3a3,3,0,0,1,3,3Z' : // Square with rounded corners
'M6,6H-6V-6H6Z'; // Normal square

var isVisible = (!desiredType) ? Registry.traceIs(trace, 'bar') :
(trace.visible && trace.type === desiredType);

var barpath = d3.select(lThis).select('g.legendpoints')
.selectAll('path.legend' + desiredType)
.data(isVisible ? [d] : []);
barpath.enter().append('path').classed('legend' + desiredType, true)
.attr('d', 'M6,6H-6V-6H6Z')
.attr('d', pathStr)
.attr('transform', centerTransform);
barpath.exit().remove();

Expand Down
11 changes: 10 additions & 1 deletion src/traces/bar/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,16 @@ var marker = extendFlat({
editType: 'style',
description: 'Sets the opacity of the bars.'
},
pattern: pattern
pattern: pattern,
cornerradius: {
valType: 'any',
editType: 'calc',
description: [
'Sets the rounding of corners. May be an integer number of pixels,',
'or a percentage of bar width (as a string ending in %). Defaults to `layout.barcornerradius`.',
'In stack or relative barmode, the first trace to set cornerradius is used for the whole stack.'
].join(' ')
},
});

module.exports = {
Expand Down
93 changes: 92 additions & 1 deletion src/traces/bar/cross_trace_calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) {
else excluded.push(calcTrace);
}

// If any trace in `included` has a cornerradius, set cornerradius of all bars
// in `included` to match the first trace which has a cornerradius
standardizeCornerradius(included);

if(included.length) {
setGroupPositionsInStackOrRelativeMode(gd, pa, sa, included, opts);
}
Expand All @@ -119,10 +123,57 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) {
}
break;
}

setCornerradius(calcTraces);
collectExtents(calcTraces, pa);
}

// Set cornerradiusvalue and cornerradiusform in calcTraces[0].t
function setCornerradius(calcTraces) {
var i, calcTrace, fullTrace, t, cr, crValue, crForm;

for(i = 0; i < calcTraces.length; i++) {
calcTrace = calcTraces[i];
fullTrace = calcTrace[0].trace;
t = calcTrace[0].t;

if(t.cornerradiusvalue === undefined) {
cr = fullTrace.marker ? fullTrace.marker.cornerradius : undefined;
if(cr !== undefined) {
crValue = isNumeric(cr) ? +cr : +cr.slice(0, -1);
crForm = isNumeric(cr) ? 'px' : '%';
t.cornerradiusvalue = crValue;
t.cornerradiusform = crForm;
}
}
}
}

// Make sure all traces in a stack use the same cornerradius
function standardizeCornerradius(calcTraces) {
if(calcTraces.length < 2) return;
archmoj marked this conversation as resolved.
Show resolved Hide resolved
var i, calcTrace, fullTrace, t;
var cr, crValue, crForm;
for(i = 0; i < calcTraces.length; i++) {
calcTrace = calcTraces[i];
fullTrace = calcTrace[0].trace;
cr = fullTrace.marker ? fullTrace.marker.cornerradius : undefined;
if(cr !== undefined) break;
}
// If any trace has cornerradius, store first cornerradius
// in calcTrace[0].t so that all traces in stack use same cornerradius
if(cr !== undefined) {
crValue = isNumeric(cr) ? +cr : +cr.slice(0, -1);
crForm = isNumeric(cr) ? 'px' : '%';
for(i = 0; i < calcTraces.length; i++) {
calcTrace = calcTraces[i];
t = calcTrace[0].t;

t.cornerradiusvalue = crValue;
t.cornerradiusform = crForm;
}
}
}

function initBase(sa, calcTraces) {
var i, j;

Expand Down Expand Up @@ -713,6 +764,23 @@ function normalizeBars(sa, sieve, opts) {
}
}

// Add an `_sMin` and `_sMax` value for each bar representing the min and max size value
// across all bars sharing the same position as that bar. These values are used for rounded
// bar corners, to carry rounding down to lower bars in the stack as needed.
function setHelperValuesForRoundedCorners(calcTraces, sMinByPos, sMaxByPos, pa) {
var pLetter = getAxisLetter(pa);
// Set `_sMin` and `_sMax` value for each bar
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only need these when having bars with rounded corners.
This function is pretty slow and it should not be called when it is not really needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@archmoj Yes good point. Would it make sense then to check whether any of the traces in calcTraces have a cornerradius value, and skip this function if not?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's a good idea.

for(var i = 0; i < calcTraces.length; i++) {
var calcTrace = calcTraces[i];
for(var j = 0; j < calcTrace.length; j++) {
var bar = calcTrace[j];
var pos = bar[pLetter];
bar._sMin = sMinByPos[pos];
bar._sMax = sMaxByPos[pos];
}
}
}

// find the full position span of bars at each position
// for use by hover, to ensure labels move in if bars are
// narrower than the space they're in.
Expand Down Expand Up @@ -745,6 +813,18 @@ function collectExtents(calcTraces, pa) {
return String(Math.round(roundFactor * (p - pMin)));
};

// Find min and max size axis extent for each position
// This is used for rounded bar corners, to carry rounding
// down to lower bars in the case of stacked bars
var sMinByPos = {};
var sMaxByPos = {};

// Check whether any trace has rounded corners
var anyTraceHasCornerradius = calcTraces.some(function(x) {
var trace = x[0].trace;
return 'marker' in trace && trace.marker.cornerradius;
});

for(i = 0; i < calcTraces.length; i++) {
cd = calcTraces[i];
cd[0].t.extents = extents;
Expand All @@ -770,8 +850,19 @@ function collectExtents(calcTraces, pa) {
di.p1 = di.p0 + di.w;
di.s0 = di.b;
di.s1 = di.s0 + di.s;

if(anyTraceHasCornerradius) {
var sMin = Math.min(di.s0, di.s1) || 0;
var sMax = Math.max(di.s0, di.s1) || 0;
var pos = di[pLetter];
sMinByPos[pos] = (pos in sMinByPos) ? Math.min(sMinByPos[pos], sMin) : sMin;
sMaxByPos[pos] = (pos in sMaxByPos) ? Math.max(sMaxByPos[pos], sMax) : sMax;
}
}
}
if(anyTraceHasCornerradius) {
setHelperValuesForRoundedCorners(calcTraces, sMinByPos, sMaxByPos, pa);
}
}

function getAxisLetter(ax) {
Expand Down
48 changes: 39 additions & 9 deletions src/traces/bar/defaults.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

var isNumeric = require('fast-isnumeric');

var Lib = require('../../lib');
var Color = require('../../components/color');
var Registry = require('../../registry');
Expand Down Expand Up @@ -47,7 +49,6 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
});

handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout);

var lineColor = (traceOut.marker.line || {}).color;

// override defaultColor for error bars with defaultLine
Expand All @@ -61,22 +62,50 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
function crossTraceDefaults(fullData, fullLayout) {
var traceIn, traceOut;

function coerce(attr) {
return Lib.coerce(traceOut._input, traceOut, attributes, attr);
function coerce(attr, dflt) {
return Lib.coerce(traceOut._input, traceOut, attributes, attr, dflt);
}

if(fullLayout.barmode === 'group') {
for(var i = 0; i < fullData.length; i++) {
traceOut = fullData[i];
for(var i = 0; i < fullData.length; i++) {
traceOut = fullData[i];

if(traceOut.type === 'bar') {
traceIn = traceOut._input;
// `marker.cornerradius` needs to be coerced here rather than in handleStyleDefaults()
// because it needs to happen after `layout.barcornerradius` has been coerced
var r = coerce('marker.cornerradius', fullLayout.barcornerradius);
if(traceOut.marker) {
traceOut.marker.cornerradius = validateCornerradius(r);
}

if(traceOut.type === 'bar') {
traceIn = traceOut._input;
if(fullLayout.barmode === 'group') {
handleGroupingDefaults(traceIn, traceOut, fullLayout, coerce);
}
}
}
}

// Returns a value equivalent to the given cornerradius value, if valid;
// otherwise returns`undefined`.
// Valid cornerradius values must be either:
// - a numeric value (string or number) >= 0, or
// - a string consisting of a number >= 0 followed by a % sign
// If the given cornerradius value is a numeric string, it will be converted
// to a number.
function validateCornerradius(r) {
if(isNumeric(r)) {
r = +r;
if(r >= 0) return r;
} else if(typeof r === 'string') {
r = r.trim();
if(r.slice(-1) === '%' && isNumeric(r.slice(0, -1))) {
r = +r.slice(0, -1);
if(r >= 0) return r + '%';
}
}
return undefined;
}

function handleText(traceIn, traceOut, layout, coerce, textposition, opts) {
opts = opts || {};
var moduleHasSelected = !(opts.moduleHasSelected === false);
Expand Down Expand Up @@ -133,5 +162,6 @@ function handleText(traceIn, traceOut, layout, coerce, textposition, opts) {
module.exports = {
supplyDefaults: supplyDefaults,
crossTraceDefaults: crossTraceDefaults,
handleText: handleText
handleText: handleText,
validateCornerradius: validateCornerradius,
};
10 changes: 9 additions & 1 deletion src/traces/bar/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,13 @@ module.exports = {
'Sets the gap (in plot fraction) between bars of',
'the same location coordinate.'
].join(' ')
}
},
barcornerradius: {
valType: 'any',
editType: 'calc',
description: [
'Sets the rounding of bar corners. May be an integer number of pixels,',
'or a percentage of bar width (as a string ending in %).'
].join(' ')
},
};
4 changes: 4 additions & 0 deletions src/traces/bar/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ var Axes = require('../../plots/cartesian/axes');
var Lib = require('../../lib');

var layoutAttributes = require('./layout_attributes');
var validateCornerradius = require('./defaults').validateCornerradius;


module.exports = function(layoutIn, layoutOut, fullData) {
function coerce(attr, dflt) {
Expand Down Expand Up @@ -47,4 +49,6 @@ module.exports = function(layoutIn, layoutOut, fullData) {

coerce('bargap', (shouldBeGapless && !gappedAnyway) ? 0 : 0.2);
coerce('bargroupgap');
var r = coerce('barcornerradius');
layoutOut.barcornerradius = validateCornerradius(r);
};
Loading