我用d3制作了一个简单的动画pie/donut chart,我想知道是否有可能同时为半径和增长设置动画。
从下面的示例或代码段中可以看到,只有增长动画。
const dataset = {
apples: [{
label: 'Category A',
value: 53245,
isSelected: true
}, {
label: 'Category B',
value: 28479,
isSelected: false
}, {
label: 'Category C',
value: 24037,
isSelected: false
}, {
label: 'Category D',
value: 40245,
isSelected: false
}, {
label: 'Category E',
value: 30245,
isSelected: false
}],
oranges: [{
label: 'Category A',
value: 200,
isSelected: false
}, {
label: 'Category B',
value: 200,
isSelected: true
}, {
label: 'Category C',
value: 200,
isSelected: false
}, {
label: 'Category D',
value: 200,
isSelected: false
}]
};
/**
* Pie chart class
*/
function PieChart(options) {
// Observable stream source
this.selectionSource = new Rx.Subject();
// Observable stream
this.selection = this.selectionSource.asObservable();
// Chart options/settings
this.width = options.width;
this.height = options.height;
this.radius = Math.min(this.width, this.height) / 2;
this.multiple = options.multiple;
this.legend = options.legend;
this.colorRange = d3.scale.category20();
this.color = d3.scale.ordinal()
.range(this.colorRange.range());
// Animation directions
this.clockwise = {
startAngle: 0,
endAngle: 0
};
this.counterclock = {
startAngle: Math.PI * 2,
endAngle: Math.PI * 2
};
// Create the SVG on which the plot is painted.
this.svg = d3.select(options.target)
.append('svg:svg')
.attr('width', this.width)
.attr('height', this.height)
.append('g')
.attr('transform', `translate(${this.width / 2}, ${this.height / 2})`);
// Initial path creation.
this.path = this.svg.selectAll('path');
// Create the pie layout.
this.pie = d3.layout.pie()
.value(function(d) {
return d.value;
})
.sort(null);
// Create arc functions.
this.arc = d3.svg.arc()
.innerRadius(this.radius - 100)
.outerRadius(this.radius - 20);
// Arc when a slice is selected/toggled on.
this.arcSelected = d3.svg.arc()
.innerRadius(this.radius - 90)
.outerRadius(this.radius - 10);
this.arcTween = arcTween;
this.arcTweenOut = arcTweenOut;
this.updateSelection = updateSelection;
// Used by some of the functions that get a different context when called by d3.
const thisRef = this;
// Store the displayed angles in `current`.
// Then, interpolate from `current` to the new angles.
// During the transition, `current` is updated in-place by d3.interpolate.
function arcTween(a) {
const i = d3.interpolate(this.current, a);
this.current = i(0);
const slice = d3.select(this);
return arcFn(slice, i);
}
function arcTweenOut() {
const i = d3.interpolate(this.current, {
startAngle: Math.PI * 2,
endAngle: Math.PI * 2,
value: 0
});
this.current = i(0);
const slice = d3.select(this);
return arcFn(slice, i);
}
function arcFn(slice, i) {
return function(t) {
if (slice.classed('selected')) {
return thisRef.arcSelected(i(t));
}
return thisRef.arc(i(t));
};
}
// NOTE: `this` will not be the class context,
// but the contaxt set
function updateSelection(d) {
const node = this;
const slice = d3.select(node);
const isToggled = slice.classed('selected');
const event = {
data: d.data
};
if (thisRef.multiple) {
// Allow multiple slice toggling.
toggle();
} else {
// Find previously selected slice.
const selected = thisRef.svg.selectAll('path')
.filter(function() {
return !this.isEqualNode(node) && d3.select(this).classed('selected');
});
// Deselect previous selection.
if (!selected.empty()) {
selected.classed('selected', false)
.transition()
.attr('d', thisRef.arc);
}
// Toggle current slice.
toggle();
}
function toggle() {
if (isToggled) {
event.selected = false;
slice.classed('selected', false)
.transition()
.attr('d', thisRef.arc)
.each('end', emit);
} else {
event.selected = true;
slice.classed('selected', true)
.transition()
.attr('d', thisRef.arcSelected)
.each('end', emit);
}
}
function emit() {
thisRef.selectionSource.onNext(event);
}
}
}
PieChart.prototype.direction = function direction() {
// Set the start and end angles to Math.PI * 2 so we can transition counterclockwise to the actual values later.
let direction = this.counterclock;
// Set the start and end angles to 0 so we can transition clockwise to the actual values later.
if (!this.painted) {
direction = this.clockwise;
}
return direction;
}
PieChart.prototype.update = function update(data) {
const direction = this.direction();
const thisRef = this;
this.path = this.path
.data(this.pie(data), function(d) {
return d.data.label;
})
.classed('selected', selected.bind(this));
function selected(datum) {
return datum.data.isSelected;
}
// Append slices when data is added.
this.path.enter()
.append('svg:path')
.attr('class', 'slice')
.style('stroke', '#f3f5f6')
.attr('stroke-width', 2)
.attr('fill', function(d, i) {
return thisRef.color(d.data.label);
})
.attr('d', this.arc(direction))
// Store the initial values.
.each(function(d) {
this.current = {
data: d.data,
value: d.value,
startAngle: direction.startAngle,
endAngle: direction.endAngle
};
})
.on('click', this.updateSelection);
// Remove slices when data is removed.
this.path.exit()
.transition()
.duration(450)
.attrTween('d', this.arcTweenOut)
// Now remove the exiting arcs.
.remove();
// Redraw the arcs.
this.path.transition()
.duration(450)
.attrTween('d', this.arcTween);
// Add legend
this.addLegend();
// Everything is painted now,
// we only do updates from this point on.
if (!this.painted) {
this.painted = true;
}
}
PieChart.prototype.addLegend = function addLegend() {
// The legend does not need to be repainted when we update the slices.
if (this.painted || !this.legend) {
return;
}
const thisRef = this;
const rect = this.radius * 0.04;
const spacing = this.radius * 0.02;
const legend = this.svg.selectAll('.legend')
.data(this.color.domain());
legend.enter()
.append('g')
.attr('class', 'legend')
.attr('fill-opacity', 0)
.attr('transform', function(d, i) {
const height = rect + spacing * 2;
const offset = height * thisRef.color.domain().length / 2;
const horizontal = -4 * rect;
const vertical = i * height - offset;
return `translate(${horizontal}, ${vertical})`;
});
legend.append('rect')
.attr('width', rect)
.attr('height', rect)
.style('fill', this.color);
legend.append('text')
.attr('x', rect + spacing)
.attr('y', rect)
.text(function(d) {
return d;
});
legend.transition()
.duration(450)
.attr('fill-opacity', 1);
};
// DEMO/USAGE
const pieChart = new PieChart({
target: '#chart',
multiple: true,
legend: true,
width: 400,
height: 400
});
console.log(pieChart);
pieChart.selection.subscribe(function(selection) {
console.log(selection);
});
// Paint the plot.
pieChart.update(dataset.apples);
// This is only here for demo purposes
d3.selectAll("input")
.on("change", update);
var timeout = setTimeout(function() {
d3.select("input[value=\"oranges\"]").property("checked", true).each(update);
}, 2000);
function update() {
clearTimeout(timeout); // This is only here for demo purposes
// Update the data.
pieChart.update(dataset[this.value]);
}

