Forum Stats

  • 3,780,918 Users
  • 2,254,456 Discussions
  • 7,879,492 Comments

Discussions

animation-on-data-change not firing on replacement of 'Node' array in oj-diagram

JSmydo
JSmydo Member Posts: 88 Blue Ribbon

Hi

I have an oj-diagram setup in oracle apex but am having issues getting the animation working when adding new nodes. The data for the nodes of the diagram consists of nested json objects so I figured that stringifying the current node json , making the changes to the string and then parsing the updated string as a JSON object would be a good way to simply replace the observable array - it doesn't work however. At the bottom of this page you will see the assignment made to replace the node observable array:

nodes(JSON.parse(nodeStr));

The array for the 'links' in the diagram is different though, any changes to that were made using the js 'push' method as the structure is much simpler:

links.push(ciLinks["links"][i]);

This does work and the new links are drawn. Is replacing the full observable array for the nodes completely the wrong approach? Are changes only registered through individual changes to the existing array?

The html of the diagram is as follows:

<div id='diagram-container' style="min-height:100%">
        <oj-toolbar
        chroming="outlined"
        aria-controls="diagram-container2">
          <oj-button class="t-button" id="addNodeButton" on-oj-action="[[addNodeButtonClick]]">Add CI Nodes</oj-button>
      </oj-toolbar>
 
      <oj-diagram style="height:1000px" id='diagram-container2'
        animation-on-data-change='auto'
        animation-on-display='auto'
        node-data = '[[nodeDataProvider]]'
        link-data = '[[linkDataProvider]]'
        node-content.renderer = '[[nodeRendererFunc]]' 
        layout = '[[layoutFunc]]'
        max-zoom = '5.0'
        min-zoom = '0.1'
        zooming = 'auto'
        panning= 'auto'
        promoted-link-behavior = 'full'
        selection-mode = 'multiple'
        expanded = '{{expandedNodes}}'>
           
        <template slot="nodeTemplate" data-oj-as="node">
          <oj-diagram-node
            label='[[node.data.name]]'
            icon.width='130'
            icon.height='30'
            icon.shape='rectangle'
            icon.color='[[node.data.nodeColour]]'
            icon.border-radius='1px'
            icon.border-width='0.5'
            icon.border-color='#444444'
            short-desc='[["CI " + node.data.id]]'>
            show-disclosure='{{node.data.showDisclosure}}'
          </oj-diagram-node>
        </template>
        <template slot="linkTemplate" data-oj-as="link">
          <oj-diagram-link
          start-node="[[link.data.startNode]]" 
          end-node="[[link.data.endNode]]"
          start-connector-type="circle"
          end-connector-type="arrow"
          short-desc='[[link.data.startNodeName + " " + link.data.relType + " " + link.data.endNodeName]]'>
          </oj-diagram-link>
        </template>
      </oj-diagram>
     </div>


The constructor is:


