动画增长和半径同时

时间:2016-09-20 13:15:24

标签: javascript d3.js

我用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;
&#13;
&#13;

1 个答案:

答案 0 :(得分:2)

这是一个jsfiddle示例,展示了如何实现这一目标:https://jsfiddle.net/kmandov/9jrb1qLr/

enter image description here

我已使用Mike Bostock's pie chart example作为基础,但您可以将代码调整为PieChart实现。

基本思想是,只要切换类别(oranges/apples),就会重新计算饼弧以匹配新数据。动画通过change函数中的转换完成:

function change() {
    // ... calculate arcs
    path.transition().duration(750).attrTween("d", arcTween(selected)); 
}

然后真正的魔法发生在arcTween函数中。在原始示例中,仅更新了startend 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);
      };
    }
}