How to center rectangles on position in D3.js? - javascript

I have a graph where I have both circles and rectangles along the ticks of my x-axis. I'd like to be able to center all elements on the ticks of the x-axis. Circles are automatically placed in the center and scaled with a radius attribute, but with rectangles, I am not able to move it as I am using an ordinal scale to create my x-axis values.
This is how much graph looks like: http://puu.sh/gmCkZ/37ab176161.png
It can be seen that the squares upper-left corner is the position I want to be it's center.
My code for the x-axis looks like the following:
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], 1);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
d3.tsv("data.tsv", type, function(error, data) {
x.domain(data.map(function(d) { return d.name; }));
How can i center a figure like the square in a certain position? In this case, on a specific area of the x-axis?
EDIT
Added SVG code for square:
var square = svgbody
.selectAll("nodes.rect")
.data(["B", "L"]);
square.exit()
.style("opacity", 1)
.transition()
.duration(500)
.style("opacity", 0)
.remove();
square.enter()
.append("rect")
.attr("class", "squareNodes");
square
.attr("x", function(d){ return x(d);})
.attr("y", function(d){return y(statusText);} )
.attr("width",19)
.attr("height",19)
.attr("rx", "3")
.attr("ry", "3" )
.style('opacity', NODE_OPACITY)
.style('fill', "green");

squareSize = 19;
square
.attr("x", function(d){ return x(d) - squareSize/2;})
.attr("y", function(d){return y(statusText) - squareSize/2;} )
.attr("width",squareSize)
.attr("height",squareSize)
...

If the square is 50px wide, and you have a class called .square:
.square{
position: relative;
right: 25px;
}
Just move it over half. See if that works.
Here is a fiddle to show you what I mean.
http://jsfiddle.net/plushyObject/hkwwxq9j/

Related

How do I draw gridlines in d3.js with zoom and pan

I have been able to make a scatter plot with zoom and pan functionality where the axes scale properly and everything works well. Now I am trying to figure out how to add gridlines, but running into some issues. I have started with only adding x-axis gridlines to figure things out. I have attached a fiddle with a working example to build from.
I commented out the initial gridlines when the graph is generated, because they would remain after zooming causing clutter, and I will add them back later when I get things working. When zooming the gridlines appear to be drawn correctly, but they do not match up with the x-axis labels, and the x-axis labels disappear after zooming or panning.
If you comment out line 163 and uncomment line 164 you can see the basic graph without any gridlines. Clicking the plot button will always generate a new graph. I have left behind some commented out code of different things that I have tried from searching through stackoverflow.
Example is using d3.js - 5.9.2
JSFiddle: https://jsfiddle.net/eysLvqkh/11/
HTML:
<div id="reg_plot"></div>
<button id="b" class="myButton">plot</button>
Javascript:
var theButton = document.getElementById("b");
theButton.onclick = createSvg;
function createSvg() {
// clear old chart when 'plot' is clicked
document.getElementById('reg_plot').innerHTML = ""
// dimensions
var margin = {top: 20, right: 20, bottom: 30, left: 55},
svg_dx = 1200,
svg_dy =600,
chart_dx = svg_dx - margin.right - margin.left,
chart_dy = svg_dy - margin.top - margin.bottom;
// data
var y = d3.randomNormal(400, 100);
var x_jitter = d3.randomUniform(-100, 1400);
var d = d3.range(1000)
.map(function() {
return [x_jitter(), y()];
});
// fill
var colorScale = d3.scaleLinear()
.domain(d3.extent(d, function(d) { return d[1]; }))
.range([0, 1]);
// y position
var yScale = d3.scaleLinear()
.domain(d3.extent(d, function(d) { return d[1]; }))
.range([chart_dy, margin.top]);
// x position
var xScale = d3.scaleLinear()
.domain(d3.extent(d, function(d) { return d[0]; }))
.range([margin.right, chart_dx]);
// y-axis
var yAxis = d3.axisLeft(yScale);
// x-axis
var xAxis = d3.axisBottom(xScale);
// append svg to div element 'reg_plot' and set zoom to our function named 'zoom'
var svg = d3.select("#reg_plot")
.append("svg")
.attr("width", svg_dx)
.attr("height", svg_dy);
svg.call(d3.zoom().on("zoom", zoom));
// clip path - sets boundaries so points will not show outside of the axes when zooming/panning
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("id", "clip-rect")
.attr("x", "0")
.attr("y", "0")
.attr('width', chart_dx)
.attr('height', chart_dy);
// plot data
var circles = svg.append("g")
.attr("id", "circles")
.attr("transform", "translate(75, 0)")
.attr("clip-path", "url(#clip)")
.selectAll("circle")
.data(d)
.enter()
.append("circle")
.attr("r", 4)
.attr("cx", function(d) { return xScale(d[0]); })
.attr("cy", function(d) { return yScale(d[1]); })
.style("fill", function(d) {
var norm_color = colorScale(d[1]);
return d3.interpolateInferno(norm_color)
});
// add y-axis
var y_axis = svg.append("g")
.attr("id", "y_axis")
.attr("transform", "translate(75,0)")
.call(yAxis).style("font-size", "10px")
// add x-axis
var x_axis = svg.append("g")
.attr("id", "x_axis")
.attr("transform", `translate(${margin.left}, ${svg_dy - margin.bottom - margin.top})`)
.call(xAxis).style("font-size", "10px")
// add x and y grid lines
x_axis.call(xAxis.scale(xScale).ticks(20).tickSize(-chart_dy));
y_axis.call(yAxis.scale(yScale).ticks(20).tickSize(-chart_dx));
function zoom(e) {
// re-scale y axis during zoom
y_axis.transition()
.duration(50)
.call(yAxis.scale(d3.event.transform.rescaleY(yScale)));
// re-scale x axis during zoom
x_axis.transition()
.duration(50)
.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
// re-draw circles using new scales
var new_xScale = d3.event.transform.rescaleX(xScale);
var new_yScale = d3.event.transform.rescaleY(yScale);
// re-scale axes and gridlines
x_axis.call(xAxis.scale(new_xScale).ticks(20).tickSize(-chart_dy));
y_axis.call(yAxis.scale(new_yScale).ticks(20).tickSize(-chart_dx));
circles.data(d)
.attr('cx', function(d) {return new_xScale(d[0])})
.attr('cy', function(d) {return new_yScale(d[1])});
}
}
For anyone looking, I have solved this problem. I have updated the javascript in the original post, and updated the jsfiddle. If you are copying this code to your local machine where you are using d3.js 7.4.4 or higher then you need to change the lines that say d3.event.transform.... to just e.transform.

D3 custom text label axis

I'm trying to customize my x axis on a d3 chart; I want to add two labels, "left" and "right" at both ends of it.
I tried this, but it doesn't work:
var xlabels = ["Left", "Right"]
var xAxis = d3.svg.axis()
.scale(xScale)
.tickValues(xScale.domain())
.orient("bottom")
.ticks(2)
.tickFormat(xlabels)
;
Do you know how to do it?
axis.tickFormat as the name implies defines the label for each of the ticks, if you want to add new labels at both ends you need to add them on your own:
Assuming that you have a reference to the root svg in the var svg
svg.append('text')
.attr('text-anchor', 'start')
.attr('y', function () {
// y position of the left label
// typically a value less than the height of the svg
})
.attr('x', function () {
// x position of the left label
// typically a value near to 0
})
.text('Left')
svg.append('text')
.attr('text-anchor', 'end')
.attr('y', function () {
// y position of the right label
// typically a value less than the height of the svg
})
.attr('x', function () {
// x position of the right label
// typically a value near the width of the svg
})
.text('Right')
Also have a look at http://bl.ocks.org/mbostock/1166403, these lines define a label like the one you need:
svg.append("text")
.attr("x", width - 6)
.attr("y", height - 6)
.style("text-anchor", "end")
.text(values[0].symbol);

rangeRoundBands outerPadding in bar chart way too big

I am new to D3.js and have a problem with my vertical bar chart. For some reason, the distance between the axis and the bars is way too big when I use rangeRoundBands for scaling.
In the API, it is explained like this:
So the problem seems to be the outerPadding. But setting the outerPadding to zero does not help. However, when I use rangeBands instead, the problem disappears and the bars are positioned correctly, right below the axis. But then I will get these nasty antialiasing effects, so this is not really an option. Here is my code:
var margin = {top: 40, right: 40, bottom: 20, left: 20},
width = 900 - margin.left - margin.right,
height = x - margin.top - margin.bottom;
var x = d3.scale.linear().range([0, width]);
var y = d3.scale.ordinal().rangeRoundBands([0, height], .15, 0);
var xAxis = d3.svg.axis()
.scale(x)
.orient("top");
var xAxis2 = d3.svg.axis()
.scale(x)
.orient("bottom");
var xAxis3 = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(-height, 0, 0)
.tickFormat("");
var svg = d3.select("#plotContainer").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 + ")");
x.domain(d3.extent(data, function(d) {
return d.size;
})).nice();
y.domain(data.map(function(d) {
return d.name;
}));
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis2);
svg.append("g")
.attr("class", "grid")
.attr("transform", "translate(0," + height + ")")
.call(xAxis3);
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", function(d) {
return d.size < 0 ? "bar negative" : "bar positive";
})
.attr("x", function(d) {
return x(Math.min(0, d.size));
})
.attr("y", function(d) {
return y(d.name);
})
.attr("width", function(d) {
return Math.abs(x(d.size) - x(0));
})
.attr("height", y.rangeBand())
.append("title")
.text(function(d) {
return "This value is " + d.name;
});
;
svg.selectAll(".bar.positive")
.style("fill", "steelblue")
.on("mouseover", function(d) {
d3.select(this).style("fill", "yellow");
})
.on("mouseout", function(d) {
d3.select(this).style("fill", "steelblue");
});
svg.selectAll(".bar.negative")
.style("fill", "brown")
.on("mouseover", function(d) {
d3.select(this).style("fill", "yellow");
})
.on("mouseout", function(d) {
d3.select(this).style("fill", "brown");
});
svg.selectAll(".axis")
.style("fill", "none")
.style("shape-rendering", "crispEdges")
.style("stroke", "#000")
.style("font", "10px sans-serif");
svg.selectAll(".grid")
.style("fill", "none")
.style("stroke", "lightgrey")
.style("opacity", "0.7");
svg.selectAll(".grid.path")
.style("stroke-width", "0");
EDIT:
Please take a look at this fiddle:
http://jsfiddle.net/GUYZk/9/
My problem is reproducible there. You cannot alter the outerPadding with rangeRoundBands, whereas rangeBands behaves normal.
TL;DR: this is a consequence of the math. To work around, use rangeBands to lay out the bars, and use shape-rendering: crispEdges in CSS to align them to pixel boundaries.
Full explanation:
Because the rangeRoundBands function must distribute the bars evenly throughout the provided pixel range AND it must also provide an integer rangeBand, it uses Math.floor to chop off the fractional bit of each successive bar.
The reason this extra outer padding compounds with longer datasets is because all those fractional pixels have to end up somewhere. The author of this function chose to evenly split them between the beginning and the end of the range.
Because the fraction pixel of each rounded bar is on the interval (0, 1), the extra pixels glommed onto each end will span about 1/4 of the data bar count. With 10 bars, 2-3 extra pixels would never be noticed, but if you have 100 or more, the extra 25+ pixels become much more noticeable.
One possible solution that appears to work in Chrome for svg:rect: use rangeBands to lay out, but then apply shape-rendering: crispEdges as a CSS style to your bar paths/rects.
This then leaves the onus on the SVG renderer to nudge each bar to a pixel boundary, but they are more evenly spaced overall, with occasional variance in the spacing to account for the error over the whole chart.
Personally, I use shape-rendering: optimizeSpeed and let the rendering agent make whatever tradeoffs it must to quickly render the (potentially fractional) bar positions.
I know this is old, but I came to this answer with the same problem; Ben's answer suggests another simple (but likely slower) workaround; you could fix a padding by shifting all x values to the left by the first value in the computed range.
Example:
// set your desired gap between y-axis and first bar
var yGap = 5;
// create your scale
var xScale = d3.scale.ordinal()
.domain(dataArray)
.rangeRoundBands([0, width], .1);
// obtain the *computed* left edge of the first bar
var leftOuter = xScale.range()[0];
// when setting the x attribute of your rects:
.attr("x", function(d) { return xScale(d) - leftOuter + yGap; });
The last line shifts everything to the left by an amount such that the left outer padding is your chosen yGap and the right outer padding is whatever it needs to be to make up the difference. Essentially this overrides the creator's intention of splitting the excess padding between the left and right sides.
I hope someone else finds this useful!
I have the same issue, and no matter I used for inner padding or outer padding, I just could not get the ticks align at the center of my vertical bars on the X-Axis. So I have not used the inner or outer padding. I have used my own padding, say 0.1.
for the bar rect, I set the width as
width: (1.0 - padding) * xScale.rangeBand()
for the x I just add half of the padding like this.
x: padding * xScale.rangeBand() / 2
This made the ticks perfectly align with the vertical bands.

