animate grow and radius at the same time - javascript

I made a simple animated pie/donut chart with d3 and I was wondering if it would be possible to animate the radius and the grow at the same time.
As you can see from the example or the snippet below, only the grow is animated.
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>

Here is a jsfiddle example showing how you can achieve that: https://jsfiddle.net/kmandov/9jrb1qLr/
I've used Mike Bostock's pie chart example as a base, but you can adapt the code to your PieChart implementation.
The basic idea is that as soon as you switch category(oranges/apples), the pie arcs are recalculated to match the new data. The animation is done via a transition in the change function:
function change() {
// ... calculate arcs
path.transition().duration(750).attrTween("d", arcTween(selected));
}
then the real magic is happening in the arcTween function. In the original example only the start and end angles are updated. You can store the target outerRadius and then update the arc generator on each step of the transition:
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);
};
}
}

Related

D3 only render to certain depth

For d3, given an array of nested objects.
Is it possible to only render a certain amount of depth?
I am basing off of some sunburst examples online like :
https://github.com/Nikhilkoneru/react-d3-zoomable-sunburst/blob/master/src/index.js
Using the .selectAll method from d3, can I only limit the sunburst to render 'X' depths, instead of rendering the entire array of nested objects?
I'm trying to render a really large array, and it causes a really laggy experience.
This is the selectAll that I'm using from the github example.
svg.selectAll('path')
.data(partition(root)
.descendants())
.enter()
.append('path')
.style('fill', (d) => {
let hue;
const current = d;
if (current.depth === 0) {
return '#33cccc';
}
if (current.depth <= 1) {
hue = hueDXScale(d.x0);
current.fill = d3.hsl(hue, 0.5, 0.6);
return current.fill;
}
if(current.depth <= 2){
console.log("depth > 2: ", d)
current.fill = current.parent.fill.brighter(0.5);
const hsl = d3.hsl(current.fill);
hue = hueDXScale(current.x0);
const colorshift = hsl.h + (hue / 4);
return d3.hsl(colorshift, hsl.s, hsl.l);
}
})
.attr('stroke', '#fff')
.attr('stroke-width', '1')
.on('click', d => click(d, node, svg, x, y, radius, arc))
.on('mouseover', function (d) {
if (props.tooltip) {
d3.select(this).style('cursor', 'pointer');
tooltip.html(() => {
const name = utils.formatNameTooltip(d);
return name;
});
return tooltip.transition().duration(50).style('opacity', 1);
}
return null;
})
.on('mousemove', () => {
if (props.tooltip) {
tooltip
.style('top', `${d3.event.pageY - 50}px`)
.style('left', `${props.tooltipPosition === 'right' ? d3.event.pageX - 100 : d3.event.pageX - 50}px`);
}
return null;
})
.on('mouseout', function () {
if (props.tooltip) {
d3.select(this).style('cursor', 'default');
tooltip.transition().duration(50).style('opacity', 0);
}
return null;
});
As you can see, from the current.depth, i'm able to filter out what depth is more than 2.
Is it possible to add display: none to anything more than depth of 2, or is there a better way to not render anything more than depth of 2 from current

Multiple donut chart not showing percentage

