Scale padding in D3 v 4 - d3.js

How to make non-padding edge of the x-scale? There is .rangeBand() in v3, but I am using D3 v4.
var x = d3.scaleBand().rangeRound([0, width], .05).padding(0.1),
y = d3.scaleLinear().rangeRound([height, 0]);

Short answer:
You can't anymore.
Long answer:
In D3 v4.x there is a paddingOuter method for band scales:
band.paddingOuter([padding]):
If padding is specified, sets the outer padding to the specified value which must be in the range [0, 1]. If padding is not specified, returns the current outer padding which defaults to 0. The outer padding determines the ratio of the range that is reserved for blank space before the first band and after the last band. (emphasis mine)
However, as you can see, that value goes only from 0 to 1.
Here is a demo with zero outer padding:
var w = 500, h = 100;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var scale = d3.scaleBand()
.domain("ABCDEFGHIJKL".split(""))
.range([20, w - 20])
.paddingOuter(0)
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
And here is another with the maximum (1) outer padding:
var w = 500, h = 100;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var scale = d3.scaleBand()
.domain("ABCDEFGHIJKL".split(""))
.range([20, w - 20])
.paddingOuter(1)
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
There is an open issue on GitHub, asking for unlimited outer padding, with a comment from Mike Bostock (D3 creator) in the pull request.
Possible solutions
Of course, you can download D3 v4 and tweak the band scale function, creating your own function, nothing forbids you. If you want to follow that approach, have a look at the pull request linked above, it just removes Math.min(1, _) from the source code.
A way easier solution is this hacky one below: create fake values at the beginning and end of your domain...
.domain(["foo", "bar", "baz"].concat(domain).concat(["foobar", "foobaz", "barbaz"]))
... and ignore them in the axis:
var axis = d3.axisBottom(scale)
.tickValues(domain);
Here is the demo:
var w = 500, h = 100;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var domain = "ABCDEFGHIJKL".split("")
var scale = d3.scaleBand()
.domain(["foo", "bar", "baz"].concat(domain).concat(["foobar", "foobaz", "barbaz"]))
.range([20, w - 20]);
var axis = d3.axisBottom(scale)
.tickValues(domain);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>

Related

What Transformation values to calculate for scale and translate if you want to zoom