d3js graph plotting at incorrect coordinates

i was trying to draw simple d3js graph.I got through drawing the axis and even plotted the data but the data isn't appearing where it is expected to be.
As per my json data below
var d1 = [{
value1: "30",
value2: "10"
}];
i'm trying to plot a circle at coordinates x axis 30 and y axis 10but the circle on the graph appears some where else.
Here is the jsfiddle demo
Here is my code
var d1 = [{
value1: "30",
value2: "10"
}];
function Update(){
var circles = vis.selectAll("circle").data(d1)
circles
.enter()
.insert("svg:circle")
.attr("cx", function (d) { return d.value1; })
.attr("cy", function (d) { return d.value2; })
.style("fill", "red")
circles
.transition().duration(1000)
.attr("cx", function (d) { return d.value1; })
.attr("cy", function (d) { return d.value2; })
.attr("r", function (d) { return 5; })
circles.exit ()
.transition().duration(1000)
.attr("r", 0)
.remove ();
}
/*************************************************/
/*******************Real Stuff starts here*******************/
var vis = d3.select("#visualisation"),
WIDTH = 600,
HEIGHT = 400,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 50
},
xRange = d3.scale.linear().range([MARGINS.left, WIDTH - MARGINS.right]).domain([0,100]),
yRange = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([0,300]),
xAxis = d3.svg.axis() // generate an axis
.scale(xRange) // set the range of the axis
.tickSize(5) // height of the ticks
.tickSubdivide(true), // display ticks between text labels
yAxis = d3.svg.axis() // generate an axis
.scale(yRange) // set the range of the axis
.tickSize(5) // width of the ticks
.orient("left") // have the text labels on the left hand side
.tickSubdivide(true); // display ticks between text labels
function init() {
vis.append("svg:g") // add a container for the axis
.attr("class", "x axis") // add some classes so we can style it
.attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")") // move it into position
.call(xAxis); // finally, add the axis to the visualisation
vis.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + (MARGINS.left) + ",0)")
.call(yAxis);
}
init();
$('#btn').click(function(){
Update();
});
It works if you
define the numbers as numbers and not as strings (i.e. value1: 30 instead of value1: "30") and
use the scales you define (i.e. return xRange(d.value1) instead of return d.value1).
Working jsfiddle here.
Your circle is appearing at pixel (30,10), but that doesn't correspond to the place 30,10 as labeled by your axes. Use your scales to set the point's location.
.attr("cx", function (d) { return xRange(d.value1); })
.attr("cy", function (d) { return yRange(d.value2); })
You will need to apply xScale and yScale to your coordinates to transform them into the plotting space.
See this jsFiddle
.attr("cx", function (d) { return xRange(d.value1); })
.attr("cy", function (d) { return yRange(d.value2); })
Actually it is working fine. It is just that top left corner is (0,0) and not bottom left (as I suspect, you must be assuming).
Set both x,y to 0. Circle will appear at top left corner.

