Why is my D3 stacked area chart flipped? - javascript

I have a D3 chart that is supposed to look like this:
But instead it looks like this:
This is the code i'm using:
<!DOCTYPE html>
<meta charset="utf-8">
<head>
</head>
<body>
<svg id="chart"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>
var NSW = "NSW";
var QLD = "QLD";
var width = 600;
var height = 400;
var years = [];
var getStat = function(year, volatility, basis) {
return {
d: year,
x: basis,
vol: volatility,
value: 45 * Math.pow(basis, year),
high: 45 * Math.pow(basis+volatility, year),
low: 45 * Math.pow(basis-volatility, year),
}
}
for(i = 0; i < 25; i++) {
years.push(i);
}
var data = years.map(function(year){ return [getStat(year, 0.03, 1.08),getStat(year, 0.02, 1.08), getStat(year, 0.01, 1.08)]; }); // generate bogus data
var set_one = data.map(function(d) { return d[0];});
var set_two = data.map(function(d) { return d[1];});
var set_three = data.map(function(d) { return d[2];});
var chart = d3.select("#chart").attr("width", width).attr("height", height).append("g");
var x = d3.scale.linear().domain([0, years.length]).range([0, width]);
var y = d3.scale.linear().domain([0, d3.max(data, function(d){ return Math.max(d[0].high, d[1].high); })]).range([0, height]);
var area = d3.svg.area().x(function(d,i) { return x(i); })
.y0(function(d, i){ return d.low}) //FUNCTION FOR BASE-Y
.y1(function(d, i){ return d.high * 0.99;}); //FUNCTION FOR TOP-Y
chart
.selectAll("path.area")
.data([set_one,set_two,set_three]) // !!! here i can pass both arrays in.
.enter()
.append("path")
.attr("fill", "rgba(0,0,0,0.5)")
.attr("class", function(d,i) { return [NSW,QLD,"T"][i]; })
.attr("d", area);
</script>
What am I doing wrong?

Actually your doing nothing wrong the y-axis goes downwards starting at 0 from the top down to height. So to flip it you can set the y values to height - yValue:
var area = d3.svg.area().x(function(d,i) { return x(i); })
.y0(function(d, i){ return (height - (d.low))}) //FUNCTION FOR BASE-Y
.y1(function(d, i){ return (height - (d.high * 0.99))}); //FUNCTION FOR TOP-Y
Fiddle Example

the y ordinate in SVG increases downwards. Try this...
var y = d3.scale.linear().domain([d3.max(data, function(d){ return Math.max(d[0].high, d[1].high); }), 0]).range([0, height]);

Like everything in HTML / CSS / Canvas, the Y axis starts with 0 at the top and goes down to height at the bottom.
So according to your setup, the graph behaves correctly.
There are multiple ways to change the graphs direction.
a) You can change the range of your axis var y = d3.scale.linear().domain([...]).range([height, 0]);
b) You can change the domain of your axis var y = d3.scale.linear().domain([d3.max(data, function(d){ return Math.max(d[0].high, d[1].high); }), 0]).range([...]);
or c) change the way the graph gets its y-values with d3.svg.area().y0(...) and d3.svg.area().y1(...)
I would recommend the first option, because this actually specifies the range your domain gets projected on.

