So I've just started my D3 journey, and wanted to ask about how one would create a small 1px border around the chart.
I created the variables "border" and "bordercolor" and then I added .attr("border",border) to the var svg = d3.select("body") portion of my code. It doesn't crash, but I get no border either.
I guess the question is how do i add this border, and if someone could explain why what i did is wrong.
<script type="text/javascript">
//Width and height
var w = 800;
var h = 400;
var padding = 20;
var border=1;
var bordercolor='black';
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],[-50,-100],[50,-45],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88],[-480, 90], [3,-90]
];
// create scale functions
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 * 2]);
var yScale = d3.scale.linear()
.domain([d3.min(dataset, function(d) { return d[0]; }), d3.max(dataset, function(d) { return d[1]; })])
.range([h - padding, padding]);
var rScale = d3.scale.linear()
.domain( [-100, d3.max(dataset, function(d) { return d[1]; })] )
.range([2,5]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("border",border)
;
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", 3);
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d[0] + "," + d[1];
})
.attr("x", function(d) {
return xScale(d[0]);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "red");
</script>
Use the style attribute to place an outline around the svg:
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("style", "outline: thin solid red;") //This will do the job
.attr("width", w)
.attr("height", h);
The svg var is just a container. You need to add a path or element to the container and then give it the stroke color and width you want for your border. There is more than one way to do this. In this gist I did it by adding a rect with the following values:
var borderPath = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", h)
.attr("width", w)
.style("stroke", bordercolor)
.style("fill", "none")
.style("stroke-width", border);
IMHO it's better to keep separated shape from style instructions:
.append("rect")
.attr("x", 5)
.attr("y", 5)
.attr("height", 40)
.attr("width", 50)
.attr("class","foo")
...
CSS:
svg rect.foo {
fill: white;
stroke-width: 0.5;
stroke: grey;
}
Simply use css:
svg {
border:1px solid black;
}
If X and Y Axis are used, other option is to use tickSizeOuter()
Example:
var yAxis = d3.axisLeft(y).ticks(5).tickSizeOuter(-width);
var xAxis = d3.axisBottom(x).ticks(5).tickSizeOuter(-height);
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
when g tag is bonded to a mouseover event whether elements within the g tag response in concert to the mouseover and don't need another rectangle to fill the g tag?
I am following a d3.js book, it refers to a situation that one visible element (here is a text tag) overlaps another (here is a rectangle), and if I bind an event (here is mouseover) to the deeper element, it won't call the event when your mouse over the upper element (the text tag) .
and it gives a solution that you can group the rect and text tags together, and bind the event to the g element so that both rect and text could work.
but it also needs a CSS code: pointer-events: all and another rectangle to overlap the whole the rectangle and text tags inside the g tag, in this case, it will work.
<head>
<meta charset="utf-8">
<title>D3: Smoother highlight transitions</title>
<script type="text/javascript" src="../d3.js"></script>
<style type="text/css">
/* No style rules here yet */
</style>
</head>
<body>
<script type="text/javascript">
//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 bars
svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
.append("rect")
.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
svg.selectAll("g")
.append("text")
.text(function(d) {
return d;
})
.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");
//*****************to add a mouseover event to g tag and overlap the rectangle and text tags above with a rectangle***************************
svg.selectAll("g")
.style("pointer-events", "all")
.on("mouseover", function() {
d3.select(this)
.select("rect")
.attr("fill", "orange");
})
.on("mouseout", function(d) {
d3.select(this)
.select("rect")
.transition()
.duration(250)
.attr("fill", "rgb(0, 0, " + (d * 10) + ")");
})
.append("rect")
.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);
})
//*****************make it invisible*********************************
.attr("fill", "none");
</script>
</body>
you could copy it into an empty HTML file, and load it, it's a bar chart.
the only question is if a rectangle is needed to fill the unempty g tag to make the bar rectangles and text response together.
I'm following the D3 tutorial but adding the axis makes half of my data disappear and I don't understand why. I thought that maybe the axis is taking up the space that's meant for the data so I added an extra 10px to the transform property, but it doesn't make any difference.
var GIST = "https://gist.githubusercontent.com/charisseysabel/f8f48fbf11b8a1b0d62cbe2d6bdc2aa6/raw/2ead1537adb822fbd59a666afd5334d525480a13/nano-2017.tsv"
var width = 1000,
height = 550,
margin = {top: 20, right: 30, bottom: 30, left: 4};
var y = d3.scaleLinear()
.range([height, 0]);
var yScale = d3.scaleLinear()
.range([height, 0]);
var xScale = d3.scaleLinear()
.range([0, width]);
var xAxis = d3.axisLeft(yScale);
var yAxis = d3.axisBottom(xScale);
var chart = d3.select(".chart")
.attr("width", width)
.attr("height", height);
chart.append("g")
.attr("transform", "translate(10, 0)")
.call(xAxis);
chart.append("g")
.attr("transform", "translate(0, 540)")
.call(yAxis);
d3.tsv(GIST, type, function(error, data) {
y.domain([0, d3.max(data, function(d) { return d.value; })]);
var barWidth = width / data.length;
var bar = chart.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d, i) {
return "translate(" + ((i * barWidth) + 10) + ",0)"; }
);
bar.append("rect")
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", barWidth - 1);
bar.append("text")
.attr("x", (barWidth / 2) - 2)
.attr("y", function(d) { return y(d.value) + 3; })
.attr("dy", ".75em")
.text(function(d) { return d.value; });
});
function type(d) {
d.value = +d.value;
return d;
}
When you do this...
var bar = chart.selectAll("g").etc...
... you're selecting group elements that already exist in the SVG, which are the axes, and binding your data to them.
There are two easy solutions:
Move your code that creates the axes to the bottom of the d3.tsv, that is, after you have appended the bars.
Select something that doesn't exist, like
var bar = chart.selectAll(null).etc. To read more about the logic behind selectAll(null), have a look at my answer here.
I have this single horizontal bar chart and I want to make the following adjustments:
Show the tick value to the right of the bar chart, instead of by the axis.
Show a background for the bar chart scale instead of left and bottom axis.
Current version:
What I'd like to get to:
JS
var data = [
{"yAxis":"score", "xAxis":"72"}
];
// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 80},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// set the ranges
var y = d3.scaleBand()
.range([height, 0])
.padding(0.4);
var x = d3.scaleLinear()
.range([0, width]);
var svg = d3.select(".barChartContainer").append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 960 500")
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Scale the range of the data in the domains
x.domain([0, d3.max(data, function(d){ return d.xAxis; })])
y.domain(data.map(function(d) { return d.yAxis; }));
//y.domain([0, d3.max(data, function(d) { return d.prereqs; })]);
// append the rectangles for the bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("y", function(d) { return y(d.yAxis); })
.attr("height", y.bandwidth())
.transition()
.duration(1000)
.delay(function(d, i) {
return i * 100
})
.attr("width", function(d) {return x(d.xAxis); } );
// add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickValues(d3.range(x.domain()[0], x.domain()[1] + 1, 1))
.tickFormat(d3.format("d"))
);
svg.append("g")
.attr("class", "yAxis")
.call(d3.axisLeft(y));
I got this code from a codepen and I've been trying to adapt it, but it has been breaking so I stopped and was hoping you could help.
Thanks.
For showing the background for the bars, just copy your selection and chose a value of 100% for the rectangles, in a light gray fill:
var backgroundBar = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
//etc...
.attr("width", function(d) {
return x(100);
});
Of course, you'll have to change the domain of the x scale:
var x = d3.scaleLinear()
.domain([0, 100]);
Then, drop both axis and print the labels using a text selection.
Finally, use another text selection for the values:
var values = svg.selectAll(null)
.data(data)
.enter()
.append("text")
//etc...
.text(function(d) {
return +d.xAxis
})
If you want, you can tween the text:
.attrTween("text", function(d) {
var self = this
var i = d3.interpolateNumber(0, +d.xAxis);
return function(t) {
return d3.select(self).text(~~i(t));
}
});
This is the result:
var data = [{
"yAxis": "score",
"xAxis": "72"
}];
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 80
},
width = 500 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
var y = d3.scaleBand()
.range([height, 0])
.padding(0.4);
var x = d3.scaleLinear()
.range([0, width])
.domain([0, 100]);
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 200)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
y.domain(data.map(function(d) {
return d.yAxis;
}));
var backgroundBar = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("fill", "lightgray")
.attr("y", function(d) {
return y(d.yAxis);
})
.attr("height", y.bandwidth())
.attr("width", function(d) {
return x(100);
});
var bar = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("y", function(d) {
return y(d.yAxis);
})
.attr("height", y.bandwidth())
.transition()
.duration(2000)
.delay(function(d, i) {
return i * 100
})
.attr("width", function(d) {
return x(d.xAxis);
});
var labels = svg.selectAll(null)
.data(data)
.enter()
.append("text")
.attr("y", function(d) {
return y(d.yAxis) + y.bandwidth() / 2;
})
.attr("x", -10)
.attr("text-anchor", "end")
.text(function(d) {
return d.yAxis
});
var values = svg.selectAll(null)
.data(data)
.enter()
.append("text")
.attr("y", function(d) {
return y(d.yAxis) + y.bandwidth() / 2;
})
.attr("x", 10)
.text(function(d) {
return +d.xAxis
})
.transition()
.duration(2000)
.delay(function(d, i) {
return i * 100
})
.attr("x", function(d) {
return x(d.xAxis) + 10;
})
.attrTween("text", function(d) {
var self = this
var i = d3.interpolateNumber(0, +d.xAxis);
return function(t) {
return d3.select(self).text(~~i(t));
}
});
<script src="https://d3js.org/d3.v4.min.js"></script>
Please look at this index.html with simple d3.js scaling:
<body>
<script>
var canvasWidth = 750;
var canvasHeight = 600;
// Setup scales
d3.json("tt.json",
function(data)
{
var widthScale = d3.scaleLinear()
.domain([0, 3])
.range(0, canvasWidth);
var colorScale = d3.scaleLinear()
.domain([0, 10])
.range(["red", "blue"]);
var canvas = d3.select("body")
.append("svg")
.attr("width", canvasWidth)
.attr("height", canvasHeight)
canvas.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", function(d) { return widthScale(d.t_count); })
.attr("height", 30)
.attr("y", function(d, i) { return i * 50; })
.attr("fill", "blue")
canvas.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("fill", "black")
.attr("y", function(d, i) { return (i * 50 + 22); })
.text(function(d) { return d.ttext; })
}) // of function(data)
</script>
</body>
The .json file on the server looks like this:
[{"ttext":"Architect","t_count":"1"},
{"ttext":"Entertainment","t_count":"2"},
{"ttext":"Food","t_count":"2"},
{"ttext":"Gujarati","t_count":"1"},
{"ttext":"Laundry","t_count":"1"},
{"ttext":"Milk","t_count":"2"},
{"ttext":"Rajasthani","t_count":"1"}]
The output is just this:
(nothing but the ttext attribute from the json file.)
A hardcoded factor i.e., return (d.t_count * 30); does plot the rectangle. But when the scaling is introduced, the text is all that is output.
Here's the SVG DOM that is generated (strangely missing width in the rect):
Struggling with this for a day almost. Can anyone review and please help?
In a scale, both domain and range have to be arrays.
So, change to this:
var widthScale = d3.scaleLinear()
.domain([0, 3])
.range([0, canvasWidth]);//range as an array