ogobrecht / d3-force-apex-plugin

Oracle APEX Region Type Plugin: D3 Force Network Chart
https://ogobrecht.github.io/d3-force-apex-plugin
MIT License
24 stars 9 forks source link

Having trouble adding a new node without refreshing graph page #37

Closed Ignacius68 closed 5 years ago

Ignacius68 commented 5 years ago

Hi Ottmar,

I've been working to add new nodes to my graphs without refreshing the page, like I did already with links, but I can't seem to get it working as I want. Maybe you could point out what I might have wrong? I'm trying to add the new node in the center of the current visual field, by using the current transform values. The node isn't appearing where I set its x and y, however. Do I perhaps need to set the x and y after creating the node??

Global Declaration code, called when I have the node data captured and want to add it.

curGraph = null; nodeType = ""; updateBtn = "";

function setWorkingGraph() { // CURRENT_TAB is populated every time one of the tab controls for my graphs are clicked, setting this value to the current graph name. graphID = jQuery("#P60_CURRENT_TAB").val();

switch(graphID) {
    case 'SystemsGraph':
        curGraph = d3_force_SystemsGraph;
        nodeType = "System";
        updateBtn = "#P60_gotoAddSystem";
        break;
    case 'LayersGraph':
        curGraph = d3_force_LayersGraph;
        nodeType = "Layer";
        updateBtn = "#P60_gotoAddLayer";
        break;
    case 'ComponentsGraph':
        curGraph = d3_force_ComponentsGraph;
        nodeType = "Component";
        updateBtn = "#P60_gotoAddComponent";
        break;
    case 'OperationsGraph':
        curGraph = d3_force_OperationsGraph;
        nodeType = "Operation";
        updateBtn = "#P60_gotoAddOperation";
        break;
}  

}

function addNode() { // Use a delay to account for time to save the data to DB and close modal. setTimeout(function (){ if (curGraph === null) { setWorkingGraph(); } transform = curGraph.transform(); scale = transform.scale; width = curGraph.width(); height = curGraph.height(); // Calculate current center of visual field posX = Math.round(( -1 transform.translate[0] ) + ( 0.5 ( width/scale ) )); posY = Math.round(( -1 transform.translate[1] ) + ( 0.5 ( height/scale ) ));

    nodeID = jQuery("#P60_NODE_ID").val();
    colorVal = jQuery("#P60_COLOR_ID").val();
    colorLabel = jQuery("#P60_COLOR_LABEL").val();
    nodeLabel = jQuery('#P60_NODE_LABEL').val();

    curNodes = curGraph.nodes();
    curNodes.push(
        {
            ID: nodeID
            , SIZEVALUE: 20
            , COLORVALUE: colorVal
            , COLORLABEL: colorLabel
            , LABEL: nodeLabel
            , x: posX
            , y: posY
            , fixed: 1
        }
    );
    curGraph.start();
}, 750);   

}

I have a modal form that captures the node definition, saves it to DB, then populates the values back down to the parent graph page. Once that's done, then it calls the addNode() function. I'm not seeing any errors anywhere, but I'm not getting the node where I want it, either.

Help?

ogobrecht commented 5 years ago

Hi Ignacius,

I don't really understand why you do it in this way. You are working with Oracle APEX? If so, the graph supports the APEX dynamic action "Refresh" on its region. So, the easiest way would be to add the node to your data via an insert (you can do this also with a PL/SQL dynamic action) and then refresh simply the graph with a dynamic action, when your insert is done. If your query picks up the new node then the graph takes care about only pushing the new node to the DOM. Everything is then done without reloading the page. At the end is it the same as with any other report in APEX.

Hope this helps. Let me know, if I think in the wrong direction...

Best regards Ottmar

Ignacius68 commented 5 years ago

Hi Ottmar,

There are a couple of reasons I'm using this pattern. For one, I haven't gotten the Refresh action to work either on individual graph objects, or the entire region that contains my four graph tabs. The larger reason, however, is that the positions and other view settings are separated from the graph data itself, so that multiple views can be created as desired. In other words, the positions aren't tied to the nodes themselves, rather Views are saved with the entire set of positions, with the initially loaded graph data configured to not have any fixed positions.

With the refresh not working, and not wanting to reload the whole page without the positioning accounted for, you get the pattern I'm using. If I do any sort of reload, then the view, zoom and location I'm currently looking at are lost, as the graph reloads and re-centers. With this approach where I insert a new object and do a graph.start(), it is maintaining my view, zoom and current position, as well as not taking any time to reload anything via query/ajax.

