d3: position text element dependent on length of element before - javascript

I'm stuck in d3 (or JavaScript in general).
I want to make a legend with d3. The position of the 9 items should be dependent on each other. More specifically:
This is my simplified array:
var dataset = ["Africa","Asia", "Caribbean", "Central America", "Europe", "Middle East", "North America", "Oceania", "South America"];
On the x-axis, I want to draw the next text 40px futher (to the right) then the last text lable ended. My intention is to have the same space between the circles every time. So the next text is always dependent on the length of the last country name.
I tried this:
.attr("x", function(d, i) {return i * 40 + d[i-1].length + 7;})
but the console says d[i-1] is undefined.
What am I missing? How would you solve this?
Many thanks in advance! Your help is very much appreciated!
Ewa
UPDATE:
Actually the legend I want to draw not only consists of the text, but also little circles.
Here is the array (with hard coded x_pos as d[2]: var dataset = [
["Africa", "#4B7985", 5], ["Asia", "#58AB86", 55], ["Caribbean", "#63A4B5", 100], ["Central America", "#818181", 165], ["Europe", "#E9726C", 255], ["Middle East", "#E3AC73", 310], ["North America", "#B65856", 383], ["Oceania", "#287E5C", 470], ["South America", "#AC8358", 530]
];
How do I draw the circles dependent on the length of the country names and get the same spacing between the cirlces?

You can draw text element to get bounding box on canvas. Then adjust position based on the last element's width:
svg.selectAll("text")
.data(data).enter()
.append("text")
.attr("x", function(d) {
return x_pos;
})
.attr("y", 50)
.style("display", "none")
.text(function(d) { return d; });
svg.selectAll("text")
.style("display", "block")
.attr("x", function(d) {
var c_pos = x_pos;
x_pos = x_pos + this.getBBox().width + distance;
return c_pos;
});
Full example: https://vida.io/documents/C5CSjbWLhoJ8rQmhF

This is how I would do it.
//This will be your array of legends
var legendItems = []
var legendCount = legendItems.length;
var legendSectionWidth = width / (legendCount+1);
//This will be your "y" value going forward. You can assign anything you want. I have used the following if else case, you should replace it with your logic to calculate y.
var vert = 0;
if(legendPosition == "bottom"){
if(showAxes)
vert = height + legendVerticalPad + containerHeight*0.04;
else
vert = height + legendVerticalPad + containerHeight*0.02;
}
for(var i = 0; i < legendCount; i++){
var text = svg.append('text')
.attr('x', (i+1)*legendSectionWidth)
.attr('y', vert)
.attr('class', 'legend-text '+legendItems[i])
.style('text-anchor', 'middle')
.style('dominant-baseline', 'central')
.text(function() {
return legendItems[i];
});
var len = text[0][0].getComputedTextLength();
// you could use circles here, just change the width n height to rx and x and y to cx and cy// you could use circles here, just change the width n height to rx and x and y to cx and cy`enter code here`
//The value 5 is your choice, i use 5px between my rect and text
svg.append('rect')
.attr('x', (i+1)*legendSectionWidth - len/2 - 5 - legendRectSize)
.attr('y', vert - legendRectSize/2)
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.attr('class', function () { return 'legend '+ legendItems[i];} )
.attr('label', function() {
return legendItems[i];
})
}
The result is something like this
The following images prove that the legends(combo of rect and text) are equi-distant from each and place right in the center of the provided width. And with this logic, no matter what is the no of legends you need, all will be placed equi-distant from each other and show up right in the middle of the screen
I hope this helps.

First off, d refers to an individual, bound data point, while i refers to its index in the dataset. To look at the previous data point, you would need to reference the original dataset, rather than the provided datapoint.
Assuming you had:
var dataset = ["Africa","Asia", "Caribbean", "Central America", "Europe", "Middle East", "North America", "Oceania", "South America"];
d3.select('.foo').data(dataset)....
You would want to change your d[i - 1] references in your position handler to dataset[i - 1]
With that fixed, your first element will still blow up, since it's at dataset[0]. dataset[i - 1] is dataset[-1] in that case.
You could change your return statement to:
return i ? (i * 40 + dataset[i-1].length + 7) : i;

Related

D3: Select a circle by x and y coordinates in a scatter plot

Is there any possibility in d3.js to select the elements by their position, i.e. by their x and y coordinates? I have a scatter plot which contains a large amount of data. And i have also an array of coordinates. the dots with these coordinates should be red. I am doing something like this for that:
bestHistory() {
var that = this;
var best = d3.select("circle")
.attr("cx", that.runData[0].best_history[0].scheduling_quality)
.attr("cy", that.runData[0].best_history[0].staffing_cost)
.classed("highlighted", true)
}
This method should set the class attribute of the circles on this certain positions equal to highlighted.
And then the appropriate CSS:
circle.highlighted {
fill: red;
}
But instead getting red this dot just disappears.
How can I achieve that what I want to ?
You can calculate the actual distance of each point to the point of interest and determine points color based on this distance like:
var threshold=...
var p =...
d3.select('circle').each(function(d){
var x = p.x - d.x;
var y = p.y - d.y;
d.distance = Math.sqrt(x*x + y*y);
}).attr('fill', function(d){
return d.distance < threshold? 'red' : 'blue'
})
Ps. Sorry, answered from mobile

d3.js Checking/Count series chart

I am working on an application that uses foursquare data.
//this is the series chart that has had some delving into - but there are some bugs still running around here.
So we have a batch of data - Health & Beauty, Restaurants, Cafe, Public Houses.
-- there would be a COUNT of them -- and a SUMMATION of checkout information.
So I want this chart to be able to show the NUMBER of venues, but also indicate how POPULAR they are.. so for example the number of pubs may be smaller, but the number of checkins higher as they are more popular. So in that instance want to reverse the colors of the circles.
There are some bugs with the current code attempts.
the swapping of the circles/circle spacing causes tears in black paths and odd behaviors
with the lines I would like to have a black line under the blue circle, but inside the blue circle show a cropped circle path orange line -- so a kind of masking ability.
_latest jsfiddle
phase1
using "V" instead of "L" but couldn't make it work properly for the time being.
phase 2
I think it works more consistently but there are some issues. Also, I am not sure about the data and the scaling of the circles. (I've added extra labels so that it is visible what the value of the circles are)
phase 3
changed the getCircleSize a bit even though I believe a more consistent thing to do would be something like this layerSet.push(parseInt(getPercentage(layerArray[i], meansPerGroup[0])*60, 10));
so here the first step draws the circles by size order first... so in this case by count.. but maybe there is a bug here reversing the color to indicate the checkin count instead - so maybe we need to sort by count,checkin order - that way the first circle to get painted follows correctly.
// Create Circles
function setCircles(items) {
// sort elements in order to draw them by size
items.sort(function(a, b) {
return parseFloat(b.value) - parseFloat(a.value);
});
var circlelayer = svg.append("g")
.attr("class", "circlelayer");
var circle = circlelayer.selectAll("circle")
.data(items);
circle.enter().append("circle")
.attr("class", function(d, i) {
if (d.l == 0) {
return "blue";
}
return "gold";
})
.attr("cy", 60)
.attr("cx", function(d, i) {
var distance = calculateDistance(d, items);
if (d.l == 1) {
distancesL1.push(distance);
} else {
distancesL0.push(distance);
}
return distance;
})
.attr("r", function(d, i) {
return Math.sqrt(d.value);
})
.attr("filter", function(d) {
return "url(#drop-shadow)";
});
circle.exit().remove();
}
json structure to look something like this
[{
"label": "Health and Beauty",
"count": 30,
"checkin": 100
}, {
"label": "Restaurants",
"count": 23,
"checkin": 200
}, {
"label": "Cafes",
"count": 11,
"checkin": 900
}, {
"label": "Public Houses",
"count": 5,
"checkin": 1000
}]
I'm not sure I understand what is your problem but I've decided to try to create that chart from you screenshot with sample data from your plunker. Here is my result:
My script is making sure that smaller circle is always on top of the rage one so both circles are always visible.
Here you can find my code:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
html {
font-family: sans-serif;
}
</style>
</head>
<body>
<div id="container">
</div>
<script>
const data = [{
"name": "Twitter",
"vists": "15 billion",
"unit": "per day",
"layer1value": 15000000,
"layer2value": 450
}, {
"name": "Facebook",
"vists": "5 billion",
"unit": "per day",
"layer1value": 4000000,
"layer2value": 5000000
}, {
"name": "Google",
"vists": "5 billion",
"unit": "per day",
"layer1value": 5000000,
"layer2value": 25000
}, {
"name": "Netflix",
"vists": "10 billion",
"unit": "per day",
"layer1value": 3000000,
"layer2value": 2200
}, {
"name": "Ebay",
"vists": "8 billion",
"unit": "per day",
"layer1value": 2500000,
"layer2value": 4900000
}, {
"name": "Klout",
"vists": "2 billion",
"unit": "per day",
"layer1value": 1000000,
"layer2value": 45
}];
/*
* Finding max and min layer size
*/
const values = data.reduce((acumulator, datum) => [...acumulator, datum.layer1value, datum.layer2value], []);
const maxValue = Math.max(...values);
const minValue = Math.min(...values);
/*
* Creating scale based on the smallest and largest layer1value or layer2value
*/
const radiusScale = d3.scaleLinear()
.domain([minValue, maxValue])
.range([10, 150]); // min and max value of the circle
const width = 900;
const height = 500;
const orangeColour = '#ffb000';
const blueColour = '#00a1ff';
// Creating svg element
const svg = d3.select('#container').append('svg').attr('width', width).attr('height', height);
let xPos = 0; // position of circle
/*
* iterate over each datum and render all associated elements: two circles, and two labels with pointer lines
*/
for (let i = 0; i < data.length; i++) {
const d = data[i]; // current data point
const currMaxRadius = radiusScale(Math.max(d.layer1value, d.layer2value)); // get largest radius within current group of two layers
xPos += currMaxRadius; // add that radius to xPos
// create group element containing all view elements for current datum
const group = svg.append('g')
.attr('transform', `translate(${xPos}, ${height / 2})`);
group.append('circle')
.attr('r', radiusScale(d.layer1value))
.style('fill', blueColour);
group.insert('circle', d.layer2value > d.layer1value ? ':first-child' : null) // if layer2value is larger than layer1value then insert this circle before the previous one
.attr('r', radiusScale(d.layer2value))
.style('fill', orangeColour);
xPos += currMaxRadius * 0.9;
/*
* ADDING LABEL UNDERNEATH THE CIRCLES
*/
group.append('text')
.text(d.name)
.attr('dy', radiusScale(maxValue) + 40) // add 40px of padding so label is not just bellow the circle
.style('text-anchor', 'middle');
group.append('line')
.attr('y1', radiusScale(d.layer2value))
.attr('y2', radiusScale(maxValue) + 20) // add 20px of padding so the pointer line is not overlapping with label
.style('stroke', orangeColour);
/*
* ADDING LABEL AT THE ANGLE OF 45deg RELATIVE TO THE CIRCLES
*/
// we are creating new group element so we can easily rotate both line and label by -45deg
const rotatedLabelGroup = group.append('g').style('transform', 'rotate(-45deg)');
rotatedLabelGroup.append('line')
.attr('x1', radiusScale(d.layer2value))
.attr('x2', radiusScale(maxValue) + 20)
.style('stroke', orangeColour);
rotatedLabelGroup.append('text')
.text(d.vists)
.attr('dx', radiusScale(maxValue))
.attr('dy', -5); // this way label is slightly above the line
}
</script>
</body>

Creating variable number of elements with D3.js

I am pretty new to d3.js and maybe my question is very basic, but I haven't been able to find an answer...
I am trying to achieve the following, based on an array of data like this:
var data = [
{
"items": 10,
"selected": 8
},
{
"items": 12,
"selected": 4
}];
I would like to create a row of circles for every element of the array. The number of circles should be the equal to the items property and in every row, the circle in selected position should be special (like a different color). For the example above, it should display something similar to:
OOOOOOO*OO
OOO*OOOOOOOO
For the first step, any tips on how to create a variable number of SVG elements based on data values would be a great help.
Here's an example I made on codepen.
Check out the code below and/or fork the codepen and have a play.
Essentially, what is happening here is that I add a g element for each item in your data array. Normally we might be-able to create a circle for each data element, but since that is contained within a property and variable, I've used an each loop (d3 each). This creates a loop of the number of items, and creates a circle for each. If the element is selected, the fill color changes.
things to note:
The g element has a transform of 30 * i on the y axis. This means that each group will stack down the page.
In the for loop we get access to the g element using this, but we must use the d3 select function to reference it as a d3 object so we can append.
The code:
//add an svg canvas to the page
var svg = d3.select("body")
.append("svg")
.attr("transform", "translate(" + 20 + "," + 20 + ")"); //this will give it a bottom and left margin
//set up some basic data
var data = [
{
"items": 10,
"selected": 8
},
{
"items": 12,
"selected": 4
}];
var groups = svg.selectAll("g")
.data(data)
.enter()
.append("g").attr("transform", function(d, i) {
return "translate(0," + i * 30 + ")";
});
groups.each(function(d,i) {
for (var x = 1; x <= d.items; x++) {
d3.select(this).append('circle').attr({cx: function(d,i) { return (x + 1) * 22; } ,
cy: 10 ,
r: 10 ,
fill: function(d,i) { return x == d.selected ? "yellow" : "blue" }
});
}
});

Color scale not working appropriately in D3/JavaScript [duplicate]

This question already has answers here:
d3.scale.category10() not behaving as expected
(3 answers)
Closed 8 years ago.
Im using a color scale :
var color = d3.scale.category10();
Im using this to color the edges of a force layout graph according to their value
var links = inner.selectAll("line.link")
.style("stroke", function(d) { return color(d.label); });
Now I need a 'legend' to show the user what each color means:
edgesArray = [];
edgesArrayIndex = [];
for (i=0;i<data.edges.length;i++) {
if(!edgesArray[data.edges[i].name])
{
edgesArray[data.edges[i].name]=1;
edgesArrayIndex.push(data.edges[i].name);
}
}
var colourWidth = 160;
var colourHeight = 25;
for(i=0; i<edgesArrayIndex.length; i++){
if (edgeColour == true){
svg.append('rect')
.attr("width", colourWidth)
.attr("height", colourHeight)
.attr("x", Marg*2)
.attr("y", Marg*2 + [i]*colourHeight)
.style("fill", color(i))
;
svg.append("text")
.attr("x", Marg*3)
.attr("y", Marg*2 + [i]*colourHeight + colourHeight/2)
.attr("dy", ".35em")
.text(color(i) + " : " + data.edges[i].name);
}
//console.log(edgesArrayIndex);
}
The colors are all wrong. When I attach the colors to the graph the first 6 colors get attached to the edges as there are 6 different types of edges.
But when I apply the color scale to the 6 'rects' I appended to the SVG its as if the first 6 colors of the color scale array got used up when applying then to the edges and when i do the for loop starting at color(0) it actually gives me the color at color(5) (as 0 is first).
For example, say ive got red,blue,green,white,black,pink,orange,yellow,indigo,aqua.
My edges get - red,blue,green,white,black,pink
and now when i want to apply the scale to the rects I made, I would expect the rects to have the same values - red,blue,green,white,black,pink.
But they actually have : orange,yellow,indigo,aqua, red, blue.
They start at color[5] and its as if they wrap round to the beginning and go back to red, blue and so on.
You are using two different parts of the data for the colour scale -- first the label, then the name. By definition, this will give you different colours (different inputs map to different outputs as far as possible given the output range).
To get the same colours, pass in the same attributes, e.g. the labels (if I understood your data structure correctly):
svg.append('rect')
.style("fill", color(data.edges[i].label))
this question helped me -
d3.scale.category10() not behaving as expected
basically you have to put a domain on the scale. So instead of
var color = d3.scale.category10();
Change it to:
var color = d3.scale.category10().domain([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);//-colour scale

D3 rect + color does not display

I am toying around with D3 to create a heat map. I am creating NxN squares which color I would like to change adjust. However, only the first square displays in blue, the remaining are there according to the html inspector and have the color set, but they dont show up on the page.
size = 30;
length = myJSON.length;
numRows = length /2;
numCols = length / 2;
var svg = d3.select("div#heatchart").append("svg").attr("width",size).attr("height",size);
svg.selectAll("rect").data(myJSON).enter().append("rect").attr("x",function(d,i){
var x = Math.floor(i%numRows) * (size+1);
return x;
}).attr("y",function(d,i){
var y = Math.floor(i/numCols) * (size+1);
return y;
}).attr("width",function(d,i){
return size;
}).attr("height",function(d,i){
return size;
}).attr("fill", function(d,i) {
return "rgb(0, 0, 255)";
});
The reason you're only seeing one square is that you've set the dimensions of your <svg> element to be equal to the variable size which is only 30. The other squares are being drawn, but they are outside the bounds of the svg document and so are invisible.
Once you fix that, you are still going to run into issues with the layout because you are setting the number of rows and columns based on dividing length in half. What you really want to do to make an NxN square is to base the number of rows and columns on the square root of length. You can use Math.ceil to round up in case your data length is not a perfect square. Try it like this:
var size = 30;
var n = myJSON.length;
var numRows = Math.ceil(Math.sqrt(n)),
numCols = numRows;
Then you can set the size of your svg based on how many rows and columns you need to display:
var svg = d3.select('div#heatchart').append('svg')
.attr('width', size * numCols)
.attr('height', size * numRows);
Finally, arrange them in a square, setting the fill to blue:
svg.selectAll('rect')
.data(myJSON)
.enter().append('rect')
.attr('x', function(d,i) { return (i % numCols) * size; })
.attr('y', function(d,i) { return Math.floor(i / numRows) * size; })
.attr('width', size)
.attr('height', size)
.attr('fill', 'blue');
HERE is an example.
You need an svg group element (<g> - this is not true see edit).
Modify so it looks like this
var svg = d3.select("div#heatchart")
.append("svg")
.append("g")
.attr("width", size)
.attr("height", size);
e.g. http://jsfiddle.net/4tz5wk91/
edit
This example works but not for the reason stated. The attr functions no longer affect the svg element and as a result it gets a default size which is big enough to display all elements. The width and height attributes applied to the group actually have no effect in this case. See #jshanley's more complete and correct solution.

Categories