D3 Javascript - Repeat a group of shapes with data bind - javascript

I want to repeat a group of shapes specifically
text rect text circles
Where in circles is again a repeat of circle
My data is
Jsondata =[
{ "name":"A", "WidthOfRect":50, "justAnotherText":"250", "numberOfCircles" :3 },
{ "name":"B", "WidthOfRect":150, "justAnotherText":"350","numberOfCircles" :2 },
{ "name":"C", "WidthOfRect":250, "justAnotherText":"450","numberOfCircles" :1 }]
Basically Out of this data i am trying to construct a customized bar chart.
The width of the rect is based upon the data widthofrect from the json, as well as number of circles is based upon numberofcircles property.
I looked out for a number of options to repeat group of shapes but couldn't find one.

First of all, you're right in your comment: do not use loops to append elements in a D3 code. Also, your supposition about the length of the data is correct.
Back to the question:
The text and rect part is pretty basic, D3 101, so let's skip that. The circles is the interesting part here.
My proposed solution involves using d3.range to create an array whose number of elements (or length) is specified by numberOfCircles. That involves two selections.
First, we create the groups (here, scale is, obviously, a scale):
var circlesGroups = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(20," + scale(d.name) + ")"
});
And then we create the circles. Pay attention to the d3.range:
var circles = circlesGroups.selectAll(null)
.data(function(d) {
return d3.range(d.numberOfCircles)
})
.enter()
.append("circle")
//etc...
Here is a demo, I'm changing the numberOfCircles in your data to paint more circles:
var width = 500,
height = 200;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var data = [{
"name": "A",
"WidthOfRect": 50,
"justAnotherText": "250",
"numberOfCircles": 13
},
{
"name": "B",
"WidthOfRect": 150,
"justAnotherText": "350",
"numberOfCircles": 22
},
{
"name": "C",
"WidthOfRect": 250,
"justAnotherText": "450",
"numberOfCircles": 17
}
];
var scale = d3.scalePoint()
.domain(data.map(function(d) {
return d.name
}))
.range([20, height - 20])
.padding(0.5);
var colorScale = d3.scaleOrdinal(d3.schemeCategory10);
var circlesGroups = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(20," + scale(d.name) + ")"
})
.style("fill", function(d) {
return colorScale(d.name)
})
var circles = circlesGroups.selectAll(null)
.data(function(d) {
return d3.range(d.numberOfCircles)
})
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d) {
return 10 + 12 * d
});
var axis = d3.axisLeft(scale)(svg.append("g").attr("transform", "translate(20,0)"));
<script src="https://d3js.org/d3.v5.min.js"></script>
PS: I'm using D3 v5.

Related

Extra legend present

