Skip to content

Implement attributes base, offset and width in bar traces (issue 80) #3

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

Closed
wants to merge 31 commits into from

Conversation

n-riesco
Copy link
Owner

@etpinard @alexcjohnson

This PR implements attributes base, offset and width as discussed in plotly#80 .

I've tested the PR mostly by playing with mock images, and I'm yet to commit the jasmine tests.

Although I've been careful to maintain the original behaviour of all barmodes, I agree with Étienne, here, that more jasmine tests are needed to avoid any regressions.

@etpinard Are there any cases in particular that you'd like to see tested?


* Simplified algorithm to identify overlapping bars.

* Removed the closure setBarCenter.

* Checked that jasmine tests still pass.
* Converted the closure that groups vertical and horizontal bars into a
  function.

* This change will help split setPositions into functions for each
  barmode.
* Moved code to group bars from setGroupPositions to setPositions.
* Converted closure `barpositions` into function `setOffsetAndWidth`.

* This change will help split setPositions into functions for each
  barmode.
* Don't assume the position axis is `xa` in the calculation of
  `barDelta` in `bar/hover.js`.

* Updated `tests/bar_test.js` to account for the replacement of `t.bar`
  with `t.bargroupwidth`.

* Updated the group case in `tests/bar_test.js` to test the use of
  `layout.bargroupgap`.
* Refactored the code to set bar positions in overlay mode into function
  `setGroupPositionsInOverlayMode`.
* Refactored the code for setting bar positions into
  setGroupPositionsInOverlayMode,
  setGroupPositionsInGroupMode and
  setGroupPositionsInStackOrRelativeMode.

* Refactored code for stacking bars and computing minDiff into helper
  class Sieve.
* Renamed setBaseAndSize to stackBars.

* Refactor code to normalize bars into function normalizeBars.
* Allow minDtick update if barmode is group and bars overlap.
* Function sieveBars sieves all the bars without updating the bar bases.
  This step is required before calling normalizeBars.

* Function stackBars sieves all the bars and updates the bar bases and
  tops accordingly.
@etpinard
Copy link

Are there any cases in particular that you'd like to see tested?

Making sure that the bar calc / set-position steps have 100% test coverage would be a great start. Unfortunately, we haven't hooked in a test coverage tool in our jasmine runner yet, so this process will be somewhat manual. My apologies.


Thanks again for making a PR onto your fork first. 🎉 This should make the commit history cleaner in the main plotly.js repo. I'll give your PR a first pass now.

