Plotting a Histogram in D3 without knowing number of bins - javascript

I'm trying to create a function that creates a histogram for a given array. Clearly, there's no data for the X-Axis and I have to choose the bins arbitrarily. What would be the best way to do so?
My code:
var width = 700, height = 500, pad = 30, barPadding = 1;
function plotHistogram(element, dataset) {
var svg = d3.select(element)
.append("svg")
.attr("width", width)
.attr("height", height)
var bars = svg.append("g")
// Container box
var rectangle = svg.append("rect")
.attr("height", height)
.attr("width", width)
.attr("stroke-width", 2).attr("stroke", "#ccc")
.attr("fill", "transparent")
var xScale = d3.scaleLinear()
// Using default domain (0 - 1)
.range([pad, width - pad * 2])
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d; })])
.range([height - pad, pad])
var xAxis = d3.axisBottom(xScale)
var yAxis = d3.axisLeft(yScale)
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (height - pad) + ")")
.call(xAxis)
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + pad +", 0)")
.call(yAxis)
svg.selectAll("bars")
.data(dataset)
.enter()
.append("rect")
// Evenly spacing out bars
.attr("x", function(d, i) { return i * width/dataset.length; })
// Top of each bar as the top of svg. To remove inverted bar graph.
.attr("y", function(d) { return height - (d * 4); })
// To give padding between bars
.attr("width", width / dataset.length - barPadding)
// To make the bars taller
.attr("height", function(d) { return d * 4; })
.attr("fill", "teal");
}
// #normal is the id of the div element.
plotHistogram("#normal", [10, 20, 30, 40, 50, 60, 70, 80]);
Edit 1: I have decided to use the default bin size (0 - 1) for the xScale above. I'm facing problems creating the bars.

Generate the bins as in https://bl.ocks.org/mbostock/3048450
The array in your plotHistogram is like the array data in #mbostock's bl.ock...
HTH

Related

D3 Bar Chart Zoom Not Centered

