I just had an idea with d3s consisting of two steps:
Make this nice clock interactive, thus allowing the user to specify a date graphically. http://bl.ocks.org/mbostock/1096355
Adding several of these as nodes to a force graph http://bl.ocks.org/mbostock/929623
For 1. I'd like to have the user click and drag on the time arcs to modify their position, so they indicate the date the user wants to specify. What would be the easiest and most straight forward way to achieve this?
For 2. How could I scale my interactive clocks down and append them as node elements?
Thanks for your help!
Update:
So I managed to react on drag events and calculate a diff with Math.tan().
Now I'm stuck on modifying the arc itself.
var width = 961,
height = 800,
radius = Math.min(width, height) / 5,
spacing = .10;
var formatSecond = d3.time.format("%S s"),
formatMinute = d3.time.format("%M m"),
formatHour = d3.time.format("%H h"),
formatDay = d3.time.format("%a"),
formatDate = d3.time.format("%d d"),
formatMonth = d3.time.format("%b");
var color = d3.scale.linear()
.range(["hsl(-180,50%,50%)", "hsl(180,50%,50%)"])
.interpolate(interpolateHsl);
var arc = d3.svg.arc()
.startAngle(0)
.endAngle(function(d) { return d.value * 2 * Math.PI; })
.innerRadius(function(d) { return d.index * radius; })
.outerRadius(function(d) { return (d.index + spacing) * radius; })
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var field = svg.selectAll("g")
.data(fields)
.enter().append("g");
var dragTransformation = [];
var dragOrig = [];
var drag = d3.behavior.drag()
.on("dragstart", function() {
dragTransformation = [0, 0];
dragOrig = d3.mouse(this);
})
.on("drag", function(d,i) {
thisArc = d3.select(this);
dragTransformation[0] += d3.event.dx;
dragTransformation[1] += d3.event.dy;
})
.on("dragend", function() {
var xDiff = dragTransformation[0] / dragOrig[0];
var yDiff = dragTransformation[1] / dragOrig[1];
var diffFactor = Math.tan(xDiff / yDiff);
/**
* -----------------------------------------------------------------------
* Change arc start + end angle here
*
*/
d3.select(this).
attr("value", function(d) { console.log("HERE", d); return 3; });
});
field.append("path");
field.append("text");
// Calls drag on arc element
field.call(drag);
tick();
//d3.transition().duration(0).each(tick);
d3.select(self.frameElement).style("height", height + "px");
function arcTween(d) {
var i = d3.interpolateNumber(d.previousValue, d.value);
return function(t) { d.value = i(t); return arc(d); };
}
function tick() {
field = field
.each(function(d) { this._value = d.value; })
.data(fields)
.each(function(d) { d.previousValue = this._value; });
field.select("path")
.transition()
.ease("elastic")
.attrTween("d", arcTween)
.style("fill", function(d) { return color(d.value); });
field.select("text")
.attr("dy", function(d) { return d.value < .5 ? "-.5em" : "1em"; })
.text(function(d) { return d.text; })
.transition()
.ease("elastic")
.attr("class", "white")
.attr("transform", function(d) {
return "rotate(" + 360 * d.value + ")"
+ "translate(0," + -(d.index + spacing / 50) * radius + ")"
+ "rotate(" + (d.value < .5 ? -90 : 90) + ")"
+ "rotate(90)"
});
//setTimeout(tick, 1000 - Date.now() % 1000);
}
function fields() {
var now = new Date;
return [{
index: .7,
text: formatSecond(now),
value: now.getSeconds() / 60
},{
index: .6,
text: formatMinute(now),
value: now.getMinutes() / 60
},{
index: .5,
text: formatHour(now),
value: now.getHours() / 25
},{
index: .3,
text: formatDay(now),
value: now.getDay() / 7
},{
index: .2,
text: formatDate(now),
value: (now.getDate() - 1) / (32 - new Date(now.getYear(), now.getMonth(), 32).getDate())
},{
index: .1,
text: formatMonth(now),
value: now.getMonth() / 12
}
];
}
// Avoid shortest-path interpolation.
function interpolateHsl(a, b) {
var i = d3.interpolateString(a, b);
return function(t) {
return d3.hsl(i(t));
};
}
The arcs were drawn with data generated from the current date (fields function) which is updated on every tick.
How would I change the arc's start and endAngle with the value i've generated with the oneMouseDown function and map it to the date data (thus actually setting a new date when modifying the arc)? I've found approaches with
d3.select(this)...attr()...
but didn't find way to access ther start or endAngle attribute.
Related
I know there are other solutions to this problem, but honestly they are very hard to parse out and implement into my own code. If someone can point me in the right direction in the code snippets below, I would greatly appreciate it.
ISSUE: Overlapping lines in d3 drilldown pie chart:
GOAL: To show d3 drilldown pie chart without overlapping labels.
VisualizationScopeModel.prototype.renderDrilldownPieGraph = function (data) {
let width = 950,
height = 500,
margin = 75,
radius = Math.min(width - margin, height - margin) / 2,
pieChart = d3.layout.pie().sort(null).value(function (d) {
return d.hasOwnProperty('genres') ? d.genres.length : d.count;
}),
arc = d3.svg.arc().outerRadius(radius),
outerArc = d3.svg.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9),
polylineArc = d3.svg.arc()
.innerRadius(radius * 0.7)
.outerRadius(radius * 0.9);
// clear existing svg element
d3.select("svg").text("");
let svg = d3.select("#my_dataviz")
.append("svg"),
defs = svg.append("svg:defs"),
// Declare a main gradient with the dimensions for all gradient entries to refer
mainGrad = defs.append("svg:radialGradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("cx", 0).attr("cy", 0).attr("r", radius).attr("fx", 0).attr("fy", 0)
.attr("id", "master"),
// The pie sectors container
arcGroup = svg.append("svg:g")
.attr("class", "arcGroup")
.attr("filter", "url(#shadow)")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")"),
// The sector text labels
labels = svg.append("g")
.attr("class", "labels")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")"),
// The text lines point a sector
lines = svg.append("g")
.attr("class", "lines")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")");
// Declare shadow filter
let shadow = defs.append("filter").attr("id", "shadow")
.attr("filterUnits", "userSpaceOnUse")
.attr("x", -1 * (width / 2)).attr("y", -1 * (height / 2))
.attr("width", width).attr("height", height);
shadow.append("feGaussianBlur")
.attr("in", "SourceAlpha")
.attr("stdDeviation", "4")
.attr("result", "blur");
shadow.append("feOffset")
.attr("in", "blur")
.attr("dx", "4").attr("dy", "4")
.attr("result", "offsetBlur");
shadow.append("feBlend")
.attr("in", "SourceGraphic")
.attr("in2", "offsetBlur")
.attr("mode", "normal");
// "Fold" pie sectors by tweening its current start/end angles
// into 2*PI
function tweenOut(data) {
data.startAngle = data.endAngle = (2 * Math.PI);
var interpolation = d3.interpolate(this._current, data);
this._current = interpolation(0);
return function (t) {
return arc(interpolation(t));
};
}
// "Unfold" pie sectors by tweening its start/end angles
// from 0 into their final calculated values
function tweenIn(data) {
var interpolation = d3.interpolate({ startAngle: 0, endAngle: 0 }, data);
this._current = interpolation(0);
return function (t) {
return arc(interpolation(t));
};
}
// Helper function to extract color from data object
function getColor(data, index) {
return data.color;
}
// Helper function to extract a darker version of the color
function getDarkerColor(data, index) {
return d3.rgb(getColor(data, index)).darker();
}
function findChildenByName(name) {
for (i = 0; i < data.length; i++) {
if (data[i].name == name) {
return data[i].genres;
}
}
return data;
}
// Redraw the graph given a certain level of data
function updateGraph(name) {
var currData = data;
if (name != undefined) currData = findChildenByName(name);
// Create a gradient for each entry (each entry identified by its unique category)
var gradients = defs.selectAll(".gradient").data(currData, function (d) { return d.name; });
gradients.enter().append("svg:radialGradient")
.attr("id", function (d, i) { return "gradient" + d.name; })
.attr("class", "gradient")
.attr("xlink:href", "#master");
gradients.text("").append("svg:stop").attr("offset", "0%").attr("stop-color", getColor);
gradients.append("svg:stop").attr("offset", "90%").attr("stop-color", getColor);
gradients.append("svg:stop").attr("offset", "100%").attr("stop-color", getDarkerColor);
// Create a sector for each entry in the enter selection
var paths = arcGroup.selectAll("path")
.data(pieChart(currData), function (d) { return d.data.name; });
paths.enter().append("svg:path").attr("class", "sector");
// Each sector will refer to its gradient fill
paths.attr("fill", function (d, i) { return "url(#gradient" + d.data.name + ")"; })
.transition().duration(1000)
.attrTween("d", tweenIn).each("end", function () {
this._listenToEvents = true;
});
// Mouse interaction handling
paths.on("click", function (d) {
if (this._listenToEvents) {
// Reset inmediatelly
d3.select(this).attr("transform", "translate(0,0)")
// Change level on click if no transition has started
paths.each(function () {
this._listenToEvents = false;
});
if (d.data.genres) updateGraph(d.data.name);
else updateGraph()
}
}).on("mouseover", function (d) {
// Mouseover effect if no transition has started
if (this._listenToEvents) {
// Calculate angle bisector
var ang = d.startAngle + (d.endAngle - d.startAngle) / 2;
// Transformate to SVG space
ang = (ang - (Math.PI / 2)) * -1;
// Calculate a 10% radius displacement
var x = Math.cos(ang) * radius * 0.1;
var y = Math.sin(ang) * radius * -0.1;
d3.select(this).transition()
.duration(250).attr("transform", "translate(" + x + "," + y + ")");
}
}).on("mouseout", function (d) {
// Mouseout effect if no transition has started
if (this._listenToEvents) {
d3.select(this).transition()
.duration(150).attr("transform", "translate(0,0)");
}
});
// Collapse sectors for the exit selection
paths.exit().transition().duration(1000)
.attrTween("d", tweenOut).remove();
// Create sector texts
var texts = labels.text("").selectAll('text')
.data(pieChart(currData), function (d) { return d.data.org_name; });
texts.enter().append("text").attr("dy", ".75em")
.text(function (d) {
return `${d.data.org_name} (${d.data.hasOwnProperty('genres') ? d.data.genres.length : d.data.count})`;
});
function midAngle(d) {
return d.startAngle + (d.endAngle - d.startAngle) / 2;
}
texts.transition().duration(1000)
.attrTween("transform", function (d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function (t) {
var d2 = interpolate(t);
var pos = outerArc.centroid(d2);
pos[0] = (radius + margin / 2) * (midAngle(d2) < Math.PI ? 1 : -1);
return "translate(" + pos + ")";
};
})
.styleTween("text-anchor", function (d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function (t) {
var d2 = interpolate(t);
return midAngle(d2) < Math.PI ? "start" : "end";
};
});
texts.exit().transition().duration(1000)
.attrTween("d", tweenOut).remove();
// Create text lines point sectors
var polylines = lines.text("").selectAll("polyline")
.data(pieChart(currData), function (d) { return d.data.name; });
polylines.enter().append("polyline");
polylines.transition().duration(1000)
.attrTween("points", function (d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function (t) {
var d2 = interpolate(t);
var pos = outerArc.centroid(d2);
pos[0] = (radius + margin / 2) * (midAngle(d2) < Math.PI ? 1 : -1);
return [polylineArc.centroid(d2), outerArc.centroid(d2), pos];
};
});
polylines.exit().transition().duration(1000)
.attrTween("d", tweenOut).remove();
}
updateGraph();
}
I am working on a d3 application - which features a hooped chart. I have a version which is a radial 360 type of chart - but I am unsure how you'd configure the path to arc in this fashion.
//old js fiddle of a crescent chart
http://jsfiddle.net/2wfktc3g/
var $this = $('.crescentchart');
var data = [{
label: 'Fudge Brownie',
value: 5,
},
{
label: 'Cherry Vanilla',
value: 60,
},
{
label: 'Pistachio',
value: 5,
},
{
label: 'Caramel',
value: 10,
}
];
var oldData = "";
var width = $this.data('width'),
height = $this.data('height'),
radius = $this.data('r'),
thickness = $this.data("thickness"),
spacing = $this.data("spacing");
var color = d3.scaleOrdinal()
.range(["#bc658d", "#82c4c3", "#f9d89c", "#f5a7a7"]);
var svg = d3.select($this[0])
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr('class', 'crescentchart')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var segments = svg.append('g').attr('class', 'segments');
var data = setData(data, radius);
//append previous value to it.
$.each(data, function(index, value) {
if (oldData[index] != undefined) {
data[index]["previousEndAngle"] = oldData[index].endAngle;
} else {
data[index]["previousEndAngle"] = 0;
}
});
var arcpaths = segments.selectAll("path")
.data(data);
arcpaths.enter().append("path")
.style("fill", function(d, i) {
return color(i);
})
.transition()
.ease(d3.easeElastic)
.duration(750)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
});
arcpaths.transition()
.ease(d3.easeElastic)
.style("fill", function(d, i) {
return color(i);
})
.duration(750)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
});
arcpaths.exit().transition()
.ease(d3.easeBounce)
.duration(750)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
})
.remove();
function arcTween(b, thickness, ir) {
var prev = JSON.parse(JSON.stringify(b));
prev.endAngle = b.previousEndAngle;
var i = d3.interpolate(prev, b);
return function(t) {
return getArc(thickness, ir)(i(t));
};
}
function getRadiusRing(ir, i) {
return ir - (i * (thickness + spacing));
}
function getArc(thickness, ir) {
var arc = d3.arc()
.innerRadius(function(d) {
return getRadiusRing(ir, d.index);
})
.outerRadius(function(d) {
return getRadiusRing(ir + thickness, d.index);
})
.startAngle(function(d, i) {
return d.startAngle;
})
.endAngle(function(d, i) {
return d.endAngle;
});
return arc;
}
function setData(data, r) {
var diameter = (2 * Math.PI) * r;
var segmentValueSum = 0;
$.each(data, function(ri, va) {
segmentValueSum += va.value;
});
$.each(data, function(ri, va) {
var segmentValue = va.value;
var fraction = segmentValue / segmentValueSum;
var arcBatchLength = fraction * (2 * Math.PI);
var arcPartition = arcBatchLength;
var startAngle = Math.PI;
var endAngle = startAngle + arcPartition;
data[ri]["startAngle"] = startAngle;
data[ri]["endAngle"] = endAngle;
data[ri]["index"] = ri;
});
return data;
}
//legend
var legendsvgw = 150;
var legendsvgh = 100;
var ringRadius = 5;
var vertical = 20;
var legendsvg = d3.select($this[0])
.append("svg")
.attr("width", legendsvgw)
.attr("height", legendsvgh)
.append("g")
.attr('class', 'legendsvg')
.attr("transform", "translate(" + 10 + "," + 10 + ")");
var legend = legendsvg.append("g")
.attr("class", "legend");
var labels = legend.selectAll("text.labels")
.data(data);
labels.enter().append("text")
.attr("class", "labels")
.attr("dx", 15)
.attr("dy", function(d, i) {
return (vertical * i) + ringRadius * 2;
})
.attr("text-anchor", function(d) {
return "start";
})
.text(function(d) {
return d.label;
});
labels.exit().remove();
var ring = legend.selectAll("circle")
.data(data);
ring.enter().append("circle")
.attr("cy", function(d, i) {
return (vertical * i) + ringRadius;
})
.attr("r", ringRadius)
.attr("width", ringRadius * 2)
.attr("height", ringRadius * 2)
.style("fill", function(d, i) {
return color(i);
});
ring.exit().remove();
body {
background: #eeeeee;
}
.arc path {
stroke: #fff;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<h1>CrescentChart I</h1>
<div class="crescentchart" data-width="300" data-height="300" data-r="90" data-thickness="6" data-spacing="10" />
Trying to keep it in line with your current implementation, I'd consider two different cases. One where d.fraction <= 0.25, so the arc part is the only thing required to draw the bar. In that case, just use the arc. The other, when d.fraction > 0.25, you can draw yourself, because you can assume already that the arc goes exactly from 180 to 270 degrees, and all you need it to draw the bar on top before closing it. I used d3.path() extensively for this example, I recommend you keep it next to you when reading the code.
var $this = $('.crescentchart');
var data = [{
label: 'Fudge Brownie',
value: 45,
},
{
label: 'Cherry Vanilla',
value: 60,
},
{
label: 'Pistachio',
value: 5,
},
{
label: 'Caramel',
value: 10,
}
];
var oldData = "";
var width = $this.data('width'),
height = $this.data('height'),
radius = $this.data('r'),
thickness = $this.data("thickness"),
spacing = $this.data("spacing");
var color = d3.scaleOrdinal()
.range(["#bc658d", "#82c4c3", "#f9d89c", "#f5a7a7"]);
var svg = d3.select($this[0])
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr('class', 'crescentchart')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var segments = svg.append('g').attr('class', 'segments');
var data = setData(data, radius);
//append previous value to it.
$.each(data, function(index, value) {
if (oldData[index] != undefined) {
data[index]["previousFraction"] = oldData[index].fraction;
} else {
data[index]["previousFraction"] = 0;
}
});
var arcpaths = segments.selectAll("path")
.data(data);
arcpaths.enter().append("path")
.style("fill", function(d, i) {
return color(i);
})
.transition()
.ease(d3.easeElastic)
.duration(3000)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
});
arcpaths.transition()
.ease(d3.easeElastic)
.style("fill", function(d, i) {
return color(i);
})
.duration(3000)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
});
arcpaths.exit().transition()
.ease(d3.easeBounce)
.duration(3000)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
})
.remove();
function arcTween(b, thickness, ir) {
var prev = JSON.parse(JSON.stringify(b));
prev.endAngle = b.previousEndAngle;
prev.fraction = b.previousFraction;
var i = d3.interpolate(prev, b);
const arc = getArc(thickness, ir);
return function(t) {
return arc(i(t));
};
}
function getRadiusRing(ir, i) {
return ir - (i * (thickness + spacing));
}
// The portion of the fraction that is drawn as an arc,
// instead of as a straight part
const arcPortion = 3 / 8;
function getArc(thickness, ir) {
var arc = d3.arc()
.innerRadius(function(d) {
return getRadiusRing(ir, d.index);
})
.outerRadius(function(d) {
return getRadiusRing(ir + thickness, d.index);
})
.startAngle(- arcPortion * 2 * Math.PI)
.endAngle(function(d, i) {
return -(d.fraction + arcPortion) * 2 * Math.PI;
});
// This function is only called when endAngle is greater than
// 3 * Math.PI, otherwise we simply draw an arc, because it makes
// no difference.
function jShape(d, i) {
const context = d3.path();
const innerRadius = getRadiusRing(ir, d.index);
const outerRadius = getRadiusRing(ir + thickness, d.index);
const startAngle = -arcPortion * 2 * Math.PI - Math.PI / 2;
const endAngle = 0;
// Start at the correct position on the inside
context.moveTo(
innerRadius * Math.cos(startAngle),
innerRadius * Math.sin(startAngle)
);
context.arc(0, 0, innerRadius, startAngle, endAngle, true);
// Now draw the straight part
const fullLength = innerRadius * 2 * Math.PI;
// The first 0.25 corresponds to the curved part drawn earlier.
const straightLength = (d.fraction - arcPortion) * fullLength
context.lineTo(
innerRadius * Math.cos(endAngle),
innerRadius * Math.sin(endAngle) - straightLength
);
// Move to the outside
context.lineTo(
outerRadius * Math.cos(endAngle),
innerRadius * Math.sin(endAngle) - straightLength
);
context.lineTo(
outerRadius * Math.cos(endAngle),
outerRadius * Math.sin(endAngle)
);
// And curve back
context.arc(0, 0, outerRadius, endAngle, startAngle, false);
context.closePath();
return context + "";
}
return function(d, i) {
return d.fraction <= arcPortion ? arc(d, i) : jShape(d, i);
}
}
function setData(data, r) {
var segmentValueSum = 0;
$.each(data, function(ri, va) {
segmentValueSum += va.value;
});
$.each(data, function(ri, va) {
var segmentValue = va.value;
var fraction = segmentValue / segmentValueSum;
data[ri]["index"] = ri;
data[ri]["fraction"] = fraction;
});
return data;
}
body {
background: #eeeeee;
}
.arc path {
stroke: #fff;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v4.js"></script>
<h1>CrescentChart I</h1>
<div class="crescentchart" data-width="300" data-height="300" data-r="90" data-thickness="6" data-spacing="10" />
I am trying to figure out how to arrange the labels so that they do not overlap. Here is a picture of the chart
As you can see, with really small values, the text labels overlap. I tried to iterate over each text element and modify it's position, but that doesn't seem to be working. You can see at the bottom of this function that I tried to get the position of each text element and then modify it. What am I doing wrong? I've been at it for hours.
_renderDonutChart() {
let self = this;
// console.log("Donut Chart is beginning render")
let textOffset = 14;
self.graph.data[0].forEach(function (d) {
d.value = +d.value;
})
console.log(self.graph.data[0])
let boxSize = (self.options.radius + self.options.padding) * 2;
let parent = d3.select(self.ui.parent);
//let color = d3.scaleOrdinal(['#dc8710', '#9e3400', '#f19b12']);
let color = d3.scaleOrdinal(d3.schemeCategory20c);
let svg = parent.append('svg')
.attr('width', boxSize * 2)
.attr('height', boxSize)
.attr('transform', 'translate(-111,0)')
.append('g')
.attr('transform', 'translate(' + boxSize + ',' + boxSize / 2 + ')');
svg.append('g')
.attr('class', 'slices')
svg.append("g")
.attr("class", "labelName")
svg.append("g")
.attr("class", "labelValue")
svg.append("g")
.attr("class", "lines")
svg.append("div")
.attr("class", "progress-circle__box progress-circle__box--victorytype")
let arc = d3.arc()
.innerRadius(self.options.radius - self.options.border)
.outerRadius(self.options.radius);
let outerArc = d3.arc()
.innerRadius((self.options.radius - self.options.border) * 1.2)
.outerRadius((self.options.radius) * 1.2);
let legendRectSize = self.options.radius * 0.05;
let legendSpacing = self.options.radius * 0.02;
let pie = d3.pie()
.value(function(d) { return d.value; })
.sort(null);
let slice = svg.select('.slices')
.selectAll('path.slice')
.data(pie(self.graph.data[0]))
.enter()
.append('path')
.attr("class", "slice")
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data.label);
})
.transition().duration(1000)
.attrTween("d", function(d) {
this._current = this._current || 0;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc(interpolate(t));
};
})
function midAngle(d){
return d.startAngle + (d.endAngle - d.startAngle)/2;
}
let text = svg.select(".labelName").selectAll("text")
.data(pie(self.graph.data[0]))
.enter()
.append("text")
.attr('class', 'label')
.attr("dy", ".35em")
.attr('transform', function(d) {
// effectively computes the centre of the slice.
// see https://github.com/d3/d3-shape/blob/master/README.md#arc_centroid
var pos = outerArc.centroid(d);
// changes the point to be on left or right depending on where label is.
pos[0] = self.options.radius * 0.97 * (midAngle(d) < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d) {
// if slice centre is on the left, anchor text to start, otherwise anchor to end
return (midAngle(d)) < Math.PI ? 'start' : 'end';
})
.style("fill", "white")
.text(function(d) {
return (" " + d.data.label+": " +d.value+"");
})
.transition().duration(1000)
.attrTween("transform", function(d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
var d2 = interpolate(t);
var pos = outerArc.centroid(d2);
pos[0] = self.options.radius * (midAngle(d2) < Math.PI ? 1 : -1);
return "translate("+ pos +")";
};
})
.styleTween("text-anchor", function(d){
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
var d2 = interpolate(t);
return midAngle(d2) < Math.PI ? "start":"end";
};
})
.text(function(d) {
return (d.data.label+": "+d.value+"%");
})
let polyline = svg.select(".lines").selectAll("polyline")
.data(pie(self.graph.data[0]))
.enter()
.append("polyline")
.attr('points', function(d) {
var pos = outerArc.centroid(d);
pos[0] = self.options.radius * 0.95 * (midAngle(d) < Math.PI ? 1 : -1);
return [arc.centroid(d), outerArc.centroid(d), pos]
})
.style("fill", "none")
.style("stroke", "white")
.style("stroke-width", "1px");
let prev;
text.each(function(d, i) {
if(i > 0) {
let thisbb = this.getBoundingClientRect(),
prevbb = prev.getBoundingClientRect();
// move if they overlap
console.log(thisbb.left);
console.log(prevbb.right);
if(!(thisbb.right < prevbb.left ||
thisbb.left > prevbb.right ||
thisbb.bottom < prevbb.top ||
thisbb.top > prevbb.bottom)) {
var ctx = thisbb.left + (thisbb.right - thisbb.left)/2,
cty = thisbb.top + (thisbb.bottom - thisbb.top)/2,
cpx = prevbb.left + (prevbb.right - prevbb.left)/2,
cpy = prevbb.top + (prevbb.bottom - prevbb.top)/2,
off = Math.sqrt(Math.pow(ctx - cpx, 2) + Math.pow(cty - cpy, 2))/2;
d3.select(this).attr("transform",
"translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) *
(self.options.radius + textOffset + off) + "," +
Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) *
(self.options.radius + textOffset + off) + ")");
}
}
prev = this;
});
// console.log("Donut Chart is ending render")
}
I had the same issue. The best solution for me was to increase the size of the Svg area (based on current window) and add some padding to the legends based on the radius of the donut.
ie:
var margin = {top: 20, right: 100, bottom: 30, left: 40};
var svgWidth = window.innerWidth - (window.innerWidth/4);
var width = svgWidth,
height = (Math.min(width) / 2) + 100,
radius = Math.min(width, height) / 3;
var legendRectSize = (radius * 0.08);
var legendSpacing = (radius * 0.05);
This worked for me until the item count became too high. I amended from labels around the edge to display in the center, on slice/arc hover events. (There is an animation on the data value, which is why they are slanted in this snapshot)
I thought this made the unreadable data labels less confusing.
I thing you had same problem with this, you need to add more margin to your pie chart segment, assume your pie return 3% value, and you draw a text with line, it will stack on another, you need to devine margin if that value < 3 draw text with y+margin
if(percent<3){
o[1]
pos[1] += i*15
}
//return [label.centroid(d),[o[0],0[1]] , pos];
return [label.centroid(d),[o[0],pos[1]] , pos];
})
try to append this code structure to your chart
I'm new to programming D3, we are having a problem updating the sunburst chart without causing it to break its transitions. When the data is changed, parts of the chart stop resizing for the zooming feature and appear to be over each other. Additionally, I get errors resulting, apparently, from the interpolation of the flags each "arc" in the graph (i.e. Error: attribute d: Expected arc flag ('0' or '1'), "…125862517296 0 0.987032831999999…".). We are using a tween that is specific for this problem and thus can't understand what might be causing it.
Since I'm inexperienced in this language I've spent several days on this problem only to see it come back. If any of you could help I would appreciate it immensely.
Here's a working fiddle that I created to illustrate our problem:
- https://jsfiddle.net/v9ab0vms/1/
The problem can be seen by waiting 5 seconds and clicking the "Commercial" and then "Communications" arcs. Causing the transition errors and breaking some of the arcs. Zooming the graph while the data gets changed causes it to break in other ways, any help on that would also be greatly appreciated. I was just trying to fix it step by step.
Picture of broken chart arcs
The code has to be posted as well so I'm just inserting the two main functions for the sunburst:
function genSunburst2() {
var width = d3.select("#container_sunburst").style("width").split("px")[0],
height = d3.select("#container_sunburst").style("height").split("px")[0],
radius = Math.min(width, height) / 2;
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.sqrt()
.range([0, radius]);
var color = d3.scale.category20c();
var svg = d3.select("#sunburst")
.attr("id", "sunburst")
.attr("width", width)
.attr("height", height)
.select("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.value(function(d) {
return d.size;
});
var arc = d3.svg.arc()
.startAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x)));
})
.endAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx)));
})
.innerRadius(function(d) {
return Math.max(0, y(d.y));
})
.outerRadius(function(d) {
return Math.max(0, y(d.y + d.dy));
});
function computeTextRotation(d) {
var angle = x(d.x + d.dx / 2) - Math.PI / 2;
return angle / Math.PI * 180;
}
function arcTween(d) {
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d, i) {
return i ? function(t) {
return arc(d);
} : function(t) {
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
return arc(d);
};
};
}
function arcTweenUpdate(a) {
console.log(path);
var _self = this;
var i = d3.interpolate({
x: this.x0,
dx: this.dx0
}, a);
return function(t) {
var b = i(t);
console.log(window);
_self.x0 = b.x;
_self.dx0 = b.dx;
return arc(b);
};
}
updateSunburst3 = function() {
if (sunburstClick) {
sunburstClick = false;
return;
}
var root = createJsonDataset();
// DATA JOIN - Join new data with old elements, if any.
var gs = svg.selectAll("g").data(partition.nodes(root));
// ENTER
var g = gs.enter().append("g").on("click", click);
// UPDATE
var path = g.append("path");
gs.select('path')
.style("fill", function(d) {
return color(d.name);
})
.on("click", click)
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
})
.transition().duration(500)
.attr("d", arc);
var text = g.append("text");
gs.select('text')
.attr("x", function(d) {
return y(d.y);
})
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.attr("transform", function(d) {
return "rotate(" + computeTextRotation(d) + ")";
})
.text(function(d) {
return d.name;
})
.style("fill", "white");
function click(d) {
sunburstClick = true;
console.log(d);
// fade out all text elements
/*if (d.size !== undefined) {
d.size += 100;
};*/
text.transition().attr("opacity", 0);
console.log(path);
for (var i = 0; i < path[0].length; ++i) {
if (path[0][i] === undefined || path[0][i] === null) {
path[0].splice(i, 1);
--i;
}
}
path.transition()
.duration(750)
.attrTween("d", arcTween(d))
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in d
if (e.x >= d.x && e.x < (d.x + d.dx)) {
// get a selection of the associated text element
var arcText = d3.select(this.parentNode).select("text");
// fade in the text element and recalculate positions
arcText.transition().duration(750)
.attr("opacity", 1)
.attr("transform", function() {
return "rotate(" + computeTextRotation(e) + ")"
})
.attr("x", function(d) {
return y(d.y);
});
}
});
userSelection = undefined;
purposeSelection = undefined;
// TODO: alterar para ele ter em conta a hierarquia da selecao corrente
// para so sair de uma hierarquia
if (typeof d.name != "undefined" && d.name != "") {
if (typeof d.size === "undefined") {
userSelection = d.name;
purposeSelection = undefined;
} else {
userSelection = d.parent.name;
purposeSelection = d.name;
}
} else {
// if only the user was selected, back out to no selection
if (purposeSelection === undefined && userSelection != undefined) {
userSelection = undefined;
// if both the user and the purpose were selected, back out to just user selection
} else if (purposeSelection != undefined && userSelection != undefined) {
purposeSelection = undefined;
}
}
applySelection();
}
// EXIT - Remove old elements as needed.
gs.exit().transition().duration(500).style("fill-opacity", 1e-6).remove();
}
}
Thank you in advance
Edit: To clarify:
This is in D3 V3.
We have two variables one with the full dataset
and another with the "working" dataset. We apply filters to the full
dataset and store the result in the working dataset.
My goal is that given a value in seconds(resp_time), I want to create a counter in anticlock direction that would end once resp_time becomes 0.
I am following this tutorial: http://bl.ocks.org/mbostock/1096355 to create a polar clock. But I need the arc to decrease as in go anti-clockwise. I tried updating the endAngle value for the same, but it doesn't seem to work.
Here's my code:
var width = 960,
height = 800,
radius = Math.min(width, height) / 1.9,
spacing = .09;
var resp_time = 61;
var rh = parseInt(resp_time/3600), rm = parseInt((resp_time- rh*3600)/60), rs = parseInt((resp_time- rh*3600 - rm*60)%60);
var color = d3.scale.linear()
.range(["hsl(-180,50%,50%)", "hsl(180,50%,50%)"])
.interpolate(interpolateHsl);
var t;
var arc = d3.svg.arc()
.startAngle(0)
.endAngle(function(d) { return d.value * 2 * Math.PI; })
.innerRadius(function(d) { return d.index * radius; })
.outerRadius(function(d) { return (d.index + spacing) * radius; });
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var field = svg.selectAll("g")
.data(fields)
.enter().append("g");
field.append("path");
field.append("text");
d3.transition().duration(0).each(tick);
d3.select(self.frameElement).style("height", height + "px");
function tick() {
field = field
.each(function(d) { this._value = d.value; })
.data(fields)
.each(function(d) { d.previousValue = this._value; });
field.select("path")
.transition()
.ease("elastic")
.attrTween("d", arcTween)
.style("fill", function(d) { return color(d.value); });
field.select("text")
.attr("dy", function(d) { return d.value < .5 ? "-.5em" : "1em"; })
.text(function(d) { return d.text; })
.transition()
.ease("elastic")
.attr("transform", function(d) {
return "rotate(" + 360 * d.value + ")"
+ "translate(0," + -(d.index + spacing / 2) * radius + ")"
+ "rotate(" + (d.value < .5 ? -90 : 90) + ")"
});
if (resp_time > 0)
{
resp_time = resp_time - 1;
rh = parseInt(resp_time/3600), rm = parseInt((resp_time- rh*3600)/60), rs = parseInt((resp_time- rh*3600 - rm*60)%60);
t = setTimeout(tick, 1000);
}
}
function arcTween(d) {
console.log(d);
var i = d3.interpolateNumber(d.previousValue, d.value);
return function(t) { d.value = i(t); return arc(d); };
}
function fields() {
console.log(rs);
return [
{index: .3, text: rs+"s", value: rs},
{index: .2, text: rm+"m", value: rm},
{index: .1, text: rh+"h", value: rh}
];
}
function interpolateHsl(a, b) {
var i = d3.interpolateString(a, b);
return function(t) {
return d3.hsl(i(t));
};
}
This is just resulting in 3 static concentric circles(since I'm plotting only the minute, seconds and hours) with no transition.
I'm not sure how to proceed from here. What change should I make to get it working? Please help me out.
d.value in this code should be in the range [0, 1], so you need to divide by the maximum values in the fields generator:
function fields() {
console.log(rs);
return [
{index: .3, text: rs+"s", value: rs/60},
{index: .2, text: rm+"m", value: rm/60},
{index: .1, text: rh+"h", value: rh/24}
];
}
I noticed this because console.log(rs) was printing out values in the range of [0, 60] whereas arc.endAngle expects a radian value. If you substitute [0, 60] with the endAngle provider in the code, you get an over wound clock hand.
var arc = d3.svg.arc()
.startAngle(0)
.endAngle(function(d) { return d.value * 2 * Math.PI; })
var width = 960,
height = 800,
radius = Math.min(width, height) / 1.9,
spacing = .09;
var resp_time = 61;
var rh = parseInt(resp_time/3600), rm = parseInt((resp_time- rh*3600)/60), rs = parseInt((resp_time- rh*3600 - rm*60)%60);
var color = d3.scale.linear()
.range(["hsl(-180,50%,50%)", "hsl(180,50%,50%)"])
.interpolate(interpolateHsl);
var t;
var arc = d3.svg.arc()
.startAngle(0)
.endAngle(function(d) { return d.value * 2 * Math.PI; })
.innerRadius(function(d) { return d.index * radius; })
.outerRadius(function(d) { return (d.index + spacing) * radius; });
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var field = svg.selectAll("g")
.data(fields)
.enter().append("g");
field.append("path");
field.append("text");
d3.transition().duration(0).each(tick);
d3.select(self.frameElement).style("height", height + "px");
function tick() {
field = field
.each(function(d) { this._value = d.value; })
.data(fields)
.each(function(d) { d.previousValue = this._value; });
field.select("path")
.transition()
.ease("elastic")
.attrTween("d", arcTween)
.style("fill", function(d) { return color(d.value); });
field.select("text")
.attr("dy", function(d) { return d.value < .5 ? "-.5em" : "1em"; })
.text(function(d) { return d.text; })
.transition()
.ease("elastic")
.attr("transform", function(d) {
return "rotate(" + 360 * d.value + ")"
+ "translate(0," + -(d.index + spacing / 2) * radius + ")"
+ "rotate(" + (d.value < .5 ? -90 : 90) + ")"
});
if (resp_time > 0)
{
resp_time = resp_time - 1;
rh = parseInt(resp_time/3600), rm = parseInt((resp_time- rh*3600)/60), rs = parseInt((resp_time- rh*3600 - rm*60)%60);
t = setTimeout(tick, 1000);
}
}
function arcTween(d) {
console.log(d);
var i = d3.interpolateNumber(d.previousValue, d.value);
return function(t) { d.value = i(t); return arc(d); };
}
function fields() {
console.log(rs);
return [
{index: .3, text: rs+"s", value: rs/60},
{index: .2, text: rm+"m", value: rm/60},
{index: .1, text: rh+"h", value: rh/24}
];
}
function interpolateHsl(a, b) {
var i = d3.interpolateString(a, b);
return function(t) {
return d3.hsl(i(t));
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>