jcp
jcp

Reputation: 21

understanding functions in d3js

Can some one explain me this function:

var transitions = function () 
                {
                    return states.reduce(function (initial, state) {
                        return initial.concat(
                                state.transitions.map(function (transition) {
                                    return {source: state, transition: transition};
                                })
                                );
                    }, []);
                };

and this line: var gTransitions = svg.append('g').selectAll("path.transition"); - how path.transition is getting selected?

I am new d3 and javascript and I am really stuck at this point in my project.

The above snippet is taken out of below code. I have put comments saying "QUESTION1" and "QUESTION2" to find it.

window.onload = function ()
            {
                var radius = 40;

                window.states = [
                    {x: 43, y: 67, label: "first", transitions: []},
                    {x: 340, y: 150, label: "second", transitions: []},
                    {x: 200, y: 250, label: "third", transitions: []}
                ];

                window.svg = d3.select('body')
                        .append("svg")
                        .attr("width", "960px")
                        .attr("height", "500px");

                // define arrow markers for graph links
                svg.append('svg:defs').append('svg:marker')
                        .attr('id', 'end-arrow')
                        .attr('viewBox', '0 -5 10 10')
                        .attr('refX', 4)
                        .attr('markerWidth', 8)
                        .attr('markerHeight', 8)
                        .attr('orient', 'auto')
                        .append('svg:path')
                        .attr('d', 'M0,-5L10,0L0,5')
                        .attr('class', 'end-arrow')
                        ;


                // line displayed when dragging new nodes
                var drag_line = svg.append('svg:path')
                        .attr({
                            'class': 'dragline hidden',
                            'd': 'M0,0L0,0'
                        })
                        ;
                //QUESTION1
                var gTransitions = svg.append('g').selectAll("path.transition");
                var gStates = svg.append("g").selectAll("g.state");


                //QUESTION2
                var transitions = function () 
                {
                    return states.reduce(function (initial, state) {
                        return initial.concat(
                                state.transitions.map(function (transition) {
                                    return {source: state, transition: transition};
                                })
                                );
                    }, []);
                };

                var transformTransitionEndpoints = function (d, i) {
                    var endPoints = d.endPoints();

                    var point = [
                        d.type == 'start' ? endPoints[0].x : endPoints[1].x,
                        d.type == 'start' ? endPoints[0].y : endPoints[1].y
                    ];

                    return "translate(" + point + ")";
                }

                var transformTransitionPoints = function (d, i) {
                    return "translate(" + [d.x, d.y] + ")";
                }

                var computeTransitionPath = (function () {
                    var line = d3.svg.line()
                            .x(function (d, i) {
                                return d.x;
                            })
                            .y(function (d, i) {
                                return d.y;
                            })
                            .interpolate("cardinal");

                    return function (d) {

                        var source = d.source,
                        target = d.transition.points.length && d.transition.points[0] || d.transition.target,
                        deltaX = target.x - source.x,
                        deltaY = target.y - source.y,
                        dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
                        normX = deltaX / dist,
                        normY = deltaY / dist,
                        sourcePadding = radius + 4, //d.left ? 17 : 12,
                        sourceX = source.x + (sourcePadding * normX),
                        sourceY = source.y + (sourcePadding * normY);

                        source = d.transition.points.length && d.transition.points[ d.transition.points.length - 1] || d.source;
                        target = d.transition.target;
                        deltaX = target.x - source.x;
                        deltaY = target.y - source.y;
                        dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
                        normX = deltaX / dist;
                        normY = deltaY / dist;
                        targetPadding = radius + 8;//d.right ? 17 : 12,
                        targetX = target.x - (targetPadding * normX);
                        targetY = target.y - (targetPadding * normY);

                        var points =
                                [{x: sourceX, y: sourceY}].concat(
                                d.transition.points,
                                [{x: targetX, y: targetY}]
                                )
                                ;

                        var l = line(points);

                        return l;
                    };
                })();

                var dragPoint = d3.behavior.drag()
                        .on("drag", function (d, i) {
                            console.log("transitionmidpoint drag");
                            var gTransitionPoint = d3.select(this);

                            gTransitionPoint.attr("transform", function (d, i) {
                                d.x += d3.event.dx;
                                d.y += d3.event.dy;
                                return "translate(" + [d.x, d.y] + ")"
                            });

                            // refresh transition path
                            gTransitions.selectAll("path").attr('d', computeTransitionPath);
                            // refresh transition endpoints
                            gTransitions.selectAll("circle.endpoint").attr({
                                transform: transformTransitionEndpoints
                            });

                            // refresh transition points
                            gTransitions.selectAll("circle.point").attr({
                                transform: transformTransitionPoints
                            });

                            d3.event.sourceEvent.stopPropagation();
                        });

                var renderTransitionMidPoints = function (gTransition) {
                    gTransition.each(function (transition) {
                        var transitionPoints = d3.select(this).selectAll('circle.point').data(transition.transition.points, function (d) {
                            return transition.transition.points.indexOf(d);
                        });

                        transitionPoints.enter().append("circle")
                                .attr({
                                    'class': 'point',
                                    r: 4,
                                    transform: transformTransitionPoints
                                })
                                .call(dragPoint);
                        transitionPoints.exit().remove();
                    });
                };

                var renderTransitionPoints = function (gTransition) {
                    gTransition.each(function (d) {
                        var endPoints = function () {
                            var source = d.source,
                                    target = d.transition.points.length && d.transition.points[0] || d.transition.target,
                                    deltaX = target.x - source.x,
                                    deltaY = target.y - source.y,
                                    dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
                                    normX = deltaX / dist,
                                    normY = deltaY / dist,
                                    sourceX = source.x + (radius * normX),
                                    sourceY = source.y + (radius * normY);

                                    source = d.transition.points.length && d.transition.points[ d.transition.points.length - 1] || d.source;
                                    target = d.transition.target;
                                    deltaX = target.x - source.x;
                                    deltaY = target.y - source.y;
                                    dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
                                    normX = deltaX / dist;
                                    normY = deltaY / dist;
                                    targetPadding = radius + 8;//d.right ? 17 : 12,
                                    targetX = target.x - (radius * normX);
                                    targetY = target.y - (radius * normY);

                            return [{x: sourceX, y: sourceY}, {x: targetX, y: targetY}];
                        };

                        var transitionEndpoints = d3.select(this).selectAll('circle.endpoint').data([
                            {endPoints: endPoints, type: 'start'},
                            {endPoints: endPoints, type: 'end'}
                        ]);

                        transitionEndpoints.enter().append("circle")
                                .attr({
                                    'class': function (d) {
                                        return 'endpoint ' + d.type;
                                    },
                                    r: 4,
                                    transform: transformTransitionEndpoints
                                })
                                ;
                        transitionEndpoints.exit().remove();
                    });
                };

                var renderTransitions = function () {
                    gTransition = gTransitions.enter().append('g')
                            .attr({
                                'class': 'transition'
                            })

                    gTransition.append('path')
                            .attr({
                                d: computeTransitionPath,
                                class: 'background'
                            })
                            .on({
                                dblclick: function (d, i) {
                                    gTransition = d3.select(d3.event.target.parentElement);
                                    if (d3.event.ctrlKey) {
                                        var p = d3.mouse(this);

                                        gTransition.classed('selected', true);
                                        d.transition.points.push({x: p[0], y: p[1]});

                                        renderTransitionMidPoints(gTransition, d);
                                        gTransition.selectAll('path').attr({
                                            d: computeTransitionPath
                                        });
                                    } else {
                                        var gTransition = d3.select(d3.event.target.parentElement),
                                                transition = gTransition.datum(),
                                                index = transition.source.transitions.indexOf(transition.transition);

                                        transition.source.transitions.splice(index, 1)
                                        gTransition.remove();

                                        d3.event.stopPropagation();
                                    }
                                }
                            });

                    gTransition.append('path')
                            .attr({
                                d: computeTransitionPath,
                                class: 'foreground'
                            });

                    renderTransitionPoints(gTransition);
                    renderTransitionMidPoints(gTransition);

                    gTransitions.exit().remove();
                };

                var renderStates = function () {
                    var gState = gStates.enter()
                            .append("g")
                            .attr({
                                "transform": function (d) {
                                    return "translate(" + [d.x, d.y] + ")";
                                },
                                'class': 'state'
                            })
                            .call(drag);

                    gState.append("circle")
                            .attr({
                                r: radius + 4,
                                class: 'outer'
                            })
                            .on({
                                mousedown: function (d) {
                                    console.log("state circle outer mousedown");
                                    startState = d, endState = undefined;

                                    // reposition drag line
                                    drag_line
                                            .style('marker-end', 'url(#end-arrow)')
                                            .classed('hidden', false)
                                            .attr('d', 'M' + d.x + ',' + d.y + 'L' + d.x + ',' + d.y);

                                    // force element to be an top
                                    this.parentNode.parentNode.appendChild(this.parentNode);
                                    //d3.event.stopPropagation();
                                },
                                mouseover: function () {
                                    svg.select("rect.selection").empty() && d3.select(this).classed("hover", true);
                                },
                                mouseout: function () {
                                    svg.select("rect.selection").empty() && d3.select(this).classed("hover", false);
                                    //$( this).popover( "hide");
                                }
                            });

                    gState.append("circle")
                            .attr({
                                r: radius,
                                class: 'inner'
                            })
                            .on({
                                mouseover: function () {
                                    svg.select("rect.selection").empty() && d3.select(this).classed("hover", true);
                                },
                                mouseout: function () {
                                    svg.select("rect.selection").empty() && d3.select(this).classed("hover", false);
                                },
                            });
                };

                var startState, endState;
                var drag = d3.behavior.drag()
                        .on("drag", function (d, i) {
                            console.log("drag");
                            if (startState) {
                                return;
                            }

                            var selection = d3.selectAll('.selected');

                            // if dragged state is not in current selection
                            // mark it selected and deselect all others
                            if (selection[0].indexOf(this) == -1) {
                                selection.classed("selected", false);
                                selection = d3.select(this);
                                selection.classed("selected", true);
                            }

                            // move states
                            selection.attr("transform", function (d, i) {
                                d.x += d3.event.dx;
                                d.y += d3.event.dy;
                                return "translate(" + [d.x, d.y] + ")"
                            });

                            // move transistion points of each transition 
                            // where transition target is also in selection
                            var selectedStates = d3.selectAll('g.state.selected').data();
                            var affectedTransitions = selectedStates.reduce(function (array, state) {
                                return array.concat(state.transitions);
                            }, [])
                                    .filter(function (transition) {
                                        return selectedStates.indexOf(transition.target) != -1;
                                    });
                            affectedTransitions.forEach(function (transition) {
                                for (var i = transition.points.length - 1; i >= 0; i--) {
                                    var point = transition.points[i];
                                    point.x += d3.event.dx;
                                    point.y += d3.event.dy;
                                }
                            });

                            // reappend dragged element as last 
                            // so that its stays on top 
                            selection.each(function () {
                                this.parentNode.appendChild(this);
                            });

                            // refresh transition path
                            gTransitions.selectAll("path").attr('d', computeTransitionPath);

                            // refresh transition endpoints
                            gTransitions.selectAll("circle.endpoint").attr({
                                transform: transformTransitionEndpoints
                            });
                            // refresh transition points
                            gTransitions.selectAll("circle.point").attr({
                                transform: transformTransitionPoints
                            });

                            d3.event.sourceEvent.stopPropagation();
                        })
                        .on("dragend", function (d) {
                            console.log("dragend");
                            // needed by FF
                            drag_line.classed('hidden', true)
                                    .style('marker-end', '');

                            if (startState && endState) {
                                startState.transitions.push({label: "transition label 1", points: [], target: endState});
                                update();
                            }
                            startState = undefined;
                            d3.event.sourceEvent.stopPropagation();
                        });

                svg.on({
                    mousedown: function () {
                        console.log("mousedown", d3.event.target);
                        if (d3.event.target.tagName == 'svg') {
                            if (!d3.event.ctrlKey) {
                                d3.selectAll('g.selected').classed("selected", false);
                            }
                            var p = d3.mouse(this);
                        }
                    },
                    mousemove: function () {
                        var p = d3.mouse(this);
                            // update drag line
                            drag_line.attr('d', 'M' + startState.x + ',' + startState.y + 'L' + p[0] + ',' + p[1]);
                            var state = d3.select('g.state .inner.hover');
                            endState = (!state.empty() && state.data()[0]) || undefined;
                    },
                    mouseup: function () {
                        console.log("mouseup");
                        // remove temporary selection marker class
                        d3.selectAll('g.state.selection').classed("selection", false);
                    },
                    mouseout: function () 
                    {
                        if (!d3.event.relatedTarget || d3.event.relatedTarget.tagName == 'HTML') {
                            // remove temporary selection marker class
                            d3.selectAll('g.state.selection').classed("selection", false);
                        }
                    }
                });

                update();

                function update() {
                    gStates = gStates.data(states, function (d) {
                        return states.indexOf(d);
                    });
                    renderStates();

                    var _transitions = transitions();
                    gTransitions = gTransitions.data(_transitions, function (d) {
                        return _transitions.indexOf(d);
                    });
                    renderTransitions();
                }
                ;
            };