class DiagramModel {
                constructor() {
                    
                    this.data = JSON.parse(pData.CINodesAndLinks);
                    this.nodeValues = ko.observableArray(this.data.nodes);
                    this.linkValues = ko.observableArray(this.data.links);
                    this.nodeDataProvider = new ArrayTreeDataProvider(this.nodeValues, {
                        keyAttributes: "id",
                        childrenAttribute: "nodes",
                    });
                    this.linkDataProvider = new ArrayDataProvider(this.linkValues, {
                        keyAttributes: "id",
                    });
                    this.expandedNodes = new ojknockout_keyset_1.ObservableKeySet().add(["Top Level CIs", "Non-Parent CIs"]);
                    debugger;
                    this.layoutFunc = layout.containerLayout;
                    /******************************************************************************************** */
                    this.addNodeButtonClick = () => {
                        
                        const nodes = this.data.nodes;
                        const links = this.data.links;
                       
                        let diagNodes = this.nodeValues;
                        let diagLinks = this.linkValues;


.....

diagLinks(links)
                               nodes(JSON.parse(nodeStr));
                                diagNodes(nodes);

Best Answer

  • Natalia Balatskova-Oracle
    Natalia Balatskova-Oracle Member Posts: 37 Employee
    Accepted Answer

    Hi John,

    Re: Is it right to expect that the diagram will transfer changes from the observable arrays into the this.nodeDataProvider, which effectively is the data for the diagram?

    It is the other way around. As you mutate data in nodeDataProvider, the provider detects the changes and triggers update events on diagram.

    It seems that since you have a top level observable only and your child providers are not observables, the change in the child items is not triggering an update.

    There are a couple of ways to deal with that.

    For example, instead of defining diagNodes as an observable array, you can define the nodeDataProvider as an observable object. Then you can just mutate the provider object when you get new nodes. The diagram will refresh.

    this.nodeDataProvider = ko.observable(new ArrayTreeDataProvider(this.data.nodes, {keyAttributes: "id",  childrenAttribute: "nodes"}));

    In your action button mutate nodeDataProvider.

    this.addNodeButtonClick = () => {
     ...
     //get the new nodes, parse to get an array
     const newNodes = JSON.parse(nodeStr);
     this.nodeDataProvider(new ArrayTreeDataProvider(newNodes, {  keyAttributes: "id", childrenAttribute: "nodes"}));
    }
    

    The other way is to create observable arrays on each level, but it does not seem to be worth it.

    Natalia

«1

Answers

  • JSmydo
    JSmydo Member Posts: 88 Blue Ribbon

    I'm hoping someone can steer me in the right direction here. To be clear, I'm attempting to update the diagram I outlined above by following the Cookbook 'Animations' example (in conjunction with the 'containers' example) where I am using ajax to pull data from an Oracle database and from within that ajax call, update the nodesValues obervable array via a reference variable (the this.nodevalues is out of scope from within the apex.server.process I'm using)

    (observable array constructor) - this.nodeValues = ko.observableArray(this.data.nodes);
    (reference arrays) - const nodes = this.data.nodes;
    let diagNodes = this.nodeValues;

    I can successfully update these arrays with the new data and I can then see that the nodes in the array carry the data within the 'itemData'. I assumed that the 'itemData' would be used to draw the nodes but the Cookbook example actually uses 'context.content' which does NOT reflect the new nodes. I think this 'context.content' comes from the

    this.nodeDataProvider = new ArrayTreeDataProvider(this.nodevalues, { keyAttributes: "id", childrenAttribute: "nodes",});

    which, is obviously built when the constructor was run. How can I push these new nodes seen in the 'itemData' into the ArrayTreeDataProvider so that the SVGs will appear in the 'context.content'? I attempted to update the this.nodeDataProvider via, again a reference array, and also included the 'nodeDataProvider.treeData.valueHasMutated()' call but it has no effect.

    I'd be really grateful if someone can provide some help here as this is my first attempt at using Jet outside the built in Apex widgets and I've spent a long, long time trying to get this to work.

    Thanks


  • Hi John,

    What you need to be updating is the ObservableArray (nodeValues).

    In this line

    this.nodeDataProvider = new ArrayTreeDataProvider(this.nodeValues, {
                            keyAttributes: "id",
                            childrenAttribute: "nodes",
                        });
    

    you are setting the data for the nodeDataProvider to be this.nodeValues, which is correct.

    Now, when you update this.data.nodes, update the observableArray with those new values.

    this.nodeValues(this.data.nodes);

    I believe that should work for you. If that doesn't work, let me know and I'll pull in one of the Diagram engineers to take a look at what you're doing.

  • JSmydo
    JSmydo Member Posts: 88 Blue Ribbon
    edited Dec 1, 2021 12:08AM

    Thanks John. Yep, this what I was effectively doing in here (I think):

     this.addNodeButtonClick = () => {
                            const nodes = this.data.nodes;
                            const links = this.data.links;
                        let diagNodes = this.nodeValues;
                            let diagLinks = this.linkValues;
    apex.server.process(
    ..blah
    }, datatype: "json"
    , success: function(newData){

    .....

    diagLinks(links)
                                   nodes(JSON.parse(nodeStr));
                                    diagNodes(nodes);

    the '.....' represents the code that was updating the 'nodes' and 'links' reference arrays, which seemed effective as, as I say, the diagNodes() nodes (following them through on the browser console) reflected the new data in the itemData attribute. This though, unfortunately, doesn't follow through into the 'context.context' in the following:

    this.nodeRendererFunc = (context) => {
                                    const color = context.data._itemData.selectedCI == 'Y' ? "red" : "#c7c7c7";
                                    const stroke = context.state.selected || context.state.hovered ? 3 : 1;
                                    let rootElement = context.rootElement;
                                    if (!rootElement) {
                                        // initial rendering - create an svg element with a node content in it
                                        const nodeId = context.data["id"];
                                        const fillColour = context.data._itemData.nodeColour;
                                        const imgClass = context.data._itemData.icon;
                                        if (context.state.expanded) {
                                            //render expanded node
                                            const childContent = context.content;
                                            // add 20 px for the each side padding and
                                            // additional 20 px on top for the header
                                            let w = childContent["w"] + 40,
                                                h = childContent["h"] + 60;
                                            rootElement = this.createSVG("nodeSvg" + nodeId, w, h);
                                            const group = this.addGroup(rootElement, "topGroup" + nodeId);
                                            (w -= 2), (h -= 2); //reduce width and height for inner elements
                                            this.addRect(group, "rect" + nodeId, "1", "1", w, h, "#f9f9f9");
                                            this.addRect(group, "rectHdr" + nodeId, "1", "1", w, "20", color, imgClass);
                                            this.addChildContent(group, childContent.element);


    so, I'm therefore assuming that the 'this.nodeDataProvider' is not updating.

    I did pursue a different tact however, which may be ok for me but it goes against the notion of the 'animation' aspect I was looking for and seems against the whole idea of setting a diagram up and then updating it:


     this.addNodeButtonClick = () => {
    apex.server.process(
                                            "SET_SESSION_STATE_PLACEHOLDER", {
                                                pageItems: '#P65_CI_ID_STR'
                                            }, {
                                                dataType: "text",
                                                success: function() {
                                                    ko.cleanNode(document.getElementById("diagram-container"));
                                                    initializeJetDiagram();
                                                }
                                            }
                                        );


    There's a fair chance I may be missing/misconstruing fundamental elements of this object oriented structure ( as an Oracle developer) but I'd appreciate if you or one of your team team could indicate where I'm going wrong here. The workaround I've shown does do the job by effectively redrawing the whole diagram with a fresh dataset but, for future efforts I'd like to have a little more control...Is the workaround very bad practice btw?

  • John 'JB' Brock-Oracle
    John 'JB' Brock-Oracle Posts: 2,710 Employee
    edited Dec 1, 2021 1:19AM

    That does seem a bit harsh. Did you try calling the .refresh() method on the oj-diagram element first?

    document.getElementById('myDiagram').refresh();

    It can get a little tricky when mixing in with APEX as we don't always know when something is happening on the server, or the client. JET can only be run from the client-side.

  • JSmydo
    JSmydo Member Posts: 88 Blue Ribbon

    I did indeed John, no joy. The 'this.nodeDataProvider' remained the same. It might be something very specific that I'm doing wrong somewhere, but the updates I made were all present in the itemData of the nodes but never represented. Curiously, one of the new nodes did actually appear but with no ID and with none of its children - the other new nodes that were pushed as children of existing nodes did not - this was prior to the 'refresh' attempt btw. I'm so at the mercy of the cookbook examples that I'm not sure which way is up...

  • Thanks for the update John,

    I'll ask one of the Diagram engineers to take a look at the scenario and see if they have any better advice for you.

    --jb

  • JSmydo
    JSmydo Member Posts: 88 Blue Ribbon
  • JSmydo
    JSmydo Member Posts: 88 Blue Ribbon

    Not heard from anyone John, so I'll put this out to anyone who might be able to help. I said that the

    ko.cleanNode(document.getElementById("diagram-container"));

    provided a solution for me and it does but only up to a point, when I'm attempting to clear out the container I'm using I'm getting (not always but usually when there are big data changes):

    'Uncaught Typerror: cannot read properties of null (reading 'removeChild')

    I've since read that using this method is not at all good practice and that using 'With' or 'template' bindings to control the adding or removal of data is the way to go. THe only thing is, I have no idea how to implement this in this oj-diagram context because I see nothing at all related to it in the cookbook. Would anyone have any clues at all for me?

  • Hi John,

    I've got someone internally looking at it. They will reply here shortly.

  • JSmydo
    JSmydo Member Posts: 88 Blue Ribbon

    Sure John, many thanks. Didn't want to appear impatient there