I've been trying to use d3.js to display a table using two input strings, I've added the code below. When displaying the second string, only the characters with indices that are greater than the length of the string x are displayed.
I think it's something related to the anonymous functions, when iterating through the second string, i begins with the index value it finished off with in the first string e.g., only "fo" is displayed from the second string instead of "fresihnfo". Can anyone give me some pointers on how to fix this?
Thanks!
var x = ["a", "e", "d", "i", "r", "z"];
var y = ["f", "r", "s", "i", "h", "n", "f", "o"];
var w = (x.length + 1) * 50;
var h = (y.length + 1) * 50;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
/*Displays the first string*/
svg.selectAll("rect")
.data(x)
.enter()
.append("rect")
.attr("x", function(d, i) {
return (i * 45) + 45;
})
.attr("y", "0px")
.attr("width", "40px")
.attr("height", "40px")
.attr("fill", "rgb(0, 0, 102)");
svg.selectAll("text")
.data(x)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return (i * 45) + 65;
})
.attr("y", "27px")
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "white");
/*Displays the second string*/
svg.selectAll("rect")
.data(y)
.enter()
.append("rect")
.attr("x", "0px")
.attr("y", function(d, i) {
return ((i - x.length) * 45) + 45;
})
.attr("width", "40px")
.attr("height", "40px")
.attr("fill", "rgb(0, 0, 102)");
svg.selectAll("text")
.data(y)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", "20px")
.attr("y", function(d, i) {
return ((i - x.length) * 45) + 70;
})
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "white");
Current output:
Desired output would be to have the rest of the string y displayed in the left column.
As Marc said, the problem is that your second selection each time is being interpretted as an update to your first selection, not as a new set of elements. And since you only work on the enter() part of the update, you don't even see the fact that you've changed the data for your first set of rectangles.
To confirm that the first set of rectangles have been given the data from your second array, right-click on one, choose "Inspect Element" and then open up the properties tab in your inspector -- the __data__ property holds the element's D3 data object.
So how do you fix it? You need a way of distinguishing between the two groups of rectangles in your select statement. There are two options:
Option 1: Use sub-selections on SVG group elements (<g>)
You've got two groups of rectangles, so it makes sense to use the svg grouping element to keep them organized. Instead of adding your rectangles directly to your svg, add a group element to the svg and then add the rectangles/text to it.
svg.append("g").selectAll("rect") //etc.
Do the same for the second set of rectangles, and they'll all be nicely arranged in the second group, so long as your select statement is always called from the group selection, it will only select elements that are part of that group.
Option 2: Use a class attribute to distinguish the two element types
You've got two types of values, x and y, so you should distinguish which type your svg elements belong to by setting a corresponding class attribute. Then, in your select statement, make sure you only select elements of the correct class. The CSS selector format for an element of a certain class is elementName.ClassName, so your code would look like:
.selectAll("rect.x")
.data(x)
enter()
.append("rect")
.classed("x", true)
// etc.
Or, Option 3: Use both
If you're going to want to update the rectangles in the future, just putting them in two groups isn't good enough -- you need a way to distinguish the groups, too. So add an x or y class when you append the <g> elements and use a "g.x" or "g.y" selector when you create them.
I highly recommend you read up on selections, selectors, nested selections, and the update process if you want to keep your D3 code straight. There's a list of tutorials on the wiki.
P.S. The i values that you create as named parameters of your anonymous functions are always limited in scope to that function. You could give them different names if you wanted, their value will always be the value that D3 passes in to them -- the data object for the first parameter, and the index within the current selection for the second parameter.
the main goal of selectAll() is to select exist elements in the container, when worked with data()
it will join data with elements so we can
use enter() to add new element for new data items;
use update() to update elements which match the data.
use exit() to remove elements without data to match
when you use svg.selectAll("rect").data(x).enter() to append rect selectAll() will return 0 element
data(x).enter() will produce placeholder for each data so you can append all data in x
but when use svg.selectAll("rect").data(y).enter() to append rect for y. selectAll() will return 6
rects, so data(y).enter() will product placeholder for 'f' and 'o' this is why you only get two
elements
solution: use different selector for x and y, such as different class name
svg.selectAll(".x").data(x).enter().append("rect").attr("class", "x"); // other operation
svg.selectAll(".y").data(x).enter().append("rect").attr("class", "y"); // other operation
you can get deep understand of d3 selection and data join with these articles:
http://bost.ocks.org/mike/join/
http://bost.ocks.org/mike/selection/
Related
I want to create a kind of infographic where I can represent percentages intuitively using a kind of fill logic.
Example
For the sake of simplicity let's just assume intervals of 25%. For the task of 75% of households, there would be four houses in total and 3 of them would be filled in. The remaining house would remain fill:'none'.
I had something in mind like:
It would be in SVG form.
The only way I can think of to achieve this is pre-draw the houses as a collective image and link the file like:
var fileMap = { 50:'fifty.svg', 75:'seventy-five.svg'};
But this doesn't seem to be very modular, and it doesn't utilize d3 hardly.
Question: Is it possible/feasible to create a simple 25% interval conditional fill using d3 compatible logic? What would my .data() call expect? It has to be an array, maybe a binary:
var data = [1,1,1,0] //75%;
Maybe there's a better way altogether, but that's the best I have got.
"I want to create a kind of infographic where I can represent percentages intuitively using a kind of fill logic"... The technical name for this is pictogram.
For creating a pictogram you don't need anything special, you can use a common enter selection. So, given your data...
var data = [1,1,1,0]
... we will create one house for each array element...
var house = svg.selectAll(null)
.data(data)
.enter()
.append("path")
... and fill them according to the datum:
.style("fill", function(d){
return d ? "blue" : "white"
})
Here is a basic demo:
var d = "m787.67 1599.58l148.83 157.74 124.02-131.45v630.95h396.87 198.44 396.87v-630.95l124.02 131.45 148.83-157.74-768.94-814.97-768.94 814.97m1066.6-709.82v78.868l198.44 210.32v-289.18h-198.44z";
var svg = d3.select("svg");
var data = [1, 1, 1, 0];
var house = svg.selectAll(null)
.data(data)
.enter()
.append("path")
.attr("d", d)
.attr("transform", function(_, i) {
return "translate(" + (i * 70) + ",100) matrix(.04 0 0 .03-4.159-50.852)"
})
.style("stroke", "black")
.style("stroke-width", "50px")
.style("fill", function(d) {
return d ? "blue" : "white"
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
I'm just starting with d3js and I wanted to know if it's possible to create a scatterplot with custom icons for the data points similar to this method for Forced Layout?
I don't want to use d3.svg.symbol() as I want to use a custom icon with my company logo to mark each data point.
My final goal is to translate a point (with a custom icon) along a horizontal axis depending on the x value. I've searched high and low on how to do this with d3js but have had no luck.
To use an icon instead of a symbol, just swap out the path element (that the symbol generator is called on) with an image element.
Given a data set, D, with elements like {src: http.myImageURL, x: 10, y : 20} it would look something like this:
var svg = d3.select('body').append('svg');
svg.append('g').selectAll('.myPoint')
.data(D)
.enter()
.append('image')
.attr("xlink:href", function(d){ return d.src })
.attr("x", function(d){ return d.x })
.attr("y", function(d){ return d.y })
.attr("width", 16)
.attr("height", 16);
I am using D3 to plot a rectangle for each object in an array, the height of the rectangle being dependant on the 'Size' property of the object. These rectangles are stacked on top of each other. I currently set the y position by summing the 'Size' of each subsequent rect that gets plotted - but this seems wrong - and I was wondering if there was a better way to do this, such as accessing the 'y' attribute of the previous item (and how?) or another way...
This is what the essence of my code looks like. There is a link to the fiddle below.
var cumY = 0;
var blocks1 = sampleSVG.selectAll("rect")
.data(fpp)
.enter()
.append("rect")
.sort(SortBySize)
.style("stroke", "gray")
.style("opacity", blockOpacity)
.style("fill", function (d) {return d.Colour})
.attr("width", 80)
.attr("height", function (d) {return d.Size})
.attr("x", 5)
.attr("y", function (d, i) {
var thisY = cumY;
cumY += d.Size;
// perhaps I could just return something like d.Size + previousItem.GetAttribute("y") ???
return thisY;
});
http://jsfiddle.net/ninjaPixel/bvER3/
This is tricky do! You're right that keeping track of the cumulative height 'seems wrong' - it works now but it isn't very idiomatic d3 and will get pretty messy once you start trying to do something more complicated.
I would try using d3's built in stack-layout which was created solve this problem. You might want to start working off of this example and posting an updated fiddle if you get stuck. Good luck!
I've created a line chart based on the example found here:
http://bl.ocks.org/mbostock/3884955
However, with my data the line labels (cities) end up overlapping because the final values on the y-axis for different lines are frequently close together. I know that I need to compare the last value for each line and move the label up or down when the values differ by 12 units or less. My thought is to look at the text labels that are written by this bit of code
city.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")"; })
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
If the y(d.value.temperature) values differ by 12 or less, move the values apart until they have at least 12 units between them. Any thoughts on how to get this done? This is my first d3 project and the syntax is still giving me fits!
You're probably better off passing in all the labels at once -- this is also more in line with the general d3 idea. You could then have code something like this:
svg.selectAll("text.label").data(data)
.enter()
.append("text")
.attr("transform", function(d, i) {
var currenty = y(d.value.temperature);
if(i > 0) {
var previousy = y(data[i-1].value.temperature),
if(currenty - previousy < 12) { currenty = previousy + 12; }
}
return "translate(" + x(d.value.date) + "," + currenty + ")";
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
This does not account for the fact that the previous label may have been moved. You could get the position of the previous label explicitly and move the current one depending on that. The code would be almost the same except that you would need to save a reference to the current element (this) such that it can be accessed later.
All of this will not prevent the labels from being potentially quite far apart from the lines they are labelling in the end. If you need to move every label, the last one will be pretty far away. A better course of action may be to create a legend separately where you can space labels and lines as necessary.
Consider using a D3 force layout to place the labels. See an example here: https://bl.ocks.org/wdickerson/bd654e61f536dcef3736f41e0ad87786
Assuming you have a data array containing objects with a value property, and a scale y:
// Create some nodes
const labels = data.map(d => {
return {
fx: 0,
targetY: y(d.value)
};
});
// Set up the force simulation
const force = d3.forceSimulation()
.nodes(labels)
.force('collide', d3.forceCollide(10))
.force('y', d3.forceY(d => d.targetY).strength(1))
.stop();
// Execute thte simulation
for (let i = 0; i < 300; i++) force.tick();
// Assign values to the appropriate marker
labels.sort((a, b) => a.y - b.y);
data.sort((a, b) => b.value - a.value);
data.forEach((d, i) => d.y = labels[i].y);
Now your data array will have a y property representing its optimal position.
Example uses D3 4.0, read more here: https://github.com/d3/d3-force
I'm exploring D3.js. I primarily used their tutorial to get what I have. But I have made some adjustments.
What I'm doing is counting the number of active & inactive items in a specific table. It then displays a graph with those values. Most everything works fines. I have 2 issues with it though:
It doesn't update automatically with my AJAX call when items are deleted. Only updates when items are added. But I'm not concerned about this for this post.
My primary issue: duplicate values aren't being treated as individual numbers. Instead it sees [10,10] and outputs it as a single bar in the graph as 10 (instead of 2 bars).
Looking at the D3 docs, it would seem the issue lies with .data. Documentation mentions that it joins data.
$(document).on("DOMSubtreeModified DOMNodeRemoved", ".newsfeed", function() {
var columns = ['created','deleted'];
var data = [numN, numD];
var y = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, 420]);
var x = d3.scale.ordinal()
.domain(columns)
.rangeBands([0, 120]);
chart.selectAll("rect")
.data(data)
//.enter().append("rect")
.attr("x", x)
.attr("height", y)
.attr("width", x.rangeBand())
.attr("rx", 10)
.attr("ry", 10);
chart.selectAll("text")
.data(data)
//.enter().append("text")
.attr("y", y)
.attr("x", function(d) {
return x(d) + x.rangeBand() / 2;
}) //offset
.attr("dy", -10) // padding-right
.attr("dx", ".35em") // vertical-align: middle
.attr("text-anchor", "end") // text-align: right
.text(String);
});
How can I make each value to display? If I pass in two different values, the chart displays as it should.
Your problem is at .attr("x", x). So the way you're doing it assigns the same x coordinate for both rects.
So try offsetting x coordinate.
.attr("x", function(d, i) { x + i * width_of_your_rect); })