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 'tickformatstops' #1965

Merged
merged 17 commits into from
Oct 16, 2017
Merged

add 'tickformatstops' #1965

merged 17 commits into from
Oct 16, 2017

Conversation

apalchys
Copy link
Contributor

Related to #1946

This PR adds tickformatstops axis config attribute. The purpose of new attribute is to help customize formatting of axis ticks by dtick value aka "zoom level". The following types of axis support tickformatstops:

  • date
  • linear

Usage example:

tickformatstops:
[
    {
        dtickrange: [null, 1000],
        value: "%H:%M:%S.%L"
    },
    {
        dtickrange: [1000, 60000],
        value: "%H:%M:%S"
    }
]

dtickrange should be specified using 2-value array with dtick values.
Also it is possible to use null as value if start/end of range is not defined. In this case it will be interpred as absolute mix/max value.
The value should be valid tickformat string

I will add tests later if the implementation looks good.

Question to discuss:

M1 is specified in ranges #1 and #2 and the code will apply a format string associated with range #2.
Is it ok?

  • Do we need to support log axis?

@etpinard
Copy link
Contributor

Looking solid so far! Thanks very much @apalchys 🎉

If ranges overlaps, the code takes max applicable range

At first glance, that sounds right to me. 👍

Do we need to support log axis

Yes please!

I will add tests later if the implementation looks good.

😏