Upvotes: 1

Views: 148

Answers (1)

potatopeelings
potatopeelings

Reputation: 41065

I assume this is from http://bl.ocks.org/lgersman/5370827.


Background

states (=window.states) is an array of state objects (3 in your case). Each state object has a property transitions (which represents possible changes to other states from this state), which is an array.

Question 1

This uses the reduce, concat and map method of the Array prototype to build a function that returns an array of objects of the form { source: state, transition: transition } using the transition arrays inside the state array.

The 1st layer is pretty simple - just a function definition. You call it eventually using var _transitions = transition();

var transitions = function () {
    return ...
};

Note that each call returns the list based on the states / transitions that exist at the time the function is called.


The 2nd layer builds an array by concatenating array fragments from the 3rd layer. From the documentation (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce), reduce effectively gets a single value from an array.

In our case, that single value is a larger array built by concatenating array fragments. The 2nd parameter to the reduce function is the intial value (in this case an empty array)

return states.reduce(function (initial, state) {
    return initial.concat(
        ...
    );
}, []);

So we first pass in an empty array. The output of the 3rd layer (... in the section above) using the 1st element of states (i.e. states[0]) is concatenated to it to build a new array. This new array is then concatenated with the 2nd output of the 3rd layer (i.e. using states[1]) and so on


