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

can the standalone Plot.legend returned object have .scale to get the scale Descriptor? #624

Closed
tx0c opened this issue Dec 20, 2021 · 5 comments · Fixed by #639
Closed

Comments

@tx0c
Copy link

tx0c commented Dec 20, 2021

sometimes it's better to create a standalone legend (with Plot.legend), not from any pre-existing plot, and use htl.html ... with css to organize better layout,

from https://observablehq.com/@observablehq/plot-legends?collection=@observablehq/plot I'm playing around a bit:

image

you see the sportsPlot (returned value from Plot.plot) is an HTMLElement with .scale and .legend

HTMLElement {scale: ƒ(e), legend: ƒ(n, r)}

but if I give it a name to wx = Plot.legend({...}) to whatever value returned from Plot.legend, it has nothing other than an empty SVGSVGElement {}

wx = Plot.legend({
  color: {
    // interpolate: (t) => wavelengthToColor(400 + t * 350),
    domain: [400, 750]
  },
  width: 350,
  ticks: 10,
  label: "Wavelength (nm) →"
})

then what's an easy way to get the scale object? and where to get d3-scale function from it?

image

I'm expecting it can have a way to return the scale object, something like sportsScale in the example,

> lgd.scale("color")
Object {
  type: "ordinal",
  domain: Array(4) ["A", "B", "H", "I", "J", ...],
  range: Array(10) ["#1f77b4", "#ff7f0e", ...],
  label: "...",
  apply: ƒ(e),
}

current workaround may be to use fakePlot = Plot.plot(...) create a fake plot first, and get the scale object from ... = fakePlot.scale(...), and then the fakePlot is not needed and wasted

@Fil
Copy link
Contributor

Fil commented Dec 20, 2021

It's true that Plot uses d3-scale internally, but we don't expose the object on purpose, for two reasons:

  • we don't want it to be mutable
  • a d3 scale object doesn't have enough information to feed back into Plot (in particular, we don't know from a d3-scale if its domain has been set or not).

However the scales descriptions which are exposed contain enough information if one needs to recreate them in d3-scale, and, maybe more importantly, two methods (apply and invert) that can be used directly on data.

Exposing the scale on the Plot.legend sounds like a good idea! PR in #625.

@tx0c
Copy link
Author

tx0c commented Dec 20, 2021

👍

good, I've just see it called a scale Descriptor I meant the same by scale object not a d3-scale function,

btw, another way I've tried, is to create a d3-scale function first, then need to pass a scale Descriptor for Plot.legend

const scaleColor = d3.scaleOrdinal()
  .domain(["A", "B", "C", ...])
  .range(d3.schemeCategorial...)

but didn't find a convenient way to do so:

lgd = Plot.legend({
  color: {
    type: "ordinal",                  // does a d3-scale function have a way to infer a type for plot?
    domain: scaleColor.domain(),
    range:  scaleColor.range(),
    apply: scaleColor,
    // ... infer more options from the d3-scale function, like clamping, ... etc
  },
})

can there be a feature request for Plot.legend to take a d3-scale function if already have one?

even got the scale Descriptor it has .apply but then it's still not a d3-scale function, .invert is still missing; and probably many other features from a d3-scale function; the .invert will be necessary to reverse lookup for e.g. mouse interactivity design

is there some way (or plan) to use a d3-scale function and plot scale Descriptor more interchangeably ?

@tx0c tx0c changed the title can the standalone Plot.legend returned object have .scale get the scale object? can the standalone Plot.legend returned object have .scale to get the scale Descriptor? Dec 20, 2021
@Fil
Copy link
Contributor

Fil commented Dec 20, 2021

can there be a feature request for Plot.legend to take a d3-scale function if already have one?

If the scale is categorical or linear, we can pass scale.domain() and scale.range(); if it's non-linear, we can generally pass it as an interpolator and ticks. However if we wanted to cover all the supported scales, there would be quite a lot of work to do. (Just to work on the color legend we have more than a hundred commits, and dozens of unit tests.)

This is not something we want to support (and document) officially, but (exercise for the reader…) it is probably possible to fork https://observablehq.com/@d3/color-legend so that it returns a useful scale descriptor instead of a legend.

The scale descriptor has no invert for ordinal scales, since ordinal scales have no invert. This is something that might be added in some cases, but it's a separate issue.

@tx0c
Copy link
Author

tx0c commented Dec 22, 2021

then can still request the internal d3-scale function be exposed somehow?

under discussion #611 the actual wanted is a mid point of (xScale(currentMonth) + xScale(nextMonth)) / 2; but just because the xScale is linear, we can do a mid month calculation instead, it equals to xScale((currentMonth + nextMonth) / 2)

x: d => (+d3.utcMonth.floor(d["date_of_birth"]) + +d3.utcMonth.ceil(1 + +d["date_of_birth"])) / 2

but, when xScale is not linear, the calculation is not that simple, and specific to different scale, e.g. d3.scaleSqrt would be Math.sqrt(current * next) to get mid-point,

can I suggest all marks not only accept an object, but also accept a function signature like:

Plot.rect({ ... })

Plot.rect((...arguments) => ({ ... }))

Plot.rect((index, scales, channels, dimensions, axises) => ({
  x: ... // can use scale.x; the d3-scale function for any calculation...
})) // from current internal `render` API

@mbostock
Copy link
Member

then can still request the internal d3-scale function be exposed somehow?

We’re not going to expose the D3 scale object, but we do expose an apply function that calls the scale (and likewise an invert function that calls scale.invert). So, you can call that and it’s equivalent to calling the underlying D3 scale. We’re just not exposing the scale’s getter and setter methods.

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