I try to show multiple donut charts on the same page. But it's not showing. I am using a loop for creating this chart and added multiple html id to show this.
var details = [{category: "Doctor", number: 25}, {category: "Shopping", number: 20}, { category: "Restaurants", number: 10}, { category: "FastFood", number: 5}, { category: "Internet", number: 8},{ category: "Other", number: 32}];
for (i = 1; i <= 4; i++)
chartScript(details, '#custom-chart-' + i);
function chartScript(details, cls) {
var width = 300,
height = 500;
var colors = d3.scaleOrdinal(d3.schemeDark2);
var svg = d3.select(cls).append("svg")
.attr("width", width).attr("height", height);
details.sort(function (a, b) {
return b['number'] - a['number'];
});
var data = d3.pie().sort(null).value(function (d) {
return d.number;
})(details);
var segments = d3.arc()
.innerRadius(80)
.outerRadius(150)
.padAngle(.05)
.padRadius(30);
var sections = svg.append("g").attr("transform", "translate(150,250)").selectAll("path").data(data);
sections.enter().append("path").attr("d", segments).attr("fill", function (d) {
return colors(d.data.number);
});
var content = d3.select("g").selectAll("text").data(data);
content.enter().append("text").classed("inside", true).each(function (d) {
var center = segments.centroid(d);
d3.select(this).attr("x", center[0]).attr("y", center[1]).text(d.data.number + '%')
})
}
I got output like this(1st one is showing % but others are not):
You are using d3 like this
var sections = svg.append("g").attr("transform", "translate(150,250)")
and in this line
var content = d3.select("g").selectAll("text").data(data);
instead of d3 You should use
var content = sections.select("g").selectAll("text").data(data);
That way it will select all the <g> tags in the sections
As a rule of thumb: in a D3 code, never use a loop (for, while, forEach etc...) for appending elements.
So, just change that awkward loop for a proper D3 selection...
var cls = d3.select("body").selectAll(null)
.data(d3.range(4))
.enter()
.append("div");
... and not only you will fix the issue you mentioned but also avoid future problems.
Here is a demo using (most of) your code:
var details = [{
category: "Doctor",
number: 25
}, {
category: "Shopping",
number: 20
}, {
category: "Restaurants",
number: 10
}, {
category: "FastFood",
number: 5
}, {
category: "Internet",
number: 8
}, {
category: "Other",
number: 32
}];
var cls = d3.select("body").selectAll(null)
.data(d3.range(4))
.enter()
.append("div");
var width = 180,
height = 180;
var colors = d3.scaleOrdinal(d3.schemeDark2);
var svg = cls.append("svg")
.attr("width", width).attr("height", height);
details.sort(function(a, b) {
return b['number'] - a['number'];
});
var data = d3.pie().sort(null).value(function(d) {
return d.number;
})(details);
var segments = d3.arc()
.innerRadius(45)
.outerRadius(85)
.padAngle(.05)
.padRadius(30);
var g = svg.append("g").attr("transform", "translate(90,90)");
var sections = g.selectAll("path").data(data);
sections.enter().append("path").attr("d", segments).attr("fill", function(d) {
return colors(d.data.number);
});
var content = g.selectAll("text").data(data);
content.enter().append("text").classed("inside", true).each(function(d) {
var center = segments.centroid(d);
d3.select(this).attr("x", center[0]).attr("y", center[1]).text(d.data.number + '%')
})
div {
display: inline-block;
}
text {
text-anchor: middle;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

d3js prevent forceSimulation from recalculating position all the time

I'm trying to build a force layout that will allow me to visualize the flow of objects in a system.
I want to show how many objects are on a specific state and when a state change I want to update my graph.
I've built a prototype, but I've noticed that D3.js is recalculating transform of each node even when they don't need to move:
can this be fixed? maybe there is an option to add minimum value for an update?
I've declared force layout this way:
const force = d3.forceSimulation()
.force('link', d3.forceLink().id((d) => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-500))
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.on('tick', tick);
After changing alphaTarget to alpha recalculation stopped, but I got another bug:
I've added drag functionality and it stopped working with above changes.
Here is the version with fixed recalculation but with drag problem.
The culprit is in your restart() function:
force.alphaTarget(0.3).restart();
The way you are reheating your simulation by setting .alphaTarget(0.3) is not correct. alphaTarget is a configuration parameter which controls the way alpha decreases. Figuratively speaking, alpha—for as long as it is greater than alphaMin— is headed towards alphaTarget. The heat in the system is measured by alpha which can be thought of as dynamic data; alphaTarget, on the other hand, resembles more or less static data.
Furthermore, it is important to have alphaTarget set to a value less than alphaMin or else your simulation is going to run indefinitely because alpha, while on its way towards alphaTarget so to speak, is never going to be less than alphaMin.
Thus, if you want to reheat your system, you have to manipulate alpha instead of alphaTarget. Changing above mentioned line to the following is all it takes to get the desired effect.
force.alpha(0.3).restart();
Have a look at the following snippet, which is basically a fork of your JSFiddle, to see it in action.
document.getElementById("a").addEventListener("click", function() {
AddNewLink(null, 1);
});
document.getElementById("b").addEventListener("click", function() {
AddNewLink(1, 2);
});
document.getElementById("c").addEventListener("click", function() {
AddNewLink(2, 3);
});
document.getElementById("d").addEventListener("click", function() {
AddNewLink(1, 3);
});
document.getElementById("e").addEventListener("click", function() {
AddNewLink(3, 4);
});
document.getElementById("f").addEventListener("click", function() {
AddNewLink(4, 5);
});
function AddNewLink(from, to) {
var startNode;
var start = availableNodes.find(x => x.id === from);
if (start !== undefined) {
//check if this node is already added
var foundStart = nodes.find(x => x.id == start.id);
if (foundStart === undefined) {
nodes.push(start);
startNode = start;
} else {
foundStart.value--;
if (foundStart.value < 0) foundStart.value = 0;
startNode = foundStart;
}
}
var endNode;
var end = availableNodes.find(x => x.id === to);
if (end !== undefined) {
//check if this node is already added
var foundEnd = nodes.find(x => x.id == end.id);
if (foundEnd === undefined) {
nodes.push(end);
endNode = end;
end.value++;
} else {
foundEnd.value++;
endNode = foundEnd;
}
}
//console.log(startNode, endNode);
if (startNode !== undefined && endNode !== undefined) {
links.push({
source: startNode,
target: endNode
});
}
restart();
}
// set up SVG for D3
const width = 400;
const height = 400;
const colors = d3.scaleOrdinal(d3.schemeCategory10);
const svg = d3.select('svg')
.on('contextmenu', () => {
d3.event.preventDefault();
})
.attr('width', width)
.attr('height', height);
var availableNodes = [{
id: 1,
name: "Start",
value: 0,
reflexive: false
}, {
id: 2,
name: "Node 1",
value: 0,
reflexive: false
}, {
id: 3,
name: "Node 2",
value: 0,
reflexive: false
}, {
id: 4,
name: "Node 3",
value: 0,
reflexive: false
}, {
id: 5,
name: "Finish",
value: 0,
reflexive: false
}];
// set up initial nodes and links
// - nodes are known by 'id', not by index in array.
// - reflexive edges are indicated on the node (as a bold black circle).
// - links are always source < target; edge directions are set by 'left' and 'right'.
let nodes = [
availableNodes[0], availableNodes[1], availableNodes[2]
];
let links = [{
source: nodes[0],
target: nodes[1]
},
{
source: nodes[1],
target: nodes[2]
}
];
// init D3 force layout
const force = d3.forceSimulation()
.force('link', d3.forceLink().id((d) => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-500))
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.on('tick', tick);
// define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
// handles to link and node element groups
let path = svg.append('svg:g').attr('id', 'lines').selectAll('path');
let circle = svg.append('svg:g').attr('id', 'circles').selectAll('g');
// update force layout (called automatically each iteration)
function tick() {
// draw directed edges with proper padding from node centers
path.attr('d', (d) => {
const deltaX = d.target.x - d.source.x;
const deltaY = d.target.y - d.source.y;
const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const normX = deltaX / dist;
const normY = deltaY / dist;
const sourcePadding = d.left ? 17 : 12;
const targetPadding = d.right ? 17 : 12;
const sourceX = d.source.x + (sourcePadding * normX);
const sourceY = d.source.y + (sourcePadding * normY);
const targetX = d.target.x - (targetPadding * normX);
const targetY = d.target.y - (targetPadding * normY);
return `M${sourceX},${sourceY}L${targetX},${targetY}`;
});
circle.attr('transform', (d) => `translate(${d.x},${d.y})`);
}
// update graph (called when needed)
function restart() {
// path (link) group
path = path.data(links);
// remove old links
path.exit().remove();
// add new links
path = path.enter().append('svg:path')
.attr('class', 'link')
.style('marker-end', 'url(#end-arrow)')
.merge(path);
// circle (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
circle = circle.data(nodes, (d) => d.id);
// update existing nodes (reflexive & selected visual states)
circle.selectAll('circle')
.style('fill', (d) => colors(d.id))
.classed('reflexive', (d) => d.reflexive);
circle.selectAll('text.value').text((d) => d.value);
// remove old nodes
circle.exit().remove();
// add new nodes
const g = circle.enter().append('svg:g');
g.append('svg:circle')
.attr('class', 'node')
.attr('r', 12)
.style('fill', (d) => colors(d.id))
.style('stroke', (d) => d3.rgb(colors(d.id)).darker().toString())
.classed('reflexive', (d) => d.reflexive)
// show node IDs
g.append('svg:text')
.attr('x', 0)
.attr('y', 4)
.attr('class', 'value')
.text((d) => d.value);
g.append('svg:text')
.attr('x', 20)
.attr('y', 4)
.attr('class', 'name')
.text((d) => d.name);
circle = g.merge(circle);
// set the graph in motion
force
.nodes(nodes)
.force('link').links(links);
force.alpha(0.3).restart();
}
restart();
svg {
background-color: #FFF;
cursor: default;
user-select: none;
}
path.link {
fill: none;
stroke: #000;
stroke-width: 3px;
cursor: default;
}
path.link.selected {
stroke-dasharray: 10, 2;
}
path.link.dragline {
pointer-events: none;
}
path.link.hidden {
stroke-width: 0;
}
circle.node.reflexive {
stroke: #000 !important;
stroke-width: 2.5px;
}
text {
font: 12px sans-serif;
pointer-events: none;
}
text.value {
text-anchor: middle;
font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.1/d3.js"></script>
<button id='a'>1</button>
<button id='b'>1>2</button>
<button id='c'>2>3</button>
<button id='d'>1>3</button>
<button id='e'>3>4</button>
<button id='f'>4>5</button>
<svg width="400" height="400"></svg>

D3 path tween changes side

I'm trying to do a path tween like this one: https://bl.ocks.org/mbostock/3916621
Problem: The path is changing the side. The gray path is changing from left to right and the white path is changing from bottom to top. This isn't the expected transition!
Edit: My expected transition should be a simple grow transition. So the small one should grow to the bigger one.
Example: https://jsfiddle.net/wdv3rufs/
const PATHS = {
FULL: {
GRAY: 'M1035,429l-4.6-73.7L1092,223l-66,1l-66.3-36.4l-102.5,67.6L623.8,0L467.4,302.1l-218.7-82.9L77.6,317.4L0,214.5V429H1035z',
WHITE: 'M0,429V292l249.4-72.9l135.4,56.6L623.8,0L824,232.5l135.7-44.9l26.7,190.5l29.3,16.8l19.3,34H0z'
},
SMALL: {
GRAY: 'M0,429h834l-134.2-34.6l-37,23.2l-130.6-35.2l-112.7,33.5l-144.2-67l-96.7,65.2L43.5,377.5L0,391.4V429z',
WHITE: 'M0,429h834l-134.2-34.6l-83.5,29l-84.1-41l-126.9,31.2l-130-64.7l-144.8,58.6l-87-30L0,386.1V429z'
}
};
const pathTween = (d1, precision) => {
return function() {
const path0 = this;
const path1 = path0.cloneNode();
const n0 = path0.getTotalLength();
const n1 = (path1.setAttribute('d', d1), path1).getTotalLength();
const distances = [0];
const dt = precision / Math.max(n0, n1);
let i = 0;
while ((i += dt) < 1) distances.push(i);
distances.push(1);
const points = distances.map(t => {
const p0 = path0.getPointAtLength(t * n0);
const p1 = path1.getPointAtLength(t * n1);
return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]);
});
return t => {
return t < 1 ? 'M' + points.map(p => p(t)).join('L') : d1;
};
};
};
const pathTransition = (path, d1) => {
path.transition()
.duration(10000)
.attrTween('d', pathTween(d1, 4));
}
var svg = d3.select('svg');
svg.append('path')
.attr('class', 'white-mountain')
.attr('d', PATHS.SMALL.WHITE)
.call(pathTransition, PATHS.FULL.WHITE);
svg.append('path')
.attr('class', 'gray-mountain')
.attr('d', PATHS.SMALL.GRAY)
.call(pathTransition, PATHS.FULL.GRAY);
How can I get this working?

