Skip to content Skip to sidebar Skip to footer

D3js Transforming Nested Group Images

I am working on [this][1] d3 project. Basically I am trying to create a SQL like query builder. I can drop boxes to the drawing area & other operators inside the box. Then I sh

Solution 1:

After all sort of troubles, I found my answer. Actually it is all about the coordinate system and where to put stuff and how to organize it. Once I fugured that out, Answer is pretty obvious.

<svg width="500" height="500" style="background-color: blue">


    <g id="draw">

        <rect class="container" height="500" width="500" x="0" y="0" style="fill:yellow"></rect>

        <line class="dummyLineOutsideQbox"></line>

        <g class="qbox" id="qbox">

            <line class="dummyLineInsideQbox"></line>


            <image x="10" y="10" class="container" initial-x="10" initial-y="10" xlink:href="images/query.png"
                   width="110"
                   height="110"></image>
            <circle class="left" id="qbox-left" initial-cx="10" initial-cy="65" cx="10" cy="65" r="5"
                    style="fill:red"></circle>
            <circle class="right" id="qbox-right" initial-cx="120" initial-cy="65" cx="120" cy="65" r="5"
                    style="fill:red"></circle>

            <g id="op1" class="op">
                <image class="opim" x="10" y="10" class="container" initial-x="10" initial-y="10"
                       xlink:href="images/filter.png" width="50"
                       height="50"></image>
                <circle id="op1-left" class="left" initial-cx="10" initial-cy="35" cx="10" cy="35" r="5"
                        style="fill:red"></circle>
                <circle id="op1-right" class="right" initial-cx="60" initial-cy="35" cx="60" cy="35" r="5"
                        style="fill:red"></circle>
            </g>

            <g id="op2" class="op">
                <image class="opim" x="60" y="60" initial-x="60" initial-y="60"
                       xlink:href="images/filter.png" width="50"
                       height="50"></image>
                <circle id="op2-left" class="left" initial-cx="60" initial-cy="85" cx="60" cy="85" r="5"
                        style="fill:red"></circle>
                <circle id="op2-right" class="right" initial-cx="110" initial-cy="85" cx="110" cy="85" r="5"
                        style="fill:red"></circle>
            </g>

        </g>

        <g class="qbox" id="qbox2">

            <line class="dummyLineInsideQbox"></line>

            <image x="110" y="110" class="container" initial-x="110" initial-y="110" xlink:href="images/query.png"
                   width="110"
                   height="110"></image>
            <circle class="left" id="qbox2-left" initial-cx="110" initial-cy="165" cx="110" cy="165" r="5"
                    style="fill:red"></circle>
            <circle class="right" id="qbox2-right" initial-cx="220" initial-cy="265" cx="220" cy="165" r="5"
                    style="fill:red"></circle>

            <g id="op3" class="op">
                <image class="opim" x="110" y="110" class="container" initial-x="110" initial-y="110"
                       xlink:href="images/filter.png" width="50"
                       height="50"></image>
                <circle id="op3-left" class="left" initial-cx="110" initial-cy="135" cx="110" cy="135" r="5"
                        style="fill:red"></circle>
                <circle id="op3-right" class="right" initial-cx="160" initial-cy="135" cx="160" cy="135" r="5"
                        style="fill:red"></circle>
            </g>
            <g id="op4" class="op">
                <image class="opim" x="160" y="160" initial-x="160" initial-y="160"
                       xlink:href="images/filter.png" width="50"
                       height="50"></image>
                <circle id="op4-left" class="left" initial-cx="160" initial-cy="185" cx="160" cy="185" r="5"
                        style="fill:red"></circle>
                <circle id="op4-right" class="right" initial-cx="210" initial-cy="185" cx="210" cy="185" r="5"
                        style="fill:red"></circle>
            </g>


        </g>


    </g>

</svg>

