Changing number displayed as svg text gradually, with D3 transition - d3.js

I am looking for a simple way to gradually change the value of a number displayed as svg text with d3.
var quality = [0.06, 14];
// qSVG is just the main svg element
qSVG.selectAll(".txt")
.data(quality)
.enter()
.append("text")
.attr("class", "txt")
.text(0)
.transition()
.duration(1750)
.text(function(d){
return d;
});
Since text in this case is a number i hope there is an easy way to just increment it to the end of the transition.
Maybe someone of you has an idea.
Cheers

It seems d3JS already provides a suitable function called "tween"
Here is the important part of the code example.
.tween("text", function(d) {
var i = d3.interpolate(this.textContent, d),
prec = (d + "").split("."),
round = (prec.length > 1) ? Math.pow(10, prec[1].length) : 1;
return function(t) {
this.textContent = Math.round(i(t) * round) / round;
};
});​
http://jsfiddle.net/c5YVX/280/
You can increment them over a given time interval from any start to any end value regardless their number precision.
Its implemented for SVG text but of course works the same for standard html text.
If you only need the plain tween function for rounded numbers, it gets a bit more leightweight.
.tween("text", function(d) {
var i = d3.interpolate(this.textContent, d),
return function(t) {
this.textContent = Math.round(i(t));
};
});​

Related

How to filter views with an opacity range in d3/dc.js?

I don't know if this is possible in dc.js and crossfilter.js, but I decided to ask anyways.
I combined a scatterplot and a barChart example from dc to make an interactive dashboard:
var chart1 = dc.scatterPlot("#test1");
var chart2 = dc.scatterPlot("#test2");
d3.csv("output.csv", function(error, data) {
data.forEach(function (x) {
x.x = +x.x;
x.y = +x.y;
x.z = +x.z;
});
var ndx = crossfilter(data),
dim1 = ndx.dimension(function (d) {
return [d.x, d.y];
}),
dim2 = ndx.dimension(function (d) {
return Math.floor(parseFloat(d.z) * 10) / 10;
}),
group1 = dim1.group(),
group2 = dim2.group(),
chart1.width(300)
.height(300)
.x(d3.scale.linear().domain([-2, 2]))
.y(d3.scale.linear().domain([-2, 2]))
.yAxisLabel("y")
.xAxisLabel("x")
.clipPadding(10)
.dimension(dim1)
//.excludedOpacity(0.5)
.excludedColor('#ddd')
.group(group1)
.symbolSize([2.5]);
chart2
.width(600)
.dimension(dim2)
.group(group2)
.x(d3.scale.linear().domain([0,3]))
.elasticY(true)
.controlsUseVisibility(false)
.barPadding([0.1])
.outerPadding([0.05]);
chart2.xAxis().tickFormat(function(d) {return d}); // convert back to base unit
chart2.yAxis().ticks(10);
dc.renderAll();
});
Result when brushing the bar chart:
I want to change the filtering so that when I brush the bar chart, brushed points in the scatterplot will have an opacity value, which is 1 in the middle of the brush, and decreases towards end of the range of brush.
The other points (outside the brush) should just be grey, instead of invisible as in the current script. Illustration:
Is this possible to do with the dc.js and crossfilter.js?
PS: The attached scatterplot isn't the desired outcome. It is not filtered based on opacity. I just attached it to show how the other points(grey) should look like after brushing the bar chart.
I couldn't get this working with animated transitions, because there is something I am missing about how to interrupt transitions, and the original dc.scatterPlot is already applying opacity transitions.
So, to start off, let's turn transitions on the original scatter plot:
chart1
.transitionDuration(0)
We also need to add Z to the input data for the scatter plot. Although it would make more sense to add it to the value, it's easy to add it to the key (and the scatter plot will ignore extra elements in the key):
dim1 = ndx.dimension(function (d) {
return [d.x, d.y, d.z];
}),
Then we can add a handler to to the scatter plot to apply opacity to the dots, based on the range of the filter in the bar chart:
chart1.on('pretransition', function(chart) {
var range = chart2.filter(); // 1
console.assert(!range || range.filterType==='RangedFilter'); // 2
var mid, div; // 3
if(range) {
mid = (range[0] + range[1])/2;
div = (range[1] - range[0])/2;
}
chart1.selectAll('path.symbol') // 4
.attr('opacity', function(d) {
if(range) { // 5
if(d.key[2] < range[0] || range[1] < d.key[2])
op = 0; // 6
else
op = 1 - Math.abs(d.key[2] - mid)/div; // 7
//console.log(mid, div, d.key[2], op);
return op;
}
else return 1;
})
});
Get the current brush/filter from the bar chart
It should either be null or it should be a RangedFilter
Find the midpoint and the distance from the midpoint to the edges of the brush
Now apply opacity to all symbols in the scatter plot
If there is an active brush, apply opacity (otherwise 1)
If the symbol is outside the brush, opacity is 0
Otherwise the opacity is linear based on the distance from the midpoint
You could probably use d3.ease to map the distance [0,1] to opacity [0,1] using a curve instead of linearly. This might be nice so that it emphasizes the points closer to the midpoint
This demo is not all that cool because the data is purely random, but it shows the idea:
https://jsfiddle.net/gordonwoodhull/qq31xcoj/64/
EDIT: alright, it's a total abuse of dc.js, but if you really want to use it without filtering, and displaying the excluded points in grey, you can do that too.
This will disable filtering on the bar chart:
chart2.filterHandler(function(_, filters) { return filters; });
Then apply opacity and color to the scatter plot like this instead:
chart1.selectAll('path.symbol')
.attr('opacity', function(d) {
if(range && range.isFiltered(d.key[2]))
return 1 - Math.abs(d.key[2] - mid)/div;
else return 1;
})
.attr('fill', function(d) {
if(!range || range.isFiltered(d.key[2]))
return chart1.getColor(d);
else return '#ccc';
})
With this data it's tricky to see the difference between the light blue dots and the grey dots. Maybe it will work better with non-random data, maybe not. Maybe another color will help.
Again, you might as well use straight D3, since this disables most of what dc.js and crossfilter do. But you'd have to start from scratch to ask that question.
Updated fiddle.
EDIT 2: sort the dots by filteredness like this:
.sort(function(d) {
return range && range.isFiltered(d.key[2]) ? 1 : 0;
})
Fiddle 3