The 3rd layer is a simple map (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). For each transition array entry in the state, it returns an object of the form { source: state, transition: transition }, using this to build an array (which is used by the 2nd layer as we saw above)

state.transitions.map(function (transition) {
    return { source: state, transition: transition };
})

So, if we were to trace this for the "first" state and assuming you had 2 transition entries (your code has an empty array, but the original example inserts a couple of transitions), you'd get something like

[
    {
        source: <<"first" state object>>
        transition: <<transition1a of "first" state - from it's transition array, 1st element>>
    },
    {
        source: <<"first" state object>>
        transition: <<transition1b of "first" state - from it's transition array, 2nd element>>
    },
]

Carrying this up to the 2nd layer, you'd get something like this (assuming state "second" had 3 transitions emanating from it)

[
    {
        source: <<"first" state object>>
        transition: <<transition1a of "first" state - from it's transition array, 1st element>>
    },
    {
        source: <<"first" state object>>
        transition: <<transition1b of "first" state - from it's transition array, 2nd element>>
    },
    {
        source: <<"second" state object>>
        transition: <<transition2a of "second" state - from it's transition array, 1st element>>
    },
    {
        source: <<"second" state object>>
        transition: <<transition2b of "second" state - from it's transition array, 2nd element>>
    },
    {
        source: <<"second" state object>>
        transition: <<transition2c of "second" state - from it's transition array, 3rd element>>
    },
    ...
    ... and so on for all the states
]

And the 1st layer is effectively a function which does all the steps above when called.

Question 2

This effectively builds a d3 selection (see https://github.com/mbostock/d3/wiki/Selections) - the selection's d3 data comes from the output of the 1st question. The very end of your code has this link

gTransitions = gTransitions.data(_transitions, function (d) {
    return _transitions.indexOf(d);
});

_transitions being set by a call to transitions(); in the line just above that.

This d3 selection is then used as d3 selections normally are (with an enter() / exit()), to update the svg element DOM. If you search for gTransitions.enter() and gTransitions.exit() you can find the related bits of code that keep your svg DOM updated. Note that the enter() involves a number of steps (append a g, set it's class, attach behaviour, append a path to the g...)

The first time, the update() function is called takes care of syncing the DOM to the initial data (in your case since your transition properties are empty arrays, nothing is created).

Subsequently, DOM event handlers update the respective states's transition arrays and the update() function is called at the end of the handler to reattach updated data (i.e. output of the transition() call) and hence drive the creation / removal of DOM elements for the transitions (via a call to renderTransitions()) - these are effectively the svg paths between (state) svg circles

Upvotes: 2

Related Questions