d3 setting multiple attributes from the same function? - javascript

Is there any way to set multiple attributes from the same function?
d3.selectAll('.myshape')
.attr('y',function(d,i) { ... calculates something ... })
.attr('height',function(d,i) { ... calculates something very similar... })
I would like to calculate y1 and y2 at the same time and then set y = y1 and height = y2-y1. But the standard way of doing this in d3 seems to be having separate functions per attribute. Is there a better way?

If I understand your question correctly, you have an intensive calculation that you would like to compute only once per element, regardless the number of attributes you're setting.
That being the case, you can use an each to pass the current element (this), along with the datum, the index and the group, if you need them:
d3.selectAll(".myShape").each(function(d,i,n){
//Here you put the complex calculation
//which will assign the values of y1 and y2.
//This calculation runs only once per element
d3.select(this).attr("y", y1).attr("height", y2 - y1)
});

Not exactly. But you could solve the issue in a indirect way. This adds slightly more overhead but worth it imo. JS doesn't have nice "compile-time" (as far as the js engine is concerned) macros like C family, that give you nice expansions without runtime cost.
like this (you may already know):
let myshape = d3.selectAll('.myshape')
['y', 'height', ...].forEach(attr => myshape.attr(attr, calcAttr));
function calcAttr(a, i) {
// shared code
switch (a) {
case 'y':
//attr specific
break;
}
}

Related

How should I bind data to appended elements to a transformed D3.js group?

