Related
I'm following the book Interactive Data Visualization for the Web (2nd Ed). While learning about adding interactivity to a bar chart, the text states:
Throw an invisible rect with a fill of none and pointer-events value of all on the top of each group. Even though the rect is invisible, it will still trigger mouse events, so you could have the rect span the whole height of the chart. The net effect is that mousing anywhere in that column—even in “empty” whitespace above a short blue bar—would trigger the highlight effect.
I believe I've successfully created the invisible rect in the proper place (at the end, so as to not be behind the visible rects). I can mouse anywhere in the column, even in the empty whitespace above the short blue bar. However, I cannot figure out how to only highlight the blue bar and not the entire container rect.
Fiddle
//Width and height
var w = 600;
var h = 250;
var dataset = [5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
11, 12, 15, 20, 18, 17, 16, 18, 23, 25];
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.rangeRound([0, w])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset)])
.range([0, h]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create groups to hold the bars and text for each data point
var groups = svg.selectAll(".groups")
.data(dataset)
.enter()
.append("g")
.attr("class", "gbar");
//Create bars
groups.append("rect")
.attr("class", "actualRect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", function (d) {
return h - yScale(d);
})
.attr("width", xScale.bandwidth())
.attr("height", function (d) {
return yScale(d);
})
.attr("fill", function (d) {
return "rgb(0, 0, " + Math.round(d * 10) + ")";
});
//Create labels
groups.append("text")
.text(function (d) {
return d;
})
/*.style("pointer-events", "none")*/
.attr("text-anchor", "middle")
.attr("x", function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr("y", function (d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
// Create container rect
// The goal is to be able to hover *above* a bar, but only highlight the visible blue bar to orange.
// I don't understand how to select (this).('actualRect'), instead of (this).("containerRect")
groups.append("rect")
.attr("class", "containerRect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", 0)
.attr("width", xScale.bandwidth())
.attr("height", h)
.attr("fill", "none")
.style("pointer-events", "all")
.on("mouseover", function () {
d3.select(this) // trying to target (this) -> .actualBar
.attr("fill", "orange");
});
You can select the sibling rect by first selecting this.parentNode from within your event callback function, and then making the desired selection.
d3.select(this.parentNode).select('.actualRect').attr("fill", "orange");
//Width and height
var w = 600;
var h = 250;
var dataset = [5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
11, 12, 15, 20, 18, 17, 16, 18, 23, 25];
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.rangeRound([0, w])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset)])
.range([0, h]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create groups to hold the bars and text for each data point
var groups = svg.selectAll(".groups")
.data(dataset)
.enter()
.append("g")
.attr("class", "gbar");
//Create bars
groups.append("rect")
.attr("class", "actualRect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", function (d) {
return h - yScale(d);
})
.attr("width", xScale.bandwidth())
.attr("height", function (d) {
return yScale(d);
})
.attr("fill", function (d) {
return "rgb(0, 0, " + Math.round(d * 10) + ")";
});
//Create labels
groups.append("text")
.text(function (d) {
return d;
})
/*.style("pointer-events", "none")*/
.attr("text-anchor", "middle")
.attr("x", function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr("y", function (d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
// Create container rect
// The goal is to be able to hover *above* a bar, but only highlight the visible blue bar to orange.
// I don't understand how to select (this).('actualRect'), instead of (this).("containerRect")
groups.append("rect")
.attr("class", "containerRect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", 0)
.attr("width", xScale.bandwidth())
.attr("height", h)
.attr("fill", "none")
.style("pointer-events", "all")
.on("mouseover", function () {
d3.select(this.parentNode).select('.actualRect').attr("fill", "orange");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Fiddle
I want to append each rect element exactly after previous element x-axis coordinate. My current code is:
var data = [50,100,150]
var svg = d3.select("#bar_chart")
.append("svg")
.attr("width", "1000")
.attr("height", "500")
var rect = svg.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(d) { return (d * i) })
.attr("y", "300")
.attr("width", function(d, i){ return d})
.attr("height", function(d, i){ return d})
.attr("fill", "blue")
.each(function(d, i) {console.log(i.x)})
Which gives the following:
Code with x-axis position set to return (d * i)
What I would like:
Code with x-axis from each element start immediately after the previous one
Thanks in advance
You need to add all the widths prior to your current width.
https://jsfiddle.net/8dv1y74e/
var data = [50,100,150]
var svg = d3.select("#bar_chart")
.append("svg")
.attr("width", "1000")
.attr("height", "500")
var rect = svg.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", getPreviousWidths)
.attr("y", "300")
.attr("width", function(d, i){ return d})
.attr("height", function(d, i){ return d})
.attr("fill", "blue")
.each(function(d, i) {console.log(i.x)})
function getPreviousWidths(d,i){
return data.slice(0,i).reduce((memo,num)=>num+memo,0)
}
An alternative method to the other answer, you can use a variable to keep track of the x coordinate for the most recent rectangle, adding to it each time you append a rectangle:
var data = [50,100,150];
var svg = d3.select("body")
.append("svg")
.attr("width", "500")
.attr("height", "500");
var positionX = 0; // where we are on the x axis, first element should be at 0 pixels.
var rect = svg.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(d) {
var x = positionX; // The x value for this rectangle
positionX = d + x; // The x value for the next rectangle
return x; // Return the x value for this rectangle.
})
.attr("y", "10")
.attr("width", function(d, i){ return d})
.attr("height", function(d, i){ return d})
.attr("fill", "blue");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have created a waterfall chart using D3 (V4) with three values (ticks) for the y axis.
The x axis tick values are automatically calculated.
How can I add an additional tick value (today's date) on the x axis (date values)?
function risklevels(d) {
if (d <= 25 && d >= 13.5) {
return "High";
} else if (d <= 13.5 && d > 7) {
return "Med";
}
return "Low";
}
function drawWaterfall(){
var margin = {top: 20, right: 20, bottom: 30, left: 50};
var width = 800 - margin.left - margin.right;
var height = 400 - margin.top - margin.bottom;
dt = new Date();
var x = d3.scaleTime()
.rangeRound([0, width]);
var y = d3.scaleLinear()
.rangeRound([height, 1]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y).tickFormat(risklevels).tickValues([4, 10.25, 19.125]);
var parseDate = d3.timeParse("%Y-%m-%d");
var riskwaterfall = d3.select('#riskwaterfall').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+')');
riskwaterfall.append('rect')
.attr('class', 'high')
.attr("x", 0) // start rectangle on the good position
.attr("y", 0) // no vertical translate
.attr("width", width) // correct size
.attr("height", height*((25.0-13.5)/25.0) + height*0.5/25)
.attr("fill", "#ee0000"); // full height
riskwaterfall.append('rect')
.attr('class', 'high')
.attr("x", 0) // start rectangle on the good position
.attr("y", height*((25.0-13.5)/25.0) + height*0.5/25.0) // no vertical translate
.attr("width", width) // correct size
.attr("height", height*((13.5-7.0)/25.0) + height*0.5/25.0)
.attr("fill", "#eeee00"); // full height
riskwaterfall.append('rect')
.attr('class', 'high')
.attr("x", 0) // start rectangle on the good position
.attr("y", (25-7)*height/25 + height*0.5/25.0)// no vertical translate
.attr("width", width) // correct size
.attr("height", 7*height/25 - height*0.5/25.0)
.attr("fill", "#00ee00"); // full height
var line = d3.line()
.curve(d3.curveStepAfter)
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.risk); });
line('step-after');
risk.forEach(function(d) {
d.date = parseDate(d.date);
d.risk = +d.risk;
});
x.domain(d3.extent(risk, function(d) { return d.date; }));
y.domain(d3.extent(risk, function(d) { return d.risk; }));
riskwaterfall.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,'+height+')')
.call(xAxis);
riskwaterfall.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
riskwaterfall.append('path')
.datum(risk)
.attr('d', line(risk));
for (var i = 0; i < risk.length; i++)
riskwaterfall.append('circle')
.datum(risk[i])
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.risk); })
.attr("stroke-width", "2px")
.attr("fill", "black" )
//.attr("fill-opacity", .5)
//.attr("visibility", "hidden")
.attr("r", 5);
}
Right now, you're creating a new date for today:
dt = new Date();
But this has no effect on the x scale (which is used by the axis generator). So, instead of:
x.domain(d3.extent(risk, function(d) { return d.date; }));
Which only goes to the maximum date in the risk data, it should be:
x.domain([d3.min(risk, function(d) { return d.date; }), dt]);
After that, to make sure that the last tick shows up, you can use nice() or concat the end domain in your tick values.
Hi want a rectangle of width 250, and i want to fill the rectangle with the color based on the input value.I tried to create one rectangle of gray and another one of color skyblue on the same position to acheive this but it update the rectangle only. cant create another rectangle on the previous one. what to do.? My Js fiddle is http://jsfiddle.net/sSqmV/ i want to create an second rectangle of sky blue over the previous one of white color to acheive my task.
var chart = d3.select("div.dev1").append("svg")
.attr("width", 292)
.attr("height", 300);
var dataset = [0, 1, 2];
var dataset2 = [0, 1, 2,3];
var rects1 = chart.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", 10)
.attr("y", function (d, i) { return (i + 1) * 60; })
.attr("height", 6)
.attr("width", 250)
.attr("fill", "white");
var rects = chart.selectAll("rect")
.data(dataset2)
.enter()
.append("rect")
.attr("x", 10)
.attr("y", function (d, i) { return (i + 1) * 60; })
.attr("height", 6)
.attr("width", 250)
.attr("fill", "skyblue");
var texts = chart.selectAll("text").data(dataset2).enter().append("text")
.text("18% of the questions were ANSWERED")
.attr("x", 10)
.attr("y", function (d, i) { return 90+(i*60); });
You can do something like this:
var chart = d3.select("div.dev1").append("svg")
.attr("width", 292)
.attr("height", 300);
var dataset = [0, 1, 2];
var dataset2 = [0, 1, 2, 3];
var rects = chart.selectAll(".rect1")
.data(dataset2)
.enter()
.append("rect")
.attr('class', 'rect1')
.attr("x", 10)
.attr("y", function (d, i) {
return (i + 1) * 60;
})
.attr("height", 6)
.attr("width", 250)
.attr("fill", "skyblue");
var rects1 = chart.selectAll(".rect")
.data(dataset)
.enter()
.append("rect")
.attr('class', 'rect')
.attr("x", 10)
.attr("y", function (d, i) {
return (i + 1) * 60;
})
.attr("height", 6)
.attr("width", 125)
.attr("fill", "white");
var texts = chart.selectAll("text").data(dataset2).enter().append("text")
.text("18% of the questions were ANSWERED")
.attr("x", 10)
.attr("y", function (d, i) {
return 90 + (i * 60);
});
Here is jsfiddle: http://jsfiddle.net/cuckovic/sSqmV/2/
I'm looking for a way of limiting the column width in a chart, I'm sure this ought to be relatively easy but I cant find a way of doing it.
I'm populating a chart from some dynamic data, where the number of columns can vary quite dramatically - between 1 and 20.
e.g: sample of csv
Location,Col1
"Your house",20
Location,Col1,Col2,Col3,Col4,Col5
"My House",12,5,23,1,5
This is working fine, and the col widths are dynamic, however when there is only one column in the data, I end up with one bar of width 756 (the whole chart), and I dont like the way this looks.
What I'd like to do is only ever have a maximum column of width 100px irrespective of the number of columns of data.
Below is my script for the chart
Many thanks,
<script>
var margin = {
top : 40,
right : 80,
bottom : 80,
left : 40
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear().range([ 0, width ]);
var y = d3.scale.linear().range([ height, 0 ]);
var x0 = d3.scale.ordinal()
.rangeRoundBands([0, width], .05);
var x1 = d3.scale.ordinal();
var y = d3.scale.linear()
.range([height, 0]);
var chart = d3.select("body").append("svg")
.attr("class","chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var legendChart = d3.select("body").append("svg")
.attr("class","chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("/sampledata.csv.txt", function(error, data) {
// Use the first row of csv for header names
var reasonNames = d3.keys(data[0]).filter(function(key) {
return key !== "Location";
});
//console.log(reasonNames);
data.forEach(function(d) {
d.reasons = reasonNames.map(function(name) {
return {
name : name,
value : +d[name]
};
});
//console.log(d.reasons);
});
x0.domain(data.map(function(d) {return d.Location; }));
x1.domain(reasonNames).rangeRoundBands([0, x0.rangeBand()]);
console.log(x0.rangeBand());
y.domain([0, d3.max(data, function(d) { return d3.max(d.reasons, function(d) { return d.value; }); })]);
var maxVal = d3.max(data, function(d) { return d3.max(d.reasons, function(d) { return d.value; }); });
//console.log(maxVal);
var xAxis = d3.svg.axis()
.scale(x0)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
//.tickFormat(d3.format(".2s"));
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
var location = chart.selectAll(".name")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x0(d.Location) + ",0)"; });
location.selectAll("rect")
.data(function(d) { return d.reasons; })
.enter().append("rect")
.attr("width", x1.rangeBand()-2)
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d,i) { return "#"+3+"9"+i; /*color(d.name);*/ });
chart.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", function(d) { return x1(d.name)+ x.rangeBand() / 2; })
.attr("y", function(d) { return y(d.value); })
.attr("dx", -3) // padding-right
.attr("dy", ".35em") // vertical-align: middle
.attr("text-anchor", "end") // text-align: right
.text("String");
var legend = legendChart.selectAll(".legend")
.data(reasonNames.slice().reverse())
.enter()
.append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")";
});
legend.append("rect")
//.attr("x", width - 18)
.attr("x", 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {/*console.log(i);*/return "#" + 3 + "9" + i;
});
legend.append("text")
//.attr("x", width - 24)
.attr("x", 48)
.attr("y", 9).attr("dy",".35em")
//.style("text-anchor", "end")
//.text(function(d,i) { return String.fromCharCode((65+i))+i; });
.text(function(d) { return d; });
});
</script>
The easiest way to achieve this is by changing the line
.attr("width", x1.rangeBand()-2)
to
.attr("width", Math.min(x1.rangeBand()-2, 100))
You might also want to adjust the starting position and/or padding.
Code for adjusting starting position if anyone is stuck on it:
.attr("x", function(d, i) { return x1(d.seriesName) + (x1.rangeBand() - 100)/2 ;})
P.S. : referring answer from Lars.
Setting an absolute maximum width for the columns doesn't allow proper rendering for different screen resolutions, div sizes, etc.
In my case, I just wanted the columns not to look so large when the number of columns itself is small
I found it easier and more straight-forward to play with the scale definition, by changing the maximum width (where all columns will fit), their inner and outer paddings.
var w = 600
// var w = parseInt(d3.select(parentID).style('width'), 10) // retrieve the div width dynamically
var inner_padding = 0.1
var outer_padding = 0.8
var xScale = d3.scale.ordinal().rangeRoundBands([0, w], inner_padding, outer_padding)
When rendering the plot, I just ran a switch/if-else statement, which assigns different padding values. The lower the number of columns to plot, the greater the outer_padding (and eventually inner-padding) values I use.
This way, I keep the plots responsive.
I am able to change the width of the bar using the above answer. But unfortunately, my X Axis labels are not aligned when there is a single bar in the chart and it uses the max width set.
var tradeGroup = svg.selectAll("g.trade")
.data(trades)
.enter()
.append("svg:g")
.attr("class", "trade")
.style("fill", function (d, i) {
return self.color(self.color.domain()[i]);
})
.style("stroke", function (d, i) {
return d3.rgb(self.color(self.color.domain()[i])).darker();
});
var aWidth = Math.min.apply(null, [x.rangeBand(), 100]);
// Add a rect for each date.
var rect = tradeGroup.selectAll("rect")
.data(Object)
.enter()
.append("svg:rect")
.attr("x", function (d) {
return x(d.x);
})
.attr("y", function (d) { return y( (d.y || 0) + (d.y0 || 0)); })
.attr("height", function (d) { return y(d.y0 || 0) - y((d.y || 0) + (d.y0 || 0)); })
.attr("width", Math.min.apply(null, [x.rangeBand(), 100]));
For completeness the full answer would look like this:
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", (d) -> x1(d.name) + (x1.rangeBand() - d3.min([x1.rangeBand(), 100]))/2)
.attr("width", d3.min([x1.rangeBand(), 100]))
.attr("y", (d) -> y(d.grade) )
.attr("height", (d)-> height - y(d.value) )
(coffeescript syntax)
Note this include the full answer, the 'width' and the 'x' settings. Also 'x' settings is accounting for a when 100 width is not the min value.
Thought I'd share that I came up with a slightly different answer to this. I didn't want to hard code in a maximum bar width because 1) it wasn't responsive to different screen sizes and 2) it also required playing with the x-coordinate attribute or accepting some irregular spacing.
Instead, I just set a minimum number of bars, based on the point where the bars became too wide (in my case, I found that less than 12 bars made my chart look weird). I then adjusted the scaleBand's range attribute, proportionately, if there were less than that number of bars. So, for example, if the minimum was set to 12 and there were only 5 items in the data, rather than rendering each of them at 1/5th of the full width, I scaled the range down to 5/12ths of the original width.
Basically, something like this:
// x is a scaleBand() that was previously defined, and this would run on update
var minBarSlots = 12;
if (data.length < minBarSlots) {
x.range([0, width*(data.length/minBarSlots)])
}
else {
x.range([0, width])
}`