D3 y-axis percent display

D3 Fiddle
/**
* A bar chart. Required data format:
* [ { name : x-axis-bar-label, value : N }, ...]
*
* Sample use:
* var bargraph = d3.select('#bargraph')
* .append('svg')
* .chart('BarChart')
* .yFormat(d3.format("d"))
* .height(400)
* .width(800)
* .max(1.0);
* bargraph.draw(bardata);
*/
d3.chart('BarChart', {
initialize: function() {
var chart = this;
// chart margins to account for labels.
// we may want to have setters for this.
// not sure how necessary that is tbh.
chart.margins = {
top : 10,
bottom : 15,
left : 50,
right : 0,
padding : 10
};
// default chart ranges
chart.x = d3.scale.linear();
chart.y = d3.scale.linear();
chart.base
.classed('Barchart', true);
// non data driven areas (as in not to be independatly drawn)
chart.areas = {};
// cache for selections that are layer bases.
chart.layers = {};
// make sections for labels and main area
// _________________
// |Y| bars |
// | | |
// | | |
// | | |
// | |______________|
// | X |
// -- areas
chart.areas.ylabels = chart.base.append('g')
.classed('ylabels', true)
.attr('width', chart.margins.left)
.attr('transform',
'translate('+(chart.margins.left-1)+','+(chart.margins.top + 1)+')');
// -- actual layers
chart.layers.bars = chart.base.append('g')
.classed('bars', true)
.attr('transform',
'translate(' + chart.margins.left + ',' + (chart.margins.top + 1)+')');
chart.layers.xlabels = chart.base.append('g')
.classed('xlabels', true)
.attr('height', chart.margins.bottom);
chart.on("change:width", function() {
chart.x.range([0, chart.w - chart.margins.left]);
chart.layers.bars.attr('width', chart.w - chart.margins.left);
chart.layers.xlabels.attr('width', chart.w - chart.margins.left);
});
chart.on("change:height", function() {
chart.y.range([chart.h - chart.margins.bottom - chart.margins.top, 0]);
chart.areas.ylabels
.attr('height', chart.h - chart.margins.bottom - chart.margins.top - 1);
chart.layers.bars
.attr('height', chart.h - chart.margins.bottom - chart.margins.top);
chart.layers.xlabels
.attr('transform', 'translate(' + chart.margins.left + ',' +
(chart.h - chart.margins.bottom + 1) + ')');
});
// make actual layers
chart.layer('bars', chart.layers.bars, {
// data format:
// [ { name : x-axis-bar-label, value : N }, ...]
dataBind : function(data) {
chart.data = data;
// how many bars?
chart.bars = data.length;
// compute box size
chart.bar_width = (chart.w - chart.margins.left - ((chart.bars - 1) *
chart.margins.padding)) / chart.bars;
// adjust the x domain - the number of bars.
chart.x.domain([0, chart.bars]);
// adjust the y domain - find the max in the data.
chart.datamax = chart.usermax ||
d3.extent(data, function(d) { return d.value; })[1];
chart.y.domain([0, chart.datamax]);
// draw yaxis
var yAxis = d3.svg.axis()
.scale(chart.y)
.orient('left')
.ticks(5)
.tickFormat(chart._yformat || d3.format('.0%'));
chart.areas.ylabels
.call(yAxis);
return this.selectAll('rect')
.data(data, function(d) {
//console.log(d);
return d.name;
});
},
insert : function() {
return this.append('rect')
.classed('bar', true)
.classed('highlight', function(d) {
return d.highlight;
});
},
events: {
exit: function() {
this.remove();
}
}
});
// a layer for the x text labels.
chart.layer('xlabels', chart.layers.xlabels, {
dataBind : function(data) {
// first append a line to the top.
this.append('line')
.attr('x1', 0)
.attr('x2', chart.w - chart.margins.left)
.attr('y1', 0)
.attr('y2', 0)
.style('stroke', '#222')
.style('stroke-width', '1')
.style('shape-rendering', 'crispEdges');
return this.selectAll('text')
.data(data, function(d) { return d.name; });
},
insert : function() {
return this.append('text')
.classed('label', true)
.attr('text-anchor', 'middle')
.attr('x', function(d, i) {
return chart.x(i) - 0.5 + chart.bar_width/2;
})
.attr('dy', '1em')
.text(function(d) {
return d.name;
});
},
events: {
exit: function() {
this.remove();
}
}
});
// on new/update data
// render the bars.
var onEnter = function() {
this.attr('x', function(d, i) {
return chart.x(i) - 0.5;
})
.attr('y', function(d) {
return chart.h - chart.margins.bottom -
chart.margins.top - chart.y(chart.datamax - d.value) - 0.5;
})
.attr('val', function(d) {
return d.value;
})
.attr('width', chart.bar_width)
.attr('height', function(d) {
return chart.y(chart.datamax - d.value);
});
};
chart.layer('bars').on('enter', onEnter);
chart.layer('bars').on('update', onEnter);
},
// return or set the max of the data. otherwise
// it will use the data max.
max : function(datamax) {
if (!arguments.length) {
return this.usermax;
}
this.usermax = datamax;
if (this.data) this.draw(this.data);
return this;
},
yFormat: function(format) {
if (!arguments.length) {
return this._yformat;
}
this._yformat = format;
return this;
},
width : function(newWidth) {
if (!arguments.length) {
return this.w;
}
// save new width
this.w = newWidth;
// adjust the x scale range
this.x.range([this.margins.left, this.w - this.margins.right]);
// adjust the base width
this.base.attr('width', this.w);
this.trigger("change:width");
if (this.data) this.draw(this.data);
return this;
},
height : function(newHeight) {
if (!arguments.length) {
return this.h;
}
// save new height
this.h = newHeight;
// adjust the y scale
this.y.range([this.h - this.margins.top, this.margins.bottom]);
// adjust the base width
this.base.attr('height', this.h);
this.trigger("change:height");
if (this.data) this.draw(this.data);
return this;
}
});
var barchart = d3.select('#vis')
.append('svg')
.chart('BarChart') //**Moving transform position out of here**
.yFormat(d3.format("d"))
.height(400)
.width(800);
var data = [
{ month : 'January', temperature : 29 },
{ month : 'February', temperature : 32 },
{ month : 'March', temperature : 48 },
{ month : 'April', temperature : 49 },
{ month : 'May', temperature : 58 },
{ month : 'June', temperature : 68 },
{ month : 'July', temperature : 74 },
{ month : 'August', temperature : 73 },
{ month : 'September', temperature : 65 },
{ month : 'October', temperature : 54 },
{ month : 'November', temperature : 45 },
{ month : 'December', temperature : 35 }
];
//**Moving transform function out of barchart definition
//so it is called only once and not everytime
//the chart is redrawn**
function transform(data) {
return data.map(function(d) {
return { name : d.month, value : d.temperature };
});
}
barchart.draw(transform(data));
In this fiddle, I am trying to display the y-axis values in percentages. Not exactly calculating the percentage from the data, but just appending the "%" sign to the existing data. I am fetching the data through an array in the actual code, it is dynamic. I have tried appending using tick format but somehow it doesn't seem to work. Any help?
Do this to append % to the y tick:
var yAxis = d3.svg.axis()
.scale(chart.y)
.orient('left')
.ticks(5)
.tickFormat(function(d){return d+ "%"});
working code here

Categories