The current design allows me to quickly "sketch" the broad generalities of a new solution, since I don't reload anything until the user officially decides to reload the page. Once I sketch it, then I can save the view with the new positions and I lose no work on moving nodes or finding the objects I was working with.

Back to the Refresh action, perhaps my problem there is the multiple graphs in tabs within the same region?

Hopefully that explains why I am where I am. :)

Ignacius68 commented 5 years ago

UPDATE: I have found out that the .start() method actually retrieves new objects. I think I am onto how to solve my UI need. Just insert the new data to DB, call .start(), then impose position on the one new node.

I need to work with this, and I'll likely be closing this issue. :)

ogobrecht commented 5 years ago

Hi Ignacius,

now I understand, why your are doing this. And since you told me that start retrieves new data I now know what your problem is: If you are in an APEX context than the start method is doing an implicit AJAX call to fetch the data with your defined query. You can suppress this by providing new data to the start method. You can also use the render method (this method does not to try to fetch data).

The nodes method is only for direct modifications of existing nodes in the DOM. If you want to add new nodes and rerender the graph, than you need to provide the new data to the start or render method.

An example (not tested, plugged together with your code):

var myData = exampleGraph.data();
myData.nodes.push({
  ID: nodeID,
  SIZEVALUE: 20,
  COLORVALUE: colorVal,
  COLORLABEL: colorLabel,
  LABEL: nodeLabel,
  x: posX,
  y: posY,
  fixed: 1
});
exampleGraph.render(myData);

You can play around here: https://apex.oracle.com/pls/apex/f?p=18290:4

The only difference is, that in the online playground the data is transformed into a string, because we need it in a textarea. The start and render methods can work with JSON or XML strings or a JSON object, which is the preferred format when working with pure JavaScript.

Sorry I was not aware that you are using the start method without providing data...

Hope this helps and you lost not too much time, sorry again, I had the wrong focus on your problem.

Best regards Ottmar

Ignacius68 commented 5 years ago

Hi Ottmar,

What I've suddenly noticed is that the .start() method seems to accomplish what I need, though I understand that it's retrieving everything. By using that, it loads the data, but does not write over the existing positions being used, nor does it reload/repaint the graph, which means it doesn't change position either. Unless a performance challenge happens as the system ages, the .start() method seems to do exactly what I need, and in fact removes the need to replicate some of the logic in javascript, to manually add objects to the graph with all their bells and whistles.

I'm experimenting some more, but I think the .start() method is my solution. I'm still saving all this other code, though, in case I need to revise for performance concerns. :)

Regards,

ogobrecht commented 5 years ago

Hi Ignacius,

you are now at the point to understand, why D3 is so fast. It does NOT remove all nodes from the DOM and recreates it on new data. Instead existing nodes will be updated, new nodes added and no more existing ones are deleted. This saves render time and you have no hickups on the DOM.

Sure, I had to do also some preprocessing on new data (especially for the already rendered positions), but at the end the start method is only existing to do all the APEX specific things and calls then the render method with the new data from APEX.

As already mentioned you can prevent the APEX data loading by providing data to the start method (or direct to the render method). You are able to load data from anywhere, also from textareas or whatever you need...

One question regarding the link labels: Are they now working as expected?

Best regards Ottmar

Ignacius68 commented 5 years ago

Hi Ottmar,

The link labels are mostly working as expected. The only issue that I haven't been able to diagnose yet, however, is that during one out of four loads the labels don't appear until you move one of the nodes the link is connected to. This seems to happen on short lines and long lines, so space doesn't seem to be the issue. I notice this after my load of positions, so this is after first render end and after the render end code loads the positions. My only supposition right now is that this seems to happen on a hidden graph (like the zoom and center failing). I have yet to notice the visible graph missing any of the labels.

I can hover and click the link labels and access the link object, so that's working as expected.

When I hover over a node, the lines for the links are highlighted, but the labels remain grey, which de-emphasizes the link labels. This is also working as intended.

I also like that the link lines are independent, even if there are two lines between the same nodes. Both directions function independently, so you see the label going both directions, making it easier to see when that situation is occurring.

Again, thanks so much for all the work!

Ignacius68 commented 5 years ago

Hi Ottmar,

I have found another issue. The zoomToFitOnForceEnd option does not seem to be updating the transform object of the graph after the zoom and center is complete. I found this by my code that's attempting to place a new node in the center of the current visual field. On a graph without that option turned on, that centering code works wonderfully. However, with the option turned on, the coordinates come out to the far left and down outside the visual field and then makes the graph zoom and center again.

Regards,

ogobrecht commented 5 years ago

Hi Ignacius,

I need some time to have a look at all your mentioned problems. I am currently switching my private dev environment...