Im looking at this example which shows how one can use the zoom functionality to zoom in a specified domain range
https://bl.ocks.org/mbostock/431a331294d2b5ddd33f947cf4c81319
Im confused about this part:
var d0 = new Date(2003, 0, 1),
d1 = new Date(2004, 0, 1);
// Gratuitous intro zoom!
svg.call(zoom).transition()
.duration(1500)
.call(zoom.transform, d3.zoomIdentity
.scale(width / (x(d1) - x(d0))) // I think this is to caulcuate k which is the zoom factor
.translate(-x(d0), 0)); // but what is this?
I'm having trouble understanding the calculations that are done. Correct me if my assumptions are wrong
d3.zoomIdentity This is a transformation that does nothing when applied.
.scale(width / (x(d1) - x(d0))) This is to calculate how much scale to apply by calculating the ratio between the width and the pixel difference between the two data points d0 and d1
.translate(-x(d0), 0)) I don't understand this part. Why is x(d0) negated and how does the x coordinate of d(0) relate to how much translation need to be applied?
The translate value is aligning the graph so that x(d0) is the leftmost x value visible in the plot area. This ensures the visible portion of the plot area extends from d0 through d1 (the visible subdomain). If our full domain for the x scale has a minimum of 0, then x(0) will be shifted left (negative shift) x(d0) pixels.
I'll use a snippet to demonstrate:
var svg = d3.select("svg"),
margin = {top: 10, right: 50, bottom: 70, left: 200},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
// Scale for Data:
var x = d3.scaleLinear()
.range([0, width])
.domain([0,20]);
// Scale for Zoom:
var xZoom = d3.scaleLinear()
.range([0,width])
.domain([0,width]);
var xAxis = d3.axisBottom(x).ticks(5);
var xZoomAxis = d3.axisBottom(xZoom);
var zoom = d3.zoom()
.scaleExtent([1, 32])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// plot area
g.append("rect")
.attr("width",width)
.attr("height",height)
.attr("fill","url(#stripes)");
g.append("text")
.attr("x",width/2)
.attr("y",height/2)
.style("text-anchor","middle")
.text("plot area");
g.append("line")
.attr("y1",0)
.attr("y2",height)
.attr("stroke-width",1)
.attr("stroke","black");
// zoomed plot area:
var rect = g.append("rect")
.attr("width",width)
.attr("height",height)
.attr("fill","lightgrey")
.attr("opacity",0.4);
// Axis for plot:
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Axis for zoom:
g.append("g")
.attr("class", "axis axis-zoom-x")
.attr("transform", "translate(0,"+(height+30)+")")
.call(xZoomAxis);
var text = g.append("text")
.attr("y", height+60)
.attr("text-anchor","middle")
.text("zoom units")
.attr("x",width/2);
// Gratuitous intro zoom:
var d1 = 18;
var d0 = 8;
svg.call(zoom).transition()
.duration(2000)
.call(zoom.transform, d3.zoomIdentity
.scale(width / (x(d1) - x(d0)))
.translate(-x(d0), 0));
function zoomed() {
var t = d3.event.transform, xt = t.rescaleX(x);
xZoom.range([xt(0),xt(20)]);
g.select(".axis--x").call(xAxis.scale(xt));
g.select(".axis-zoom-x").call(xZoomAxis.scale(xZoom));
rect.attr("x", xt(0));
rect.attr("width", xt(20) - xt(0));
text.attr("x", xt(10));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="180">
<defs>
<pattern id="stripes" patternUnits="userSpaceOnUse" width="8" height="8" patternTransform="rotate(45 0 0)">
<rect width="3" height="8" fill="orange"></rect>
</pattern>
</defs>
</svg>
Snippet Explanation:
Plot area: orange stripes
Full scaled extent of data: grey box.
Left hand side of plot area is x=0 (pixels) for the parent g that holds everything.
As we zoom in the bounds of our data exceeds the plot area. We want to show a specific subdomain of our data. We achieve part of that with the scale (as you correctly deduce) but the other portion is with the translate: we push values less than the lowest value of our x subdomain to the left. By pushing the entire graph left by an amount equal to x(d0), x(d0) appears as the leftmost coordinate of the plot area.

d3js: ordinal scale with not unique labels [duplicate]

I'm new to d3 and using it for creating a simple chart using array of numbers where the value '16' appears twice in it.
It generate the chart with one 'missing' 'rect' element for the 2nd '16' value, when I check the html I see that both '16' rect has same 'y' value of 72.
Please tell me what I'm doing wrong, thanks
code:
var data = [4, 8, 15, 16, 23, 16];
var chart = d3.select("body").append("svg")
.attr("class", "chart")
.attr("width", 420)
.attr("height", 20 * data.length);
var x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, 420])
var y = d3.scale.ordinal()
.domain(data)
.rangeBands([0, 120]);
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", y)
.attr("width", x)
.attr("height", y.rangeBand());
The problem with your code is that you are trying to use the values from your data array to create range bands on an ordinal scale. Since the same input value will always be mapped to the same output value that means that both inputs 16 get mapped to the same range band 72.
If you want each input value to be mapped to its own "bar" then you need to use array indices instead of array values.
First you prepare the indices
var indices = d3.range(0, data.length); // [0, 1, 2, ..., data.length-1]
Then you use them to define the y scale domain
var y = d3.scale.ordinal()
.domain(indices)
// use rangeRoundBands instead of rangeBands when your output units
// are pixels (or integers) if you want to avoid empty line artifacts
.rangeRoundBands([0, chartHeight]);
Finally, instead of using array values as inputs use array indices when mapping to y
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", function (value, index) {
// use the index as input instead of value; y(index) instead of y(value)
return y(index);
})
.attr("width", x)
.attr("height", y.rangeBand());
As an added bonus this code will automatically rescale the chart if the amount of data changes or if you decide to change the chart width or height.
Here's a jsFiddle demo: http://jsfiddle.net/q8SBN/1/
Complete code:
var data = [4, 8, 15, 16, 23, 16];
var indices = d3.range(0, data.length);
var chartWidth = 420;
var chartHeight = 120;
var chart = d3.select("body").append("svg")
.attr("class", "chart")
.attr("width", chartWidth)
.attr("height", chartHeight);
var x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, chartWidth])
var y = d3.scale.ordinal()
.domain(indices)
.rangeRoundBands([0, chartHeight]);
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", function (value, index) { return y(index); })
.attr("width", x)
.attr("height", y.rangeBand());
The way you are setting the y attribute of the rectangles will utilize the same value for all duplicate elements. You can use some offsetting like so:
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", function (d, i) {
return (i * y.rangeBand()) + y.rangeBand();})
.attr("width", x)
.attr("height", y.rangeBand());
Also you might have to adjust the height of your overall chart to see all the bands.

