Sergei Basharov
Sergei Basharov

Reputation: 53850

Make circles not go outside of the chart bounds with D3

I am working on a chart looking like this now: Chart

I use d3 scales and ranges to setup sizes and coordinates of circles, from JSON data.

All works fine but I need to make sure those circles that are close to extreme values don't overlap the sides of the chart (like orange circle on the top right and blue one on the bottom side), so I think I need to play with ranges and change coordinates in case they overlap or is there a better tried way to do this?

Upvotes: 1

Views: 977

Answers (1)

ninjaPixel
ninjaPixel

Reputation: 6382

When drawing circles, in addition to the x and y scaling functions we also use an r scaling function:

    var rScale = d3.scale.linear()
        .domain([0, maxR])
        .range([0, maxBubbleRadius]);

    var xScale = d3.scale.linear()
        .domain([minX, maxX])
        .range([0, chartWidth]);

    var yScale = d3.scale.linear()
        .domain([minY, maxY])
        .range([chartHeight, 0]);

where maxR is the largest r value in your dataset and maxBubbleRadius is however large you want the largest circle to be, when you plot it.

Using the x and y scaling functions it is easy to calculate where the centre of each circle will be plotted, we can then add on the (scaled) r value to see if the circle will spill over a chart boundary. With a scenario like the first chart below we can see that 4 of the circles spill over. The first step to remedy this is to find out how many vertical and horizontal units we spill over by and then increase the minimum and maximum x and y values to take this into account, before recalculating the xScale and yScale vars. If we were to then plot the chart again, the boundary would move out but there would probably still be some visible spillage (depending on actual values used); this is because the radius for a given circle is a fixed number of pixels and will therefore take up a different number of x and y units on the chart, from when we initially calculated how much it spilled over. We therefore need to take an iterative approach and keep applying the above logic until we get to where we want to be.

spilling circles

The code below shows how I iteratively achieve an acceptable scaling factor so that all the circles will plot without spilling. Note that I do this 10 times (as seen in the loop) - I've just found that this number works well for all the data that I've plotted so far. Ideally though, I should calculate a delta (the amount of spillage) and iterate until it is zero (this would also require overshooting on the first iteration, else we'd never reach our solution!).

updateXYScalesBasedOnBubbleEdges = function() {
var bubbleEdgePixels = [];

// find out where the edges of each bubble will be, in terms of pixels
for (var i = 0; i < dataLength; i++) {
    var rPixels = rScale(_data[i].r),
        rInTermsOfX = Math.abs(minX - xScale.invert(rPixels)),
        rInTermsOfY = Math.abs(maxY - yScale.invert(rPixels));
    var upperPixelsY = _data[i].y + rInTermsOfY;
    var lowerPixelsY = _data[i].y - rInTermsOfY;
    var upperPixelsX = _data[i].x + rInTermsOfX;
    var lowerPixelsX = _data[i].x - rInTermsOfX;
    bubbleEdgePixels.push({
        highX: upperPixelsX,
        highY: upperPixelsY,
        lowX: lowerPixelsX,
        lowY: lowerPixelsY
    });
}

var minEdgeX = d3.min(bubbleEdgePixels, function(d) {
    return d.lowX;
});
var maxEdgeX = d3.max(bubbleEdgePixels, function(d) {
    return d.highX;
});
var minEdgeY = d3.min(bubbleEdgePixels, function(d) {
    return d.lowY;
});
var maxEdgeY = d3.max(bubbleEdgePixels, function(d) {
    return d.highY;
});

maxY = maxEdgeY;
minY = minEdgeY;
maxX = maxEdgeX;
minX = minEdgeX;

// redefine the X Y scaling functions, now that we have this new information
xScale = d3.scale.linear()
    .domain([minX, maxX])
    .range([0, chartWidth]);

yScale = d3.scale.linear()
    .domain([minY, maxY])
    .range([chartHeight, 0]);
};

// TODO: break if delta is small, rather than a specific number of interations
for (var scaleCount = 0; scaleCount < 10; scaleCount++) {
updateXYScalesBasedOnBubbleEdges();
}

}

no spill

Upvotes: 3

Related Questions