I am approaching the problem of appending a complex (two or more glyphs) symbol to some data. With D3.js, it seems that the right way to do so is appending the glyphs (in this example, circles) to groups (g) joined to data:
datum <=> g <=> (circle, circle)
Both the groups and the appended glyphs have properties depending on data, so that for example g is translated by .start and the position of the second circle is given by .end for each datum.
In order to achieve this, I wrote the following code (see the notebook), which however does not work as expected
function updatea (){
a[0].start += 10*Math.sin(t);
a[0].end += 10*Math.cos(t);
console.log(a[0].end - a[0].start);
t += 0.1;
var miao = svg.selectAll('g.aaa').data(a).join('g')
.classed('aaa',true)
.attr('transform',(d, i)=>('translate('+d.start+','+(i+1)*50+')'));
miao.append('circle').attr('r', 10).attr('fill','red');
miao.append('circle').attr('r', 10).attr('cx', d=>d.end).attr('fill','red');
}
The expected result would be as follows: two circles oscillate around their initial position, with a phase of period/4 between them. Instead, the second circle (to which I assigned an attribute cx, in order to give the position relative to the first one) is not refreshed, but instead all of its positions are drawn one after the other, oscillating with the translation in the attribute "transform".
I think that the problem is appending circles every time I update data; but how should I then append them? I tried something like this, following https://bost.ocks.org/mike/nest/:
var groups = svg.selectAll('g').data(a).enter().append('g').attr('transform',(d, i)=>('translate('+d.start+','+(i+1)*50+')'));
var circle_start = groups.selectAll('circle').data((d)=>{return d.start;}).enter().append('circle').attr('cx', d=>d).attr('cy', d=>100).attr('r', 10);
var circle_end = groups.selectAll('circle').data((d)=>{return d.end;}).enter().append('circle').attr('cx', d=>d).attr('cy', d=>100).attr('r', 10);
but it gives no output. Doing a bit of debug, for example assigning another dataset to one of the two circles, apparently the problem lies in .data(d)=>{return d.end;}).
Problem
On the pragmatic side, your update function doesn't work as expected because each update you append two new circles to each g entered or updated with selectAll().join():
function updatea (){
// join g's
var miao = svg.selectAll('g.aaa').data(a).join('g')
.classed('aaa',true)
.attr('transform',(d, i)=>('translate('+d.start+','+(i+1)*50+')'));
// append two circles to each g entered or updated:
miao.append('circle').attr('r', 10).attr('fill','red');
miao.append('circle').attr('r', 10).attr('cx', d=>d.end).attr('fill','red');
}
If you inspect the page you'll see two new circles appended each update. You're never updating the circles that are already on the page, just the translate on the g.
On the more theoretical side, you are unclear if your approach is most appropriate for binding data to complex symbols.
Solution
In proposing a solution let's consider the theoretical side first.
D3 was designed with data binding in mind. Normally in D3 you want to bind one datum per element (or one datum per element with a single child in each level of nesting). Where data is grouped and each datum is represented with multiple children, we would often see a second, nested, selectAll().data().join() statement.
However, if your visualization uses a symbol that is always comprised of the same set of child elements, then we don't need to do a nested join. In fact we do not need to in order to stay true to the data binding philosophy in D3: we'll bind one datum to one symbol (symbol in the data visualization sense).
This is the approach I'll propose here.
Rationale
This approach has advantages depending on situation, for example, there may be cases where the symbol's elements share parts of the datum (as in your case where d.start and d.end are both used to set the position of one of the sub-components) - splitting the datum into a new data array would be unnecessarily cumbersome. Changes in the symbol's representation/behavior/etc may require different parts of the datum as well, in which case it doesn't make sense to split the parent datum up.
Also, another reason why the proposed approach is attractive is that if you break the datum into smaller sub-components by using a nested selection:
svg.selectAll("g").data(data).enter().append("g")
.selectAll("circle").data(function(d) { return [d.start,d.end]; })
...
Or by flattening your array:
svg.selectAll("g").data([data[0].start,data[0].end,data[1].start,...])
...
It isn't as clear what child datum corresponds to what property when entering/updating your elements or what even what child datum corresponds to what parent datum. But also, say you dislike the symbol and now want a circle and rect, or two circles and a rect, then you need to substantially adjust the above approaches (perhaps by creating a fancy enter function that returns different types of shapes depending on index or on some identifier that tells you what symbol sub-component the datum corresponds to).
I believe attempting to create one unique datum per element is not ideal in this case, which is why I'm advocating for one datum per symbol.
Implementation
So, let's do one datum per symbol (where the symbols have child elements). This should be fairly easy to implement, I'll go over a simple method to do this here:
We can create the symbol in the join's enter function, and update it in the update function:
function updatea (){
a[0].start += 10*Math.sin(t);
a[0].end += 10*Math.cos(t);
t += 0.1;
var miao = svg.selectAll('g').data(a).join(
enter => {
// create the parent element to hold the symbol
let entered = enter.append('g')
.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
.attr('class','symbol');
// append the sub-components of the symbol
entered.append('circle').attr('r', 10).attr('fill','red');
entered.append('circle').attr('class','end').attr('r', 15).attr('fill','yellow').attr('cx',d=>d.end);
},
update => {
// update overall positioning
update.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
// update the sub-components
update.select('.end').attr('cx',d=>d.end);
return update
},
exit => exit.remove()
)
First, it's important to note, even though you've likely noticed, the parent datum is passed to child elements when using selection.append().
In the enter function passed to selection.join() We enter the g, style it as appropriate. Then we add the symbol sub-components and set their initial properties.
In the update function we update the overall position of the symbol and then the sub components.
Nothing occurs outside the join method in this case.
I cannot fork your observable without creating another account somewhere, so I'll just make a snippet of your example:
const svg = d3.select("body").append("svg")
.attr("width", 600)
.attr("height", 150);
var a = [{'start': 100, 'end': 200},{'start':100, 'end':200}];
var t = 0;
function updatea (){
a[0].start += 5*Math.sin(t);
a[0].end += 5*Math.cos(t);
a[1].start += 5*Math.cos(t);
a[1].end += 5*Math.sin(t);
t += 0.1;
var miao = svg.selectAll('g').data(a).join(
enter => {
// create the parent element to hold the symbol
let entered = enter.append('g')
.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
.attr('class','symbol');
// append the sub-components of the symbol
entered.append('circle').attr('r', 10).attr('fill','red');
entered.append('circle').attr('class','end').attr('r', 15).attr('fill','yellow').attr('cx',d=>d.end);
},
update => {
// update overall positioning
update.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
// update the sub-components
update.select('.end').attr('cx',d=>d.end);
return update
},
exit => exit.remove()
)
}
updatea();
setInterval(updatea, 100)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
The idea is that since we have two circles, have two data with them. (Ignore what's in them for now:)
var a = [{...},
{...}];
Let's create a group:
var group = svg.append("g")
Then, d3 will "join" the data to DOM nodes. For each data, d3 creates a DOM node. In this case, since we're join()ing to circles, d3 will create a circle for each data. See this page for more details.
group
.selectAll('circle')
.data(a)
.join('circle')
.attr('r', 10)
.attr('fill','red')
.attr('transform', (d, i) => ('translate('+d.start+','+(i+1)*50+')'));
As for the actual logic, there's a couple things I changed.
Each circle now stores its own t and nothing else:
var a = [{t: 0},
{t: Math.PI/2}];
Then, the start and end attributes are set in order to have a representation independent of the current object's state. This allows us to have circles which have different t phases:
a.forEach((d, i) => {
d.start = 200 + 100*Math.sin(d.t);
d.end = 200 + 100*Math.cos(d.t);
d.t += 0.1;
})
Breaking it down:
(initial (range)
position)
200 + 100*Math.cos(d.t);
So it starts at position 200 and can either go to +100 or -100: the effective range is [100, 300].
You notice we do a lot of number crunching here. Basically, we're converting one domain of numbers (-1, 1) to a range (100, 300). This is a common use case for a scale, which can convert any domain to any range.
Observable notebook

Javascript variable what is less efficient

I am learning Javascript currently.I was wondering if there is any difference between:
var factor=0.1;
var limit=10;
var x;
var y;
x= limit*factor;
y= limit*factor;
//or
var limit=10;
var x;
var y;
x=limit *0.1;
y=limit*0.1;
Does it make any difference (when looking at performance for example)? If so, why it is different? The second example looks less promising to me, because I keep thinking that I am declaring the variable 0.1 twice. Thanks for your help in advance.
There is a very small difference. When you use factor in the two multiplications, the JavaScript engine has to go look up the value of factor in the current lexical environment object each time — in theory, anyway; optimization may well be able to get rid of that, if and when the code is chosen for optimization by the JavaScript engine.
But regardless: Worry about performance problems when you have a performance problem to worry about. Instead, concentrate on readability and maintainability. If x and y are meant to be multiplied by the same value, put that value in something (a var, or perhaps a const with ES2015+), and use it in both places.
I would suggest you go ahead with the first example, but with a modification. Variables are meant to hold dynamic data, it is better to hold 0.1 in a variable, so you can change it over time if required.
// have a function so that you don't repeat the code
function getFactor(factor, limit) {
return limit * factor;
}
//declare the variables and set the required default values
var factor = 0.1,
limit = 10,
x, y;
//assign getFactor to x and y variables
x = y = getFactor(factor, limit);

Detect which type of d3 scale I am working with

I'm writing some charting components using D3. Some of these components are deliberately generic, so they can be re-used in various scenarios.
In one function, I receive a scale object as an argument. I want to take slightly different actions depending on whether this is a linear scale, a time scale, or an ordinal scale.
However I cannot easily see how to detect which type of scale I've got.
instanceof does not work - these objects aren't create with new and don't have a prototype set. They are Function objects with additional attributes added to them.
typeof returns 'object' in all cases (naturally)
at least in the case of ordinal scales, there are some additional attributes that I can use for detection, so I can use:
if (scale.rangeBound) {
// do something specific to ordinal scales
}
but that does not seem to be the case for different types of quantative scales, which seem to have identical sets of attributes.
I can add some attribute to each scale I create indicating it's type, but I would prefer not to as it reduces the generality of the functions I am creating (they would require scales passed to them which had these attributes added). Also I would risk clashing with some attribute added by a future version of D3.
In fact this question could be extended to many of the objects within D3. Is there any way to tell them apart?
You could insulate yourself against code changes by coding your check against the behaviour of the scales, than it's internals
function checkType(scale) {
var s = scale.copy();
if (s.domain([1, 2]).range([1, 2])(1.5) === 1)
return "ordinal";
else if (s.domain([1, 2]).range([1, 2]).invert(1.5) === 1.5)
return "linear";
else if (s.domain([1, 2]).range([1, 2]).invert(1.5) instanceof Date)
return "time";
else
return "not supported";
}
Note, that this would still trip up for other / custom scales that have the same behaviour.
Fiddle - http://jsfiddle.net/enon2rjs/
I played around a little, and found something that might be useful. I'm not sure how reliable this is, but comparing the string version of a method that all three scales share seems to give the desired result.
The statement
console.log(""+ordinal.domain);
Prints some representation of the function ordinal.domain (which, at least for me, is the source code). This should be the same for any two instances of ordinal.domain. It seems to work, but it does feel like there should be a better way.
var ordinal = d3.scale.ordinal();
var ordinal2 = d3.scale.ordinal();
var time = d3.time.scale();
var range = d3.scale.linear();
ordinal.domain([1,2]);
ordinal.range([2,3])
console.log(""+ordinal.domain == ""+ordinal2.domain); //true
console.log(""+time.domain == ""+ordinal.domain); //false
console.log(""+range.domain == ""+time.domain); //false

simplifying the contents of an input using the javascript math system

This obviuosly works perfectly:<script>alert(5*8-4)</script>
but i need to solve whatever someone puts inside an input box.
Heres what I thought of doing: I would get the value of the input, into a variable. then I would use
document.write("<script>alert("+theinputvalue+")<script>");
or do this:
var string="<script>alert("+theinputvalue+")<script>";document.write(string);
but nothing works.
Is it even possible to do this? if not, tell my what simple other system I could use.
eventually, I will use it to graph lines like this:
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d")
for(var x=-100; x<100; x=x+.2){
y = .1*(x*x)
ctx.fillRect(x+50, -1*y+50, 2, 2);
}
http://jsfiddle.net/KGgq4/
eval('5*8-4')
will result in 36
I'm not aware of any library that is doing that (this doesn't mean that there are no such it simply means I never actually needed that) but what you should end up doing is to build an automata that will parse input string and transform it to a proper graph with proper transformations. This is not very easy topic and if you want to go this route you should start reading on arithmetic expressions parsing algorithms (sorry I do not have any solution in place).
Or you can cheat and define types of equations that will be selected by user. Once user selects type of equation you should be able show user inputs where user will be able to select coefficients. You can read those coefficients into different variables and apply transformations in your draw procedure (For example if user will select type sin(x) you know that general equation has following formula: y = k*sin(a*x + b) + c. So once it is selected you can allow user to enter k, a, b, c and based on that input calculate appropriate locations of points for your graph.)
Well, third solution could involve "eval ", but usually you should avoid eval at any cost (B/c it is straight forward JavaScript injection which may be an OK for this case but may get you in trouble later in your life. ).
You can use math.js, which comes with an advanced expression parser. It supports definition of variables and functions.
// create an instance of math.js
var math = mathjs();
// evaluate an expression
math.eval('5*8-4'); // 36
// user defined function (returns a native JavaScript function)
var f = math.eval('f(x) = 2*x^2 + 6');
// use the function (for graphing or something)
f(2); // 14

How do I implement something like pointers in javascript?

I know that javascript doesn't have pointers in terms of a variable referring to a place in memory but what I have is a number of variables which are subject to change and dependent on each other.
For example:
Center (x,y) = (offsetLeft + width/scale , offsetTop + height/scale)
As of now I have rewritten the equation in terms of each individual variable and after any changes I call the appropriate update function.
For example:
If scale changes, then then the center, height, and width stay the same. So I call
updateoffset() {
offsetLeft = centerx - width/scale;
offsetTop = centery - height/scale;
}
Is this the easiest way to update each of these variables when any of them changes?
I read your question in two different ways, so two answers:
Updating calculated values when other values change
The two usual ways are: 1. To require that the values only be changed via "setter" functions, and then you use that as an opportunity to recalcuate the things that changed, or 2. To require that you use "getter" functions to get the calculated value, which you calculate on the fly (or if that's expensive, you retrieve from a cached calculation).
Returning multiple values from a function
If you're looking for a way of returning multiple values from a single function, you can do that easily by returning an object. Example:
// Definition:
function center(offsetLeft, offsetTop, width, height, scale) {
return {
x: offsetLeft + width/scale,
y: offsetTop + height/scale
};
}
// Use:
var pos = center(100, 120, 10, 20, 2);
// pos.x is now 105
// pos.y is now 130

Categories