I think there was an issue with your y-scaling.I have inverted the range from range([height, 0] which was initially range([0,height]) as this should be the way as per d3 norms otherwise you have to change the logic while calculating the height of plot.
Here I am attaching the fixed code.
<!DOCTYPE html>
<meta charset="utf-8">
<head>
</head>
<body>
<svg id="chart"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>
var NSW = "NSW";
var QLD = "QLD";
var width = 600;
var height = 400;
var years = [];
var getStat = function(year, volatility, basis) {
return {
d: year,
x: basis,
vol: volatility,
value: 45 * Math.pow(basis, year),
high: 45 * Math.pow(basis+volatility, year),
low: 45 * Math.pow(basis-volatility, year),
}
}
for(i = 0; i < 25; i++) {
years.push(i);
}
var data = years.map(function(year){ return [getStat(year, 0.03, 1.08),getStat(year, 0.02, 1.08), getStat(year, 0.01, 1.08)]; }); // generate bogus data
var set_one = data.map(function(d) { return d[0];});
var set_two = data.map(function(d) { return d[1];});
var set_three = data.map(function(d) { return d[2];});
var chart = d3.select("#chart").attr("width", width).attr("height", height).append("g");
var x = d3.scale.linear().domain([0, years.length]).range([0, width]);
var y = d3.scale.linear().domain([0, d3.max(data, function(d){ return Math.max(d[0].high, d[1].high); })]).range([height, 0]);
var area = d3.svg.area().x(function(d,i) { return x(i); })
.y0(function(d, i){ return y(d.low)}) //FUNCTION FOR BASE-Y
.y1(function(d, i){ return y(d.high * 0.99);}); //FUNCTION FOR TOP-Y
chart
.selectAll("path.area")
.data([set_one,set_two,set_three]) // !!! here i can pass both arrays in.
.enter()
.append("path")
.attr("fill", "rgba(0,0,0,0.5)")
.attr("class", function(d,i) { return [NSW,QLD,"T"][i]; })
.attr("d", area);
</script>

Related

Split an SVG path lengthwise into multiple colours

I have a tree visualisation in which I am trying to display paths between nodes that represent a distribution with multiple classes. I want to split the path lengthwise into multiple colours to represent the frequency of each distribution.
For example: say we have Class A (red) and Class B (black), that each have a frequency of 50. Then I would like a path that is half red and half black between the nodes. The idea is to represent the relative frequencies of the classes, so the frequencies would be normalised.
My current (naive) attempt is to create a separate path for each class and then use an x-offset. It looks like this.
However, as shown in the image, the lines do not maintain an equal distance for the duration of the path.
The relevant segment of code:
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "red")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + -2.5 + "," + 0.0 + ")"; });
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "black")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + 2.5 + "," + 0.0 + ")"; });
It would be great if anyone has some advice.
Thanks!
A possible solution is to calculate the individual paths and fill with the required color.
Using the library svg-path-properties from geoexamples.com you can calculate properties (x,y,tangent) of a path without creating it first like it is done in this SO answer (this does not calculate the tangent).
The code snippet does it for 2 colors but it can be easy generalized for more.
You specify the colors, percentage and width of the stroke with a dictionary
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
percent is the amount color[0] takes from the stroke width.
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
The pathPoints parameters
path that needs to be stroked, can be generated by d3.line path example from SO answer
var lineGenerator = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveNatural);
var curvePoints = [[0,0],[0,10],[20,30]];
var duoPath = pathPoints(lineGenerator(curvePoints), 10, duoProp);
path length interval at which to sample (unit pixels). Every 10 pixels gives a good approximation
dictionary with the percent and width of the stroke
It returns an array with the paths to be filled, 1 for each color.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/svg-path-properties#0.4.4/build/path-properties.min.js"></script>
</head>
<body>
<svg id="chart" width="350" height="350"></svg>
<script>
var svg = d3.select("#chart");
function pathPoints(path, stepLength, duoProp) {
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(d => props.getPropertiesAtLength(d));
var pFactor = percent => (percent - 0.5) * duoProp.width;
tProps.forEach(p => {
p.x0 = p.x - pFactor(0) * p.tangentY;
p.y0 = p.y + pFactor(0) * p.tangentX;
p.xP = p.x - pFactor(duoProp.percent) * p.tangentY;
p.yP = p.y + pFactor(duoProp.percent) * p.tangentX;
p.x1 = p.x - pFactor(1) * p.tangentY;
p.y1 = p.y + pFactor(1) * p.tangentX;
});
var format1d = d3.format(".1f");
var createPath = (forward, backward) => {
var fp = tProps.map(p => forward(p));
var bp = tProps.map(p => backward(p));
bp.reverse();
return 'M' + fp.concat(bp).map(p => `${format1d(p[0])},${format1d(p[1])}`).join(' ') + 'z';
}
return [createPath(p => [p.x0, p.y0], p => [p.xP, p.yP]), createPath(p => [p.xP, p.yP], p => [p.x1, p.y1])]
}
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
</script>
</body>
</html>
As a quick follow-up to rioV8's excellent answer, I was able to get their code working but needed to generalise it to work with more than two colours. In case someone else has a similar requirement, here is the code:
function pathPoints(path, stepLength, duoProp) {
// get the properties of the path
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
// build a list of segments to use as approximation points
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(function (d) {
return props.getPropertiesAtLength(d);
});
// incorporate the percentage
var pFactor = function pFactor(percent) {
return (percent - 0.5) * duoProp.width;
};
// for each path segment, calculate offset points
tProps.forEach(function (p) {
// create array to store modified points
p.x_arr = [];
p.y_arr = [];
// calculate offset at 0%
p.x_arr.push(p.x - pFactor(0) * p.tangentY);
p.y_arr.push(p.y + pFactor(0) * p.tangentX);
// calculate offset at each specified percent
duoProp.percents.forEach(function(perc) {
p.x_arr.push(p.x - pFactor(perc) * p.tangentY);
p.y_arr.push(p.y + pFactor(perc) * p.tangentX);
});
// calculate offset at 100%
p.x_arr.push(p.x - pFactor(1) * p.tangentY);
p.y_arr.push(p.y + pFactor(1) * p.tangentX);
});
var format1d = d3.format(".1f");
var createPath = function createPath(forward, backward) {
var fp = tProps.map(function (p) {
return forward(p);
});
var bp = tProps.map(function (p) {
return backward(p);
});
bp.reverse();
return 'M' + fp.concat(bp).map(function (p) {
return format1d(p[0]) + "," + format1d(p[1]);
}).join(' ') + 'z';
};
// create a path for each projected point
var paths = [];
for(var i=0; i <= duoProp.percents.length; i++) {
paths.push(createPath(function (p) { return [p.x_arr[i], p.y_arr[i]]; }, function (p) { return [p.x_arr[i+1], p.y_arr[i+1]]; }));
}
return paths;
}
// generate the line
var duoProp = { color: ["red", "blue", "green"], percents: [0.5, 0.7], width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
Note that the percents array specifies the cumulative percentage of the stroke, not the individual percentages of the width. E.g. in the example above, the red stroke will span 0% to 50% width, the blue stroke 50% to 70% width and the green stroke 70% to 100% width.

Force directed graphs in d3v4

I'm experimenting with D3 version 4 force directed graphs and have looked at Jim Vallandingham's tutorial and code as a starting point.
http://vallandingham.me/bubble_chart_v4/
and am attempting to produce an animation similar to the example here from Nathan Yau
https://flowingdata.com/2016/08/23/make-a-moving-bubbles-chart-to-show-clustering-and-distributions/
I've stripped the bubble chart from Jim Vallandingham's code to what I think I need and can display the individual states by changing the index value, but for some reason the code does not want to animate between the different states. I assume the redraw function isn't working. It may be an obvious error or one made through complete ignorance, but if you can help it would be great.
Here's my code:
function bubbleChart() {
var width = 940;
var height = 600;
var center = { x: width / 2, y: height / 3 };
var years = ["0","2008", "2009", "2010"];
var yearCenters = {
2008: { x: width / 3, y: 2 * height / 3 },
2009: { x: width / 2, y: 2 * height / 3 },
2010: { x: 2 * width / 3, y: 2 * height / 3 }
};
// #v4 strength to apply to the position forces
var forceStrength = 0.03;
// These will be set in create_nodes and create_vis
var svg = null;
var bubbles = null;
var nodes = [];
var index= 0;
function charge(d) {
return -Math.pow(d.radius, 2.3) * forceStrength;
}
// Here we create a force layout
var simulation = d3.forceSimulation()
.velocityDecay(0.2)
.force('x', d3.forceX().strength(forceStrength).x(center.x))
.force('y', d3.forceY().strength(forceStrength).y(center.y))
.force('charge', d3.forceManyBody().strength(charge))
.on('tick', ticked);
// #v4 Force starts up automatically, which we don't want as there aren't any nodes yet.
simulation.stop();
// Nice looking colors
var fillColor = d3.scaleOrdinal()
.domain(['low', 'medium', 'high'])
.range(['#d84b2a', '#beccae', '#7aa25c']);
function createNodes(rawData) {
var myNodes = rawData.map(function (d) {
return {
id: d.id,
radius: 5,
value: +d.total_amount,
name: d.grant_title,
org: d.organization,
group: d.group,
year: d.start_year,
x: Math.random() * 900,
y: Math.random() * 800
};
});
// sort them to prevent occlusion of smaller nodes.
myNodes.sort(function (a, b) { return b.value - a.value; });
return myNodes;
}
/*
* Main entry point to the bubble chart.
*/
var chart = function chart(selector, rawData) {
// convert raw data into nodes data
nodes = createNodes(rawData);
// Create a SVG element inside the provided selector
// with desired size.
svg = d3.select(selector)
.append('svg')
.attr('width', width)
.attr('height', height);
// Bind nodes data to what will become DOM elements to represent them.
bubbles = svg.selectAll('.bubble')
.data(nodes, function (d) { return d.id; });
// Create new circle elements each with class `bubble`.
// There will be one circle.bubble for each object in the nodes array.
// Initially, their radius (r attribute) will be 0.
// #v4 Selections are immutable, so lets capture the
// enter selection to apply our transtition to below.
var bubblesE = bubbles.enter().append('circle')
.classed('bubble', true)
.attr('r', 0)
.attr('fill', function (d) { return fillColor(d.group); })
.attr('stroke', function (d) { return d3.rgb(fillColor(d.group)).darker(); })
.attr('stroke-width', 2)
// #v4 Merge the original empty selection and the enter selection
bubbles = bubbles.merge(bubblesE);
// Fancy transition to make bubbles appear, ending with the
// correct radius
bubbles.transition()
.duration(2000)
.attr('r', function (d) { return d.radius; });
// Set the simulation's nodes to our newly created nodes array.
// #v4 Once we set the nodes, the simulation will start running automatically!
simulation.nodes(nodes);
chart.redraw();
};
// Callback function that is called after every tick of the force simulation.
// These x and y values are modified by the force simulation.
function ticked() {
bubbles
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; });
}
chart.redraw = function (index){
simulation.force('x', d3.forceX().strength(forceStrength).x(nodePosX));
simulation.force('y', d3.forceY().strength(forceStrength).y(nodePosY));
simulation.alpha(1).restart();
}
function nodePosX(d) {
if (+d.year <= +years[index]) {
return yearCenters[d.year].x;
} else {
return center.x;
}
}
function nodePosY(d) {
if (+d.year <= +years[index]) {
return yearCenters[d.year].y;
} else {
return center.y;
}
}
// return the chart function from closure.
return chart;
}
var myBubbleChart = bubbleChart();
myBubbleChart('#vis', data);
for (i=0;i<4;i++){
setInterval(function(){myBubbleChart.redraw(i);}, 100);
}
I misunderstood how to use setInterval to redraw the chart, so it should be as follows:
var i = 0;
setInterval(function(){myBubbleChart.redraw(i++);}, 1000);