I have a bar chart with zoom function. The issue is, the zooming isn't actually centered. If, I place the cursor, on a bar and zoom, the bar underneath the cursor moves away as opposed to staying there, However, if I set the MARGIN.LEFT = 0, then the issue is rectified and No matter what bar I have my cursor on, when I zoom the bar stays there, right underneath. Could anyone help me with this?
Working Code Here: https://codesandbox.io/s/d3-zoom-not-centered-sfziyk
D3 Code:
const MARGIN = {
LEFT: 60,
RIGHT: 40,
TOP: 10,
BOTTOM: 130
};
// total width incl margin
const VIEWPORT_WIDTH = 1140;
// total height incl margin
const VIEWPORT_HEIGHT = 400;
const WIDTH = VIEWPORT_WIDTH - MARGIN.LEFT - MARGIN.RIGHT;
const HEIGHT = VIEWPORT_HEIGHT - MARGIN.TOP - MARGIN.BOTTOM;
const svg = d3
.select(".chart-container")
.append("svg")
.attr("width", WIDTH + MARGIN.LEFT + MARGIN.RIGHT)
.attr("height", HEIGHT + MARGIN.TOP + MARGIN.BOTTOM);
const g = svg
.append("g")
.attr("transform", `translate(${MARGIN.LEFT}, ${MARGIN.TOP})`);
g.append("text")
.attr("class", "x axis-label")
.attr("x", WIDTH / 2)
.attr("y", HEIGHT + 110)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.text("Month");
g.append("text")
.attr("class", "y axis-label")
.attr("x", -(HEIGHT / 2))
.attr("y", -60)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.text("");
const zoom = d3.zoom().scaleExtent([0.5, 10]).on("zoom", zoomed);
svg.call(zoom);
function zoomed(event) {
x.range([0, WIDTH].map((d) => event.transform.applyX(d)));
barsGroup
.selectAll("rect.profit")
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth());
barsGroup
.selectAll("rect.revenue")
.attr("x", (d) => x(d.month) + 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth());
xAxisGroup.call(xAxisCall);
}
const x = d3.scaleBand().range([0, WIDTH]).paddingInner(0.3).paddingOuter(0.2);
const y = d3.scaleLinear().range([HEIGHT, 0]);
const xAxisGroup = g
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${HEIGHT})`);
const yAxisGroup = g.append("g").attr("class", "y axis");
const xAxisCall = d3.axisBottom(x);
const yAxisCall = d3
.axisLeft(y)
.ticks(3)
.tickFormat((d) => "$" + d);
const defs = svg.append("defs");
const barsClipPath = defs
.append("clipPath")
.attr("id", "bars-clip-path")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", WIDTH)
.attr("height", 400);
const barsGroup = g.append("g");
const zoomGroup = barsGroup.append("g");
barsGroup.attr("class", "bars");
zoomGroup.attr("class", "zoom");
barsGroup.attr("clip-path", "url(#bars-clip-path)");
xAxisGroup.attr("clip-path", "url(#bars-clip-path)");
d3.csv("data.csv").then((data) => {
data.forEach((d) => {
d.profit = Number(d.profit);
d.revenue = Number(d.revenue);
d.month = d.month;
});
var y0 = d3.max(data, (d) => d.profit);
var y1 = d3.max(data, (d) => d.revenue);
var maxdomain = y1;
if (y0 > y1) var maxdomain = y0;
x.domain(data.map((d) => d.month));
y.domain([0, maxdomain]);
xAxisGroup
.call(xAxisCall)
.selectAll("text")
.attr("y", "10")
.attr("x", "-5")
.attr("text-anchor", "end")
.attr("transform", "rotate(-40)");
yAxisGroup.call(yAxisCall);
const rects = zoomGroup.selectAll("rect").data(data);
rects.exit().remove();
rects
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit));
rects
.enter()
.append("rect")
.attr("class", "profit")
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit))
.attr("fill", "grey");
const rects_revenue = zoomGroup.selectAll("rect.revenue").data(data);
rects_revenue.exit().remove();
rects_revenue
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue));
rects_revenue
.enter()
.append("rect")
.attr("class", "revenue")
.style("fill", "red")
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month) + 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue))
.attr("fill", "grey");
});
When you call the zoom on the svg, all zoom behaviour is relative to the svg.
Imagine that your x-axis is at initial zoom level of length 100 representing the domain [0, 100]. So the x-scale has range([0, 100]) and domain([0, 100]). Add a left margin of 10.
If you zoom by scale 2 at the midpoint of your axis at x=50 you would expect to get the following behaviour after the zoom:
The midpoint does not move.
The interval [25, 75] is visible.
However, since the zoom is called on the svg you have to account for the left margin of 10. The zoom does not occur at the midpoint but at x = 10 + 50 = 60. The transform is thus x -> x * k + t with k = 2 and t = -60. This results in
x = 50 -> 2 * 50 - 60 = 40,
x = 80 -> 2 * 80 - 60 = 100,
x = 30 -> 2 * 30 - 60 = 0.
Visible after the zoom is the interval [30, 80] and the point x = 50 is shifted to the left.
This is what you observe in your chart.
In order to get the expected behaviour, you can do two things:
a. Follow the bar chart example where the range of the x-scale does not start at 0 but at the left margin. The g which is translated by margin.left and margin.top is also omitted here. Instead, the ranges of the axes incorporate the margins directly.
b. Add a rect with fill: none; pointer-events: all; to the svg that is of the size of the chart without the margins. Then call the zoom on that rectangle, as done in this example.
Note that all the new examples on ObservableHQ follow the pattern "a" that needs fewer markup.

D3 bar chart add legend

I have made a bar chart in d3 but I am struggeling with 2 things.
First of all I want to add a legend because I have 2 different bars so I want people to know what each bar is.
The second I want to load data from a json file instead of this:
var data = [
{
"year":1970,
"count1":1,
"count2":0
},
{
"year":1975,
"count1":1,
"count2":0
},
{
"year":1980,
"count1":1,
"count2":1
}]
But when I try to use a json file my map function gives an error.
My code :
var svgSelect = d3.select("svg")
var width = 1020
var height = 900
var margin = {top: 35, right: 25, bottom: 35, left: 50}
var paddingBars = .2
var axisTicks = {amount: 25, outerSize: 0}
var svg = svgSelect
.append("svg")
.attr("widht", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
var xScale0 = d3.scaleBand().range([0, width - margin.left - margin.right]).padding(paddingBars)
var xScale1 = d3.scaleBand()
var yScale = d3.scaleLinear().range([height - margin.top - margin.bottom, 0])
var xAxis = d3.axisBottom(xScale0).tickSizeOuter(axisTicks.outerSize)
var yAxis = d3.axisLeft(yScale).ticks(axisTicks.amount).tickSizeOuter(axisTicks.outerSize)
xScale0.domain(data.map(d => d.year))
xScale1.domain(['count1', 'count2']).range([0, xScale0.bandwidth()])
yScale.domain([0, d3.max(data, d => d.count1 > d.count2 ? d.count1 : d.count2)])
var year = svg.selectAll(".year")
.data(data)
.enter().append("g")
.attr("class", "year")
.attr("transform", d => `translate(${xScale0(d.year)},0)`)
/* Add count1 bars */
year.selectAll(".bar.count1")
.data(d => [d])
.enter()
.append("rect")
.attr("class", "bar count1")
.style("fill","blue")
.attr("x", d => xScale1('count1'))
.attr("y", d => yScale(d.count1))
.attr("width", xScale1.bandwidth())
.attr("height", d => {
return height - margin.top - margin.bottom - yScale(d.count1)
})
/* Add coun2 bars */
year.selectAll(".bar.count2")
.data(d => [d])
.enter()
.append("rect")
.attr("class", "bar count2")
.style("fill","red")
.attr("x", d => xScale1('count2'))
.attr("y", d => yScale(d.count2))
.attr("width", xScale1.bandwidth())
.attr("height", d => {
return height - margin.top - margin.bottom - yScale(d.count2)
})
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0,${height - margin.top - margin.bottom})`)
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)