I will come back to you. Best regards Ottmar

Ignacius68 commented 5 years ago

Hi Ottmar,

Would it help if I include some recordings of what I'm seeing? I'm also wondering if my inclusion of a "default" transform object with the initial configurations could have an effect.

Do let me know if I can help in any way with the troubleshooting/diagnosing.

Ignacius68 commented 5 years ago

Hi Ottmar,

I"ve done some more digging, and I can confirm that instances of "missing" labels only happen on hidden graphs, which places the visual issues I have seen in the camp of hidden graphs. Move a node even one pixel, though, and all the "missing" labels show up at once.

The only issue that isn't on hidden graphs is the zoomToFitOnForceEnd option not updating the transform object. Moving a node and causing a re-render seems to update the transform object, then, but still seems to ignore the new scale. The result of a re-render being that the next attempt at placing a node in the center of the visual field is very much closer, but still isn't accurate to the new scale.

Again, do let me know if I can assist! I'm so excited to be this close to perfection! :)

ogobrecht commented 5 years ago

Hi Ignacius,

first of all: Thank you very much for all the feature/beta testing 👍

I haven't had a look at the moment at the issues - but some thoughts might help you in advance: Please check if some of your problems on hidden graphs are gone, when you fire a resume or render cycle after one of the regions are activated. Please have a look at the following blog post, which shows possibilities to hook into the Region Display Selector changes: https://apexplained.wordpress.com/2016/05/02/working-with-region-display-selectors-in-apex-5-0/

For the issue with placing a node in the middle of the graph: The option zoomToFitOnForceEnd is updating the translate object - here the code behind the scenes:

If you set this option to true then this code is running on force end: https://ogobrecht.github.io/d3-force-apex-plugin/d3-force.js.html#line922

                // trigger force end event
                v.tools.log("Event forceend triggered.");
                v.tools.triggerApexEvent(document.querySelector("#" + v.dom.containerId),
                    "net_gobrechts_d3_force_forceend"
                );
                if (v.conf.zoomToFitOnForceEnd) {
                    graph.zoomToFit();
                }

...which is calling graph.zoomToFit: https://ogobrecht.github.io/d3-force-apex-plugin/d3-force.js.html#line4272

    graph.zoomToFit = function(duration) {
        var svg = {},
            graph_, padding = 10,
            x, y, scale;
        duration = (isNaN(duration) ? 500 : parseInt(duration));
        svg.width = v.tools.getGraphWidth();
        svg.height = v.tools.getGraphHeight();
        graph_ = v.dom.graph.node().getBBox();
        // If the graph is hidden we get 0 for width and height. zoom will then fail because
        // the calculation results in NaN for the translation (x, y) and infinity for the scale.
        if (graph_.width > 0 && graph_.height > 0) {
            scale = Math.min((svg.height - 2 * padding) / graph_.height,
                (svg.width - 2 * padding) / graph_.width);
            x = (svg.width - graph_.width * scale) / 2 - graph_.x * scale;
            y = (svg.height - graph_.height * scale) / 2 - graph_.y * scale;
            v.main.interpolateZoom([x, y], scale, duration);
        }
        return graph;
    };

...which is calling the v.main.interpolateZoom function: https://ogobrecht.github.io/d3-force-apex-plugin/d3-force.js.html#line964

        v.main.interpolateZoom = function(translate, scale, duration) {
            if (v.status.graphStarted) {
                if (scale < v.conf.minZoomFactor) {
                    scale = v.conf.minZoomFactor;
                } else if (scale > v.conf.maxZoomFactor) {
                    scale = v.conf.maxZoomFactor;
                }
                return d3.transition().duration(duration).tween("zoom", function() {
                    var iTranslate = d3.interpolate(v.main.zoom.translate(), translate),
                        iScale = d3.interpolate(v.main.zoom.scale(), scale);
                    return function(t) {
                        v.main.zoom
                            .scale(iScale(t))
                            .translate(iTranslate(t));
                        v.main.zoomed();
                    };
                });
            }
        }; 

...which is finally calling the generic v.main.zoomed function (which does set the v.conf.transform object, because this function is running on every zoom event, also when you trigger it by the mouse): https://ogobrecht.github.io/d3-force-apex-plugin/d3-force.js.html#line953

        v.main.zoomed = function() {
            v.conf.transform = {
                "translate": v.main.zoom.translate(),
                "scale": v.main.zoom.scale()
            };
            v.dom.graph.attr("transform", "translate(" + v.main.zoom.translate() + ")scale(" +
                v.main.zoom.scale() + ")");
            v.tools.writeConfObjectIntoWizard();
        };