// set size
for(i = 0; i < cd.length; i++) {
if(isNumeric(size[i])) {
cd[i].s = size[i] - cd[i].b;
Copy link

@etpinard etpinard Oct 21, 2016

Choose a reason for hiding this comment

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

⛔ We'll have to guard against non-numeric base items when base is set to an array.

In brief, in the supply-defaults step, scalar base inputs are coerce to numbers, but the supply-defaults step is designed to never penetrate array values, so non-numeric array items have to be handled in the calc step.

Choose a reason for hiding this comment

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

Oh and by the way, half-valid array inputs are an example of test cases that should included in our jasmine suites.

@@ -21,7 +21,8 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
xa = pointData.xa,
ya = pointData.ya,
barDelta = (hovermode === 'closest') ?
t.barwidth / 2 : t.dbar * (1 - xa._gd._fullLayout.bargap) / 2,
t.barwidth / 2 :
t.bargroupwidth,

Choose a reason for hiding this comment

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

nice simplification. 👍

x0 = xa.c2p(p0, true);
x1 = xa.c2p(p1, true);
y0 = ya.c2p(s0, true);
y1 = ya.c2p(s1, true);

Choose a reason for hiding this comment

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

very clean here. 🍻

dontMergeOverlappingData = !barnorm;

// update position axis and set bar offsets and widths
calcTraces.forEach(function(calcTrace) {

Choose a reason for hiding this comment

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

let's rewrite this as a for-loop for speed 🐎

"data":[
{
"base":6,
"y":[1,2,3,4],
Copy link

@etpinard etpinard Oct 21, 2016

Choose a reason for hiding this comment

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

Oof. That took me a while to understand what's going on here. But, I think you got it right.

base always refers to the start of the bars and y always refers to their end positions - even though the bar base can be above the bar y coordinate.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Correct!

Choose a reason for hiding this comment

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

wait, doesn't y normally refer to the bar's size, not its end position?

Choose a reason for hiding this comment

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

Yeah, I really think it should be size. y is the primary data "value" attribute (for vertical bars) and for a bar chart, the bar's value is its size, where it starts or ends is a secondary level of analysis.

Copy link
Owner Author

@n-riesco n-riesco Oct 21, 2016

Choose a reason for hiding this comment

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

@alexcjohnson I think making y mean size would be confusing, because I'd expect the values in y and the ticks in yaxis to be the same.

I think you're right. The thing that made me think of y as end positions was that we store the end position in calcdata[i].y.

valType: 'any',
arrayOk: true,
role: 'info',
description: [

Choose a reason for hiding this comment

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

To make the fact that these attributes are optional, we usually set the dflt: null (e.g. contour trace start, end, size).

Note that, null are ignored upon Lib.nestedProperty(/**/).set(), so your fullTrace.base === undefined will still work.

calcTrace = calcTraces[i];
fullTrace = calcTrace[0].trace;

if(fullTrace.offset === undefined) included.push(calcTrace);

Choose a reason for hiding this comment

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

nice pattern 👍

@etpinard
Copy link

etpinard commented Oct 21, 2016

Thanks again @n-riesco for making the bar set-position step very nicely readable.

Apart from my comment about guarding against non-valid base, offset and width array items, I didn't catch any 🔴 flags.

So, before making a PR to the main plotly.js repo:

@alexcjohnson
Copy link

(for vertical bars)

reminds me we should have one test image with horizontal bars and these attributes.

* Null defaults are used to denote optional properties.

* Note that null values are ignored by `Lib.nestedProperty(/**/).set()`,
  so that the pattern `fullTrace.base === undefined` still works.
* Added note to explain the reason why setGroupPositionsInOverlayMode
  handles the case barnorm is defined.
* Commit bar tests before implementing the commit that interprets trace
  data as sizes. This should help ensure the implementation is thorough.
* Updated bar tests that set the base attribute, so that now trace data
  is interpreted as sizes.
* Update bar_attrs_* mock images to account for trace data being
  interpreted as sizes.

* bar_attrs_group.json: updated y to store sizes instead of bar tops.

* bar_attrs_group_norm.json: updated base and y, so that it's clear that
  this mock tests the use of base and normalisation.

* bar_attrs_overlay.json: updated y to store sizes instead of bar tops.
* Converted bar_attrs_group_norm into a horizontal bar plot.
* Updated baseline, because now this mock is a horizontal bar plot.
@n-riesco
Copy link
Owner Author

@etpinard @alexcjohnson I've addressed all the feedback. This PR is ready for review again.

//
// (note that `setGroupPositionsInOverlayMode` handles the case barnorm
// is defined, because this function is also invoked for traces that
// can't be grouped or stacked)

Choose a reason for hiding this comment

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

thanks!

@alexcjohnson
Copy link

💃 from my side - thanks!

});
});

describe('Bar.calc', function() {

Choose a reason for hiding this comment

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

Very nice.

Thanks very much!

@@ -348,6 +358,11 @@ function setOffsetAndWidthInGroupMode(gd, pa, sieve) {
// if width is an array, then clone it into t.barwidth.
var newBarwidth = width.slice(0, calcTrace.length);

// guard against non-numeric items
for(j = 0; j < newBarwidth.length; j++) {

Choose a reason for hiding this comment

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

@n-riesco is there a way to place this for-loop in a place common to setOffsetAndWidthInGroupMode and setOffsetAndWidth ?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Good catch! Since offset is undefined for all the traces in group mode, both functions can share the same implementation.

vpad = pMaxWidth / 2;
Axes.expand(pa, [pMax + pMaxOffset + vpad], {vpad: vpad});
}
Axes.expand(pa, [pMin, pMax], {padded: false});

Choose a reason for hiding this comment

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

@n-riesco would you mind sharing some info here about why this patch was needed? Thanks.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Before having offset and width, the range of the position axis was computed using the position data x padded with vpad = minDiff/2 (minDiff being the maximum size of a bar group).

With offset and width, bars can be shifted away from their positions x and widths can be larger than minDiff. Here I'm iterating each bar and computing their left and right sides (taking into account offset and width). This helps me compute the pMin and pMax needed to expand the position axis, so that all bars are fully within the range of the position axis.

I will add a comment in the code to explain this.

@@ -566,7 +566,7 @@ function normalizeBars(gd, sa, sieve) {

if(!isNumeric(bar.s)) continue;

var scale = Math.abs(sTop / sieve.get(bar.p, bar.b + bar.s));
var scale = Math.abs(sTop / sieve.get(bar.p, bar.s));

Choose a reason for hiding this comment

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

glad to see if was that easy to make the switch 👍

"type":"bar"
}, {
"base":[7,6,5,4],
"x":[1,2,3,4],

Choose a reason for hiding this comment

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

very nice mock!

}, {
"base":[0,1,3,2],
"width":1,
"offset":[1.5],

Choose a reason for hiding this comment

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

very nice here, offset[0] is only applied to the first bar.

@etpinard
Copy link

Awesome @n-riesco 👍

My two comments #3 (comment) and #3 (comment) are non-blocking.

After rebasing the test commits into one for all mock / baselines additions and one for all jasmine updates / additions, PR away to main repo!

@n-riesco n-riesco closed this Oct 27, 2016
@n-riesco n-riesco deleted the 20161017-issue-80-bar-base-offset-width branch October 27, 2016 16:13
@alexcjohnson
Copy link

@AdnanBoota we got the message, it's not helpful to post this question more than once.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants