plotly / plotly.js

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

Support specifying a function for tickformat #1464

Closed manderson23 closed 7 years ago

manderson23 commented 7 years ago

It would be useful if a function could be specified as the tickformat to give complete control.

For example, I would like to use the multiFormat function from https://github.com/d3/d3-time-format/blob/master/README.md to format dates.

etpinard commented 7 years ago

No, this won't happen. The plotly.js data / layout API is meant to be JSON-serializable - so functions as values are strictly forbidden.

Sorry for the inconvenience.

bputt commented 6 years ago

@etpinard What are your ideas on something like the following? This would allow you to still support JSON, but allow people who need customization to specify the name of a function to reference.

In my case, I'm looking to convert bytes and milliseconds to human-readable format. Also, I am looking into the tickvals and ticktext, but the y-axis shows all of the values and while I could limit the values coming back, I'd prefer to have all of them to enable zooming.

// tickFormat could be a string or object
tickFormat: {
    type: 'function',
    function: {
        className: 'window',
        name: 'nameOfAFunction'
    }
}
etpinard commented 6 years ago

@bputt I'm afraid that won't do hit. Guaranteeing JSON-serializability for our "data" / "layout" attribute isn't just for fun. We do so to guarantee portability across applications. Here, by referring to a function on the window object, you'd would have to copy that function from application to application to generate the same graph.

bputt commented 6 years ago

@etpinard I understand why you chose JSON and appreciate portability, just trying to think through a problem and finding solutions.

Let's say I want to take bytes and make it human-readable, so if I provided ALL of the tickVals that are used on the y-axis and then incorporated translation using the ticktext field, how can I make sure that there isn't a line per tickval on the y-axis? I could set a max number of tickVals to limit the number of lines, but lose visibility when zooming.

var tickVals = [ 10424, 2195452, 925952, 1521212, ... ];

{
    yaxis: {
        tickvals: tickVals,
        ticktext: tickVals.map(function(val){ return Math.round(val / 1024) + 'Kb'; })
    }
}
kochelmonster commented 6 years ago

@etpinard Well I am not sure if json compatibility is such a high value, that avoids a useful functionality (Infact it would not be a big deal to implement). But anyway I want to suggest a (coffeescript) solution for @bputt:

# monkey patch d3 locale to support a new "milliseconds" number format
org_locale = Plotly.d3.locale

Plotly.d3.locale = (locale) ->
  result = org_locale(locale)
  org_number_format = result.numberFormat

  twodigts = org_number_format("02d")
  threedigts = org_number_format("03d")

  result.numberFormat = (format) ->
    return org_number_format(format) if format != "milliseconds"
    return (miliseconds) ->
      seconds = miliseconds // 1000
      miliseconds %= 1000
      minutes = seconds // 60
      seconds %= 60
      hours = minutes // 60
      minutes %= 60

      miliseconds = threedigts(miliseconds)
      seconds = twodigts(seconds)
      minutes = twodigts(minutes)
      return "#{hours}:#{minutes}:#{seconds}#{locale.decimal}#{miliseconds}"\
        if hours
      return "#{minutes}:#{seconds}#{locale.decimal}#{miliseconds}"

  # there is also result.timeFormat, that can be patched
  return result

If you set tickformat to "milliseconds" you get the new format.

etpinard commented 6 years ago

Well I am not sure if json compatibility is such a high value

Sorry. JSON-compatibility is probably the most important thing about plotly.js. That's how as a company we extend plotly.js to python, R and many more languages, not to mention saving graphs in the cloud on https://plot.ly/. So yeah, there's a 0% chance of us breaking that assumption.

kochelmonster commented 6 years ago

Just a matter of thought: You will never reach full customization by a pure declarative model, but there will be always special cases, users want to implement. Instead of just rejecting you could say: There is the canonical way to do it, which is compatible to other languages. And there is a custom way for special cases, that will never be saved in a cloud but makes users happy. Anyway it is your library (and it is well implemented), and you can decide which strategy to follow.

Leprosy commented 6 years ago

I've done this...a bit ugly, but solved the issue for me because I needed to add a suffix:

        // PATCH: extend d3.format to allow a string suffix
        var oldFormat = Plotly.d3.format;

        Plotly.d3.format = function(str) {
            var data = str.split("###");

            return function(val) {
                return oldFormat(str)(val) + " " + data[1];
            }
        }

Then, in the hoverformat, anything after "###" is treated as a suffix. This way, you can express the format like .2###[unit] and the hover text will say "2.00 [unit]"

vdh commented 5 years ago

@etpinard While it's commendable that data should remain JSON-serializable, chaining the layout (i.e. the rendering) to the same standard is perhaps taking portability a little too far into the extreme.

But nevertheless, working within that restriction, what about an API to customise the format function? Something not a part of data and layout.

For example, something like:

const d3Format = require('d3-format');
Plotly.tickFormat = function tickFormat(specifier) {
  if (specifier === 'specialSauce') { return specialSauceFormatter; }
  return d3Format(specifier);
};

In theory this API could be used across all languages, if someone wanted to implement their specialSauce formatter in each language for maximum JSON layout compatability.

slowkow commented 5 years ago

@vdh Could I please ask if you might be able to provide more hints for how to get your suggestion to work?

I tried to copy your code and then use this, but it did not work:

var layout = {xaxis: {tickformat: 'specialSauce'}}

The tick labels appear unchanged, as if tickformat was left as the default value.

slowkow commented 5 years ago

Following in the footsteps of @kochelmonster, this is what worked for me:

// Somewhere in the global scope, overwrite the original locale function:

var org_locale = Plotly.d3.locale;
Plotly.d3.locale = (locale) => {
  var result = org_locale(locale);
  var org_number_format = result.numberFormat;
  result.numberFormat = (format) => {
    if (format != 'pow2') {
      return org_number_format(format)
    }
    return (x) => {
      return Math.pow(2, Number.parseFloat(x)).toString();
    }
  }
  return result;
}

// When specifying tickformat, now we can use the string we chose:

var layout = {xaxis: {tickformat: 'pow2'}}

I wanted to convert the tick labels to be 2^x instead of x, and this worked.

vdh commented 5 years ago

@slowkow It was only an API suggestion example, I didn't actually implement it.

OmriAroesti commented 3 years ago

@slowkow Odd, that doesn't seem to have any effect for me. even adding a console.log to the callback for testing if the overwrite is called, and it does not. So apparently in my case, Plotly.d3.locale is not the one containing the numberFormat method, but when I debugged the code, it did go to d3_locale_numberFormat, instead. So maybe the overwrite was done too late?

thegreencouch commented 2 years ago

@slowkow d3.locale seems to be not a part of Plotly.js anymore. Neither is d3_locale_numberFormat. Can you suggest an alternate way to achieve the same result?

froddd commented 2 years ago

Not being able to provide a custom formatter is a bit of a blocker: in our use case we need the ticks along the Y axis to automatically adjust to trillion/billion/million, and show the full word for the unit accordingly. Using @slowkow's example above works great with v1.58.5, but this means we'll most likely soon be stuck with an unsupported version.

Could the Plotly.d3 object at least be exposed as an option, so custom locales can be created?

sergei3000 commented 5 months ago

Hi all, is there any chance this will be implemented?