How do I fix the timeScale of d3 line chart?

Here are the dates that I am parsing:
2010-12-31
2011-12-31
2012-12-31
2013-12-31
2014-12-31
2015-12-31
2016-12-31
Here is my code:
this.x = d3.scaleTime().domain(d3.extent(this.dataArray, d => {return d[ this.xType ];})).range([ this.margin.left, this.width - this.margin.right ]);
this.y0 = d3.scaleLinear().domain([ this.getMin(this.yType1, this.yType0), this.getMax(this.yType1, this.yType0) ]).range([ this.height, 0 ]);
this.y1 = d3.scaleLinear().domain([ this.getMin(this.yType1, this.yType0), this.getMax(this.yType1, this.yType0) ]).range([ this.height, 0 ]);
this.xAxis = d3.axisBottom(this.x);
this.yAxisLeft = d3.axisLeft(this.y0).ticks(5);
this.yAxisRight = d3.axisRight(this.y1).ticks(5);
The problem is that the first date (2010) is being truncated from the x-axis and an additional tick is being added in the very end, however the chart is drawn right.
If I add .nice(this.dataArray.length) to this.x = ..., the year 2010 is added with 2017 at the very end.
How can I fix this problem? Thank you.
You can map your data:
var ticks = data.map((d)=>d);
And use this array in your tickValues:
var axis = d3.axisBottom(scale)
.tickValues(ticks);
Here is a demo:
var width = 500,
height = 100;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var parse = d3.timeParse("%Y-%m-%d");
var data = ["2010-12-31",
"2011-12-31",
"2012-12-31",
"2013-12-31",
"2014-12-31",
"2015-12-31",
"2016-12-31"
];
data.forEach((d, i, a) => a[i] = parse(d));
var ticks = data.map((d) => d)
var format = d3.timeFormat("%Y")
var scale = d3.scaleTime()
.domain(d3.extent(data, d => d)).range([20, width - 20]);
var axis = d3.axisBottom(scale).tickValues(ticks).tickFormat((d) => format(d));
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>

