Hi I'm trying to set an ordinal scale for a line graph in d3 using v4. For some reason the ticks do not scale properly although I have scaled them as such:
var yTicks = d3.scaleOrdinal()
.domain(["apple", "orange", "banana", "grapefruit", "mango"])
.range([0, h])
var xAxis = d3.axisBottom().scale(x).tickSize(-h);
// var xAxis = d3.svg.axis().scale(x).tickSize(-h).tickSubdivide(true);
var yAxisLeft = d3.axisLeft().scale(yTicks);
// Add the x-axis.
graph.append("svg:g").attr("class", "x axis").attr("transform", "translate(0," + h + ")").call(xAxis);
// add lines
// do this AFTER the axes above so that the line is above the tick-lines
for (var i = data.length - 1; i >= 0; i--) {
graph.append("svg:path").attr("d", line(data[i])).attr("class", "data" + (i + 1));
};
graph.append("svg:g").attr("class", "y axis").attr("transform", "translate(0,0)").call(yAxisLeft);
The full version of what I've done can be found at this fiddle here: https://jsfiddle.net/5g1fe6qd/
You cannot set the range of an ordinal scale the way you did here: you have to specify the discrete values.
An easy solution in using a point scale instead:
var yTicks = d3.scalePoint()
Here is your updated fiddle: https://jsfiddle.net/5g1fe6qd/1/
This is expected behavior:
ordinal.range([range])
If range is specified, sets the range of the ordinal scale to the
specified array of values. The first element in the domain will be
mapped to the first element in range, the second domain value to the
second range value, and so on. If there are fewer elements in the
range than in the domain, the scale will reuse values from the start
of the range. If range is not specified, this method returns the
current range.
(emphasis mine, from API documentation)
You've only specified two elements in your range, therefore, the five values in your domain are mapped to these two values in the range (hence the overlapping text). You could use something along these lines:
d3.scaleOrdinal()
.domain(["apple", "orange", "banana", "grapefruit", "mango"])
.range([0, h*0.25, h*0.5, h*0.75, h]
(fiddle: https://jsfiddle.net/bmysrmcd/)
However, Gerardo's answer provides an alternative that doesn't require you set map each element in the domain to the range, and that is a better solution.
Related
I want to create a grouped bar chart where each bar is unique and not part of a series.
For example, imagine a bar chart showing the population of each major city, grouped by state.
Is this possible with d3js and any pointers on how to to get started?
Thanks
Yes it is possible!
As with most visualisation the process is one of working out the structure of the chart and then working out how to get the data in to that structure. In this case I think the data in it's simplest form is going to look something like this ...
let groups = [
{ name:'one', values:[1,3,6,2] },
{ name:'two', values:[3,5,7,3,2,5] },
{ name:'three', values:[9,2,5] },
{ name:'four', values:[6] }
];
An array of groups, where each group has some properties including an array of values to be represented as individual bars.
The tricky bit is working out the position of each group and each bar along the horizontal axis. The way I think about this is as follows: If we say each bar has a width of 1 unit, how much space do we want between the groups? Maybe a bar and a half, so 1.5 units. Now each group is going to take up space equal to the number of bars in it (the length of the values array) and the total width required by the chart will be the sum of those values plus the spaces between them.
i.e.
let dataWidth = d3.sum(groups, d=>d.values.length) + (groups.length-1) * groupPadding;
We also want to go through the groups and work out their "start position", where the group is placed horizontally in terms of bar units, like this:
let groupPadding = 1.5;
let currentWidth = 0;
groups = groups.map(group=>{
group.width = group.values.length;
group.startPosition = currentWidth;
currentWidth += group.width+groupPadding;
return group;
});
Next we need to make an x and a y-scale using d3-scale.
For the y-scale let's keep it simple and hard code the maximum value
const yScale = d3.scaleLinear()
.domain([0,10])
.range([chartHeight, 0]); // chart height is just your charts height in pixels
and for the x scale we use the dataWidth we calculated above as the maximum for the domain, we'll use this scale to convert bar units into screen pixels
const xScale = d3.scaleLinear()
.domain([0, barWidth)
.range([0, chartWidth]); //chartWidth, the width of your chart in pixels
OK. Now we have everything we need to know to draw the chart. The structure of the chart as I see envisage it...
Each group in groups is an svg g element positioned according to its startPosition. Each of those g elements contains a set of rect elements, one for each value. Inside the g element you may also want to put stuff like a group label.
Broadly this would look something like this...
Create the groups
const barGroups = chart.selectAll('g.bar-group')
.data(groups)
.enter()
.append('g')
.classed('bar-group', true)
.attr('transform', group=>`translate(${xScale(group.startPosition)}, 0)`);
Add the rectangles
barGroups.each(function(group){
const barGroup = d3.select(this);
barGroup.selectAll('rect')
.data(group.values)
.enter()
.append('rect')
.attr('width', xScale(1))
.attr('height', value => yScale(value))
.attr('x', (value, i) => xScale(i))
.attr('y', value => chartHeight - yScale(value));
Here's a bl.ock that puts it all together and fills in the gaps to give a working example: Grouped Bars
I need to build a violin point with discrete data points in d3.
Example:
I am not sure how to align the center for each value on X axis. The default behavior will overlay all the points with same X and Y value, however I would like the points to be offset while being center aligned e.g. 5.1 has 3 values in control group and 4.5 has 2 values, all center aligned. It is easy to do so for either right or left aligned by doing a transformation of each point by a specified amount. However, the center alignment seems to be quite hacky.
A hacky way would be to manually transform the X value by maintaining a couple of arrays to see whether this is the first, even or odd number of element and place it according my specifying the value. Is there a proper way to handle this?
The only example of violin plot in d3 I found was here - which implements a probability distribution rather than the discrete values which I require.
"A hacky way would be to manually transform the X value by maintaining a couple of arrays" - that's pretty much the way most d3 layouts work :-) . Discretise your data set by the y value (weight), keeping a total of the data points in each discrete group and a group index for each datum. Then use those to calculate offsets x-ways and the rounded y-value.
See https://jsfiddle.net/n444k759/4/
// below code assumes a svg and g group element are present (they are in the jsfiddle)
var yscale = d3.scale.linear().domain([0,10]).range([0,390]);
var xscale = d3.scale.linear().domain([0,2]).range ([0,390])
var color = d3.scale.ordinal().domain([0,1]).range(["red", "blue"]);
var data = [];
for (var n = 0; n <100; n++) {
data.push({weight: Math.random() * 10.0, category: Math.floor (Math.random() * 2.0)});
}
var groups = {};
var circleR = 5;
var discreteTo = (circleR * 2) / (yscale.range()[1] / yscale.domain()[1]);
data.forEach (function(datum) {
var g = Math.floor (datum.weight / discreteTo);
var cat = datum.category;
var ref = cat+"-"+g;
if (!groups[ref]) { groups[ref] = 0; }
datum.groupIndex = groups[ref];
datum.discy = yscale (g * discreteTo); // discrete
groups[ref]++;
});
data.forEach (function(datum) {
var cat = datum.category;
var g = Math.floor (datum.weight / discreteTo);
var ref = cat+"-"+g;
datum.offset = datum.groupIndex - ((groups[ref] - 1) / 2);
});
d3.select("svg g").selectAll("circle").data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return 50 + xscale(d.category) + (d.offset * (circleR * 2)); })
.attr("r", circleR)
.attr("cy", function(d) { return 10 + d.discy; })
.style ("fill", function(d) { return color(d.category); })
;
The above example discretes into groups according to the size of the display and the size of the circle to display. You might want to discrete by a given interval and then work out the size of circle from that.
Edit: Updated to show how to differentiate when category is different as in your screenshot above
I usually place my axis ticks on the svg using this:
d3.svg.axis().scale(xScale(width)).ticks(4)
Is it possible to get these tick values and their svg coordinates so I can use a custom axis outside the svg using d3.svg.axis() ?
Yes, xScale.ticks(4) should give you the actual tick points as values, and you can pipe those back through your xScale to the the X position. You can also just pull the tick points back from the generated elements after you apply the axis to an actual element:
var svg = d3.select("svg");
var scale = d3.scale.linear()
.range([20, 280])
.domain([0, 100])
var axis = d3.svg.axis().scale(scale).orient("bottom").ticks(9);
// grab the "scale" used by the axis, call .ticks()
// passing the value we have for .ticks()
console.log("all the points", axis.scale().ticks(axis.ticks()[0]));
// note, we actually select 11 points not 9, "closest guess"
// paint the axis and then find its ticks
svg.call(axis).selectAll(".tick").each(function(data) {
var tick = d3.select(this);
// pull the transform data out of the tick
var transform = d3.transform(tick.attr("transform")).translate;
// passed in "data" is the value of the tick, transform[0] holds the X value
console.log("each tick", data, transform);
});
jsbin
In d3 v4 I ended up just parsing the rendered x values from the tick nodes
function parseX(transformText) {
let m = transformText.match(/translate\(([0-9\.]*)/);
let x = m[1];
if (x) {
return parseFloat(x);
}
}
Is there any way to find inversion of ordinal scale?
I am using string value on x axis which is using ordinal scale and i on mouse move i want to find inversion with x axis to find which string is there at mouse position?
Is there any way to find this?
var barLabels = dataset.map(function(datum) {
return datum.image;
});
console.log(barLabels);
var imageScale = d3.scale.ordinal()
.domain(barLabels)
.rangeRoundBands([0, w], 0.1);
// divides bands equally among total width, with 10% spacing.
console.log("imageScale....................");
console.log(imageScale.domain());
.
.
var xPos = d3.mouse(this)[0];
xScale.invert(xPos);
I actually think it doesn't make sense that there isn't an invert method for ordinal scales, but you can figure it out using the ordinal.range() method, which will give you back the start values for each bar, and the ordinal.rangeBand() method for their width.
Example here:
http://fiddle.jshell.net/dMpbh/2/
The relevant code is
.on("click", function(d,i) {
var xPos = d3.mouse(this)[0];
console.log("Clicked at " + xPos);
//console.log(imageScale.invert(xPos));
var leftEdges = imageScale.range();
var width = imageScale.rangeBand();
var j;
for(j=0; xPos > (leftEdges[j] + width); j++) {}
//do nothing, just increment j until case fails
console.log("Clicked on " + imageScale.domain()[j]);
});
I found a shorter implementation here in this rejected pull request which worked perfectly.
var ypos = domain[d3.bisect(range, xpos) - 1];
where domain and range are scale domain and range:
var domain = x.domain(),
range = x.range();
I have in the past reversed the domain and range when this is needed
> var a = d3.scale.linear().domain([0,100]).range([0, w]);
> var b = d3.scale.linear().domain([0,w]).range([0, 100]);
> b(a(5));
5
However with ordinal the answer is not as simple. I have checked the documentation & code and it does not seem to be a simple way. I would start by mapping the items from the domain and working out the start and stop point. Here is a start.
imageScale.domain().map(function(d){
return {
'item':d,
'start':imageScale(d)
};
})
Consider posting your question as a feature request at https://github.com/mbostock/d3/issues?state=open in case
There is sufficient demand for such feature
That I haven't overlooked anything or that there is something more hidden below the documentation that would help in this case
If you just want to know which mouse position corresponds to which data, then d3 is already doing that for you.
.on("click", function(d,i) {
console.log("Clicked on " + d);
});
I have updated the Fiddle from #AmeliaBR http://fiddle.jshell.net/dMpbh/17/
I recently found myself in the same situation as OP.
I needed to get the inverse of a categorical scale for a slider. The slider has 3 discrete values and looks and behaves like a three-way toggle switch. It changes the blending mode on some SVG elements. I created an inverse scale with scaleQuantize() as follows:
var modeArray = ["normal", "multiply", "screen"];
var modeScale = d3.scalePoint()
.domain(modeArray)
.range([0, 120]);
var inverseModeScale = d3.scaleQuantize()
.domain(modeScale.range())
.range(modeScale.domain());
I feed this inverseModeScale the mouse x-position (d3.mouse(this)[0]) on drag:
.call( d3.drag()
.on("start.interrupt", function() { modeSlider.interrupt(); })
.on("start drag", function() { inverseModeScale(d3.mouse(this)[0]); })
)
It returns the element from modeArray that is closest to the mouse's x-position. Even if that value is out of bounds (-400 or 940), it returns the correct element.
Answer may seem a bit specific to sliders but posting anyway because it's valid (I think) and this question is in the top results for " d3 invert ordinal " on Google.
Note: This answer uses d3 v4.
I understand why Mike Bostock may be reluctant to include invert on ordinal scales since you can't return a singular true value. However, here is my version of it.
The function takes a position and returns the surrounding datums. Maybe I'll follow up with a binary search version later :-)
function ordinalInvert(pos, scale) {
var previous = null
var domain = scale.domain()
for(idx in domain) {
if(scale(datum[idx]) > pos) {
return [previous, datum[idx]];
}
previous = datum[idx];
}
return [previous, null];
}
I solved it by constructing a second linear scale with the same domain and range, and then calling invert on that.
var scale = d3.scale.ordinal()
.domain(domain)
.range(range);
var continousScale = d3.scale.linear()
.domain(domain)
.range(range)
var data = _.map(range, function(i) {
return continousScale.invert(i);
});
You can easily get the object's index/data in callback
.on("click", function(d,i) {
console.log("Clicked on index = " + i);
console.log("Clicked on data = " + d);
// d == imageScale.domain()[1]
});
d is the invert value itself.
You don't need to use obj.domain()[index] .
I am new to d3, learning a lot. I have an issue I cannot find an example for:
I have two y axes with positive and negative values with vastly different domains, one being large dollar amounts the other being percentages.
The resulting graph from cobbling together examples looks really awesome with one slight detail, the zero line for each y axis is in a slightly different position. Does anyone know of a way in d3 to get the zero line to be at the same x position?
I would like these two yScales/axes to share the same zero line
// define yScale
var yScale = d3.scale.linear()
.range([height, 0])
.domain(d3.extent(dataset, function(d) { return d.value_di1; }))
;
// define y2 scale
var yScale2 = d3.scale.linear()
.range([height, 0])
.domain(d3.extent(dataset, function(d) { return d.calc_di1_di2_percent; }))
;
Here is a link to a jsfiddle with sample data:
http://jsfiddle.net/jglover/XvBs3/1/
(the x-axis ticks look horrible in the jsfiddle example)
In general, there's unfortunately no way to do this neatly. D3 doesn't really have a concept of several things lining up and therefore no means of accomplishing it.
In your particular case however, you can fix it quite easily by tweaking the domain of the second y axis:
.domain([d3.min(dataset, function(d) { return d.calc_di1_di2_percent; }), 0.7])
Complete example here.
To make the 0 level the same position, a strategy is to equalize the length/proportion of the y axes.
Here are the concepts to the solution below:
The alignment of baseline depends on the length of the y axes.
To let all value shown in the bar, we need to extend the shorter side of the dimension, which compares to the other, to make the proportion of the two axes equal.
example:
// dummy data
const y1List = [-1000, 120, -130, 1400],
y2List = [-0.1, 0.2, 0.3, -0.4];
// get proportion of the two y axes
const totalY1Length = Math.abs(d3.min(y1List)) + Math.abs(d3.max(y1List)),
totalY2Length = Math.abs(d3.min(y2List)) + Math.abs(d3.max(y2List)),
maxY1ToY2 = totalY2Length * d3.max(y1List) / totalY1Length,
minY1ToY2 = totalY2Length * d3.min(y1List) / totalY1Length,
maxY2ToY1 = totalY1Length * d3.max(y2List) / totalY2Length,
minY2ToY1 = totalY1Length * d3.min(y2List) / totalY2Length;
// extend the shorter side of the upper dimension with corresponding value
let maxY1Domain = d3.max(y1List),
maxY2Domain = d3.max(y2List);
if (maxY1ToY2 > d3.max(y2List)) {
maxY2Domain = d3.max(y2List) + maxY1ToY2 - d3.max(y2List);
} else {
maxY1Domain = d3.max(y1List) + maxY2ToY1 - d3.max(y1List);
}
// extend the shorter side of the lower dimension with corresponding value
let minY1Domain = d3.min(y1List),
minY2Domain = d3.min(y2List);
if (minY1ToY2 < d3.min(y2List)) {
minY2Domain = d3.min(y2List) + minY1ToY2 - d3.min(y2List);
} else {
minY1Domain = d3.min(y1List) + minY2ToY1 - d3.min(y1List);
}
// finally, we get the domains for our two y axes
const y1Domain = [minY1Domain, maxY1Domain],
y2Domain = [minY2Domain, maxY2Domain];