Related
I have a map already drawed. I would like to add a legend using d3.js. For example when filering by length, the map should show differents colors. Since a week, I couldn't achieve this task. My map color seem to be good but the legend does not match.
Could anybody help me with my draw link function ?
https://jsfiddle.net/aba2s/xbn9euh0/12/)
I think it's the error is about the legend function.
Here is the function that change my map color Roads.eachLayer(function (layer) {layer.setStyle({fillColor: colorscale(layer.feature.properties.length)})});
function drawLinkLegend(dataset, colorscale, min, max) {
// Show label
linkLabel.style.display = 'block'
var legendWidth = 100
legendMargin = 10
legendLength = document.getElementById('legend-links-container').offsetHeight - 2*legendMargin
legendIntervals = Object.keys(colorscale).length
legendScale = legendLength/legendIntervals
// Add legend
var legendSvg = d3.select('#legend-links-svg')
.append('g')
.attr("id", "linkLegendSvg");
var bars = legendSvg.selectAll(".bars")
//.data(d3.range(legendIntervals), function(d) { return d})
.data(dataset)
.enter().append("rect")
.attr("class", "bars")
.attr("x", 0)
.attr("y", function(d, i) { return legendMargin + legendScale * (legendIntervals - i-1); })
.attr("height", legendScale)
.attr("width", legendWidth-50)
.style("fill", function(d) { return colorscale(d) })
// create a scale and axis for the legend
var legendAxis = d3.scaleLinear()
.domain([min, max])
.range([legendLength, 0]);
legendSvg.append("g")
.attr("class", "legend axis")
.attr("transform", "translate(" + (legendWidth - 50) + ", " + legendMargin + ")")
.call(d3.axisRight().scale(legendAxis).ticks(10))
}
D3 expects your data array to represent the elements you are creating. It appears you are passing an array of all your features: but you want your scale to represent intervals. It looks like you have attempted this approach, but you haven't quite got it.
We want to access the minimum and maximum values that will be provided to the scale. To do so we can use scale.domain() which returns an array containing the extent of the domain, the min and max values.
We can then create a dataset that contains values between (and including) these two endpoints.
Lastly, we can calculate their required height based on how high the visual scale is supposed to be by dividing the height of the visual scale by the number of values/intervals.
Then we can supply this information to the enter/update/exit cycle. The enter/update/exit cycle expects one item in the data array for every element in the selection - hence why need to create a new dataset.
Something like the following shold work:
var dif = colorscale.domain()[1] - colorscale.domain()[0];
var intervals = d3.range(20).map(function(d,i) {
return dif * i / 20 + colorscale.domain()[0]
})
intervals.push(colorscale.domain()[1]);
var intervalHeight = legendLength / intervals.length;
var bars = legendSvg.selectAll(".bars")
.data(intervals)
.enter().append("rect")
.attr("class", "bars")
.attr("x", 0)
.attr("y", function(d, i) { return Math.round((intervals.length - 1 - i) * intervalHeight) + legendMargin; })
.attr("height", intervalHeight)
.attr("width", legendWidth-50)
.style("fill", function(d, i) { return colorscale(d) })
In troubleshooting your existing code, you can see you have too many elements in the DOM when representing the scale. Also, Object.keys(colorscale).length won't produce information useful for generating intervals - the keys of the scale are not dependent on the data.
eg
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.
I'm using D3 to present some data as a horizontal bar chart. Values will typically range between -10 and +10 on 8 different scales. I have the bars rendering as I want, but I can't work out how to add lables for each of the extreems of the axes.
so far I have:
but I want to achieve something like:
In other words a label for each extreme of each scale.
I have found lots of examples that add data labels to the bars them selves (e.g. the value), but I want to some how force the array of strings to be rendered at the extremes of the container.
At the moment, I am rendering the data from an array, and I have the labels stored in 2 other arrays e.g.
var data = [10, 5, -5, -10, 2, -2, 8, -8];
var leftLabels = ["label 1","label 2", ...];
var rightLabels = ["label 1", "label 2", ...];
Any ideas or links to examples most welcome.
I am not an expert in d3.js, but I think this can be easily done. There are different ways to go about it. I have created a pen for your use case.
I will paste the important part of the code below. In your chart, you will have to certainly make some adjustments to suit your needs. Feel free to play around with the values until you feel they are stable.
// Your array containing labels for left and right values
var leftSideData = ["left1", "left2", "left3", "left4", "left5", "left6", "left7", "left8"];
var rightSideData = ["right1", "right2", "right3", "right4", "right5", "right6", "right7", "right8"];
var left = svg.selectAll(".leftData")
.data(leftSideData)
.enter().append("g")
.attr("class", "leftVal")
.attr("transform", function(d, i) {
return "translate(0," + i * 57 + ")";
});
left.append("text")
.attr("x", 0)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) {
return d;
});
var right = svg.selectAll(".rightData")
.data(rightSideData)
.enter().append("g")
.attr("class", "rightVal")
.attr("transform", function(d, i) {
return "translate(0," + i * 57 + ")";
});
right.append("text")
.attr("x", width + 30)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) {
return d;
});
I won't say this is perfect, but I hope you get an idea about how to approach it. All the best!!
It's funny, just by asking the q on SE I find it helps me reformulate the problem.. and then some time later a new try yields a result. Anyone else find that?
I managed to make it work by changing the way the SVG was created. So I now have the following structure:
<SVG>
><g> (one for each bar)
>><text>
>><rect>
>><text>
><other stuff like axies>
It turns out that <text> elements cannot be added to <rect> elements (well they can, be added but they won't render).
the code is:
var data = [10,2,4,-10,...etc...];
var leftLabels = ["left 1","left 1", ...etc...];
var rightLabels = ["right 1","right 2", ...etc...];
//chart dimentions
var margin = { top: 20, right: 30, bottom: 40, left: 30 },
width = 600 - margin.left - margin.right,
barHeight = 30,
height = barHeight * data.length;
//chart bar scaling
var x = d3.scale.linear()
.range([100, width-100]);
var y = d3.scale.ordinal()
.rangeRoundBands([0, height], 0.1);
var chart = d3.select(".chartsvg")
.attr("width", width + margin.left + margin.right)
.attr("height", barHeight * data.length + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain([d3.min(data), d3.max(data)]);
//append a g for each data item
var bar = chart.selectAll(".bar")
.data(data)
.enter()
.append("g");
//in each bar add a rect for the bar chart bar
bar.append("rect")
.attr("class", function (d) { return "bar--" + (d < 0 ? "negative" : "positive"); })
.attr("x", function (d) { return x(Math.min(0, d)); })
.attr("y", function (d, i) { return i* barHeight; })
.attr("width", function (d) { return Math.abs(x(d) - x(0)); })
.attr("height", barHeight-1);
//append the labels to each g using the label data
bar.append("text")
.data(rightLabels)
.attr("x", width)
.attr("y", function (d, i) { return (i * barHeight)+barHeight/2; })
.attr("dy", ".5em")
.attr("fill","steelblue")
.attr("text-anchor","end")
.text(function (d) { return d; });
bar.append("text")
.data(leftLabels)
.attr("x", 0)
.attr("y", function (d, i) { return (i * barHeight) + barHeight / 2; })
.attr("dy", ".5em")
.attr("fill","darkorange")
.attr("text-anchor", "start")
.text(function (d) { return d; });
//then append axis etc...
Formatting: something else to note. It turns out that to color the text in the label you need to use "stroke" and "fill" attributes. These are broadly equiv to the HTML "color" attribute on text.
I'm trying to draw multiple graphs in one svg image, based on chromosomal data. The purpose is to draw 1 graph per chromosome. I want to transpose the axis (and the data) based on the chromosome number in de data.
Data entries look like this (in JSON):
[{
"chrom": 1,
"pos": 2000000,
"ratio": 0.0253,
"average": 0.0408,
"stdev": 0.0257,
"z-score": - 0.6021
}, {
"chrom": 1,
"pos": 2250000,
"ratio": 0.0304,
"average": 0.0452,
"stdev": 0.0245,
"z-score": - 0.6021
}, {
"chrom": 1,
"pos": 2500000,
"ratio": 0.0357,
"average": 0.0498,
"stdev": 0.024,
"z-score": - 0.5885
}, {
"chrom": 1,
"pos": 2750000,
"ratio": 0.0381,
"average": 0.0522,
"stdev": 0.0228,
"z-score": - 0.6146
},
{etc..}
Currently my code looks like this:
d3.json("data.json", function(error, data) {
if (error) throw error;
x.domain(d3.extent(data, function(d) { return d.pos; }));
y.domain(d3.extent(data, function(d) { return d['ratio']; }));
svg.append("g")
.attr("class", "x axis")
.data(data)
//.attr("transform", "translate(0," + graph_height + ")")
.attr('transform', function(d) {
if (d.chrom > Math.ceil(chrnumber / 2)) {
console.log('translate('+ graph_width + graph_margin.center + ',' + graph_height + d.chrom * (graph_height + graph_margin.top) + ')');
return 'translate('+ graph_width + graph_margin.center + ',' + graph_height + d.chrom * (graph_height + graph_margin.top) + ')';
}else {
console.log('translate(0,' + graph_height + d.chrom * (graph_height + graph_margin.top) + ')');
return 'translate(0,' + graph_height + d.chrom * (graph_height + graph_margin.top) + ')';
}
})
.call(xAxis);
});
But this yields no errors, nor any output. I suppose there is some kind of error in the code above, because the svg image isn't generated on the page.
Does anyone have an idea where I went wrong?
The lines here:
svg.append("g")
.attr("class", "x axis")
.data(data)
are not enough to create one axis per chromosome. One way to do it (might not be the simplest, but a very "d3-friendly" one) is to create an array representing your set of chromosomes.
var chromList = d3.set( //data structure removing duplicates
data.map(function(d) { // create a list from data by ...
return d.chrom // ...keeping only the chrom field
}).values(); // export the set as an array
//chromList=[1,2,3 ..], according to the values of chrom seen in your data.
Now you bind this list to your axis, in order to create one axis per element:
svg.append("g") //holder of all x axis
.selectAll(".axis") //creates an empty selection at first, but it will be filled next.
.data(chromList) //bind the list of chrom ids to the selection
.enter() //the selection was empty, so each chromosome is "entering"
.append("g") //for each chromosome, insert a g element: this is the corresponding axis
.attr("class", "x axis")
.attr('transform', function(d) {
//use d here as your chromosome identifier
});
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).