Понимание редактора диаграмм состояний

Из блога: http://bl.ocks.org/lgersman/5370827

Я хочу понять, как реализована линия соединения между кругами. Я пытался пройти через это, но это просто вылетело у меня из головы. Там не так много документации о примере, который я нашел в блоге. Я предполагаю, что другие новые пользователи, такие как я, столкнутся с той же проблемой.

Если кто-нибудь может объяснить приведенный ниже пример кода, это было бы здорово!

Вот код, который я минимизировал для своего требования:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>d3.js selection frame example</title>
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.js"></script>
        <link rel="stylesheet" href="<%=request.getContextPath()%>/app.css" />
        <script>
            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'
                        })
                        ;

             **// NEED EXPLANATION FROM HERE**
                var gTransitions = svg.append('g').selectAll("path.transition");
                var gStates = svg.append("g").selectAll("g.state");

                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();
                }
                ;
            };
        </script>
    </head>

    <body>
    </body>
</html>

CSS-файл:

rect.selection {
    stroke          : gray;
    stroke-dasharray: 2px;
    stroke-opacity  : 0.5;
    fill            : transparent;
}

g.state circle {
    stroke  : gray;            
}

g.state circle.inner {
    fill        : white;
    transition  : fill 0.5s;
    cursor      : move;
}

g.state circle.inner.hover,
g.state circle.outer.hover {
    fill : aliceblue;
}

g.state circle.outer.hover {
    stroke-width    : 1px;
}

g.state circle.outer {
    stroke-width    : 0px;
    stroke-dasharray: 2px;
    stroke-color    : gray;
    fill            : transparent;
    transition      : all 0.5s;
    cursor          : pointer;
}

g.state.selected circle.outer {
    stroke-width    : 1px;
}

g.state text {
    font                : 12px sans-serif;
    font-weight         : bold;
    pointer-events      : none;
}

g.transition path,
path.dragline {
    fill        : none;
    stroke      : gray;
    stroke-width: 1px;
}

g.transition path.foreground {
    marker-end  : url(#end-arrow);
}

g.transition.hover path.background {
    stroke-dasharray: none;
    stroke : aliceblue;
    stroke-opacity  : 1.0;
    transition : all 0.5s;
}

g.transition path.background {
    stroke-dasharray: none;
    stroke-width: 8px;
    stroke : transparent;
}

g.transition.selected path.foreground {
    stroke-dasharray: 2px;
    stroke-color    : gray;
}


g.transition path {
    cursor : default;
}

.end-arrow {
    fill            : gray;
    stroke-width    : 1px;
}

g.transition circle.endpoint {
    display         : none;
    fill            : none;
    cursor          : pointer;
    stroke          : gray;
    stroke-dasharray: 2px;
}

g.transition circle.point {
    display         : none;
    fill            : aliceblue;
    cursor          : move;
    stroke          : gray;
}

g.transition.selected circle.endpoint,
g.transition.selected circle.point {
    display        : inline;
    transition : all 0.5s;
}

g.transition:not( .selected).hover *,
path.dragline {
    display         : inline;

}


g.transition:not( .selected).hover {
    transition      : all 0.5s;
}

path.dragline {
    pointer-events: none;
    stroke-opacity  : 0.5;
    stroke-dasharray: 2px;
}

path.dragline.hidden {
    stroke-width: 0;
}

    /* disable text selection */
svg *::selection {
    background : transparent;
}

svg *::-moz-selection {
    background:transparent;
}

svg *::-webkit-selection {
    background:transparent;
}

Я понимаю основы d3, такие как добавление нового круга и поведение перетаскивания, но в основном часть переходов, используемая для рисования и соединения линий в круг, сдерживает меня.


person JCoder999    schedule 20.07.2015    source источник
comment
Есть ли какой-то конкретный раздел, который вам непонятен?   -  person Ian    schedule 20.07.2015


Ответы (1)


На самом деле там довольно много всего, и, глядя на это, на самом деле не используются типичные css transitions, как можно было бы ожидать. Я подытожу интересные части и расширим, если вам нужно. Интересный раздел следующий:

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();
});

Здесь происходит вся тяжелая работа. Этот код в основном говорит, что всякий раз, когда происходит событие drag (например, вы перемещаете круг), выполняйте код внутри этой функции.

Вы можете видеть, что это разбивается на разделы:

  1. Переместите точку, на которую нажали, с помощью d.x и d.y, что соответствует величине перетаскивания из предыдущего события. Это делается с помощью преобразования translate.
  2. Измените путь, это делается путем обновления параметра d, именно так вы указываете форму пути SVG. Код, вычисляющий путь, находится в функции comuteTransitionPath.
  3. Обновите круг.конечная точка — это скрытая точка на линии.
  4. Обновите круг.точку — это точки на линии, которые контролируют кривую, по умолчанию они скрыты.
person Ian    schedule 20.07.2015
comment
В приведенном выше коде вы говорите о рисовании линии или перетаскивании круга? - person JCoder999; 20.07.2015
comment
Особенно хотелось бы понять рисование линии между кругами и эту функцию var transitions = function () { return states.reduce(function (initial, state) { return initial.concat( state.transitions.map(function (transition) { return {source: state, transition: transition}; }) ); }, []); }; - person JCoder999; 20.07.2015
comment
@ Jcoder99 оба, хотя я не вдавался в подробности генерации пути - person Ian; 20.07.2015
comment
Я действительно застрял в вышеупомянутой функции в комментарии - person JCoder999; 20.07.2015
comment
В моем коде, как происходит этот выбор? svg.append('svg:path') и selectAll("path.transition") svg.path и path.transition не являются фигурами или тегами? - person JCoder999; 21.07.2015
comment
@ JCoder999: Постараюсь добавить еще немного к этому ответу позже. Кривая — это сплайн, и на каждом изгибе есть скрытая точка. - person Ian; 21.07.2015