I am using angular and d3 to create a donut (in a directive).
I can quite simply give the filled area a colour (in this plunker it is blue). But what i want to do is have the SVG change its colours smoothly from:
0% - 33.3% - red
33.4% - 66.66% - orange
66.7% - 100% green
Directive:
app.directive('donutDirective', function() {
return {
restrict: 'E',
scope: {
radius: '=',
percent: '=',
text: '=',
},
link: function(scope, element, attrs) {
var radius = scope.radius,
percent = scope.percent,
percentLabel = scope.text,
format = d3.format(".0%"),
progress = 0;
var svg = d3.select(element[0])
.append('svg')
.style('width', radius/2+'px')
.style('height', radius/2+'px');
var donutScale = d3.scale.linear()
.domain([0, 100])
.range([0, 2 * Math.PI]);
//var color = "#5599aa";
var color = "#018BBB";
var data = [
[0,100,"#b8b5b8"],
[0,0,color]
];
var arc = d3.svg.arc()
.innerRadius(radius/6)
.outerRadius(radius/4)
.startAngle(function(d){return donutScale(d[0]);})
.endAngle(function(d){return donutScale(d[1]);});
var text = svg.append("text")
.attr("x",radius/4)
.attr("y",radius/4)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("font-size","14px")
.style("fill","black")
.attr("text-anchor", "middle")
.text(percentLabel);
var path = svg.selectAll("path")
.data(data)
.enter()
.append("path")
.style("fill", function(d){return d[2];})
.attr("d", arc)
.each(function(d) {
this._current = d;
// console.log(this._current)
;});
// update the data!
data = [
[0,100,"#b8b5b8"],
[0,percent,color]
];
path
.data(data)
.attr("transform", "translate("+radius/4+","+radius/4+")")
.transition(200).duration(2150).ease('linear')
.attrTween("d", function (a) {
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
// console.log(this._current);
return function(t) {
text.text( format(i2(t) / 100) );
return arc(i(t));
};
});
}
};
});
Plunker: http://plnkr.co/edit/8qGMeQkmM08CZxZIVRei?p=preview
First give Id to the path like this:
var path = svg.selectAll("path")
.data(data)
.enter()
.append("path")
.style("fill", function(d){return d[2];})
.attr("d", arc)
.attr("id", function(d,i){return "id"+i;})//give id
Then inside the tween pass the condition and change the color of the path
.attrTween("d", function (a) {
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
return function(t) {
if(i2(t) < 33.3)
d3.selectAll("#id1").style("fill", "red")
else if(i2(t) < 66.6)
d3.selectAll("#id1").style("fill", "orange")
else if(i2(t) > 66.6)
d3.selectAll("#id1").style("fill", "green")
text.text( format(i2(t) / 100) );
return arc(i(t));
};
});
Working code here
EDIT
Inside your directive you can make gradient inside your defs like this:
var defs = svg.append("defs");
var gradient1 = defs.append("linearGradient").attr("id", "gradient1");
gradient1.append("stop").attr("offset", "0%").attr("stop-color", "red");
gradient1.append("stop").attr("offset", "25%").attr("stop-color", "orange");
gradient1.append("stop").attr("offset", "75%").attr("stop-color", "green");
Then in the path you can define the gradient like this:
var path = svg.selectAll("path")
.data(data)
.enter()
.append("path")
.style("fill", function(d, i) {
if (i == 0) {
return d[2];
} else {
return "url(#gradient1)";
}
})
Working code here
Hope this helps!
i want to do is have the SVG change its colours smoothly from:
0% - 33.3% - red
33.4% - 66.66% - orange
66.7% - 100% green
Assuming that you want a color transition/scale like this one:
See working code for this: http://codepen.io/anon/pen/vLVmyV
You can smothly make the color transition using a d3 linear scale like this:
//Create a color Scale to smothly change the color of the donut
var colorScale = d3.scale.linear().domain([0,33.3,66.66,100]).range(['#cc0000','#ffa500','#ffa500','#00cc00']);
Then, when you update the path (with the attrTween) to make the filling animation, take only the Path the represents the filled part of the donut, lets call it colorPath and change the fill of it adding the following like in the tween:
//Set the color to the path depending on its percentage
//using the colorScale we just created before
colorPath.style('fill',colorScale(i2(t)))
Your attrTween will look like this:
colorPath.data([[0,percent,color]])
.transition(200).duration(2150).ease('linear')
.attrTween("d", function (a) {
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
// console.log(this._current);
return function(t) {
text.text( format(i2(t) / 100) );
colorPath.style('fill',colorScale(i2(t)))
return arc(i(t));
};
});
Please note that we only update the data for the colorPath: colorPath.data([[0,percent,color]])
The whole working example is right here: http://plnkr.co/edit/ox82vGxhcaoXJpVpUel1?p=preview
Related
I have a donut chart that is being used as a way to show progression. I don't have a way to show you the donut chart, but the code is simple enough to copy and paste.
I added the code to show you an example. I've tried various unreasonable methods to make the transition work the first time. But for some reason it's still not working. All examples online are pretty similar so I'm not really sure why this is happening.
var data = [95, 5];
var pie = d3.pie().sort(null);
var svg = d3.select("svg"),
width = svg.attr("width"),
height = svg.attr("height"),
radius = Math.min(width, height) / 2;
var arc = d3.arc()
.innerRadius(60)
.outerRadius(radius);
function createdonut() {
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
//Inner SVG Circle
svg.append("svg:circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("r", 60)
.style("fill", "#ead4d4")
.append("g");
var color = d3.scaleOrdinal(['#4daf4a', '#377eb8', '#ff7f00', '#984ea3', '#e41a1c']);
//Generate groups
var arcs = g.selectAll("arc")
.data(pie(data))
.enter()
.append("g")
.attr("class", "arc")
//Draw arc paths
arcs.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.on('mouseover', mouseover);
function mouseover(d, i) {
$('#percentage').html(i.data + ' units');
}
}
function updateDoNut(update) {
data[0] = data[0] - update;
data[1] = data[1] + update;
var path = d3.select("svg").selectAll("path").data(pie(data));
/*path.enter().append("path")
.attr("fill", function (d, i) {
return color[i];
})
.attr("d", arc);*/
path.transition().duration(100).attrTween("d", arcTween);
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return arc(i(t));
};
}
createdonut();
//updateDoNut(0);
var inter = setInterval(function () { updateDoNut(5); }, 3000);
<script src="https://d3js.org/d3.v6.min.js"></script>
<div id="my_dataviz">
<svg width="300" height="200"> </svg>
<div id="percentage">0 units</div>
</div>
If we look at your tween function we'll see a problem:
var i = d3.interpolate(this._current, a);
this._current = i(0);
this.current is undefined when you first start transtioning - so how is D3 to interpolate between undefined and an object contianing arc properties? It doesn't. Resulting in the non-transition you are seeing. Set this._current when appending the arcs:
arcs.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function(d) {
this._current = d;
})
.on('mouseover', mouseover);
Now when you update the circle, there is a valid start point for the interpolator and you should see a transition:
var data = [95, 5];
var pie = d3.pie().sort(null);
var svg = d3.select("svg"),
width = svg.attr("width"),
height = svg.attr("height"),
radius = Math.min(width, height) / 2;
var arc = d3.arc()
.innerRadius(60)
.outerRadius(radius);
function createdonut() {
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
//Inner SVG Circle
svg.append("svg:circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("r", 60)
.style("fill", "#ead4d4")
.append("g");
var color = d3.scaleOrdinal(['#4daf4a', '#377eb8', '#ff7f00', '#984ea3', '#e41a1c']);
//Generate groups
var arcs = g.selectAll("arc")
.data(pie(data))
.enter()
.append("g")
.attr("class", "arc")
//Draw arc paths
arcs.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function(d) {
this._current = d;
})
.on('mouseover', mouseover);
function mouseover(d, i) {
$('#percentage').html(i.data + ' units');
}
}
function updateDoNut(update) {
data[0] = data[0] - update;
data[1] = data[1] + update;
var path = d3.select("svg").selectAll("path").data(pie(data));
path.transition().duration(2000).attrTween("d", arcTween);
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return arc(i(t));
};
}
createdonut();
//updateDoNut(0);
var inter = setInterval(function () { updateDoNut(5); }, 3000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<div id="my_dataviz">
<svg width="300" height="200"> </svg>
<div id="percentage">0 units</div>
</div>
Why doesn't this interpolation between undefined and an object generate an error? Well D3-interpolate will try to interpolate very hard. In this case, between undefined and an object, it'll use d3-interpolateObject, which will interpolate as follows:
For each property in b, if there exists a corresponding property in a,
a generic interpolator is created for the two elements using
interpolate. If there is no such property, the static value from b is
used in the template. (docs)
So, as there are no properties in undefined, the interpolator just uses a static value for every point in the interpolation, hence the lack of a transition on the first update: every interpolated point is the same: the end point values.
I want to change the attribute of an element back to it's old color after a onclick event in d3. problem is that the old color got calculated by a scale within a function.
I want to hightlight a selected country in a map in d3 and change back the color when I highlight another country.
d3.selectAll('.buurt').on('click', function(data){
var postcode = data.properties.postcode;
piechart(property_types, svg_piechart1, postcode);
kaart(data_buurten, data_geo, svg_kaart, key)
// verander kleur van selection
var selected_color = '#FF0000' // red
selected = d3.select(this).select('path').attr('fill', function(d) { return selected_color });
function kaart(data, data_geo, svg_kaart, key) {
// verwijder
svg_kaart.selectAll('g').remove();
// maak groep ('g') en bind data
var group = svg_kaart.selectAll('g')
.data(data_geo.features)
.enter()
.append('g')
.attr('class', 'buurt');
array_key_data = Object.values(data).map(function(i){return i[key]})
var max = Math.max.apply(Math, array_key_data)
var min = Math.min.apply(Math, array_key_data)
var color_scale = d3.scale.linear().domain([min, max]).range(['#e5f5f9','#99d8c9', '#2ca25f'])
var areas = group.append("path")
.attr('d', path)
.attr('class', 'area')
.attr('fill', function(d) {
postcode_geo_data = d.properties.postcode
data_per_buurt = data[postcode_geo_data]
// als er een plek is zonder postcode
if (data_per_buurt === undefined) {
var red = '#FF0000'
return red
} else {
// als er een plek is met postcode
value = data_per_buurt[key]
return color_scale(value)
}
})
.attr('id', function(d){ return d.properties.postcode})
}
The easiest solution is just using the same color scale for all elements inside the click function (before painting the clicked element, of course). Here is a simple demo:
var svg = d3.select("svg");
var data = [5, 30, 50, 80, 100];
var scale = d3.scaleLinear()
.domain([0, 100])
.range(["greenyellow", "darkgreen"])
var circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("r", 20)
.attr("cy", 50)
.attr("cx", function(d, i) {
return 30 + 60 * i
})
.attr("stroke", "gray")
.attr("fill", function(d) {
return scale(d)
})
.attr("cursor", "pointer");
circles.on("click", function() {
circles.attr("fill", function(d) {
return scale(d)
});
d3.select(this).attr("fill", "firebrick");
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Despite being easy, that is not a clever solution, because it repaints all the elements. For modern, fast browsers, it works just nice if you have a small amount of elements.
However, if you have a huge amount of elements, a better alternative to save resources is repainting only the clicked element and the previously clicked element. For instance:
var clickedElement, elementColour;
selection.on("click", function(d, i, n) {
if (clickedElement) {
clickedElement.attr("fill", elementColour);
};
clickedElement = d3.select(this);
elementColour = d3.select(this).attr("fill");
d3.select(this).attr("fill", "firebrick");
});
In this snippet, clickedElement holds the reference to the previously clicked element, and elementColour holds its color. Then, after repainting the previously clicked element, clickedElement is associated to the currently clicked element, as well as elementColour.
Here is the demo:
var svg = d3.select("svg");
var data = [5, 30, 50, 80, 100];
var clickedElement, elementColour;
var scale = d3.scaleLinear()
.domain([0, 100])
.range(["greenyellow", "darkgreen"])
var circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("r", 20)
.attr("cy", 50)
.attr("cx", function(d, i) {
return 30 + 60 * i
})
.attr("stroke", "gray")
.attr("fill", function(d) {
return scale(d)
})
.attr("cursor", "pointer");
circles.on("click", function(d,i,n) {
if(clickedElement){clickedElement.attr("fill", elementColour);};
clickedElement= d3.select(this);
elementColour = d3.select(this).attr("fill");
d3.select(this).attr("fill", "firebrick");
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
I am trying to add color options for my heat-map visualization. I have a predefined colors array at the beginning, and I draw rectangles like this:
plotChart.selectAll(".cell")
.data(data)
.enter().append("rect")
.attr("class", "cell")
.attr("x", function (d) { return x(d.timestamp); })
.attr("y", function (d) { return y(d.hour); })
.attr("width", function (d) { return x(d3.timeWeek.offset(d.timestamp, 1)) - x(d.timestamp); })
.attr("height", function (d) { return y(d.hour + 1) - y(d.hour); })
.attr("fill", function (d) { return colorScale(d.value); });
When I click a link in a dropdown menu, I do this:
$(".colorMenu").click(function (event) {
event.preventDefault();
// remove # from clicked link
var addressValue = $(this).attr("href").substring(1);
// get color scheme array
var newColorScheme = colorDict[addressValue];
// update color scale range
colorScale.range(newColorScheme);
// here I need to repaint with colors
});
My color scale is quantile scale, so I cannot use invert function to find values of each rectangle. I don't want to read the data again because it would be a burden, so how can I change fill colors of my rectangles?
I don't want to read the data again...
Well, you don't need to read the data again. Once the data was bound to the element, the datum remains there, unless you change/overwrite it.
So, this can simply be done with:
.attr("fill", d => colorScale(d.value));
Check this demo:
var width = 500,
height = 100;
var ranges = {};
ranges.range1 = ['#f7fbff','#deebf7','#c6dbef','#9ecae1','#6baed6','#4292c6','#2171b5','#08519c','#08306b'];
ranges.range2 = ['#fff5eb','#fee6ce','#fdd0a2','#fdae6b','#fd8d3c','#f16913','#d94801','#a63603','#7f2704'];
ranges.range3 = ['#f7fcf5','#e5f5e0','#c7e9c0','#a1d99b','#74c476','#41ab5d','#238b45','#006d2c','#00441b'];
ranges.range4 = ['#fff5f0','#fee0d2','#fcbba1','#fc9272','#fb6a4a','#ef3b2c','#cb181d','#a50f15','#67000d'];
var color = d3.scaleQuantile()
.domain([0, 15])
.range(ranges.range1);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var data = d3.range(15);
var rects = svg.selectAll(".rects")
.data(data)
.enter()
.append("rect");
rects.attr("y", 40)
.attr("x", d => d * 25)
.attr("height", 20)
.attr("width", 20)
.attr("stroke", "gray")
.attr("fill", d => color(d));
d3.selectAll("button").on("click", function() {
color.range(ranges[this.value]);
rects.attr("fill", d => color(d))
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<button value="range1">Range1</button>
<button value="range2">Range2</button>
<button value="range3">Range3</button>
<button value="range4">Range4</button>
I have created a simple Angular/D3 donut chart.
Plunker: http://plnkr.co/edit/NyH1udkjBj3zymhGaThZ?p=preview
However i want the chart to have an animation on page load. In that, i mean, i would like the filled (blue area) to transition in.
Somethign similar to: http://codepen.io/tpalmer/pen/jqlFG/
HTML:
<div ng-app="app" ng-controller="Ctrl">
<d3-donut radius="radius" percent="percent" text="text"></d3-donut>
div>
JS:
var app = angular.module('app', []);
app.directive('d3Donut', function() {
return {
restrict: 'E',
scope: {
radius: '=',
percent: '=',
text: '=',
},
link: function(scope, element, attrs) {
var radius = scope.radius;
var percent = scope.percent;
var text = scope.text;
var svg = d3.select(element[0])
.append('svg')
.style('width', radius / 2 + 'px')
.style('height', radius / 2 + 'px');
var donutScale = d3.scale.linear().domain([0, 100]).range([0, 2 * Math.PI]);
var color = "#5599aa";
var data = [
[0, 100, "#e2e2e2"],
[0, percent, color]
];
var arc = d3.svg.arc()
.innerRadius(radius / 6)
.outerRadius(radius / 4)
.startAngle(function(d) {
return donutScale(d[0]);
})
.endAngle(function(d) {
return donutScale(d[1]);
});
svg.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d", arc)
.style("fill", function(d) {
return d[2];
})
.attr("transform", "translate(" + radius / 4 + "," + radius / 4 + ")");
svg.append("text")
.attr("x", radius / 4)
.attr("y", radius / 4)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.style("fill", "#333")
.attr("text-anchor", "middle")
.text(text);
}
};
});
function Ctrl($scope) {
$scope.radius = 200;
$scope.percent = 50;
$scope.text = "40%";
}
I have combined your code with the one you cited as an example. Here is a working PLUNK.
Two things are worth noting -
1. using attrTween:
.attrTween("d", function (a) {
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
console.log(this._current);
return function(t) {
text.text( format(i2(t) / 100) );
return arc(i(t));
};
});
2. updating the data:
data = [
[0,100,"#e2e2e2"],
[0,percent,color]
];
I did some other less important changes like a more idiomatic use of the controller snippet, etc., but the above is what matters.
Would the Angular 'run' command help here?
angular.module('app', []).run(myfunction($rootScope))
See 'run' on this page https://docs.angularjs.org/api/ng/type/angular.Module
'run()' will give you one-time only execution of so-and-so a function once AngularJs has bootstrapped.
I created a multi-level pie chart but i am having trouble animate it on load.
Here is the JS that i tryied.The animation works fine on the first circle of the chart , but it hides the other 2.
Any help would be appreciated.Thanks:)
<script>
var dataset = {
final: [7000],
process: [1000, 1000, 1000, 7000],
initial: [10000],
};
var width = 660,
height = 500,
cwidth = 75;
var color = d3.scale.category20();
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("class","wrapper")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
var gs = svg.selectAll("g.wrapper").data(d3.values(dataset)).enter()
.append("g")
.attr("id",function(d,i){
return Object.keys(dataset)[i];
});
var gsLabels = svg.selectAll("g.wrapper").data(d3.values(dataset)).enter()
.append("g")
.attr("id",function(d,i){
return "label_" + Object.keys(dataset)[i];
});
var count = 0;
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) {
if(Object.keys(dataset)[j] === "final"){
return arc.innerRadius(cwidth*j).outerRadius(cwidth*(j+1))(d);
}
else{
return arc.innerRadius(10+cwidth*j).outerRadius(cwidth*(j+1))(d);
}
})
.transition().delay(function(d, i, j) {
return i * 500;
}).duration(500)
.attrTween('d', function(d,x,y) {
var i = d3.interpolate(d.startAngle+0.1, d.endAngle);
return function(t) {
d.endAngle = i(t);
return arc(d);
}
});
</script>
The main problem is that you're using the same arc generator for all of the different pie segments. That means that after the transition, all the segments will have the same inner and outer radii -- they are there, you just can't see them because they're obscured by the outer blue segment.
To fix this, use different arc generators for the different levels. You also need to initialise the d attribute to zero width (i.e. start and end angle the same) for the animation to work properly.
I've implemented a solution for this here where I'm saving an arc generator for each pie chart segment with the data assigned to that segment. This is a bit wasteful, as a single generator for each level would be enough, but faster to implement. The relevant code is below.
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) {
d._tmp = d.endAngle;
d.endAngle = d.startAngle;
if(Object.keys(dataset)[j] === "final"){
d.arc = d3.svg.arc().innerRadius(cwidth*j).outerRadius(cwidth*(j+1));
}
else{
d.arc = d3.svg.arc().innerRadius(10+cwidth*j).outerRadius(cwidth*(j+1));
}
return d.arc(d);
})
.transition().delay(function(d, i, j) {
return i * 500;
}).duration(500)
.attrTween('d', function(d,x,y) {
var i = d3.interpolate(d.startAngle, d._tmp);
return function(t) {
d.endAngle = i(t);
return d.arc(d);
}
});