D3 update data on the wrong bars

I want to update data on a click but the bars that are changing are not the right ones. There is something I cant quite fix with the select. On click the grey bars, which should be bar2 are updating. It should be bar.
Example: https://jsfiddle.net/Monduiz/kaqv37gu/
D3 chart:
var values = feature.properties;
var data = [
{name:"Employment rate",value:values["ERate15P"]},
{name:"Participation rate",value:values["PR15P"]},
{name:"Unemployment rate",value:values["URate15P"]}
];
var margin = {top: 70, right: 50, bottom: 20, left: 50},
width = 400 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom,
barHeight = height / data.length;
// Scale for X axis
var x = d3.scale.linear()
.domain([0, 100]) //set input to a scale of 0 - 1. The index has a score scale of 0 to 1. makes the bars more accurate for comparison.
.range([0, width]);
var y = d3.scale.ordinal()
.domain(["Employment rate", "Participation rate", "Unemployment rate"])
.rangeRoundBands([0, height], 0.2);
var svg = d3.select(div).select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.classed("chartInd", true);
var bar2 = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
var bar = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
bar2.append("rect")
.attr("height", y.rangeBand()-15)
.attr("fill", "#EDEDED")
.attr("width", 300);
bar.append("rect")
.attr("height", y.rangeBand()-15)
.attr("fill", "#B44978")
.attr("width", function(d){return x(d.value);});
bar.append("text")
.attr("class", "text")
.attr("x", 298)
.attr("y", y.rangeBand() - 50)
.text(function(d) { return d.value + " %"; })
.attr("fill", "black")
.attr("text-anchor", "end");
bar.append("text")
.attr("class", "text")
.attr("x", function(d) { return x(d.name) -5 ; })
.attr("y", y.rangeBand()-50)
//.attr("dy", ".35em")
.text(function(d) { return d.name; });
d3.select("p")
.on("click", function() {
//New values for dataset
var values = feature.properties;
var dataset = [
{name:"Employment rate",value:values["ERate15_24"]},
{name:"Participation rate",value:values["PR15_24"]},
{name:"Unemployment rate",value:values["URate15_24"]}
];
//Update all rects
var bar = svg.selectAll("rect")
.data(dataset)
.attr("x", function(d){return x(d.value);})
.attr("width", function(d){return x(d.value);})
});
}
var bar2 = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
var bar = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
'bar2' above generates 3 new g elements (one for each datum)
Since you don't set attr("class","bar") for these then 'bar' will also generate 3 new g elements - (if you had set the class attribute bar would return empty as no new elements would be generated and you'd see missing stuff)
Further on you add rects to all these g elements for six rectangles in total and in the click function you select all these rectangles and re-attach 3 fresh bits of data
Since bar2 was added first the rectangles in its g elements are hoovering up the new data
You need to select and set different classes on the g elements, .selectAll("g.bar") and .attr("class", "bar") for bar, and .selectAll("g.bar2") and .attr("class", "bar2") for bar2 (use the same name to keep it simple)
then in the new data you need select only the rects belonging to g elements of the bar class: svg.selectAll(".bar rect")
Another way would be to have only one set of g elements and add two types of rectangle (differentiated by class attribute)

How to improve this data-visualisation?