new data in dataset is not getting updated

I have following code to update chart1 with dataset2 but in the result mychart1 values are not updated to the new values in the dataset2. I want the updated values in dataset2 to be reflected in myChart1, but the old data is getting updated when I look at the class assinged in the debugger.
Can anybody point me where I am going wrong
function chart() {
//Width and height
var w = 200;
var h = 100;
var barPadding = 2;
var max = 0;
this.createChart = function(dataset,cls) {
//create svg element
var svg = d3.select("#chartDisplay").append("svg").attr("class",cls).attr(
"width", w).attr("height", h);
//create rect bars
var rect = svg.selectAll("rect").data(dataset,function(d, i) {return d;});
rect.enter()
.append("rect")
.attr("class","original")
.attr("x",function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return h - d * 3; //Height minus data value
})
.attr("width", w / dataset.length - barPadding)
.attr("height", function(d) {
return d * 3;
});
max = d3.max(dataset);
var xScale = d3.scale.linear().domain([ 0, max ]).range(
[ 0, w ]);
}
this.updateChart = function(dataset,cls) {
var svg = d3.select("#chartDisplay").select("svg."+cls);
var rect = svg.selectAll('rect')
.data(dataset,function(d, i) {
return d;
});
rect.attr("y", function(d) {
return h - d * 3; //Height minus data value
})
.attr("height", function(d) {
return d * 3;
});
rect.attr("class","updated");
}
this.getMax = function() {
return max;
}
}
var dataset1 = [ 5, 10, 12, 19, 21, 25, 22, 18, 15, 13 ];
var dataset2 = [ 1, 4, 14, 19, 16, 30, 22, 18, 15, 13 ];
var myChart1 = new chart();
myChart1.createChart(dataset1,"chart1");
myChart1.updateChart(dataset2,"chart1");
var myChart2 = new chart();
myChart2.createChart(dataset2,"chart2");
This is the problem:
var rect = svg.selectAll('rect')
.data(dataset,function(d, i) { return d; });
This is telling d3 to use the data value as a key into your array. Since your values are changing, the different values don't match and so they aren't being updated (you would need to use another append statement to get them to show up). Since you just have an array of values, you want to use the index of the value as the key (this makes it so that element 1 from dataset1 gets updated with the value of the new element 1 in dataset2).
You can either specifically use the index as the key:
var rect = svg.selectAll('rect')
.data(dataset,function(d, i) { return i; });
Or more simply, by doing this since using the index as the key is the default behavior.
var rect = svg.selectAll('rect')
.data(dataset);