@@ -18,7 +18,7 @@ module.exports = transformTools.makeRequireTransform('requireTransform',
var pathOut;

if(pathIn === 'd3' && opts.file !== pathToStrictD3Module) {
pathOut = 'require(\'' + pathToStrictD3Module + '\')';
pathOut = 'require(\'' + pathToStrictD3Module.replace(/\\/g, '/') + '\')';
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need it for windows users. I've updated the code and added comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks!

@@ -448,6 +448,18 @@ module.exports = {
'*%H~%M~%S.%2f* would display *09~15~23.46*'
].join(' ')
},
tickformatstops: {
valType: 'any',
Copy link
Contributor

Choose a reason for hiding this comment

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

We can do better here.

See for example the layout.slider declaration here. The steps array container is declared using the ✨ special ✨ _isLinkedToArray.

So, this here should look like:

tickformatstops: {
  _isLinkedToArray: 'tickformatstop',

  dtickrange: {
    valType: 'info_array',
    // ... declared just like 'range' above
  },
  values: {
     valType: 'any', 
     // ...
  }
}

@@ -40,6 +40,7 @@ module.exports = function handleTickLabelDefaults(containerIn, containerOut, coe

if(axType !== 'category') {
var tickFormat = coerce('tickformat');
coerce('tickformatstops');
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not category?

Copy link
Contributor

Choose a reason for hiding this comment

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

... also, you'll need to a little more work coercing tickformatstop after having addressed https://github.com/plotly/plotly.js/pull/1965/files#r134489590 - see the layout.slider defaults for an example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why not category?

Since dtick is always 1 for category axis type (at least all my tests show it), I still have no idea how tickformatstops should work for it 🤔 Maybe you have any thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, dtickrange wouldn't make sense for category axes. I'm thinking for category axes each tickformatstop item would have:

{
  ticktext: [/* */],
  tickvals: [/* */]
}

just like regular axis-level tickvals and ticktext offering a solution for #1812

The range of each tickformatstop is determined by the tickvals[0] and tickvals[1].

My apologies, I thought I had written that down somewhere before 😏

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No problem, but not sure that I got everything right :)
Let's confirm expected behaviour. For example:

I have the following categories:
['a', 'b', 'c', 'd', 'e']
https://codepen.io/anon/pen/brQwjG

then I define tickformatstops:

tickformatstops: [{
  ticktext: ['a-c'],
  tickvals: ['a', 'c']
}, {
  ticktext: ['d-e'],
  tickvals: ['d', 'e']
}]

and as result I get only 2 ticks:
['a-c', 'd-e']

Is it correct?

@talgalili

Choose a reason for hiding this comment

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

Hi @apalchys
This seems reasonable.
Question: does this need to be defined manually, or could it be automatically extracted from the values? For example, if we have 300 categories, that there would be a value setting to show only 30 categories at most. And if we are at a zoom level with many of the categories, it would spread them across the axis.
(I apologies if what I say makes little sense in this context. I more often deal with higher level implementations)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@talgalili I see what you say but not sure how to fit it into the proposed API. I am quite new in Plotly community.
@etpinard maybe you have any idea?
Also is my assumption above correct? If yes, I can start the implementation.

Choose a reason for hiding this comment

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

@apalchys checking about this further with @cpsievert - it appears that what I'm looking for is

the ability to have something like nticks to be respected when tickmode='array'

(see the discussion here: talgalili/heatmaply#78 (comment) )

Is there a way the current work would be extended to deal with this?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll try to think some more about this issue at some point this week. Sorry for the wait.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@etpinard have you had a chance to look into it?

@talgalili
Copy link

Thanks @apalchys - I'm very excited about this feature being added.

@apalchys
Copy link
Contributor Author

Added support for log axis

_isLinkedToArray: 'tickformatstop',

dtickrange: {
valType: 'data_array',
Copy link
Contributor

Choose a reason for hiding this comment

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

info_array please.

Copy link
Contributor

Choose a reason for hiding this comment

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

... just like range

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed, thanks.

coerce('dtickrange');
coerce('value');

valuesOut.push(valueOut);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nicely done. Thanks!

🎉

@@ -265,6 +265,26 @@ module.exports = {
'*%H~%M~%S.%2f* would display *09~15~23.46*'
].join(' ')
},
tickformatstops: {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind DRYing this up by adding

var axesAttrs = require('../../plots/cartesian/layout_attributes')

// ....

     tickformatstops: axesAtttrs.tickformatstops, 

// ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, thanks.

@apalchys
Copy link
Contributor Author

added tests

@etpinard
Copy link
Contributor

added tests

Great. Are you planning on addressing #1965 (comment) ?

@apalchys
Copy link
Contributor Author

apalchys commented Aug 28, 2017

Great. Are you planning on addressing #1965 (comment) ?

Yep. I will try to fix everything by tomorrow.

@apalchys
Copy link
Contributor Author

apalchys commented Sep 5, 2017

moving discussion to the root level from #1965 (comment)

the ability to have something like nticks to be respected when tickmode='array'

It seems I am stuck here and not sure what to do next :)
After re-reading issues, it seems we needs something like this:

[{
    deltarange:  [40, 30]
    nticks: 50
},
{
    deltarange:  [800, 1200]
    nticks: 5
}]

where deltarange is a range of delta between min and max current range values (https://github.com/plotly/plotly.js/blob/master/src/plots/cartesian/axes.js#L268). In this case we can configure to display fewer ticks if we zoom out.

Also for me adding a special logic for category axis to tickformatstops (where we use dticks) looks a little bit tricky. Maybe it makes sense to move it to a separate property?

@talgalili
Copy link

@apalchys who (in this repo) do you think could help you solve this?
(i.e.: to gain "the ability to have something like nticks to be respected when tickmode='array' ")

@apalchys
Copy link
Contributor Author

apalchys commented Sep 5, 2017

@talgalili not sure I know the answer to your question. it is my first PR and @etpinard is my first reviewer.

@alexcjohnson
Copy link
Collaborator

@apalchys apologies for letting this languish. I'm looking at the open pieces now. In merging master it looks like the issue you encountered with strict_d3 was also solved independently by #1994 - looked to me as though they both would work fine, I elected to keep #1994 but with your added comment. Does it still work correctly for you?

@alexcjohnson
Copy link
Collaborator

If ranges overlaps, the code takes max applicable range

I would actually prefer to take the first matching entry. Seems to me this is easier for users to parse, easier and more flexible to generate (you can start from the lowest and specify [null, max] for each of the dtickrange settings if you want, or start from the top and specify [min, null]) and more efficient in our code (we can bail out of the loop at the first match).

@alexcjohnson
Copy link
Collaborator

TBH I'm not sure what the goal is with adding category axes here.

  • category axes DO use dtick (see eg category_dtick_3 that has enough categories we don't show them all, we automatically drop to every third one)
  • category axes DO NOT use tickformat - what would it do? Categories are already explicit strings. so tickformatstops also seems to me not to apply.
  • tickvals and ticktext can be used with any axis type, but it would frankly be kind of weird to use them with category, why wouldn't you just use the label you want as the category? They're normally for putting specific ticks on other axis types.

@talgalili mentioned:

the ability to have something like nticks to be respected when tickmode='array'

That to me seems like a separate issue from what we're dealing with here. This PR is about how to format the ticks, not about which ticks to show. There's another long-neglected PR #303 that may be what you're interested in for this purpose: setting a minimum pixel distance between ticks. I think that's probably the only way you can solve this for the tickmode='array' case: nothing is guaranteed about their spacing otherwise so solutions like "display every nth tick" wouldn't work, but "iterate through the array and display each tick iff it's more than n pixels away from all the others we've already drawn" would work. @talgalili do you want to take this part of the discussion over there and see if we can revive that PR? Plotly staff may not be able to get to it any time soon though, but maybe someone else wants to pitch in...

return getRangeWidth(stop.dtickrange, convertToMs) > getRangeWidth(acc.dtickrange, convertToMs) ? stop : acc;
}
}, null);
tickstop = ax.tickformatstops.find(function(stop) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice and clean - unfortunately it doesn't look like IE supports Array.find, not even in IE11
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
We've also tended to gravitate toward for loops over Array methods for performance reasons. Not a big deal here as this array will be fairly small, but it should still help.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry, just switched from the project where is supported and don't care about IE11 support.
Replaced with for loop and fixed tests.

@apalchys
Copy link
Contributor Author

I elected to keep #1994 but with your added comment. Does it still work correctly for you?

Yes, it does. Thanks.

I would actually prefer to take the first matching entry

Updated

@alexcjohnson
Copy link
Collaborator

@apalchys great, thanks for the changes! I linted and updated the baseline image for the new "first" logic. I think this is ready to go (assuming tests pass this time - ignore appveyor) but we're going to wait a few days to merge to master, so we can give v1.31 any bugfixes it needs before adding features (which will go in v1.32.0)

},
value: {
valType: 'string',
dflt: '',
Copy link
Contributor

Choose a reason for hiding this comment

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

Is axis.tickformat: '' a valid format? More precisely, could

tickformatstop: [{
  dtickrange: [/* */],
  value: ''
}]

be used to hide tick labels for some dtick range?

Copy link
Collaborator

Choose a reason for hiding this comment

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

no, tickformat: '' gives our default formatting. I suppose we could imagine making our own "hide" tickformat (for date axes you can already hack this with tickformat: ' ' but that doesn't work for numbers) but it seems a bit of a kludgy way to handle a very esoteric edge case.


var mock = require('@mocks/tickformatstops.json');

function getZoomInButton(gd) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we merge this file into axes_test.js? I don't see the need to break src file / test file symmetry here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

done in 0e6747f

@etpinard
Copy link
Contributor

1.31.1 is out, 💃

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

Successfully merging this pull request may close these issues.

5 participants