Modifiying dimple layout grouped chart

I am using the same chart as below. I want to push the x-axis headers i.e. Regular, Premium, Budget little bit below i.e. top padding or margin. Give some styling to it like give background color and change text color. I tried using fill and it does not work as desired. I would like to hide Price Tier/Channel also
http://dimplejs.org/examples_viewer.html?id=bars_vertical_grouped
These are SVG text elements so there is no top-padding or margin. You can move them down a bit by increasing the y property though, running the following after you call the chart.draw method will move the labels down 5 pixels:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.attr("y", function (d) {
// Get the y property of the current shape and add 5 pixels
return parseFloat(d3.select(this).attr("y")) + 5;
});
To change the text colour you need to use the fill property (again that's an svg text thing):
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.style("fill", "red");
To colour the background of the text is a little less trivial, there actually isn't a thing for that in SVG, however you can insert a rectangle behind the text and do what you like with it:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
// Iterate each shape matching the selector above (all the x axis labels)
.each(function () {
// Select the shape in the current iteration
var shape = d3.select(this);
// Get the bounds of the text (accounting for font-size, alignment etc)
var bounds = shape.node().getBBox();
// Get the parent group (this the target for the rectangle to make sure all its transformations etc are applied)
var parent = d3.select(this.parentNode);
// This is just the number of extra pixels to add around each edge as the bounding box is tight fitting.
var padding = 2;
// Insert a rectangle before the text element in the DOM (SVG z-position is entirely determined by DOM position)
parent.insert("rect", ".dimple-custom-axis-label")
// Set the bounds using the bounding box +- padding
.attr("x", bounds.x - padding)
.attr("y", bounds.y - padding)
.attr("width", bounds.width + 2 * padding)
.attr("height", bounds.height + 2 * padding)
// Do whatever styling you want - or set a class and use CSS.
.style("fill", "pink");
});
These three statements can all be chained together so the final code will look a bit like this:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.attr("y", function (d) { return parseFloat(d3.select(this).attr("y")) + 5; })
.style("fill", "red")
.each(function () {
var shape = d3.select(this);
var bounds = shape.node().getBBox();
var parent = d3.select(this.parentNode);
var padding = 2;
parent.insert("rect", ".dimple-custom-axis-label")
.attr("x", bounds.x - padding)
.attr("y", bounds.y - padding)
.attr("width", bounds.width + 2 * padding)
.attr("height", bounds.height + 2 * padding)
.style("fill", "pink");
});
FYI the dimple-custom-axis-label class was added in a recent release of dimple so please make sure you are using the latest version. Otherwise you'll have to find an alternative selector

Inversion with ordinal scale

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] .

d3 line chart labels overlap

