I want to code a reusable chart in d3 - a "normalized" stacked bar chart. The data are scaled from 0% -100% on the Y-axis - see: http://bl.ocks.org/mbostock/3886394
I have understood that I need to calculate the inverse value of the data, to scale from 0 (Y: lowest value 0%) to 1 (Y: highest value 100%).
var y = d3.scale.linear()
.range([height, 0]); // no data domain
dataSet = dataSet.map(function (d) {
return d.map(function (p, i) {
return { x: i, y: (1 / (p.Value / 100))};
});
});
However, my scaling is not working correctly
please have a look # http://jsfiddle.net/dB96T/1/
thx!!
Related
I've build bar chart with sorting on click: https://codepen.io/wawraf/pen/gvpXWm. It's based on Mike Bostock's chart https://bl.ocks.org/mbostock/3885705.
It works fine, but when I tried to build it from scratch I realized there is something i do not fully understand: Line 72 contains following function:
var x0 = scaleX
.domain(data.sort(sort(direction))
.map(function(d) { return d[0]; }));
So it's using variable scaleX defined before (Line 16), but when instead of "scaleX" variable I want to use raw d3 reference (which is actually the same as scaleX):
var x0 = d3.scaleBand().rangeRound([0, width - margin * 2])
.domain(data.sort(sort(direction))
.map(function(d) { return d[0]; }));
axis sorting ("g" elements) doesn't work.
I would be glad if anyone could explain why it doesn't actually work.
When you do...
var x0 = scaleX.domain(data.sort(sort(direction)).map(function(d) {
return d[0];
}));
... you are not only setting a new variable x0, but changing the scaleX domain as well. As the axis is based on scaleX, not x0, it won't do the transition in your second case, which only sets x0 (without changing scaleX).
You can certainly do:
var x0 = d3.scaleBand()
.rangeRound([0, width - margin * 2])
.domain(data.sort(sort(direction))
.map(function(d) {
return d[0];
}));
As long as you change the axis' scale:
xAxis.scale(x0);
here is the updated CodePen with those changes: https://codepen.io/anon/pen/VQveBy?editors=0010
I have the following bubble chart coded with dc.js which is built upon d3.js.
All is good, but for some reason I cannot see the tick marks. When I inspect the DOM I can see that they are present:
<line y2="6" x2="0"></line>
And I have applied CSS styles to them, but still they do not show!
#referrals-bubble-chart .axis .tick line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
I even added a stroke-width of 2px and still nothing shows! I know I'm targeting the right elements in my CSS because when I give it a stroke width of 10px and hover (Chrome Inspector), I see that the line is now 10px wide.
Why is this happening? Chart Code is below:
// define the referrals bubble chart attributes
referralsChart
.width(700)
.height(400)
.transitionDuration(1500) // (optional) define chart transition duration, :default = 750
.margins({top: 10, right: 50, bottom: 40, left: 50})
.dimension(diagnosisDimension)
//Bubble chart expect the groups are reduced to multiple values which would then be used
//to generate x, y, and radius for each key (bubble) in the group
.group(diagnosisDimensionGroup)
.colors(colorbrewer.RdYlGn[9]) // (optional) define color function or array for bubbles
.colorDomain([0, 100]) //(optional) define color domain to match your data domain if you want to bind data or color
.colorAccessor(function (d) {
// color - mapped to internal scale
return d.value.cost % 100;
})
.keyAccessor(function (p) {
// x-axis
return p.value.avgrtt / p.value.referrals;
})
.valueAccessor(function (p) {
// y-axis
return p.value.cost / 1000;
})
.radiusValueAccessor(function (p) {
// radius size - default is [0, 100]
return p.value.referrals;
})
.maxBubbleRelativeSize(0.1)
// .x(d3.scale.linear().domain([0, 5000]))
.x(d3.scale.linear().domain([1, 15]))
.y(d3.scale.linear().domain([1000, 10000]))
.r(d3.scale.linear().domain([0, 4000]))
//##### Elastic Scaling
//`.elasticX` and `.elasticX` determine whether the chart should rescale each axis to fit data.
//The `.yAxisPadding` and `.xAxisPadding` add padding to data above and below their max values in the same unit domains as the Accessors.
.elasticY(true)
.elasticX(false)
.yAxisPadding(200)
.xAxisLabel('Average Waiting Time - (weeks)') // (optional) render an axis label below the x axis
.yAxisLabel('Cost - (£1K)') // (optional) render a vertical axis lable left of the y axis
//#### Labels and Titles
//Labels are displaed on the chart for each bubble. Titles displayed on mouseover.
.renderLabel(true) // (optional) whether chart should render labels, :default = true
.label(function (p) {
return p.key;
})
.renderTitle(true) // (optional) whether chart should render titles, :default = false
.title(function (p) {
return [p.key,
"Referrals: " + p.value.referrals,
"Cost: £" + p.value.cost,
"RTT: " + p.value.avgrtt / p.value.referrals + " weeks"]
.join("\n");
})
//#### Customize Axis
//Set a custom tick format. Note `.yAxis()` returns an axis object, so any additional method chaining applies to the axis, not the chart.
.yAxis().tickFormat(function (v) {
return v;
});
As mentioned in the comments, it's hard to help you without a complete example, but this works for me. Since I don't have your data I made my own very simple data and adjusted a few things on my bubble chart.
var data = [];
for (var i = 1; i < 10; i++) {
data.push({
val: i
});
}
var ndx = crossfilter(data);
var dim = ndx.dimension(function(d) {
return d.val;
});
var group = dim.group().reduceSum(function(d) {
return d.val;
});
bubbleChart = dc.bubbleChart("#bubbleChart");
bubbleChart
.width(700)
.height(400)
.transitionDuration(1500)
.margins({top: 10, right: 50, bottom: 40, left: 50})
.dimension(dim)
.group(group)
.keyAccessor(function (p) {
return p.value;
})
.valueAccessor(function (p) {
return p.value;
})
.maxBubbleRelativeSize(0.1)
.x(d3.scale.linear().domain([-1, 10]))
.y(d3.scale.linear().domain([0, 10]))
.radiusValueAccessor(function (p) {
return p.value;
})
.r(d3.scale.linear().domain([0, 100]))
.elasticY(true)
.elasticX(false)
.yAxisPadding(200)
.xAxisLabel('Average Waiting Time - (weeks)')
.yAxisLabel('Cost - (£1K)')
.renderLabel(true)
.label(function (p) {
return p.key;
})
.renderTitle(true)
.title(function (p) {
return "This is the title";
})
.yAxis().tickFormat(function (v) {
return v;
});
dc.renderAll();
I am building an area graph in d3.js.
For my y axis, I want to use a linear scale that extends from the minimal value to the maximal value of the data represented in the graph.
Using
y = d3.scale.linear().range([height, 0]),
yAxis = d3.svg.axis().scale(y).orient("left")
d3 shows ticks only for multiples of 10000.
I would like also to see ticks for the starting and the ending value. Is this possible? How?
The nice() approach is usually preferable, but if you do want to have explicit tick labels at the max and min values of your data, you can force the axis to include them, along with whatever default tick values would be created by the scale:
axis.tickValues( scale.ticks( 5 ).concat( scale.domain() ) );
scale.ticks(count) returns an array of approximately that many tick values from the scale's domain, rounded off to nice even numbers. scale.domain() returns the [min,max] domain array that you set based on the data. array.concat(array) concatenates one array onto the end of the other, and axis.tickValues(array) tells the axis to draw the ticks at those values specifically.
Live example: https://jsfiddle.net/zUj3E/671/
(function () {
//Set up SVG and axis//
var svg = d3.select("svg");
var width = window.getComputedStyle(svg[0][0])["width"];
width = parseFloat(width);
var height = window.getComputedStyle(svg[0][0])["height"];
height = parseFloat(height);
var margin = 50;
var scale = d3.scale.linear()
.domain([0.05, 0.95])
.range([0, height - 2*margin]);
var formatPercent = d3.format(".0%");
var axis = d3.svg.axis()
.scale(scale)
.orient("right")
.tickFormat(formatPercent);
axis.tickValues( scale.ticks( 5 ).concat( scale.domain() ) );
//set the axis tick values explicitly, as the array of ticks
//generated by the scale PLUS the max and min values from the scale domain
//concatenated into a single array
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + [margin, margin]+")")
.call(axis);
})();
g.axis line, g.axis path {
fill:none;
stroke:royalblue;
shape-rendering:crispEdges;
}
g.axis text{
fill:royalblue;
font-family:sans-serif;
}
g.axis path {
stroke-width:2;
stroke:seagreen;
}
svg {
display: block;
width: 100vw;
height: 100vh;
}
body {
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg></svg>
One way to do this is to extend the domain of a scale to be "nice", i.e. to include "round" values that will show up on the axis. You can do this by calling .nice() after setting the domain:
y = d3.scale.linear().range([height, 0]).domain(...).nice();
The alternative would be to specify the ticks explicitly, which is much more painful in general.
Hacky way
Before trying this approach just try with ticks, tickSize, etc. functions once.
To be frank, I went through lots of solutions, and unfortunately, none of them worked for me. Finally, I played with the CSS and get the job done.
Hiding the in-between elements between the first and last tick value.
// displaying only first and last x-axis tick.
#x-axis {
.tick {
display: none
&:first-child,
&:last-of-type {
display: block;
}
}
}
reference: https://github.com/mbostock/d3/wiki/Zoom-Behavior
//make zoom
var zoomFirst = d3.behavior.zoom()
.x(x)
.y(y1)
.scaleExtent([0, 3])
.size([w, h])
//.center([w/2+200, h/2-200])
.on("zoom", zoomedFirst);
function zoomedFirst() {
svgContainer.select(".x.axis").call(xAxis);
svgContainer.selectAll(".y.axis.axisLeft").call(yAxisLeft);
//set y2's scale manually
svgContainer.select(".price")
.attr("d", line1(priceData))
.attr("class", "price");
svgContainer.select(".difficulty")
.attr("d", line2(difficultyData))
.attr("class", "difficulty");
}
d3.behavior.zoom() supports autoscaling of x and y axes. However, I have to scale two y axes at the same time. When zoom() is triggered, I can get the current scale and translate info from d3.event.scale and d3.event.translate, but I cant figure out how to make appropriate scale for the second y axis(y2) with them.
I am also looking at https://github.com/mbostock/d3/wiki/Quantitative-Scales.
Since y1's range is automatically adjusted by zoom, if there is a way to get y1's current range, I can get its min and max and set y2's range based on them. However, the document doesn't specify a way to get range given a scale object.
Calling y1.range() (without any arguments) will return you the [min, max] of the scale.
From the docs:
If values is not specified, returns the scale's current output range.
Most accessor functions in D3 work like this, they return you (get) the value if you call them without any arguments and set the value if you call them with arguments and return the this object for easy chaining:
d3Object.propertyName = function (_) {
if (!arguments.length) return propertyName;
propertyName = _;
return this;
}
However, the zoom behaviour alters the domain and not the range of the scales.
From the docs:
Specifies an x-scale whose domain should be automatically adjusted when zooming.
Hence, you do do not need to get/set the range, but instead the domain of the scales y1 and y2: y2.domain(y1.domain()).
Since the zoom function already manages all the ratios, a more abbreviated answer would be:
var zoomFirst = d3.behavior.zoom()
.x(x)
.y(y1)
.scaleExtent([0, 3])
.size([w, h])
.on("zoom", function() {
zoomSecond.scale(zoom.scale());
zoomSecond.translate(zoom.translate());
// Update visual. Both y domains will now be updated
});
// Create copy for y2 scale
var zoomSecond = d3.behavior.zoom()
.x(x)
.y(y2) // <-- second scale
.scaleExtent([0, 3]) // <-- extent
This assumes you have called only zoomFirst to your visual.
Check out this jsFiddle of a heatmap done in D3.js.
By default, the y-axis goes top down. I've had to invert the y-axis on line charts before as described in this conversation.
However, I'm not quite sure how to do the required inversion here. Any ideas?
The relevant portions of my code (where the inversion would need to be applied) are as follows:
var xGrid = d3.scale.linear()
.range([0, w - 2])
.domain([0, data.influencer_range.length]);
var yGrid = d3.scale.linear()
.range([0, h - 2])
.domain([0, data.omm_range.length]);
var xOrdinal = d3.scale.ordinal()
.domain(data.influencer_range)
.rangeBands([0, data.influencer_range.length]);
var yOrdinal = d3.scale.ordinal()
.domain(data.omm_range).
rangeBands([0, data.omm_range.length]);
var x = function(point) {
return point * xGrid(1);
};
var y = function(point) {
return point * yGrid(1);
}
First, as the Google thread instructs, swap the two y-range values: .range([h - 2, 0])
Similarly, your yOrdinal needs to be reversed: .rangeBands([data.omm_range.length, 0])
Finally, the reversal breaks your calculation of the height of a row (yGrid(1) is kinda hardcode-y, but oh well), so you need to adjust it too: return point * yGrid(2)
And there you have it: http://jsfiddle.net/qrBBS/2/