I have a d3.js plot that I want to improve it but I can't figure out how to do it!
This is my plot:
Mainly I am trying to change axis and add a little legend so I can get something like this (with x and y zeros centered in the plot ):
This is how I define x and y axis in my d3.js/JavaScript code
var xScale = d3.scale.linear().domain([d3.min(dataset, function(d) { return d[0]; }), d3.max(dataset, function(d) { return d[0]; })]).range([padding, w - padding]);
var yScale = d3.scale.linear().domain([d3.min(dataset, function(d) { return d[1]; }), d3.max(dataset, function(d) { return d[1]; })]).range([h - padding, padding]);
// Create axis
var xAxis = d3.svg.axis().scale(xScale).orient("bottom").ticks(5);
//Define Y axis
var yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(5);
//Create SVG element
var svg = d3.select("#mydiv").append("svg").attr("width", w).attr("height", h);
svg.append("g").attr("class", "axis").attr("transform", "translate(0," + (h - padding) + ")").call(xAxis);
svg.append("g").attr("class", "axis").attr("transform", "translate(" + padding + ",0)").call(yAxis);
Thanks in advance:
If you provide your actual dataset I can give you more exact code for your case, without it the best we can really do is give you examples from the docs.
How to create a legend:
// draw legend
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
// draw legend colored rectangles
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
// draw legend text
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d;})
How to hardcode max/min on your axes:
var yScale = d3.scale.linear().domain([0,6]).range([h - padding, padding]);
How to add gridlines:
var yAxisGrid = yAxis
.tickSize(width, 0)
.tickFormat("")
.orient("right")
var xAxisGrid = xAxis
.tickSize(-height, 0)
.tickFormat("")
.orient("top")
svg.append("g")
.classed('y', true)
.classed('axis', true)
.call(yAxisGrid)
svg.append("g")
.classed('x', true)
.classed('axis', true)
.call(xAxisGrid)

d3 Reusable histogram

I've been trying to implement Reusability on a histogram plotted using d3.
I want that after plotting of the dataset, I want to plot statistical mean, variance etc. on the same plot.These would be user driven, basically I want to use the same plot.
Here's my attempt on coding the skeleton histogram code
function histogram(){
//Defaults
var margin = {top: 20, right: 20, bottom: 20, left: 20},
width = 760,
height = 200;
function chart(selection){
selection.each(function(d,i){
var x = d3.scale.linear()
.domain( d3.extent(d) )
.range( [0, width] );
var data = d3.layout.histogram()
//Currently generates 20 equally spaced bars
.bins(x.ticks(20))
(d);
var y = d3.scale.linear()
.domain([0, d3.max(d) ])
.range([ height - margin.top - margin.bottom, 0 ]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select(this).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var bar = svg.selectAll(".bar")
.data(data)
.enter().append("g")
.attr("class", "bar");
/*
Corrected bars
bar.append("text")
.attr("dy", ".75em")
.attr("y", 6)
.attr("x", x(data[0].dx) / 2)
.attr("text-anchor", "middle")
.text(function(d) { return formatCount(d.y); });
*/
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class","y axis")
.call(yAxis);
bar.append("rect")
.attr("x", function(d,i){ return x(d.x); })
.attr("width", x(data[0].dx) - 1)
.attr('y',height)
.transition()
.delay( function(d,i){ return i*50; } )
.attr('y',function(d){ return y(d.y) })
.attr("height", function(d) { return height - y(d.y); });
});
}
//Accessors//
chart.width = function(value) {
if (!arguments.length) return width;
width = value;
return chart;
};
chart.height = function(value) {
if (!arguments.length) return height;
height = value;
return chart;
};
return chart;
}
It's assigning a negative width for bars. My input dataset would simply be an array of numbers and I need to plot the frequency distribution
If you're asking how to implement the avg, standard deviation, once you have your histogram you can draw lines and text on it to represent the avg. I would calculate which bar the average is in, and the % of the way through the bar and then something like this:
var averageBar = vis.selectAll("g.bar:nth-child(" + (averageBarIndex + 1) + ")");
averageBar.append("svg:line")
.attr("x1", 0)
.attr("y1", y.rangeBand()*averageBarPercentage)
.attr("x2", w)
.attr("y2", y.rangeBand() * averageBarPercentage)
.style("stroke", "black");
averageBar.append("svg:text")
.attr("x", w-150)
.attr("y", y.rangeBand() * averageBarPercentage-15)
.attr("dx", -6)
.attr("dy", "10px")
.attr("text-anchor", "end")
.text("Average");
That will give you a line marking the average, you can do similar for the standard deviation.

Categories