Display only whole values along y axis in d3.js when range is 0-1 or 0-2

I am using d3js to display a realtime representation of the views of a website. For this I use a stack layout and I update my dataset by JSON at the moment.
When there is only 1 or 2 views being displayed on the y axis, which is dynamic related to the amount of views in the graph, the axis labels are: 1 => 0, 0.2, 0.4, 0.6, 0.8, 1, the axis labels are: 2 => 0, 0.5, 1, 1.5, 2 This makes no sense for my dataset since it displays views of a page, and you can't have half a view.
I have a linear scale in d3js I base my y axis on
var y_inverted = d3.scale.linear().domain([0, 1]).rangeRound([0, height]);
According to the documentation of rangeRound() I should only get whole values out of this scale. For drawing my axis I use:
var y_axis = svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0,0)")
.call(y_inverted.axis = d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5));
Because it is a realtime application I update this every second by calling:
function update(){
y_inverted.domain([yStackMax, 0]);
y_axis.transition()
.duration(interval)
.ease("linear")
.call(y_inverted.axis);
}
yStackMax is calculated from a stacklayout, as far as I know the data used for the y values only contain integers.
var yStackMax = d3.max(layers, function(layer) {
return d3.max(layer, function(d) {
return d.y0 + d.y;
});
});
I have tried several things to get a proper value for my y axis.
d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5).tickFormat(d3.format(",.0f"))
Got me the closest sofar, but it still displays 0, 0, 0, 1, 1, 1
Basically what I want is to only have 1 tick when yStackMax is 1, 2 ticks when it's 2, but it should also work if yStackMax is 12 or 1,000,000
Short answer: You can dynamically set the number of ticks. Set it to 1 to display only two tick labels:
var maxTicks = 5, minTicks = 1;
if (yStackMax < maxTicks) {
y_axis.ticks(minTicks)
}
else {
y_axis.ticks(maxTicks)
}
Long Answer (going a bit off topic):
While playing with your example I came up with a rather "complete solution" to all your formatting problems. Feel free to use it :)
var svg = d3.select("#svg")
var width = svg.attr("width")
var height = svg.attr("height")
var yStackMax = 100000
var interval = 500
var maxTicks = 5
var minTicks = 1
var y_inverted = d3.scale.linear().domain([0, 1]).rangeRound([0, height])
var defaultFormat = d3.format(",.0f")
var format = defaultFormat
var y_axis = d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(minTicks)
.tickFormat(doFormat)
var y_axis_root;
var decimals = 0;
function countDecimals(v){
var test = v, count = 0;
while(test > 10) {
test /= 10
count++;
}
return count;
}
function doFormat(d,i){
return format(d,i)
}
function init(){
y_axis_root = svg.append("g")
.attr("class", "y axis")
// I modified your example to move the axis to a visible part of the screen
.attr("transform", "translate(150,0)")
.call(y_axis)
}
// custom formatting functions:
function toTerra(d) { return (Math.round(d/10000000000)/100) + "T" }
function toGiga(d) { return (Math.round(d/10000000)/100) + "G" }
function toMega(d) { return (Math.round(d/10000)/100) + "M" }
function toKilo(d) { return (Math.round(d/10)/100) + "k" }
// the factor is just for testing and not needed if based on real world data
function update(factor){
factor = (factor) || 0.1;
yStackMax*=factor
decimals = countDecimals(yStackMax)
console.log("yStackMax decimals:",decimals, factor)
if (yStackMax < maxTicks) {
format = defaultFormat
y_axis.ticks(minTicks)
}
else {
y_axis.ticks(maxTicks)
if (decimals < 3 ) format = defaultFormat
else if(decimals < 6 ) format = toKilo
else if(decimals < 9 ) format = toMega
else if(decimals < 12) format = toGiga
else format = toTerra
}
y_inverted.domain([yStackMax, 0]);
y_axis_root.transition()
.duration(interval)
.ease("linear")
.call(y_axis);
}
init()
setTimeout(update, 200)
setTimeout(update, 400)
setTimeout(update, 600)
You can try it together with this html snippet:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.js"></script>
</head>
<body>
<div><svg id="svg" width="200" height="300"></svg></div>
<script src="axis.js"></script>
<button id="button1" onclick="update(10)">+</button>
<button id="button2" onclick="update(0.1)">-</button>
</body>
</html>
I know it is a bit off topic but I usually like to provide running examples/solutions. Regard the additional formatting stuff as a bonus to the actual problem.
If you ask for a certain number of ticks (via axis.ticks() ) then d3 will try to give you that many ticks - but will try to use pretty values. It has nothing to do with your data.
Your solutions are to use tickFormat, as you did, to round all the values to integer values, only ask for one tick as Juve answered, or explicitly set the tick values using axis.tickValues([...]) which would be pretty easy used in conjunction with d3.range
rangeRound will not help in this case because it relates to the output range of the scale, which in this case is the pixel offset to plot at: between 0 and height.
Going off of Superboggly's answer, this is what worked for me. First I got the max (largest) number from the y domain using y.domain().slice(-1)[0] and then I built an array of tick values from that using d3.range()...
var y_max = y.domain().slice(-1)[0]
var yAxis = d3.svg.axis()
.scale(y)
.tickValues(d3.range(y_max+1))
.tickFormat(d3.format(",.0f"))
Or just let the ticks as they are and "hide" decimal numbers
d3.svg.axis()
.scale(y_inverted)
.orient("left")
.ticks(5).tickFormat(function(d) {
if (d % 1 == 0) {
return d3.format('.f')(d)
} else {
return ""
}
});
Here is the code:
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(d3.format(".2s"));

Categories

Resources