plotly / plotly.js

Open-source JavaScript charting library behind Plotly and Dash
https://plotly.com/javascript/
MIT License
16.9k stars 1.85k forks source link

Identical axis labels removed in heatmap (need new category mode or axis type) #1516

Closed lvdmaaten closed 3 months ago

lvdmaaten commented 7 years ago

Consider the following plot (as JSON):

{'layout': {u'margin': {u'r': 60, u'b': 60, u't': 60, u'l': 60}}, 'data': [{u'zmax': 1.0054633722786, u'colorscale': u'Viridis', u'zmin': -1.4410649905944, u'y': [u'a', u'b', u'a'], u'x': [u'd', u'd'], u'z': [[-0.50443826056116, -0.55385051210768], [-0.52712315391279, 1.0054633722786], [-1.4410649905944, 0.50761805140936]], u'type': u'heatmap'}]}

Note how there are identical axis labels in the JSON on the x and y-axis. This leads to an incorrect rendering: screen shot 2017-03-24 at 4 09 04 pm

It appears the identical labels get removed automatically.

Additional context here and here.

etpinard commented 7 years ago

Thanks for bringing this up!

I agree that the expected behavior for heatmap traces would have two columns with label 'd'. Unfortunately, this somewhat contradicts our category mapping where each unique category gets assigned one numeric coordinate. For example:

Plotly.plot('graph', [{
  x: ['a', 'b', 'a'],
  y: [1, 2, 3]
}])

generates

image

So I'm thinking maybe we could add a new attribute (e.g. categorymode: 'unique' | 'index') or a new axis type (e.g. xaxis.type: 'category-by-index' or something less verbose :smile: ) to make the distinction between the desired heatmap behavior and the current (scatter) behavior.


In the meantime, you can make those categories unique by inputting x: ['d', 'd ']:

image

lvdmaaten commented 7 years ago

Ah, interesting work-around. I didn't realize it would remove the whitespaces when rendering. Thanks!

cpsievert commented 7 years ago

Another workaround would be to use ticktext/tickvals:

http://codepen.io/cpsievert/pen/bqKJoe

alexcjohnson commented 7 years ago

I didn't realize it would remove the whitespaces when rendering.

I don't think it does remove them... look closely at the positioning, the second d is a little left of center. So I guess you could use ' d ' to make it symmetric (though adding spaces will probably muck up hover text), or use a zero-width space, but @cpsievert has a much better solution.

Unless this problem generalizes substantially to the point that ticktext/tickvals becomes unworkable, I wouldn't be in favor of making a new axis mode or type for it. Too many strange follow-on effects if data<->position isn't a 1:1 mapping. Like what does it mean if you try to position something else on the axis at 'd'? Or if you have a second heatmap, or some scatter data, that you want to plot on top of this? What position value do we report for click or hover events?

etpinard commented 7 years ago

To solve this issue, we could:

I can't think of another possible categorymode value, so I'd vote for adding a uniquecategories boolean. Thoughts?

alexcjohnson commented 7 years ago

I still think it’s a bad idea to have an axis that doesn’t map data one-to-one to position. What if you want to tile several heatmaps? What if you want a scatter or bar trace on the same axis? What if you bind a click or hover handler, how will you know on which copy of the item the event really happened? All of these situations would be easy to manage with ticktext / tickvals but would be ambiguous, and probably in some cases impossible to get right, with a many-to-one mapping.

Perhaps we could consider a mode within the heatmap trace (and potentially in other trace types too) that automatically maps x and y string arrays to tickvals and ticktext on the appropriate axis (with an optional x0 and dx etc so tickvals need not be [0, 1, 2...])? That seems like it would yield the same visual result without the ambiguities.

jdepons commented 7 years ago

I am having the same issue with a bar chart and see this as a major oversight. I just want to print a simple bar chart for a group of clinical measurements. Some animals were measured multiple times and some were not so traces will not work correctly. I should be able to display multiple values with the same x axis label without having 10 white spaces surrounding the 10th measurement for a specific label.

As an example my x axis may look like the following Rat, Rat, Rat, Rat, Rat, Chicken, Squirrel, Chicken.

Any ideas other than the white space fix above are appreciated.

zbjornson commented 7 years ago

@jdepons the method suggested by @cpsievert in https://github.com/plotly/plotly.js/issues/1516#issuecomment-289139760 works better than the white space hack, although it's a little bit tedious.

SaucePan1 commented 6 years ago

Is there any official solution to this? Im generating the x_labels automatically in-app and the whitespace solution is definetely not an option!

Any recomendations?

