Is there a way to tell d3 to use a opacity in the colorScale?
Assuming I have something like
const colorScale = scaleLinear()
.range(colorRamp)
.clamp(true);
where colorRamp is
const colorRamp = ["#ff70....", ] <- 30 colors in the array
Is there a way to tell d3 colorScale to use an opacity?
You can specify rgba colors to include opacity in your fill using a d3 scale:
var color = d3.scaleLinear()
.domain([1,10])
.range(["rgba(0,0,0,0)","rgba(0,0,0,1)"]);
var color = d3.scaleLinear()
.domain([1,10])
.range(["rgba(0,0,0,0)","rgba(0,0,0,1)"]);
var svg = d3.select("body")
.append("svg");
var background = svg.append("rect")
.attr("width",500)
.attr("height",200)
.attr("fill","orange");
var rects = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("circle")
.attr("cy",50)
.attr("cx",function(d) { return d*15+15; })
.attr("fill", function(d) { return color(d); })
.attr("r",6);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
If using a color ramp which uses hex values, you could convert it so that it uses rgb values and add an opacity/alpha value(s) after, I have simply applied the same opacity to each color here, but it would be easy to modify it:
var ramp = ["#ff0000","#0000ff"];
ramp = ramp.map(function(color) {
color = color.substring(1);
color.split("");
var i = 0;
var r = parseInt(color[i++],16)*16+parseInt(color[i++],16);
var g = parseInt(color[i++],16)*16+parseInt(color[i++],16);
var b = parseInt(color[i++],16)*16+parseInt(color[i++],16);
r = Math.round(r/2.56);
g = Math.round(g/2.56);
b = Math.round(b/2.56);
opacity = 0.5;
return "rgba("+r+","+g+","+b+","+opacity+")";
})
console.log(ramp);
var color = d3.scaleLinear()
.domain([1,10])
.range(ramp);
var svg = d3.select("body")
.append("svg");
var rects = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("circle")
.attr("cy",50)
.attr("cx",function(d) { return d*15+15; })
.attr("fill", function(d) { return color(d); })
.attr("r",6);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
Related
I have had mixed results when combining for loops with d3 visuals; in this case it seems to be the most straight-forward solution to have a matrix of circle packs. However, one problem is that if I create the visual this way, the output can be slightly misleading. In the snippet below you will notice that the biggest circle in the third circle pack (152) looks just as big as the biggest circle in the first circle pack (200). So in its current form, the circle packs just reflect the proportions, and the changes in absolute size are not portrayed.
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 1080;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var data = [
[
{'id':'1Q19'},
{'id':'pooled','parentId':'1Q19','size':29.5},
{'id':'spv','parentId':'1Q19', 'size':11},
{'id':'single','parentId':'1Q19', 'size':200}
],
[
{'id':'2Q19'},
{'id':'pooled','parentId':'2Q19','size':31},
{'id':'spv','parentId':'2Q19', 'size':15},
{'id':'single','parentId':'2Q19', 'size':171}
],
[
{'id':'3Q19'},
{'id':'pooled','parentId':'3Q19','size':28},
{'id':'spv','parentId':'3Q19', 'size':12},
{'id':'single','parentId':'3Q19', 'size':152}
],
[
{'id':'4Q19'},
{'id':'pooled','parentId':'4Q19','size':25},
{'id':'spv','parentId':'4Q19', 'size':214},
{'id':'single','parentId':'4Q19', 'size':101}
],
];
var colorMap = {
'1Q19':"#e7eef8",
'2Q19':"#e7eef8",
'3Q19':"#e7eef8",
'4Q19':"#e7eef8",
'pooled':"#f6d18b",
'spv':"#366092",
'single':"#95b3d7"
};
var strokeMap = {
"pooled":"#000",
"single":"#000",
"spv":"#fff"
};
for (var j=0; j <(data.length); j++) {
var vData = d3.stratify()(data[j]);
var vLayout = d3.pack().size([250, 250]);
var vRoot = d3.hierarchy(vData).sum(function (d) { return d.data.size; });
var vNodes = vRoot.descendants();
vLayout(vRoot);
var thisClass = "circ"+String(j);
var vSlices = graphGroup.selectAll('.'+thisClass).data(vNodes).attr('class',thisClass).enter().append('g');
//console.log(vNodes)
vSlices.append('circle')
.attr('cx', function(d, i) {
return d.x+(j*300)
})
.attr('cy', function (d) { return d.y; })
.attr('r', function (d) { return d.r; })
.style('fill', function(d) { return colorMap[d.data.id]});
vSlices.append('text')
.attr('x', function(d,i) {return d.x+(j*300)})
.attr('y', function(d) {return d.y+5})
.attr('text-anchor','middle')
.style('fill', function(d) {return strokeMap[d.data.id]})
.text(function(d) {return d.data.data.size ? d.data.data.size : null});
}
<script src="https://d3js.org/d3.v5.min.js"></script>
Question
How can I establish a baseline/uniform scale for each of my circle packs in the circle pack matrix? I want the background/overall parent circle to be the same size, but the child circles to factor in absolute values in the packing process.
Note: I'm content with there being more empty space in the circle pack; perhaps in some instances the diameters may not fully span the parent circle. As long as the circles are tangent, the overall aesthetic theme should carry through.
The fact that you're using a loop to create elements in a D3 code is quite problematic, that's true... however, that's not the problem here. Let's see what you said:
I want the background/overall parent circle to be the same size, but the child circles to factor in absolute values in the packing process [...] I'm content with there being more empty space in the circle pack.
Well, unfortunately, that's not how a circle packing works. What you have right now is the correct data visualisation: the leaves would have different sizes, even if they have the same value, depending on the values of the other leaves. A circle packing is a dynamic process/algorithm.
That being said, my suggestion is: leave it as it is (but fix that cumbersome loop).
However, even if I disagree (from a dataviz point) with your request, here is a solution. Set a square root scale:
var radiusScale = d3.scaleSqrt()
.domain([0,250])
.range([0,125]);
And pass the size values to pack.radius:
var vLayout = d3.pack().size([250, 250])
.radius(function(d){
return radiusScale(d.data.data.size)
});
And here is the result:
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 1200;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
const radiusScale = d3.scaleSqrt()
.domain([0,250])
.range([0,125]);
var data = [
[
{'id':'1Q19'},
{'id':'pooled','parentId':'1Q19','size':29.5},
{'id':'spv','parentId':'1Q19', 'size':11},
{'id':'single','parentId':'1Q19', 'size':200}
],
[
{'id':'2Q19'},
{'id':'pooled','parentId':'2Q19','size':31},
{'id':'spv','parentId':'2Q19', 'size':15},
{'id':'single','parentId':'2Q19', 'size':171}
],
[
{'id':'3Q19'},
{'id':'pooled','parentId':'3Q19','size':28},
{'id':'spv','parentId':'3Q19', 'size':12},
{'id':'single','parentId':'3Q19', 'size':152}
],
[
{'id':'4Q19'},
{'id':'pooled','parentId':'4Q19','size':25},
{'id':'spv','parentId':'4Q19', 'size':214},
{'id':'single','parentId':'4Q19', 'size':101}
],
];
var colorMap = {
'1Q19':"#e7eef8",
'2Q19':"#e7eef8",
'3Q19':"#e7eef8",
'4Q19':"#e7eef8",
'pooled':"#f6d18b",
'spv':"#366092",
'single':"#95b3d7"
};
var strokeMap = {
"pooled":"#000",
"single":"#000",
"spv":"#fff"
};
for (var j=0; j <(data.length); j++) {
var vData = d3.stratify()(data[j]);
var vLayout = d3.pack().size([250, 250])
.radius(function(d){
return radiusScale(d.data.data.size)
});
var vRoot = d3.hierarchy(vData).sum(function (d) { return d.data.size; });
var vNodes = vRoot.descendants();
vLayout(vRoot);
var thisClass = "circ"+String(j);
var vSlices = graphGroup.selectAll('.'+thisClass).data(vNodes).attr('class',thisClass).enter().append('g');
//console.log(vNodes)
vSlices.append('circle')
.attr('cx', function(d, i) {
return d.x+(j*(j === 3 ? 320 : 310))
})
.attr('cy', function (d) { return d.y; })
.attr('r', function (d) { return d.r; })
.style('fill', function(d) { return colorMap[d.data.id]});
vSlices.append('text')
.attr('x', function(d,i) {return d.x+(j*(j === 3 ? 320 : 310))})
.attr('y', function(d) {return d.y+5})
.attr('text-anchor','middle')
.style('fill', function(d) {return strokeMap[d.data.id]})
.text(function(d) {return d.data.data.size ? d.data.data.size : null});
}
<script src="https://d3js.org/d3.v5.min.js"></script>
Pay attention to the fact that, in the last pack, the overall circle is not the same size (it's bigger). Being the same size is simple impossible, given the packing logic.
I have created a dot matrix visual as seen below in the snippet. Colored circles represent values and gray circles represent empty/unused. For instance, in my case, the three colors represent funding in percentage form for a project from 3 countries: USA, Canada and Mexico. The gray represents funding yet to be raised, also in percentage form.
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var data = [
{'country':'USA', 'value':.20},
{'country':'Canada', 'value':.15},
{'country':'Mexico', 'value':.10}
];
var circArray = new Array(50);
var circPercentage = 100/circArray.length;
var circData = new Array;
data.forEach(function(item) {
for (var i =0; i <item.value*100/circPercentage; i++) {
circData.push(item.country);
}
});
var arrayDiff = 50-circData.length;
for (var i=0; i <(arrayDiff); i++) {
circData.push("");
}
//console.log(circData)
var maxColumn = 10;
var colorMap = {
'USA':"#f6d18b",
'Canada':"#366092",
'Mexico':"#95b3d7",
"":"#a6a6a6"
};
var xScale = d3.scaleLinear()
.range([0,width])
.domain([0,1]);
var yScale = d3.scaleLinear()
.range([height,0])
.domain([0,1]);
graphGroup.selectAll('circle')
.data(circData)
.enter()
.append('circle')
.attr('cx', function(d, i) {
return (i % maxColumn) * 30
})
.attr('cy', function(d, i) {
return ~~((i / maxColumn) % maxColumn) * 30
})
.attr('r', 10)
.style('fill', function(d) {
//console.log(d)
return colorMap[d];
});
<script src="https://d3js.org/d3.v5.min.js"></script>
My use of .forEach() and my for loop work inasmuch that I created an array of the correct length to map the circles to the percentage logic, like so:
['USA','USA','USA','USA','USA','USA','USA','USA','USA','USA', ... , "", "", ""];
however I was unable to find documentation for which direction/order I want the circles to be appended. Supposed I wanted the opposite of the snippet above, where the the gray circles appear on top and the colored circles start from the bottom? Or suppose I wanted gray circles on the left and colored circles on the right?
Question
Is there a straight-forward means to toggle the direction/order of svg elements? Or is my only option to transpose the data itself? As in:
["","","",... 'USA','USA','USA','USA','USA','USA','USA','USA','USA','USA'];
This approach bothers me, I don't know why. I'm hoping there is a slightly more sophisticated way.
In your code it's the same order as circData.
But actually you control the position by cx and cy, not their order.
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var data = [
{'country':'USA', 'value':.20},
{'country':'Canada', 'value':.15},
{'country':'Mexico', 'value':.10}
];
var circArray = new Array(50);
var circPercentage = 100/circArray.length;
var circData = new Array;
data.forEach(function(item) {
for (var i =0; i <item.value*100/circPercentage; i++) {
circData.push(item.country);
}
});
var arrayDiff = 50-circData.length;
for (var i=0; i <(arrayDiff); i++) {
circData.push("");
}
circData.reverse() //<---------------------------
//console.log(circData)
var maxColumn = 10;
var colorMap = {
'USA':"#f6d18b",
'Canada':"#366092",
'Mexico':"#95b3d7",
"":"#a6a6a6"
};
var xScale = d3.scaleLinear()
.range([0,width])
.domain([0,1]);
var yScale = d3.scaleLinear()
.range([height,0])
.domain([0,1]);
graphGroup.selectAll('circle')
.data(circData)
.enter()
.append('circle')
.attr('cx', function(d, i) {
return (i % maxColumn) * 30
})
.attr('cy', function(d, i) {
return ~~((i / maxColumn) % maxColumn) * 30
})
.attr('r', 10)
.style('fill', function(d) {
//console.log(d)
return colorMap[d];
});
<script src="https://d3js.org/d3.v5.min.js"></script>
I'm trying to create a globe (Orthographic projection) with a drag, which also has circles on it.
I've been able to create the globe with a drag, and add circles. The problem is that when I drag the circles don't move with the globe.
Here is my bl.ock where you can find my code: http://bl.ocks.org/JulienAssouline/3caf0d6e01aa8220a8d4027cb9158d7e
I've looked at other bl.ock examples such as this one:https://bl.ocks.org/larsvers/f8efeabf480244d59001310f70815b4e
and this one:
https://bl.ocks.org/curran/115407b42ef85b0758595d05c825b346
but I haven't been able to get it to work for me. Their methods seem to be quite different than mine, and I don't completely understand their code.
Would anyone know what method, or what I need to add to my code?
Thanks.
Here is my javascript code:
(function(){
var h = 600;
var w = 900;
var i = 0;
var map = void 0;
var world = void 0;
var US = void 0;
var margin = {
top: 10,
bottom: 40,
left: 0,
right: 30
};
var circleScale = d3.scaleSqrt()
.domain([0, 4445])
.range([0.5, 10])
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;
var dragging = function(d){
var c = projection.rotate();
projection.rotate([c[0] + d3.event.dx/6, c[1] - d3.event.dy/6])
map.selectAll('path').attr('d', path);
}
var drag = d3.drag()
.on("drag", dragging)
var projection = d3.geoOrthographic().clipAngle(90);
var path = d3.geoPath().projection(projection);
var svg = d3.select("body")
.append("svg")
.attr("id", "chart")
.attr("width", w)
.attr("height", h)
d3.json("world.json", function(json){
d3.csv("arms_transfer_2012_2016_top - arms_transfer_2012_2016_top.csv", function(error, data){
var countries = topojson.feature(json, json.objects.countries).features
var US = countries[168]
map = svg.append('g').attr('class', 'boundary');
world = map.selectAll('path').data(countries);
US = map.selectAll('.US').data([US]);
Circles = map.selectAll(".circles").data(data)
console.log(countries[168])
world.enter()
.append("path")
.attr("class", "boundary")
.attr("d", path)
US.enter()
.append("path")
.attr("class", "US")
.attr("d", path)
.style("fill", "lightyellow")
.style("stroke", "orange")
Circles.enter()
.append("circle")
.attr("class", "importer")
.attr("r", function(d){
return circleScale(d.Millions)
})
.attr("cx", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[0];
})
.attr("cy", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[1];
})
.style("fill", "#cd0d0e")
svg.append("rect")
.attr("class", "overlay")
.attr("width", w)
.attr("height", h)
.call(drag)
})
})
})();
You have to update the position of the circles in your dragging function:
var dragging = function(d) {
var c = projection.rotate();
projection.rotate([c[0] + d3.event.dx / 6, c[1] - d3.event.dy / 6])
map.selectAll('path').attr('d', path);
map.selectAll(".circles").attr("cx", function(d) {
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[0];
})
.attr("cy", function(d) {
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[1];
})
}
Also, select them using the correct class.
Regarding performance, if you don't want to calculate coords twice, you can use an each:
var dragging = function(d) {
var c = projection.rotate();
projection.rotate([c[0] + d3.event.dx / 6, c[1] - d3.event.dy / 6])
map.selectAll('path').attr('d', path);
map.selectAll(".circles").each(function(d) {
var coords = projection([d.Longitude_imp, d.Latitude_imp])
d3.select(this).attr("cx", function(d) {
return coords[0];
})
.attr("cy", function(d) {
return coords[1];
})
})
}
Here is your bl.ocks with that change: http://bl.ocks.org/anonymous/dc2d4fc810550586d40d4b1ce9088422/40c6e199a5be4e152c0bd94a13ea94eba41f004b
PS: You have a problem with the circles at the far side of the globe... however, this is another issue, already addressed here at S.O. For instance, this answer of mine: https://stackoverflow.com/a/46441983/5768908
I read "Interactive Data Visualization for the web" by Scott Murray, but this book use d3 version3. I've tried to fix it, but some problems happen, and my code is below. I got errors about "y: Expected length, "NaN".", and maybe my stack function doesn't work. However, I don't know how to solve it. I need someone to help me.
// declare variable
var svgWidth = 500,
svgHeight = 300,
svgData = [],
maxValue = 16;
svgData = getData(svgData);
// set stack color
var color = d3.scaleOrdinal(d3.schemeCategory10);
// create stack layout
var stack = d3.stack();
stack(svgData);
// define x,y scale
var xScale = d3.scaleBand()
.domain(d3.range(svgData[0].length))
.rangeRound([0, svgWidth])
.paddingInner(0.05),
yScale = d3.scaleLinear()
.domain([0, d3.max(svgData, function(d){
return d3.max(d, function(d){
d.y0 + d.y;
});
})])
.range([0, svgHeight])
.clamp(true);
// create svg
var svg = d3.select("body")
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
// add group and fill color for each row of data
var group = svg.selectAll("g")
.data(svgData)
.enter()
.append("g")
.style("fill", function(d, i){
return color(i);
});
// add a rect for each data value
var rect = group.selectAll("rect")
.data(function(d){
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i){
return xScale(i);
})
.attr("y", function(d){
return yScale(d.y0);
})
.attr("width", xScale.bandwidth())
.attr("height", function(d){
return yScale(d.y);
});
// get data
function getData(data){
var temp =0,
tempArr = [];
data = [];
for(var i=0; i<3; i++){
tempArr = [];
for(var j=0; j<5; j++){
temp = Math.round(Math.random() *maxValue);
tempArr.push( { x: j, y: temp });
}
data.push(tempArr);
}
return data;
}
I have an area graph (supposed to represent a time series). I want to color the graph based on the y value such that for regions where the y > c it is one color and for the area where y<=c it is another color. Is this possible in D3?
This is the code I have that generates a single color graph:
var width = 700,
height = 400;
var vis = d3.select("#chart")
.append("svg")
.attr("width",width)
.attr("height",height);
var mpts = [{"x":0,"val":15}];
var n = 200;
for(var i=0;i<n;i++)
{
if(Math.random()>.5)
{
mpts = mpts.concat({"x":i+1,"val":mpts[i].val*(1.01)});
}
else
{
mpts = mpts.concat({"x":i+1,"val":mpts[i].val*(.99)});
}
}
var x = d3.scale.linear().domain([0,n]).range([10,10+width]);
var y = d3.scale.linear().domain([10,20]).range([height-10,0]);
var area = d3.svg.area()
.interpolate("linear")
.x(function(d) { return x(d.x); })
.y1(function(d) { return y(d.val); })
.y0(function(d) { return y(0); });
vis.append("svg:path")
.attr("class", "area")
.attr("d",area(mpts))
.attr("fill","orange");
</script>
Change the way you add the areas to something like
vis.selectAll("path").data(mpts).enter().append("svg:path")
.attr("class", "area")
.attr("d", area)
.attr("fill", function(d) { return (d.val > c ? "orange" : "yellow"); });