I adding two legends to my bar chart but I don't know why there is a extra legend appear. I don't know which part of my code is wrong since I only define two legend in my code.
var color_hash = { 0 : ["Male", "blue"],
1 : ["Female", "pink"]}
var legend = svg.append("g")
.attr("class", "legend")
.attr("x", width - 65)
.attr("y", 25)
.attr("height", 100)
.attr("width", 100);
legend.selectAll('g').data(data)
.enter()
.append('g')
.each(function(d, i) {
var g = d3.select(this);
g.append("rect")
.attr("x", width - 65)
.attr("y", i*25)
.attr("width", 10)
.attr("height", 10)
.style("fill", color_hash[String(i)][1]);
g.append("text")
.attr("x", width - 50)
.attr("y", i * 25 + 8)
.attr("height", 30)
.attr("width", 100)
.style("fill", color_hash[String(i)[1]])
.text(color_hash[String(i)][0]);
});
the black rectangle is the extra one:
With the enter/update/exit cycle in D3, you generally want to have a data array that contains one item for every element you want drawn. You have:
a color has object color_hash, this is what you really want to use to draw the legend, and
some data array data, though we don't know what is inside of this.
We are using data to visualize color_hash, this is not ideal.
For one, you only want to plot 2 elements, I can tell you that the length of data is at least 3:
You create an empty g with:
var legend = svg.append("g")
Then you select child g elements of that:
legend.selectAll('g')
Since there are none, this is an empty selection. Then you assign data to this selection and enter new HTML/SVG elements:
legend.selectAll('g')
.data(data)
.enter()
.append('g')
Since legend is an empty selection, the enter selection will create one HTML/SVG element for each item in the data array. After entering (and/or exiting), the number of HTML/SVG elements should be equal to the number of items in the data array. So, data must have at least 3 items in it (it could have more if additional elements are created, but they fall outside of the SVG/container bounds. This also explains why the third box has no color or text: the color hash has no values with key 2 or greater).
D3 is creates elements from data, generally in a one to one relationship between elements and items. To create our legend, the data array should be what we want to plot. As a consequence, we need to convert the color hash to an array:
var legendData = [
{name: "A", color:"crimson"},
{name: "B", color:"steelblue"}
];
Now we just supply that to selection.data()
And, since we are now binding the data we want to draw to the legend entries, we can also simplify the code, instead of:
.style("fill", color_hash[String(i)][1]);
and
.text(color_hash[String(i)][0]);
We can just use:
.style("fill",d.color);
and
.text(d.name);
This gives us:
var color_hash = { 0 : ["Male", "blue"],
1 : ["Female", "pink"]}
var width = 300;
var height = 200;
var svg = d3.select("svg")
.attr("width",width)
.attr("height",height);
var legendData = [
{name:"A",color:"crimson"},
{name:"B",color:"steelblue"}
]
var legend = svg.append("g")
legend.selectAll('g')
.data(legendData)
.enter()
.append('g')
.each(function(d, i) {
var g = d3.select(this);
g.append("rect")
.attr("x", width - 65)
.attr("y", i*25+25)
.attr("width", 10)
.attr("height", 10)
.style("fill", d.color);
g.append("text")
.attr("x", width - 50)
.attr("y", i * 25 + 33)
.attr("height", 30)
.attr("width", 100)
.style("fill", d.color)
.text(d.name);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I'm just focusing on the enter cycle here: there could be further revisions to placement and a different approach to nested appends can offer benefits compared to appending children with .each

How to add multiple images to svg in D3 (quantity based on value in dataset)

How to apply an image to svg multiple times, with the quantity based on value from a dataset? I'm trying to do smth like this:
dataset:
var dataset = [{"attribute": "att1", "data": "5"}, {"attribute": "att2", "data": "10"}]
SVG:
att1
$$$$$
att2
$$$$$$$$$$
...with $ being an image.
Below code adds an image based on number of attribute occurencies instead of the "data". So I get 2 images. How to get 2 sets of images with 5 and 10 occurencies?
var w = 300;
var h = 300;
var p = 20;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
.style("border", "1px solid black");
var img = svg.selectAll("image")
.data(dataset)
.enter()
.append("svg:image")
.attr("x", function(d,i) {return (i * 10);})
.attr("width", 50)
.attr("height", 50)
.style("border", "1px solid black")
.attr("xlink:href", "images/xyz.png");
Here is a solution using d3.range() to set the number of repetitions.
In this solution, I'm binding the data to the parent groups...
var groups = svg.selectAll("foo")
.data(data)
... and, in the image selection, I'm using an array whose length is determined by the property data in the dataset:
var images = groups.selectAll("bar")
.data(d => d3.range(d.data))
Check the demo:
var dataset = [{
image: "http://icons.webpatashala.com/icons/Blueberry-Basic-Icons/Png/rss-icon.PNG",
data: 5
}, {
image: "http://www.ucdmc.ucdavis.edu/global/images/icons/instagram-32x32.png",
data: 3
}, {
image: "http://www.axadledirect.com/assets/images/icons/share/32x32/google.png",
data: 8
}];
var svg = d3.select("svg");
var groups = svg.selectAll("foo")
.data(dataset)
.enter()
.append("g")
.attr("transform", (d, i) => "translate(10," + (10 + i * 40) + ")");
var images = groups.selectAll("bar")
.data(d => d3.range(d.data))
.enter()
.append("svg:image")
.attr("x", function(d, i) {
return (i * 35);
})
.attr("xlink:href", function(d) {
return d3.select(this.parentNode).datum().image
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

Wrong position and radius on update pack layout

I'm having some problems while updating a pack layout that is composed by a circle inside a g element, where the translation is controled by g and the radius is controled by the circle:
<g transform="translate(1,1)"><circle r="1"></circle></g>
The update was possible with the translation controlled by the circle, but I'm having the wrong x, y and r with a g as a container, as in demo.
var diameter = 300;
function translate(x, y) {
return "translate(" + x + "," + y + ")";
}
// The layout I'm using now
var pack = d3.layout.pack()
.size([diameter - 4, diameter - 4])
.value(function(d) { return 1; });
// The basic container
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(2,2)");
// My initial data source
var data = {
name: "Languages",
children: [{
name: "Functional",
children: [
{ name: "OCaml" },
{ name: "Haskell" },
{ name: "Erlang" }
]
}, {
name: "Imperative",
children: [
{ name: "BASIC" },
{ name: "Clipper" }
]
}]
};
(window.update = function() {
// Modify the current data object
data.children.push({
name: "NEW ELEMENT " + Math.floor(Math.random() * 100)
});
// Select *ALL* elements
var selection = svg.datum(data).selectAll(".node")
.data(pack.nodes);
// Select *ONLY NEW* nodes and work on them
selection
.enter()
.append("g")
.classed("node", true)
.append("circle")
.style("fill", "black");
// Recompute *ALL* elements
// Here the results aren't consistent, I always get the wrong translation
// and radius, therefore, the elements are badly positioned
selection
.transition()
.duration(500)
.attr("transform", function(d) {
return translate(d.x, d.y);
})
.selectAll("circle")
.attr("r", function(d) {
return d.r;
});
})();
// Set the height
d3.select(self.frameElement).style("height", diameter + "px");
I see I'm selecting the correct elements, but why I'm having the wrong results whenever I call update?
Well, the code is mostly correct, but selectAll("circle") will select all the circle elements and apply the radius of the inner element for all of them. We need to select just the inner circle, therefore, select("circle") solves the question (demo).

How to delete a d3 pie chart

I am trying to delete the D3 pie chart. I have created the pie chart but what I want to do is on some button click I want to delete the pie chart and want to re-draw the new pie chart with some new data. I didn't find any help from the documentation of D3's website. I am sharing the code for drawing the pie chart below.
var w =300;
var h =300;
var r =150;
data = [{"label": "joy", "value": 20},
{"label": "fear", "value": 20},
{"label": "anger", "value": 20},
{"label": "disgust", "value": 20},
{"label": "sadness", "value": 20}]
var colorArray = ['#f6cf31', '#7e53a3', '#f8522a', '#3cbf55', '#00a7de']
var vis = d3.select(".chart-holder")
.append("svg:svg")
.data([data])
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + r + "," + r + ")")
var arc = d3.svg.arc()
.outerRadius(r);
var pie = d3.layout.pie()
.value(function(d) {
return d.value;
});
var arcs = vis.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice");
arcs.append("svg:path")
.attr("fill", function(d, i) {
return colorArray[i];
})
If you set the id of the pie chart, you can then use it later to remove the element:
var arcs = vis.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice")
.attr("id", "mypiechart");
...
d3.select("#mypiechart").remove();
I have resolved it my self, well anyway thanks #ilivewithian.
The soultion is
var vis = angular.element('svg');
// var vis = d3.select(".chart-holder").selectAll("svg");
vis.remove();
Remove is actually javascript default function to remove any element.
So i get the svg element from the div i bind for pie chart.And removed the svg element.
And then i redraw it,using the same techinique i used for drawing the pie chart first time.
As long as the exit method for d3 data is working the same way like update, per documentation
http://bl.ocks.org/mbostock/3808218
you have first to assign the data to the path then remove it for updating
var path = svg.selectAll('path')
.data(pie(data));
path.enter()
.append('path')
.attr('d', arc)
.attr('class', '.path')
.attr('id', function(d) {
return d.data.key;
})
.attr('fill', function(d, i) {
return d.data.color;
})
// exit
path.exit().remove();
I hope it helps.
You should be able to simply set a new value for your data variable and the chart can update from this.
function setData(){
data = [{"label": "new", "value": 50},
{"label": "old", "value": 50}]
myChart.update();
};
Pulled from the GitHub Repo:
https://github.com/novus/nvd3/wiki/Sample-chart-%28your-first-nvd3-chart!%29

D3, plotting to map, possible data formatting

I am trying to create a map and plot some points to it using d3, I found a few good examples to build off of but I believe i am stuck. My guess is I am not handling the plot points correctly according to how I have the data structured. I could use a bit of help - this is my first attempt. Here's what I have so far :
var m_width = document.getElementById("map").offsetWidth,
width = 938,
height = 500;
var projection = d3.geo.mercator()
.scale(150)
.translate([width / 2, height / 1.5]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("#map").append("svg")
.attr("preserveAspectRatio", "xMidYMid")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("width", m_width)
.attr("height", m_width * height / width);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
var g = svg.append("g");
d3.json("scripts/world-110m2.json", function(error, us) {
g.append("g")
.attr("id", "countries")
.selectAll("path")
.data(topojson.feature(us, us.objects.countries).features)
.enter()
.append("path")
.attr("id", function(d) { return d.id; })
.attr("d", path)
});
svg.selectAll(".pin")
.data(places)
.enter().append("circle", ".pin")
.attr("r", 5)
.attr("transform", function(d) {
return "translate(" + projection([
d.earthquakes.lon,
d.earthquakes.lat
]) + ")"
});
window.addEventListener('resize', function(event){
var w = document.getElementById("map").offsetWidth;
svg.attr("width", w);
svg.attr("height", w * height / width);
});
And the "places" data is structured like so
var places = {"count":"392","earthquakes":[{"src":"us","eqid":"2010sdbk","timedate":"2010-01-31 15:18:44","lat":"-18.7507","lon":"169.3940","magnitude":"5.1","depth":"231.50","region":"Vanuatu"}
Where all the place are inside an object array "earthquakes" inside places. (lon and lat specifically inside of that).
The world map shows up fine, I am just having trouble getting these plot points to work. Would appreciate any help greatly. Thanks for reading!!
You almost had it, but couple problems here:
1.) The data you pass to .data should be an array (of where to add your circles).
2.) In your places object, you lat/lon are strings and need to be converted to numbers.
Try:
var places = {
"count": "392",
"earthquakes": [{
"src": "us",
"eqid": "2010sdbk",
"timedate": "2010-01-31 15:18:44",
"lat": "-18.7507",
"lon": "169.3940",
"magnitude": "5.1",
"depth": "231.50",
"region": "Vanuatu"
}]
};
svg.selectAll(".pin")
.data(places.earthquakes) //<-- pass array
.enter()
.append("circle")
.attr("class","pin")
.attr("r", 5)
.attr("transform", function(d) {
return "translate(" + projection([
+d.lon, //<-- coerce to number
+d.lat
]) + ")";
});
Example here.

Categories

Resources