<script>
    var qBox = d3.selectAll('.qbox')
            .on('dblclick', function () {
                var g = d3.select(this);
                var scale = 'scale(1.2,1.2)';
                g.attr('transform', g.attr('transform') + ' ' + scale);
            });
    var opBox = d3.selectAll('.op');
    var circles = d3.selectAll('circle');


    var cDrag = d3.behavior.drag()

                    .on('dragstart', function () {
                        d3.event.sourceEvent.stopPropagation();
                    })
                    .on('drag', function () {

                        var thisCircle = d3.select(this);
                        var thisGroup = thisCircle.select(function () {
                            return this.parentNode;
                        });
                        var thisGroupTransform = d3.transform(thisGroup.attr('transform')).translate;

                        var thisGroupParent = d3.select(this).select(function () {
                            return this.parentNode;
                        }).select(function () {
                            return this.parentNode;
                        });

                        var thisGroupParentId = thisGroupParent.attr('id');
                        var thisGroupParentClass = thisGroupParent.attr('class');


                        if (thisGroupParentClass == 'qbox') {
                            dummyLine = d3.select('#' + thisGroupParentId).select('.dummyLineInsideQbox');
                        }
                        else {
                            dummyLine = d3.select('#' + thisGroupParentId).select('.dummyLineOutsideQbox');

                        }

                        console.log('dummyLine ', dummyLine.attr('class'));


                        dummyLine
                                .style('visibility', 'visible')
                                .style('stroke', 'red')
                                .style('stroke-width', '3px')
                                .attr('x1', Number(thisCircle.attr('cx')) + thisGroupTransform[0])
                                .attr('real-x1', thisCircle.attr('cx'))
                                .attr('y1', Number(thisCircle.attr('cy')) + thisGroupTransform[1])
                                .attr('real-y1', thisCircle.attr('cy'))
                                .attr('x2', d3.mouse(this)[0] + thisGroupTransform[0])
                                .attr('y2', d3.mouse(this)[1] + thisGroupTransform[1])
                                .attr('startGroup', thisGroup.attr('id'))
                                .attr('startCircleClass', thisCircle.attr('class'))
                        ;


                    })
                    .on('dragend', function () {


                        var thisCircle = d3.select('#' + circleID);
                        var thisCircleClass = thisCircle.attr('class');
                        var thisGroup = thisCircle.select(function () {
                            return this.parentNode;
                        });

                        var thisGroupTransform = d3.transform(thisGroup.attr('transform')).translate;

                        var thisGroupParent = d3.select(this).select(function () {
                            return this.parentNode;
                        }).select(function () {
                            return this.parentNode;
                        });

                        var thisGroupParentId = thisGroupParent.attr('id');
                        var thisGroupParentClass = thisGroupParent.attr('class');


                        var sourceCircleClass = dummyLine.attr('startCircleClass');
                        var sourceGroup = d3.select('#' + dummyLine.attr('startGroup'));

                        console.log('SOURCE GROUP :', sourceGroup.attr('id'));

                        var targetLineGroup;
                        var lineClass;

                        var t = [0, 0];
                        var x1, x2, y1, y2;

                        if (( sourceGroup.attr('class') == 'op') || (thisGroup.attr('class') == 'op')) {

                            if (sourceGroup.attr('class') == 'op') {
                                targetLineGroup = sourceGroup.select(function () {
                                    return this.parentNode;
                                });
//                                console.log('I am on line 185');
                            } else {
                                targetLineGroup = thisGroup.select(function () {
                                    return this.parentNode;
                                });
//                                console.log('I am on line 190');

                            }
                            lineClass = 'in';
                            x1 = startCircle.attr('cx');
                            y1 = startCircle.attr('cy');
                            x2 = thisCircle.attr('cx');
                            y2 = thisCircle.attr('cy');

                        }

                        if ((sourceGroup.attr('class') == 'qbox') && (thisGroup.attr('class') == 'qbox')) {

                            targetLineGroup = sourceGroup.select(function () {
                                return this.parentNode;
                            });
                            lineClass = 'out';

                            x1 = dummyLine.attr('x1');
                            x2 = dummyLine.attr('x2');
                            y1 = dummyLine.attr('y1');
                            y2 = dummyLine.attr('y2');
                        }

                        targetLineGroup
                                .append('line')
                                .attr('class', lineClass)
                                .attr('id', function () {
                                    return sourceCircleClass + '--' + sourceGroup.attr('id') + '__' + thisCircleClass + '--' + thisGroup.attr('id');
                                })
                                .attr('x1', dummyLine.attr('x1'))
                                .attr('real-x1', dummyLine.attr('x1'))
                                .attr('y1', dummyLine.attr('y1'))
                                .attr('real-y1', dummyLine.attr('y1'))
                                .attr('x2', dummyLine.attr('x2'))
                                .attr('real-x2', dummyLine.attr('x2'))
                                .attr('y2', dummyLine.attr('y2'))
                                .attr('real-y2', dummyLine.attr('y2'))
                                .attr('startGroup', dummyLine.attr('startGroup'))
                                .attr('endGroup', thisGroup.attr('id'))
                                .style('stroke', 'green')
                                .style('stroke-width', '3px')

                        ;
                        dummyLine.style('visibility', 'hidden');


                        console.log('DRAWING LINE ON : ', targetLineGroup.attr('id'))

                    })
            ;

    var svg = d3.select('svg').node();

    var drag = d3.behavior.drag()
                    .origin(function () {
                        var t = d3.transform(d3.select(this).attr("transform")).translate;
                        return {x: t[0], y: t[1]};
                    }).on('dragstart', function () {
                        d3.event.sourceEvent.stopPropagation();
                    }).on('drag', function () {

                        var g = d3.select(this);

                        var mouse = {dx: d3.event.x, dy: d3.event.y};
                        var currentObj = {
                            x: g.select('image').attr('x'),
                            y: g.select('image').attr('y'),
                            width: g.select('image').attr('width'),
                            height: g.select('image').attr('height')
                        };
                        var parentObj = {
                            x: (Number(g.select(function () {
                                return this.parentNode;
                            }).select('.container').attr('x'))), // + Number(d3.transform(parent.attr('transform')).translate[0])),
                            y: (Number(g.select(function () {
                                return this.parentNode;
                            }).select('.container').attr('y'))), // + Number(d3.transform(parent.attr('transform')).translate[1])),
                            width: g.select(function () {
                                return this.parentNode;
                            }).select('.container').attr('width'),
                            height: g.select(function () {
                                return this.parentNode;
                            }).select('.container').attr('height')
                        };


                        var loc = getXY(mouse, currentObj, parentObj);
                        d3.select(this).attr('transform', 'translate(' + loc.x + ',' + loc.y + ')');
//                        d3.select(this).attr('transform', 'translate(' + d3.event.x + ',' + d3.event.y + ')');

                        var thisGroupId = d3.select(this).attr('id');
                        var groupClass = d3.select(this).attr('class');

                        var thisGroupClass = d3.select(this).attr('class');
                        var tLineClass;
                        if (thisGroupClass == 'qbox') {
                            tLineClass = 'out'
                        } else {
                            tLineClass = 'in'
                        }

                        d3.selectAll('line.' + tLineClass)[0].forEach(function (e1) {

                            var line = d3.select(e1);

                            var lineId = line.attr('id');




                            var lineStartGroup = lineId.split('__')[0].split('--')[1];
                            var lineStartSide = lineId.split('__')[0].split('--')[0];
                            var lineEndGroup = lineId.split('__')[1].split('--')[1];
                            var lineEndSide = lineId.split('__')[1].split('--')[0];

                            console.log('-------------------');
                            console.log(lineId);
                            console.log(lineStartGroup);
                            console.log(lineEndGroup);
                            console.log('-------------------');

                            var c;
                            var ctm;

                            if (thisGroupId == lineStartGroup) {
                                var t = d3.transform(d3.select('#' + thisGroupId).attr('transform')).translate;

                                if (lineStartSide == 'left') {
                                    c = d3.select('#' + lineStartGroup).select('.left');
                                    ctm = c.node().getCTM();
                                } else {
                                    c = d3.select('#' + lineStartGroup).select('.right');
                                    ctm = c.node().getCTM();
                                }

                                var x = d3.transform(c.select(function () {
                                    return this.parentNode;
                                }).attr('transform')).translate;


                                console.log('START e'+ctm['e']);
                                console.log('START ex'+x[0]);
                                console.log('START f'+ctm['f']);
                                console.log('START fx'+x[0]);
                                line.attr('x1', Number(c.attr('cx')) + Number(x[0]));
                                line.attr('y1', Number(c.attr('cy')) + Number(x[1]));
                            }

                            if (thisGroupId == lineEndGroup) {
//                                var t = d3.transform(d3.select('#' + thisGroupId).attr('transform')).translate;
//
//                                line.attr('x2', Number(line.attr('real-x2')) + Number(t[0]));
//                                line.attr('y2', Number(line.attr('real-y2')) + Number(t[1]));

                                if (lineEndSide == 'left') {
                                    c = d3.select('#' + lineEndGroup).select('.left');

                                } else {
                                    c = d3.select('#' + lineEndGroup).select('.right');

                                }

                                var x = d3.transform(c.select(function () {
                                    return this.parentNode;
                                }).attr('transform')).translate;

                                ctm = c.node().getCTM();
                                console.log('END circleId'+c.attr('id'));

                                line.attr('x2', Number(c.attr('cx')) + Number(x[0]));
                                line.attr('y2', Number(c.attr('cy')) + Number(x[1]));

                            }


                        });


                    })

            ;

    opBox.call(drag);
    qBox.call(drag);
    circles.call(cDrag);

    var circleID;

    var dummyLine;

    circles.on('mouseover', function () {
        circleID = d3.select(this).attr('id');
    }).on('mouseout', function () {
        circleID = null;
    })

function getXY(mouse, current, parent) {

    var obj = {
        x: 0,
        y: 0
    };

    var dx = mouse.dx;
    var dy = mouse.dy;

    obj.x = dx;
    obj.y = dy;

    var xGap = current.x - parent.x;
    var yGap = current.y - parent.y;

    if (dx < 0) {
        if ((dx + xGap) < 0) {
            obj.x = -1 * xGap + 10;
        }
    } else {
        if ((dx + xGap) > parent.width) {
            obj.x = parent.width - xGap - current.width - 10;
        }
    }

    if (dy < 0) {
        if ((dy + yGap) < 0) {
            obj.y = -1 * yGap + 10;
        }

    } else {
        if ((dy + yGap) > parent.height) {
            obj.y = parent.height - yGap - current.height - 10;
        }
    }


    return obj;
}

</script>

Solution 2:

From my experience developing visual editors, I can say that relative positions (such as x,y in Operator) can and shoud be managed as data.

Try changing that data (and not directly the x,y attributes of the svg element), and binding the data in a d3js way. It will be much more idiomatic and fast.

PS: I know about separation of model and view, and I know about the bad side of storing visual properties along the model, but if it will be only a view for that model, it is for sure the best approach.


Post a Comment for "D3js Transforming Nested Group Images"