body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.container {
position: relative;
}
form {
position: absolute;
right: 10px;
top: 10px;
}
// Graph
.slice {
cursor: pointer;
}
.legend {
font-size: 12px;
}

<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.1.0/rx.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div class="container">
<form>
<label>
<input type="radio" name="dataset" value="apples" checked> Apples</label>
<label>
<input type="radio" name="dataset" value="oranges"> Oranges</label>
</form>
<div id="chart"></div>
</div>
&#13;
答案 0 :(得分:2)
这是一个jsfiddle示例,展示了如何实现这一目标:https://jsfiddle.net/kmandov/9jrb1qLr/
我已使用Mike Bostock's pie chart example作为基础,但您可以将代码调整为PieChart实现。
基本思想是,只要切换类别(oranges/apples
),就会重新计算饼弧以匹配新数据。动画通过change
函数中的转换完成:
function change() {
// ... calculate arcs
path.transition().duration(750).attrTween("d", arcTween(selected));
}
然后真正的魔法发生在arcTween
函数中。在原始示例中,仅更新了start
和end angles
。您可以存储目标outerRadius
,然后在转换的每一步更新arc
生成器:
function arcTween(selected) {
return function(target, i) {
target.outerRadius = radius - (i === selected ? 0 : 20);
var arcInterpolator = d3.interpolate(this._current, target);
this._current = arcInterpolator(0);
return function(t) {
var interpolatedArc = arcInterpolator(t);
arc.outerRadius(interpolatedArc.outerRadius);
return arc(interpolatedArc);
};
}
}