I have been working with dc.js for a year now. Recently I have been tasked to implement a pie chart as below:
I want to replace the text labels in the pie chart slices with appropriate images.
I saw this implemented in pure d3.js. Can someone help me translate the implementation to dc.js?
http://jsfiddle.net/LLwr4q7s/
pie
.width(600)
.height(500)
.radius(200)
.innerRadius(120)
.dimension(disastersDimension)
.group(disastersGroup)
.on("filtered", function (chart, filter) {
var sel = filter;
let percentage = 0,
value = 0;
let disastersBuffer = [];
totalAmount = 0;
pie.selectAll("text.pie-slice").text((d) => {
percentage = dc.utils.printSingleValue(
((d.endAngle - d.startAngle) / (2 * Math.PI)) * 100
);
disastersBuffer.push({ ...d.data, percentage });
totalAmount += parseFloat(d.data.value);
});
filterPiechart(sel, percentage, totalAmount, disastersBuffer, value);
})
.on("renderlet", (chart) => {
if (!chart.selectAll("g.selected")._groups[0].length) {
chart.filter(null);
filterPiechart("", 100, totalAmount, [], 0);
}
var arc = chart.radius(250).innerRadius(100);
console.log(arc);
var g = chart.selectAll(".pie-slice");
chart
.selectAll(".pie-slice")
.append("image")
.attr("xlink:href", "img/disasters/Floods.png")
.attr("transform", "translate(-10,10) rotate(315)")
.attr("width", "26px")
.attr("hight", "26px")
.style("background-color", "white")
.attr("x", function (d) {
var bbox = this.parentNode.getBBox();
return bbox.x;
})
.attr("y", function () {
var bbox = this.parentNode.getBBox();
return bbox.y;
});
g.append("g")
.append("svg:image")
.attr("xlink:href", function (d) {
let filteredImage = self.piedata.find(
(i) => i.label == d.data.key
);
let image = filteredImage ? filteredImage.image : "";
return image;
})
.attr("width", 30)
.attr("height", 40)
.attr("x", function (d) {
var bbox = this.parentNode.getBBox();
return bbox.x;
})
.attr("y", function (d) {
var bbox = this.parentNode.getBBox();
return bbox.y;
});
})
.addFilterHandler(function (filters, filter) {
filters.length = 0; // empty the array
filters.push(filter);
return filters;
});
I took the fiddle and added a couple of 'Meteoicons' I found here.
(Of course, those icons are taken as an example and I have no permission to use them commercially)
The icons are stored in a separated <svg> elements. To render an icon, just select its root <g> element and copy its content to another <g> you create in your piechart:
g.append("g")
.attr("transform", d => `translate(${arc.centroid(d)}) scale(0.25)`)
.append('g')
.attr('transform', 'translate(-256,-256)') // The original icons are 256 x 256
.html(d => d3.select(`#meteo-icon-${... some attribute of d ...} > g`).html())
The code is for demonstration purposes only, you will need to modify it for your needs.
See the result in the snippet below:
var width = 550,
height = 550,
radius = 250,
colors = d3.scale.ordinal()
.range(['#336699 ','#336699 ','#ACD1E9','#ACD1E9','#ACD1E9']);
var image_width=40,
image_height=40;
var piedata = [
{
label: "test",
image: "http://placeimg.com/40/40/any",
value: 50
},
{
label: "",
image: "http://placeimg.com/42/42/any",
value: 50
},
{
label: "Jonathan",
image: "http://placeimg.com/44/44/any",
value: 50
},
{
label: "Lorenzo",
image: "http://placeimg.com/46/46/any",
value: 50
},
{
label: "Hillary",
image: "http://placeimg.com/38/38/any",
value: 50
}
]
var pie = d3.layout.pie()
.value(function(d) {
return d.value;
})
var arc = d3.svg.arc()
.outerRadius(250)
.innerRadius(100)
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate('+(width-radius)+','+(height-radius)+')');
var g = svg.selectAll(".arc")
.data(pie(piedata))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d,i) { return colors(i); });
g.append("g")
.attr("transform", d => `translate(${arc.centroid(d)}) scale(0.25)`)
.append('g')
.attr('transform', 'translate(-256,-256)')
.html(() => d3.select(`#meteo-icon-${Math.random() < 0.5 ? 1 : 2} > g`).html())
.selectAll('path')
.style('fill', 'orange');
path {
stroke: #fff;
fill-rule: evenodd;
}
text {
font-family: Arial, sans-serif;
font-size: 12px;
}
.meteo-icon {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<svg id="meteo-icon-1" class="meteo-icon" width="24" height="24" viewBox="0 0 512 512">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#1D1D1B" d="M177.615,288c7.438-36.521,39.688-64,78.396-64
c38.709,0,70.958,27.479,78.376,64h32c-7.834-54.125-54.084-96-110.376-96c-56.271,0-102.541,41.875-110.375,96H177.615z
M256.011,160c8.833,0,16-7.167,16-16v-32c0-8.833-7.167-16-16-16c-8.832,0-16,7.167-16,16v32
C240.011,152.833,247.179,160,256.011,160z M403.073,156.917c-6.249-6.25-16.374-6.25-22.625,0l-22.625,22.625
c-6.249,6.25-6.249,16.375,0,22.625c6.251,6.25,16.376,6.25,22.625,0l22.625-22.625
C409.323,173.292,409.323,163.167,403.073,156.917z M154.177,179.542l-22.625-22.625c-6.249-6.25-16.373-6.25-22.625,0
c-6.249,6.25-6.249,16.375,0,22.625l22.625,22.625c6.252,6.25,16.376,6.25,22.625,0
C160.429,195.917,160.429,185.792,154.177,179.542z M352.011,320h-192c-8.832,0-16,7.167-16,16s7.168,16,16,16h192
c8.833,0,16-7.167,16-16S360.844,320,352.011,320z M320.011,384h-128c-8.832,0-16,7.167-16,16s7.168,16,16,16h128
c8.833,0,16-7.167,16-16S328.844,384,320.011,384z"/>
</g>
</svg>
<svg id="meteo-icon-2" class="meteo-icon" width="24" height="24" viewBox="0 0 512 512">
<g>
<path fill="#1D1D1B" d="M349.852,343.15c-49.876,49.916-131.083,49.916-181,0c-49.916-49.917-49.916-131.125,0-181.021
c13.209-13.187,29.312-23.25,47.832-29.812c5.834-2.042,12.293-0.562,16.625,3.792c4.376,4.375,5.855,10.833,3.793,16.625
c-12.542,35.375-4,73.666,22.249,99.917c26.209,26.228,64.501,34.75,99.917,22.25c5.792-2.062,12.271-0.583,16.625,3.792
c4.376,4.333,5.834,10.812,3.771,16.625C373.143,313.838,363.06,329.941,349.852,343.15z M191.477,184.754
c-37.438,37.438-37.438,98.354,0,135.771c40,40.021,108.125,36.417,143-8.167c-35.959,2.25-71.375-10.729-97.75-37.084
c-26.375-26.354-39.333-61.771-37.084-97.729C196.769,179.796,194.039,182.192,191.477,184.754z"/>
</g>
</svg>
First, apologies for an incomplete example, but I ran out of time and I think this shows the principles.
I agree with #MichaelRovinsky that SVG icons would be better than images, but I couldn't find a CDN for SVG icons that would be suitable for the example, and I think the principles are exactly the same, since you could embed SVGs as image just as well.
Using placeimg.com for this purpose leads to weird results because the same URL will yield different results when read twice, so e.g. two slices may end up with the same image, and images change when the chart redraws.
Luckily these are both beside the point of customizing dc.js!
Adding things to dc.js pie slices
It would be nice if dc.js used an svg g group element to put the text in. Then we could just add to it and the position would be correct.
Instead, we have to add our image element and read the corresponding data from the pie label to get the placement:
chart.on('pretransition', chart => {
let labelg = chart.select('g.pie-label-group');
let data = labelg.selectAll('text.pie-label').data();
console.log('data', data);
Then we can add image elements in the same layer/g:
let pieImage = labelg.selectAll('image.pie-image');
let arcs = chart._buildArcs();
pieImage.data(data)
.join(
enter => enter.append('image')
.attr('class', 'pie-image')
.attr('x', -19)
.attr('y', -19))
.attr('href', d => images[d.data.key === 'Others' ? 'Others' : d.data.key.slice(4)])
.attr('transform', d => chart._labelPosition(d, arcs));
});
Notice that the attributes which only need to be set once (on enter) are inside the join call, and the attributes which need to be set every redraw (on update) are outside the join call.
x and y are negative one half the image size to center the images.
I used an object to store the URLs but you could use whatever.
Demo fiddle
Limitation
As with any customization of the pie chart, this doesn't account for animations well. The images will move before the animation is complete. If you care, I think I wrote an answer some years ago which dealt with this properly. I can probably dig it up but it was quite complicated and IMHO not worth it.
Using D3.js, I have something like this:
var sets = [
{ data:[{date:1980,value:10},{date:1981,value:20},{date:1982,value:30}] },
{ data:[{date:1981,value:10},{date:1982,value:20},{date:1983,value:30}] },
{ data:[{date:1982,value:10},{date:1983,value:20},{date:1984,value:30}] }
];
And I bind it to make a chart like this:
var paths = g.selectAll("path")
.data(sets);
paths.enter()
.append("path")
.datum(function(d) { return d.data; })
.attr("class","line")
.attr("d", line);
Where g is a g element inside an svg element. This works. For each item in set I get a path using the values in data. Now what I want to do is click an element and replace the data with a different set:
var altData = [
{ data:[{date:1980,value:30},{date:1981,value:20},{date:1982,value:10}] },
{ data:[{date:1981,value:10},{date:1982,value:20},{date:1983,value:30}] },
{ data:[{date:1982,value:10},{date:1983,value:20},{date:1984,value:0}] }
];
d3.select("#transition").on("click", function() {
paths.data(altData);
console.log("click");
});
But the paths.data(altData) doesn't appear to do anything. There are no console errors, but the chart doesn't change. What do I need to do to tell it that the data has changed and the lines should be redrawn? As a bonus, I'd really like this transition to be animated.
Full fiddle
Basically you need to tell d3 to redraw it. In your case, it is by calling attr("d", line).
For transition, put transition() between two attr("d", fnc). Your onclick function will look like the following
d3.select("#transition").on("click", function() {
paths.attr("d", line)
.transition()
.attr("d", function(d, i){
return line(altData[i].data)
})
});
Jsfiddle http://jsfiddle.net/8fLufc65/
Look at this plnkr that will change the data when transition is clicked.
I made the part that draws the lines into a function and pass the data for which it should be drawing the lines.
drawPaths(sets) ;
function drawPaths(sets) {
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var paths = g.selectAll("path")
.data(sets);
paths.enter()
.append("path")
.datum(function(d) { console.log(d); return d.data; })
.attr("class","line")
.attr("d", line);
}
I'm trying to highlight some points in a time series modeled using a nvd3.js eventLineChart - more precisely I have a json object with time-stamps and for each time-stamp I would like to add a vertical line at this particular date/time. The highlighted points may not exist in the time-series data source and are global over all groups of the time-series data (like ticks).
Any ideas on how this could be achieved? - I tried adding a standard line to my plot (fixed y1 and y2 and x according to the timestamp of the events i want to highlight) but wasn't able to have the timestamps scaled to the same range as the original time series.
Here are some parts of the model I started to build for that purpose based on nv.models.lineChart. - (just an excerpt of the model as most of the code is just a copy from the lineChart model):
nv.models.eventLineChart = function() {
// adds vertical highlights to line chart
"use strict";
var chartEvents = {}
function chart(selection) {
selection.each(function(data) {
// Setup Scales
x = lines.xScale();
y = lines.yScale();
// Setup containers and skeleton of chart
var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-lineChart').append('g');
var g = wrap.select('g');
gEnter.append('g').attr('class', 'nv-eventLinesWrap');
//------------------------------------------------------------
// Main Chart Component(s)
var eventWrap = wrap
.select('.nv-eventLinesWrap')
.selectAll('.nv-eventLines')
.data(function(d) {return d });
eventWrap
.enter()
.append('g')
.attr('class', 'nv-eventLines');
// chartEvents json ==> [{decsription: "test,"timestamp: 1375031820000}]
var eventLine = eventWrap
.selectAll('.nv-eventLine')
.data(chartEvents, function(d){return (d.timestamp)});
var eventLineEnter = eventLine.enter()
.append('line').attr('class', 'nv-eventLine')
.style('stroke-opacity', 0);
// #todo set ymin and ymax
eventLine
.attr('x1', function(d){ return x(d.timestamp);})
.attr('x2', function(d){ return x(d.timestamp);})
.attr('y1', 300)
.attr('y2', 800)
.style('stroke', function(d,i,j) { return color(d,j) })
.style('stroke-opacity', function(d,i) {
return 1;
});
});
return chart;
}
chart.setChartEvents = function(_) {
if (!arguments.length) return chartEvents;
chartEvents = _;
return chart;
};
return chart;}
This model is called by using:
nv.addGraph(function() {
var nv3dChart = nv.models.eventLineChart().useInteractiveGuideline(true).setChartEvents(json.chartEvents);
// json.chartEvents >> [{decsription: "EventDescription,"timestamp: 1375031820000}]
nv3dChart.xAxis
.showMaxMin(false);
.tickFormat(function(d) { return d3.time.format("%Y-%m-%d")(new Date(d)) });
nv3dChart.yAxis
.axisLabel(widgetConfig.action.data.kpiName)
.tickFormat(d3.format(',.f'));
var ndg = d3.select(renderToElementId+' svg');
ndg.datum([{
values: json.data,
key: widgetConfig.action.data.tagName
}])
.transition().duration(500);
nv.utils.windowResize(nv3dChart.update);
return nv3dChart;})
Which produces currently this svg output (events that should be displayed by vertical lines only)
<g class="nv-eventLines">
<line class="nv-eventLine" x1="1375031820000" x2="1375031820000" y1="300" y2="800" style="stroke: #1f77b4;"></line>
</g>
.. as described I haven't yet figured out a way to implement the scaling of the events x values according to the scale of the line chart
Would greatly appreciate any help regarding this problem
I now manually created all scales for x and y and added them to the nvd3 elements. I'm not particularly happy with that solution as it prevents me from creating a more modular feature for multiple nvd3 charts but it is a starting point.
Here is an outline of my current solution:
nv.models.eventLineChart = function() {
// initialize scales
var y = d3.scale.linear(),
x = d3.scale.linear();
// set scales of lines
lines = nv.models.line()
.yScale(y)
function chart(selection) {
//#todo support for multiple series
// set domain and range for scales
x
.domain(d3.extent(data[0].values, function(d){return d.x}))
.range([0,availableWidth]);
y
.domain(d3.extent(data[0].values, function(d){return d.y}))
.range([0,availableHeight]);
// add container for vertical lines
gEnter.append('g').attr('class', 'nv-eventLinesWrap');
// setup container
var eventWrap = wrap.select('.nv-eventLinesWrap').selectAll('.nv-eventLines')
.data(function(d) {return d });
eventWrap.enter().append('g').attr('class', 'nv-eventLines');
eventWrap.select('.nv-eventLinesWrap').attr('transform', 'translate(0,' + (-margin.top) +')');
var eventLine = eventWrap.selectAll('.nv-eventLine').data(chartEvents, function(d){return (d.timestamp)});
var eventLineEnter = eventLine.enter()
.append('line').attr('class', 'nv-eventLine')
// configure and style vertical lines
//#todo: set color for lines
eventLine
.attr('x1', function(d){ return x(d.timestamp)})
.attr('x2', function(d){ return x(d.timestamp)})
.attr('y1', y.range()[0])
.attr('y2', y.range()[1])
.style('stroke', function(d,i,j) { return "#000"; })
.style('stroke-width',1)
// #todo add vertical lines via transitions, add vLine content to toolbox
}}
Thank you, Lars, for your contributions .. they really helped a lot to understand certain parts in more detail.
If anyone has come up with a better idea to solve this problem I would be very grateful if you could post these suggestions here!
I am trying to create an interactive org chart such that when I click on a box that box is repositioned in the centre of the SVG container and all other elements transition as well but remain in the same relative position. So if you click the top box in the list, they all move down together. Then if you click one of the lower boxes they all move up together but always so the selected box is in the centre. If you click on a box which is already in the middle it should not move but at the moment they are flying all over the place.
I have got this working for the first click but on each subsequent click the boxes start flying all over the place. I am using the mouse listener to get the current position and calculate an offset to centre the selected box that I feed into transform/translate. I think this is where the strange behaviour is coming from because the offset is calculating correctly (viewed through console.log) but the applied transition is not equal to this calculation.
I have read many posts about transform/translate but they all seem to apply to a single transition, not multiple sequential transitions. I have tried using .attr(transform, null) before each new transition but this didn't work. I have also tried to dynamically extract the current x,y of the selected component and then update these attributes with the offset value but this didn't work either. Am really stuck with this and any help is greatly appreciated!
Thanks,
SD
<script type="text/javascript">
var cwidth = 1000;
var cheight = 500;
var bwidth = 100;
var bheight = 50;
// container definition
var svgContainer = d3.select("body").append("svg")
.attr("width",cwidth)
.attr("height",cheight)
.on("mousemove", mousemove);
// Background gray rectangle
svgContainer.append("svg:rect")
.attr("x",0)
.attr("y",0)
.attr("width",cwidth)
.attr("height",cheight)
.style("fill", "lightgrey");
// data
var secondData = [
{ "idx": 1, "name": "Commercial" },
{ "idx": 2, "name": "Finance" },
{ "idx": 3, "name": "Operations" },
{ "idx": 4, "name": "Business Services" }
];
var secondElements = secondData.length;
// group definition
var secondNodes = svgContainer.append("g")
.attr("class", "nodes")
.selectAll("rect")
.data(secondData)
.enter()
.append("g")
.attr("transform", function(d, i) {
d.x = 300;
d.y = ((cheight/secondElements)*d.idx)-bheight;
return "translate(" + d.x + "," + d.y + ")";
});
// Add elements to the previously added g element.
secondNodes.append("rect")
.attr("class", "node")
.attr("height", bheight)
.attr("width", bwidth)
.style("stroke", "gray")
.style("fill", "white")
.attr("y", function() {return -(bheight/2);})
.on("mouseover", function(){d3.select(this).style("fill", "aliceblue");})
.on("mouseout", function(){d3.select(this).style("fill", "white");})
.on("mousedown", center);
// Add a text element to the previously added g element.
secondNodes.append("text")
.attr("text-anchor", "left")
.attr("x", 15)
.attr("y",5)
.text(function(d) {return d.name;});
// gets current coordinates for transition
var current = [0,0];
var xshift = 0;
var yshift = 0;
// get offset to centre from current mouse location
function mousemove() {
//console.log(d3.mouse(this));
current = d3.mouse(this);
xshift = 500 - current[0];
yshift = 250 - current[1];
}
//applies transitions
function center(d) {
secondNodes.selectAll("rect")
.transition()
.delay(0)
.duration(500)
.attr("transform", "translate(" + xshift + "," + yshift + ")")
.each("end", function() {
secondNodes.selectAll("text")
.transition()
.delay(0)
.duration(0)
.attr("transform", null);
});
}
</script>
If you want everything to keep its relative position, it seems to me that something far easier to do would be to include everything in a g element that you can set the transform attribute one. That is, instead of moving many elements, you would have to move just the top-level container. The code you have for handling clicks would remain pretty much the same except that you only need to set the transform attribute on that one element.