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

Pad domain addition? #150

Open
mhkeller opened this issue Oct 20, 2018 · 15 comments · May be fixed by #213
Open

Pad domain addition? #150

mhkeller opened this issue Oct 20, 2018 · 15 comments · May be fixed by #213

Comments

@mhkeller
Copy link

mhkeller commented Oct 20, 2018

When I'm making scatter plots (and others), I find myself padding the scale's domain by the pixel radius of my circles to avoid them overlapping with the y-axis. I specify the number of pixels I want to increase on the left and right side of the domain, use the scale's inverse to add or subtract that amount from the domain and use that result to reset it.

I made a convenience function to do this and was curious if you would want a PR if this seems like useful functionality.

The api could be something like:

scaleLinear()
  .domain([0, 100])
  .range([0, 200])
  .padDomain([10, 10])
// new domain is [-5, 105]

[edited for better order of operations in the API example]

@jheer
Copy link
Member

jheer commented Feb 1, 2019

In case it is helpful, we implemented this functionality in Vega for various scale types (symlog coming soon):

@mbostock
Copy link
Member

mbostock commented Dec 3, 2019

I like this idea—I needed something similar in this recent scatterplot. I ended up doing a little transformation of the domain:

function padLinear([x0, x1], k) {
  const dx = (x1 - x0) * k / 2;
  return [x0 - dx, x1 + dx];
}
y = d3.scaleLinear()
    .domain(padLinear(d3.extent(data.flatMap(d => [d.R90_10_1980, d.R90_10_2015])), 0.1))
    .rangeRound([height - margin.bottom, margin.top])

I can see it being nice to specify the padding in pixel space rather than as a proportion, though.

@wetlandscapes
Copy link

wetlandscapes commented Jan 17, 2020

+1 for this -- both in pixel space and as a proportion. It would also be nice to be able to pad asymmetrically.

@mhkeller
Copy link
Author

I'll take a crack at this as a PR for d3-scale using the Vega setup as a jumping off point. It will need to be modified to pad the min and the max separately.

I think this would be good to include as a part of d3-scale instead of building into outside libraries since there's not a simple way to distinguish between all the different types of scales and what transform function you'd need to use.

@jimousse
Copy link

+1! adding a padding in pixels to scales could be very useful.
Any update on this feature?
Is that something that will be added to d3 scales in v5?

@curran
Copy link
Contributor

curran commented Jun 17, 2020

IMO it might be nice to just expose that padLinear function by @mbostock as part of d3-scale.

This one:

export function padLinear([x0, x1], k) {
  const dx = (x1 - x0) * k / 2;
  return [x0 - dx, x1 + dx];
}

It's nice that it is isolated, not required to be a method on scales.

For the pixels case, k can be computed externally as pixels / width, so maybe no need to add any pixel-specific API.

This would set a precedent and open the door for subtle implementations for other scale types, like padLog, padPow. I wonder, though, if these would require access to the scale's range as well... Perhaps not? I see some inroads for those have been made here: https://github.com/vega/vega/blob/master/packages/vega-util/src/transform.js#L66

@curran curran linked a pull request Jun 17, 2020 that will close this issue
@curran
Copy link
Contributor

curran commented Jun 17, 2020

I started a PR for padLinear: #213

Please let me know if this looks like a good direction. With some encouragement I'm happy to flesh out the PR with README updates - but first I want to confirm this direction is acceptable. Thanks for reviewing!

@Fil
Copy link
Member

Fil commented Jun 17, 2020

Related for map projections: d3/d3-geo#162 —in which the padding is named "inset" and not divided by 2.

@mhkeller
Copy link
Author

My original implementation was a bit off but this is now properly implemented in LayerCake with the Vega transform helpers so that it works on log and power scales, too. You supply the padding you want in pixels for either the min or the max.

I most likely won't have the bandwidth to do a d3-scale PR for this but someone should feel free to if they want.

@curran
Copy link
Contributor

curran commented Nov 24, 2020

Very cool! If anyone wants to branch from my PR #213 and add the implementation for log and power scales, go for it!

@curran
Copy link
Contributor

curran commented Feb 13, 2021

Stepping back a bit, could the same fundamental problem be solved instead by drawing the axes differently? For example, offsetting the tick labels, and drawing grid/domain lines such that they are slightly removed from the points that fall exactly on the min/max of the domain? Maybe with a technique like that this whole problem could be sidestepped.

@EoinGriffin-AI
Copy link

EoinGriffin-AI commented Mar 30, 2021

EDIT Nevermind, I think I figured it out. Will post it here for anyone who needs to pad a logarithmic scale with a specific amount of pixels.

const axisHeight = 1000; // pixels
const padding = 20; // pixels
const [minDomain, maxDomain] = domain;

const logMin = Math.log(minDomain);
const logMax = Math.log(maxDomain);
const logDifference = logMax - logMin;
const axisHeightMinusPadding = axisHeight - (padding * 2); // 20px to both ends of the axis
const logPadding = logDifference / axisHeightMinusPadding  * padding;

const paddedDomain = [
  Math.exp(logMin - logPadding),
  Math.exp(logMax + logPadding)
];

Can someone shed light on the math of adding padding to a logarithmic scale? 😕
Let's say I have a log-scale Y-axis, with domain [minDomain, maxDomain], and the total height in pixels is 1000. How can I adjust the domain values to add 20px of padding to the top and bottom of the axis?

@mhkeller
Copy link
Author

mhkeller commented May 7, 2021

Stepping back a bit, could the same fundamental problem be solved instead by drawing the axes differently? For example, offsetting the tick labels, and drawing grid/domain lines such that they are slightly removed from the points that fall exactly on the min/max of the domain? Maybe with a technique like that this whole problem could be sidestepped.

I run into the original problem when I have a scatter plot where I'm sizing the radius of the circle by a value in the data. To handle dots of varying diameter, I'm not sure if there's a way around calculating how much space the largest circle needs in order to not overlap with the axis lines – since it's not just about the points that fall exactly on the line. The issue is having that buffer space in the design.

@curran
Copy link
Contributor

curran commented May 7, 2021

Here's a quick demo of how one might move the axes to make space for the marks.

Before:

image

        svg
          .append('g')
          .attr('transform', `translate(${margin.left},0)`)
          .call(axisLeft(y));

        svg
          .append('g')
          .attr(
            'transform',
            `translate(0,${height - margin.bottom})`
          )
          .call(axisBottom(x));

After:

image

      const axisPadding = 7;
...
        svg
          .append('g')
          .attr('transform', `translate(${margin.left - axisPadding},0)`)
          .call(axisLeft(y));

        svg
          .append('g')
          .attr(
            'transform',
            `translate(0,${height - margin.bottom + axisPadding})`
          )
          .call(axisBottom(x));

@mhkeller
Copy link
Author

mhkeller commented May 7, 2021

I think that could definitely work for some folks – great to have put it together! I know for my use case, it may be a little tricky if you have different charts on a page with different y-axis alignments, especially if you end up with a radius that's quite bit, such as >25 pixels. I was able to sort this out as post-scale transformation so whatever you think is best from a library perspective is fine.

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

Successfully merging a pull request may close this issue.

8 participants