D3 v4 - make a horizontal bar chart with fixed width

I have made a horizontal bar chart using d3 v4, which works fine except for one thing. I am not able to make the bar height fixed. I am using bandwidth() currently and if i replace it with a fixed value say (15) the problem is that it does not align with the y axis label/tick http://jsbin.com/gigesi/3/edit?html,css,js,output
var w = 200;
var h = 400;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.attr("transform", "translate(80,30)");
var data = [
{Item: "Item 1", count: "11"},
{Item: "Item 2", count: "14"},
{Item: "Item 3", count: "10"},
{Item: "Item 4", count: "14"}
];
var xScale = d3.scaleLinear()
.rangeRound([0,w])
.domain([0, d3.max(data, function(d) {
return d.count;
})]);
var yScale = d3.scaleBand()
.rangeRound([h,0]).padding(0.2)
.domain(data.map(function(d) {
return d.Item;
}));
var yAxis = d3.axisLeft(yScale);
svg.append('g')
.attr('class','axis')
.call(yAxis);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('width', function(d,i) {
return xScale(d.count);
})
.attr('height', yScale.bandwidth())
.attr('y', function(d, i) {
return yScale(d.Item);
}).attr("fill","#000");
The y axis seemed to be off SVG in the link you provided. (Maybe you have overflow: visible; for the SVG.
Anyway, I've added a few margins to the chart so that the whole chart is visible. Here it is (ignore the link description):
DEMO: H BAR CHART WITH HEIGHT POSITIONING TO THE TICKS
Relevant code changes:
As you are using a scale band, the height is computed within. You just need to use .bandWidth().
.attr('height', yScale.bandwidth())
Added a margin and transformed the axis and the bars to make the whole chart visible :
: I'm assigning margins so that the y-axis is within the viewport of the SVG which makes it easier to adjust the left margin based on the tick value as well. And I think this should be a standard practice.
Also, if you notice, the rects i.e. bars are now a part of <g class="bars"></g>. Inspect the DOM if you'd like. This would be useful for complex charts with a LOT of elements but it's always a good practice.
var margin = {top: 10, left: 40, right: 30, bottom: 10};
var xScale = d3.scaleLinear()
.rangeRound([0,w-margin.left-margin.right])
var yScale = d3.scaleBand()
.rangeRound([h-margin.top-margin.bottom,0]).padding(0.2)
svg.append('g')
.attr('class','axis')
.attr('transform', 'translate(' + margin.left+', '+margin.top+')')
Try changing the data and the bar height will adjust and align according to the ticks. Hope this helps. Let me know if you have any questions.
EDIT:
Initially, I thought you were facing a problem placing the bars at the center of the y tick but as you said you needed fixed height bars, here's a quick addition to the above code that lets you do that. I'll add another approach using the padding (inner and outer) sometime soon.
Updated JS BIN
To position the bar exactly at the position of the axis tick, I'm moving the bar from top to the scale's bandwidth that is calculated by .bandWidth() which will the position it starting right from the tick and now subtracting half of the desired height half from it so that the center of the bar matches the tick y. Hope this explains.
.attr('height', 15)
.attr('transform', 'translate(0, '+(yScale.bandwidth()/2-7.5)+')')

How to center & scale map in d3js (use projection.center/translate/rotate?)

Given that I have topoJSON data of a given geographical feature and a specific projection.
How should I center and scale the map to fit its parent object?
It seems I can use either projection.rotate(), projection.translate() or projection.center() to center a map:
https://github.com/d3/d3-3.x-api-reference/blob/master/Geo-Projections.md
What are the differences and how does scale affect the different functions?
Use projection.fitExtent() in v4. Documentation. Example.
fitExtent takes two parameters:
extent is the top left and bottom right corner of the projection, represented by an array of two arrays – e.g. [[0, 0], [width, height]].
object is a GeoJSON object.
If the top left corner of the projection is [0, 0], you can use the convenience method projection.fitSize(), where you only pass the bottom right corner of the extent, represented by a single array of two items – e.g. [width, height].
Actually, it's a mix of both. According to the API, projection.center:
sets the projection’s center to the specified location, a two-element array of longitude and latitude in degrees and returns the projection.
So, it's used to set the center of the map. Regarding projection.translate:
If point is specified, sets the projection’s translation offset to the specified two-element array [x, y] and returns the projection. If point is not specified, returns the current translation offset which defaults to [480, 250]. The translation offset determines the pixel coordinates of the projection’s center. The default translation offset places ⟨0°,0°⟩ at the center of a 960×500 area.
As you can see, projection.translate depends on projection.center ("the translation offset determines the pixel coordinates of the projection’s center"). So, both values will determine how the map sits in its container
This is a demo showing the map of Japan (this code is not mine) in a smaller SVG, 500x500. In this one, we'll set the translate to the middle of the SVG:
.translate([width/2, height/2]);
Check the demo:
var topoJsonUrl = "https://dl.dropboxusercontent.com/u/1662536/topojson/japan.topo.json";
var width = 500,
height = 500,
scale = 1;
d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g").attr("id", "all-g");
var projection = d3.geo.mercator()
.center([138, 38])
.scale(1000)
.translate([width / 2, height / 2]);
d3.json(topoJsonUrl, onLoadMap);
function onLoadMap (error, jpn) {
var path = d3.geo.path()
.projection(projection);
var features = topojson.object(jpn, jpn.objects.japan);
var mapJapan = features;
d3.select("#all-g")
.append("g").attr("id", "path-g").selectAll("path")
.data(features.geometries)
.enter()
.append("path")
.attr("fill", "#f0f0f0")
.attr("id", function(d,i){ return "path" + i})
.attr("stroke", "#999")
.attr("stroke-width", 0.5/scale)
.attr("d", path);
}
path {
stroke: black;
stroke-width: 1.5;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://d3js.org/topojson.v0.min.js"></script>
And, in this one, to the left:
.translate([width/4, height/2]);
Check the demo:
var topoJsonUrl = "https://dl.dropboxusercontent.com/u/1662536/topojson/japan.topo.json";
var width = 500,
height = 500,
scale = 1;
d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g").attr("id", "all-g");
var projection = d3.geo.mercator()
.center([138, 38])
.scale(1000)
.translate([width / 4, height / 2]);
d3.json(topoJsonUrl, onLoadMap);
function onLoadMap (error, jpn) {
var path = d3.geo.path()
.projection(projection);
var features = topojson.object(jpn, jpn.objects.japan);
var mapJapan = features;
d3.select("#all-g")
.append("g").attr("id", "path-g").selectAll("path")
.data(features.geometries)
.enter()
.append("path")
.attr("fill", "#f0f0f0")
.attr("id", function(d,i){ return "path" + i})
.attr("stroke", "#999")
.attr("stroke-width", 0.5/scale)
.attr("d", path);
}
path {
stroke: black;
stroke-width: 1.5;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://d3js.org/topojson.v0.min.js"></script>
In both cases, however, changing projection.center will move the map in its container.

d3.js. How to make map full width?

Code available here. Map looks like full width, but not at all. There is some indents at the top and left and right sides. All examples of d3.js maps use some strange magic numbers in scale method. I try use such numbers from this answer, but looks like it is not true for my case. So what is true way to scale map? I want to make it full width without any indents on any screen.
var body = d3.select("body").node().getBoundingClientRect();
var coef = 640 / 360;
var width = body.width;
var height = width / coef;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
var projection = d3.geo.equirectangular()
.rotate([-180, 0])
.scale(width / 640 * 100)
.translate([width / 2, height / 2])
var path = d3.geo.path()
.projection(projection);
d3.json("https://dl.dropboxusercontent.com/s/20g0rtglh9xawb8/world-50m.json?dl=1", function(error, world) {
svg.append("path")
.datum(topojson.feature(world, world.objects.land))
.attr("d", path);
});

Resources