Why are there gaps between rects in a D3 linechart?

I'm working on a slightly modified example of the following linechart: http://bl.ocks.org/mbostock/3883245
Here's my JS Fiddle: http://jsfiddle.net/rQ4xE/
The goal is to have blocks of background color different for periods of time. Changing color will not be difficult. However..
Problem is the gaps of different sizes between rects. I suspect this is because coordinates for the line path are decimal and something gets messed up when calculating the width for rects.
My question is what would be a good way to draw the background rects so that there are no gaps in between?
This is how I add the rects:
svg.append("g").selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("class", "area")
.attr("x", function(d) { return x(d.date) })
.attr("y", 0)
.attr("width", function(d, i) {
not_last = i < data.length - 1;
return not_last ? (x(data[i+1].date)-x(d.date)) : 0;
})
.attr("height", height);
I believe I have found a more elegant solution.
The issue can be solved by specifying to the scale to round the numbers to the nearest integer. That can be done by using scale.rangeRound which also sets the scale's interpolator to d3.interpolateRound.
https://github.com/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_rangeRound
I changed this code:
var x = d3.time.scale().range([0, width]);
to this:
var x = d3.time.scale().rangeRound([0, width]);
JS Fiddle: http://jsfiddle.net/HcC9z/2/
There seems to be a float issue in your x-coordinates and your widths. Try to round both of them.
svg.append("g").selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("class", "area")
.attr("x", function(d) { return Math.floor(x(d.date)) })
.attr("y", 0)
.attr("width", function(d, i) {
not_last = i < data.length - 1;
return not_last ? (Math.floor(x(data[i+1].date))-Math.floor(x(d.date))) : 0;
})
.attr("height", height);
EDIT
Here's a fiddle : http://jsfiddle.net/rQ4xE/3/

Categories

Resources