So, maybe you can have a look at the first code snipped and check it together with your own code to calculate the graph center x and y position.

If you have a little bit more time then let me provide you a graph method for this - something like get_graph_center_position...

Hope this helps. Again - thank you so much for all your testing and valuable feedback :-)

Best regards Ottmar

Ignacius68 commented 5 years ago

Hi Ottmar!

It turns out I had code in there at one time to execute a render after each tab was clicked, just a simple jQuery click event method. That was back when the graphs had issues with the nodes being misplaced, which you fixed in release before last. For now, I can put similar code back in for the zoom to fit on hidden graphs. I'm thinking this "extra" render might also deal with the issue where some labels don't show on previously-hidden graphs, but then show up when you move a node with the graph visible.

I will take another look at the centering. Truthfully, I didn't go code diving to figure that out, I took a sample from Stack Overflow that seemed to do the job, just seems to have issues with the zoom to fit option turned on. I do have some time, so if you do wish to pursue a "get_center" method like that, it would be wonderful!

Always a pleasure

ogobrecht commented 5 years ago

Hi Ignacius,

I implemented two new helpers:

Both return an array with two entries (x and y position values). Here some example code, which I was running in the browser console for testing:

test = function (position) {
  example.inspect().dom.graph // align this to your graph variable name
    .append("svg:circle")
    .attr("class", "node")
    .attr("r", 10)
    .attr("cx", position[0])
    .attr("cy", position[1])
    .attr("fill","red");
};

test(example.centerPositionGraph());
test(example.centerPositionViewport());

And here the result to show the difference meaning (the right black node comes from the first test call and the left one from the second call):

center

The plugin is updated in the v3.1 branch - please test and let know if this fixes your center problem.

Hope this helps, best regards Ottmar

Ignacius68 commented 5 years ago

Hi Ottmar!

Sorry to be silent so long, but I had some distractions. First to say, AMAZING work, my new UI has achieved the "perfection" I desired! :)

Hidden Graph issues: I have updated to have a "first view" behavior for the hidden tabs, such that when a tab is viewed the first time, a .render() is fired. This has solved the zoomToFit centering AND the missing label issues on the hidden graphs. I would like to offer my vote that you could stop looking at this, unless you really wanted to. With an easy, well-performing solution, this is no longer in the range of a "show stopper."

Centering helpers: These are working wonderfully! I have updated my coding, and it's MUCH simpler, not to mention accurate. Not certain what I missed in the first attempt, but I no longer have to figure it out.

For me, I see no more issues with any of the recent additions, or the graph operation/performance in general. This version is ready for release status, if I may offer my opinion. :) I'm thinking my two lingering issues/questions can now be closed.

I look forward to hearing from you!

ogobrecht commented 5 years ago

Hi Ignacius,

these are good news. Thank you for all your testing and the valuable feedback.

I am currently on vacation. I will close the issues and release the new version when I am back.

Best regards Ottmar

Ignacius68 notifications@github.com schrieb am Di., 21. Mai 2019, 18:49:

Hi Ottmar!

Sorry to be silent so long, but I had some distractions. First to say, AMAZING work, my new UI has achieved the "perfection" I desired! :)

Hidden Graph issues: I have updated to have a "first view" behavior for the hidden tabs, such that when a tab is viewed the first time, a .render() is fired. This has solved the zoomToFit centering AND the missing label issues on the hidden graphs. I would like to offer my vote that you could stop looking at this, unless you really wanted to. With an easy, well-performing solution, this is no longer in the range of a "show stopper."

Centering helpers: These are working wonderfully! I have updated my coding, and it's MUCH simpler, not to mention accurate. Not certain what I missed in the first attempt, but I no longer have to figure it out.

For me, I see no more issues with any of the recent additions, or the graph operation/performance in general. This version is ready for release status, if I may offer my opinion. :) I'm thinking my two lingering issues/questions can now be closed.

I look forward to hearing from you!

  • Ignacius

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/ogobrecht/d3-force-apex-plugin/issues/37?email_source=notifications&email_token=ACPRIZMQMKB2EEBQNTH442TPWQR3NA5CNFSM4HD5BZF2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODV4QN3Q#issuecomment-494470894, or mute the thread https://github.com/notifications/unsubscribe-auth/ACPRIZNKUAMVJ2DURRCVJMDPWQR3NANCNFSM4HD5BZFQ .

ogobrecht commented 5 years ago

Hi Ignacius,

the new release is out. Therefore I close the ticket as discussed. Again: Thank you for your valuable feedback and the testing :-)

Best regards Ottmar