I've created a line chart based on the example found here:
http://bl.ocks.org/mbostock/3884955
However, with my data the line labels (cities) end up overlapping because the final values on the y-axis for different lines are frequently close together. I know that I need to compare the last value for each line and move the label up or down when the values differ by 12 units or less. My thought is to look at the text labels that are written by this bit of code
city.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")"; })
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
If the y(d.value.temperature) values differ by 12 or less, move the values apart until they have at least 12 units between them. Any thoughts on how to get this done? This is my first d3 project and the syntax is still giving me fits!
You're probably better off passing in all the labels at once -- this is also more in line with the general d3 idea. You could then have code something like this:
svg.selectAll("text.label").data(data)
.enter()
.append("text")
.attr("transform", function(d, i) {
var currenty = y(d.value.temperature);
if(i > 0) {
var previousy = y(data[i-1].value.temperature),
if(currenty - previousy < 12) { currenty = previousy + 12; }
}
return "translate(" + x(d.value.date) + "," + currenty + ")";
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
This does not account for the fact that the previous label may have been moved. You could get the position of the previous label explicitly and move the current one depending on that. The code would be almost the same except that you would need to save a reference to the current element (this) such that it can be accessed later.
All of this will not prevent the labels from being potentially quite far apart from the lines they are labelling in the end. If you need to move every label, the last one will be pretty far away. A better course of action may be to create a legend separately where you can space labels and lines as necessary.
Consider using a D3 force layout to place the labels. See an example here: https://bl.ocks.org/wdickerson/bd654e61f536dcef3736f41e0ad87786
Assuming you have a data array containing objects with a value property, and a scale y:
// Create some nodes
const labels = data.map(d => {
return {
fx: 0,
targetY: y(d.value)
};
});
// Set up the force simulation
const force = d3.forceSimulation()
.nodes(labels)
.force('collide', d3.forceCollide(10))
.force('y', d3.forceY(d => d.targetY).strength(1))
.stop();
// Execute thte simulation
for (let i = 0; i < 300; i++) force.tick();
// Assign values to the appropriate marker
labels.sort((a, b) => a.y - b.y);
data.sort((a, b) => b.value - a.value);
data.forEach((d, i) => d.y = labels[i].y);
Now your data array will have a y property representing its optimal position.
Example uses D3 4.0, read more here: https://github.com/d3/d3-force

Unique symbols for each data set in d3 Scatterplot

I am having trouble using d3's symbol mechanism to specify a unique symbol for each set of data. The data's like this:
[[{x: 1, y:1},{x: 2, y:2},{x: 3, y:3}], [{x: 1, y:1},{x: 2, y:4},{x: 3, y:9}], etc.]
The part of the code that writes out the symbols looks like this:
I create a series group for each vector of points. Then:
series.selectAll("g.points")
//this selects all <g> elements with class points (there aren't any yet)
.data(Object) //drill down into the nested Data
.enter()
.append("g") //create groups then move them to the data location
.attr("transform", function(d, i) {
return "translate(" + xScale(d.x) + "," + yScale(d.y) + ")";
})
.append("path")
.attr("d", function(d,i,j){
return (d3.svg.symbol().type(d3.svg.symbolTypes[j]));
}
);
Or at least that's how I'd like it to work. The trouble is that I can't return the function d3.svg.symbol() from the other function. If I try to just put the function in the "type" argument, then data is no longer scoped correctly to know what j is (the index of the series).
right, but I don't want a unique symbol for each datapoint, I want a unique symbol for each series. The data consists of multiple arrays (series), each of which can have an arbitrary number of points (x,y). I'd like a different symbol for each array, and that's what j should give me. I associate the data (in the example, two arrays shown, so i is 0 then 1 for that) with the series selection. Then I associate the data Object with the points selection, so i becomes the index for the points in each array, and j becomes the index of the original arrays/series of data. I actually copied this syntax from somewhere else, and it works ok for other instances (coloring series of bars in a grouped bar chart for example), but I couldn't tell you exactly why it works...
Any guidance would be appreciated.
Thanks!
What is the question exactly? The code that you give answers your question. My bad, j does return a reference to the series. Simpler example.
var data = [
{id: 1, pts: [{x:50, y:10},{x:50, y:30},{x:50, y:20},{x:50, y:30},{x:50, y:40}]},
{id: 2, pts: [{x:10, y:10},{x:10, y:30},{x:40, y:20},{x:30, y:30},{x:10, y:30}]}
];
var vis = d3.select("svg");
var series = vis.selectAll("g.series")
.data(data, function(d, i) { return d.id; })
.enter()
.append("svg:g")
.classed("series", true);
series.selectAll("g.point")
.data(function(d, i) { return d.pts })
.enter()
.append("svg:path")
.attr("transform", function(d, i) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("d", function(d,i, j) { return d3.svg.symbol().type(d3.svg.symbolTypes[j])(); })
The only difference is that I added parenthesis after d3.svg.symbol().type(currentType)() to return the value rather than the function. D3js uses chaining, jquery style. This let you use symbol().type('circle') to set a value and symbol().type() to get it. Whenever accessors are used, what is returned is a reference to a function that has methods and attributes. Keep in mind that, in Javascript functions are first class objects - What is meant by 'first class object'?. In libraries that use that approach, often, there is an obvious getter for retrieving meaningful data. With symbol, you have to use symbol()().
The code beyond the symbol functionality can be seen at: https://github.com/mbostock/d3/blob/master/src/svg/symbol.js
d3.svg.symbol = function() {
var type = d3_svg_symbolType,
size = d3_svg_symbolSize;
function symbol(d, i) {
return (d3_svg_symbols.get(type.call(this, d, i))
|| d3_svg_symbolCircle)
(size.call(this, d, i));
}
...
symbol.type = function(x) {
if (!arguments.length) return type;
type = d3_functor(x);
return symbol;
};
return symbol;
};
Just in case you haven't. Have you tried?
.append("svg:path")
.attr("d", d3.svg.symbol())
as per https://github.com/mbostock/d3/wiki/SVG-Shapes.

Resources