Dantalion commented 6 years ago

This is actually severe. The priority to fix this should be highest. It would be best if we had a data attribute to set x and y values to unique or not. The functionality without that is highly impacted and holds the potential back at the moment.

alexcjohnson commented 6 years ago

@SaucePan1 @Dantalion have you tried the tickvals/ticktext suggestion @cpsievert mentioned and demonstrated in http://codepen.io/cpsievert/pen/bqKJoe ? I feel pretty strongly that we cannot make an axis type where data do not map 1:1 to position, it just causes unending headaches when you try to do anything beyond a single trace.

One current limitation I see of this approach is that we always show every tick when using tickvals/ticktext. That's a more general problem that has its own issue https://github.com/plotly/plotly.js/issues/1812

RBVI commented 5 years ago

Just to add another voice to this. I've got a heatmap where I'm showing the results of differential expression experiment with 6 clusters. I'm showing the top 10 and bottom 10 genes for each cluster, so the Y: genes, X: clusters. In many cases, the genes overlap. I'll certainly try the ticktext approach, but I might have 100's of genes, so the ability to automatically choose the tick interval is extremely nice...

LeUser111 commented 5 years ago

Hey guys!

A little late to the party - however we've just run into this exact issue with the stacked-bar-chart. We are trying to display the properties of tools that are used in a process. Of course tools can be used multiple times.

Thanks to the solution of @cpsievert it works like a charm! (Thank you very much!).

Here's a simple example where I just mixed the example code for stacked-bar-charts with the solution proposed by @cpsievert :

var trace1 = { 
  y: [20, 14, 23],
  name: 'Primary Processing',
  type: 'bar'
};

var trace2 = { 
  y: [12, 18, 29],
  name: 'Secondary Processing',
  type: 'bar'
};

var data = [trace1, trace2];
var layout = {barmode: 'stack', "xaxis": {
    "ticktext": ["tool1", "tool2", "tool1"], "tickvals": [0, 1, 2]
  }};

Plotly.newPlot("graph", data, layout);

The reason why I'm posting this:

Would you kindly consider adding this information to the official documentation? (e.g. here)

As this issue shows it's not an uncommon requirement and since there is a practical solution for this it would be great if it was described close to the charts themselves - maybe as an additional example (e.g. "bars with multiple occurrences of x-values").

(Sorry if it's already there and I just missed it!)

RBVI commented 4 years ago

Help! With the latest plotly.js (1.51.3) the tickvals/ticktext solution no longer works. If you look at http://codepen.io/cpsievert/pen/bqKJoe you can see that only one category is shown. This is broken some important functionality for our pipeline.

zbjornson commented 4 years ago

@RBVI your tickvals and x/y values need to match up:

var dat = [{
  "x": [0,1],
  "y": [2,3],
  "z": [[1,2], [3,4]],
  "type": "heatmap"
}];

Plotly.plot(
  "graph", dat,
  {
    "xaxis": {
      "ticktext": ["a", "a"], "tickvals": [0, 1]
    },
    "yaxis": {
      "ticktext": ["b", "c"], "tickvals": [2, 3]
    }
  }
);

(don't want to sign up for codepen to save that.)

The original version of this solution posted in https://github.com/plotly/plotly.js/issues/1516#issuecomment-289139760 matches by ticktext. I'm not sure why that worked or if it actually worked reliably.

jackparmer commented 4 years ago

This issue has been tagged with NEEDS SPON$OR

A community PR for this feature would certainly be welcome, but our experience is deeper features like this are difficult to complete without the Plotly maintainers leading the effort.

Sponsorship range: $10k-$15k

What Sponsorship includes:

Please include the link to this issue when contacting us to discuss.

thammegowda commented 2 years ago

If we are going to use a spacer workaround, using zero-width-space[1] would save some space on the screen

function pad_unique(arr, pad='\u200b'){
  let mem = new Set()
  for (let idx = 0; idx < arr.length; idx++) {
    let val = arr[idx]
    while (mem.has(val)) {
      val += pad
    }
    mem.add(val)
    arr[idx] = val
  }
  return arr
}
// call this on x and y arrays to make them unique 
pad_unique(x)
pad_unique(y)

demo https://codepen.io/thammegowda/pen/jOGEqrG


-[1] https://unicode-table.com/en/200B/

gvwilson commented 3 months ago

Hi - this issue has been sitting for a while, so as part of our effort to tidy up our public repositories I'm going to close it. If it's still a concern, we'd be grateful if you could open a new issue (with a short reproducible example if appropriate) so that we can add it to our stack. Cheers - @gvwilson