Efficient approach to find position of a cell in a table loaded with itemArray

Hi,

Note: Docs for panel.findObject(name) says “This does not recurse into the elements inside a Panel that holds elements for an itemArray.”

Question: How can I find the location and dimensions of a sub-part in a diagram where that sub-part is loaded as an itemArray, given a known piece of data ? See example below which adds 6 cells into a table. What is most efficient process to find details of part with id=‘c2-3’ ?

See codepen: http://codepen.io/JEE42/pen/PPBWzw enable console.

var $ = go.GraphObject.make;  // for conciseness in defining templates
myDiagram =
  $(go.Diagram, "myDiagram",  // Diagram refers to its DIV HTML element by id
    {
});

// Add an event listener for the first draw of the diagram - not critical in this tiny case but worthwhile when many elements to be drawn
this.myDiagram.addDiagramListener("InitialLayoutCompleted",
function (e) {
  runTests()
})


var cellTemplate =   
    $(go.Panel, go.Panel.Auto, { margin: 0, stretch: go.GraphObject.Fill }
      , new go.Binding("row", "row")
      , new go.Binding("column", "column")

      , $(go.Shape, {strokeWidth: 0, stretch: go.GraphObject.Fill, fill: 'gold', width: 50, height: 50}

          ,new go.Binding("fill", "itemIndex",  // for fun and exploring use of itemIndex
                          function(i) { return (i%2 == 0) ? "lightgreen" : "lightyellow"; }).ofObject()  // bound to this Panel itself, not to the Panel.data item                          
         )
      , $(go.TextBlock, { font: "normal 10pt sans-serif", textAlign: "center"}
          , new go.Binding("text", "id")

         ) // end textblock
     );// end cell

var tableTemplate = 
    $(go.Node, "Auto",
      $(go.Shape, { fill: "white", stroke: "gray", strokeWidth: 0.5 })
      , $(go.Panel, "Table"
          , {  
      defaultRowSeparatorStroke: "gray"
      , defaultRowSeparatorStrokeWidth: 0.5
      , defaultColumnSeparatorStroke: "gray"
      , defaultColumnSeparatorStrokeWidth: 0.5
      , defaultAlignment: go.Spot.MiddleLeft 
      , defaultSeparatorPadding: 0
      , padding: 0 // around table
      , itemTemplate: cellTemplate} // points to above template for use in cells

          ,new go.Binding('itemArray', 'cells')  ) 		

     )// end table template

var templmap=new go.Map("string", go.Node);
templmap.add("table", tableTemplate);    
myDiagram.nodeTemplateMap=templmap    // link the map to the diagram

var nodeDataArray=[
  { key: "table1"
   , category: "table"
   , cells: [
     {id: 'c1-4', row: 1, column: 4, name: 'c1-4'}
     ,  {id: 'c1-1', row: 1, column: 1, name: 'c1-1'}
     ,  {id: 'c1-6', row: 1, column: 6, name: 'c1-6'}
     ,  {id: 'c2-2', row: 2, column: 2, name: 'c2-2'}
     ,  {id: 'c2-3', row: 2, column: 3, name: 'c2-3'}
     ,  {id: 'c2-6', row: 2, column: 6, name: 'c2-6'}
   ] 
  }
];

var linkDataArray=[ ];
myDiagram.model=new go.GraphLinksModel(nodeDataArray, []) // finally hand the initial node info to the diagram

// Function called after diagram drawn first time
function runTests() {
  console.clear()
  console.log("How to find a reference and get position & dimensions of id='c2-3'")
}

Give a name to the Panel whose Panel.itemArray is data bound. I’ll assume it’s name: "LIST".

Then you can use this function:

  function findItemPanelById(node, id) {
    var list = node.findObject("LIST");
    if (!list) return;
    var it = list.elements;
    while (it.next()) {
      var item = it.value;
      if (item.data && item.data.id === id) return item;
    }
    return null;
  }

Once you have the Panel that you want for the item data, you can call GraphObject.getDocumentPoint to get the position in document coordinates for any particular spot on the panel.

Thanks Walter, that all works and I have updated the CodePen to confirm. However, that solution is an iteration through the contents of the node. In my case the node is a spreadsheet-like grid where each cell is a separate panel and it is those panels that will be the items in the iteration. A 100 row by 100 column sheet would contain 10k panels and I am concerned about the time taken in iterating in such a case,

I was hoping for a map-based approach.

I have two alternatives I guess:

  1. Run the full iteration once and construct my own index / map;
  2. Creating the cell panels via code instead of a pre-prepared nodeData array and constructing my own map in that way, though that approach leaves me concerned about performance tradeoff in the code-based cell creation process.

Could you provide an opinion on those options ?

If I could make a feature request it would be to enhance findNodesByExample(examples) so that it includes sub-parts.

Thanks.

It is true that internally we have a Map associating item data objects with item Panels. I suppose it would make sense to make that accessible to programmers. We’ll consider it.

In this situation you are looking for Panels inside a Node, so we could not extend the functionality of Diagram.findNodesByExample, which returns a collection of Nodes.

What is it that you are really trying to do? Have you tried using a Table Panel with 10K panels in it? I’m not sure that that will work efficiently.

An alternative would be to use separate Parts and Table Layout, in the extensions folder: Table Layout (ignore the use of Groups and the drag-and-drop construction of the table). Not only would that allow you to use Diagram.findPartForData or Diagram.findPartForKey, but you would only be dealing with positions and sizes in document coordinates.

Thanks for that.

IMHO I think it would be worthwhile extending those find functions to include sub-parts of a node, if only for completeness.

I accept the mild concern about 10k panels. My case is the left hand grid in a gantt chart, so 10k cells is probably wildly extreme, but I like to plan for the future. I developed the example below to illustrate how to add 10k cells to a table then how to build a fast associative array of those cells.

Lessons learned:
1 - a table with 10k cells can take a few seconds to render;
2 - add the cells into the model before passing it to the diagram, because if you add them using diagram.model.addArrrayItem() there is a per-addition processing overhead which is a by-design, desirable and beneficial feature but, understandably, costly for a bulk process.

In case it helps others, here is some code that illustrates adding many panels into a node; use or binding ‘fill’ to ‘itemIndex’, finding a node via its name attr, hunting for a panel within a node matching part.id attr, and creating an associative array of panels then using to get a reference to a specific panel (cell in the table via row & col).

    var $ = go.GraphObject.make;  // for conciseness in defining templates
    myDiagram =
      $(go.Diagram, "myDiagram",  // Diagram refers to its DIV HTML element by id
        {
    });
    
    // Add an event listener for the first draw of the diagram - not critical in this tiny case but worthwhile when many elements to be drawn
    this.myDiagram.addDiagramListener("InitialLayoutCompleted",
    function (e) {
      runTests()
    })
    
    // All cells are made from this tempalte
    var cellTemplate =   
        $(go.Panel, go.Panel.Auto, { margin: 0, stretch: go.GraphObject.Fill }
          , new go.Binding("row", "row")
          , new go.Binding("column", "column")
          
          // shape is to give a pleasing background to the cells with an odd-even bcolor applied
          , $(go.Shape, {strokeWidth: 0, stretch: go.GraphObject.Fill, width: 50, height: 50}
              ,new go.Binding("fill", "itemIndex",  // for fun and exploring use of itemIndex
                              function(i) { return (i%2 == 0) ? "lightgreen" : "lightyellow"; }).ofObject()                            
             )  // end shape
          , $(go.TextBlock, { font: "normal 10pt sans-serif", textAlign: "center"}
              , new go.Binding("text", "id")
             ) // end textblock
         );// end cell
    
    // This is the node that is the container of the grid of cells. When searching for cells we search from here to find
    // the panel.Table then iterate the panels (cells) inside that.
    var tableTemplate = 
        $(go.Node, "Auto",
          $(go.Shape, { fill: "white", stroke: "gray", strokeWidth: 0.5 })
          , $(go.Panel, "Table"
              , {  
          name: 'LIST'
          , defaultRowSeparatorStroke: "gray"
          , defaultRowSeparatorStrokeWidth: 0.5
          , defaultColumnSeparatorStroke: "gray"
          , defaultColumnSeparatorStrokeWidth: 0.5
          , defaultAlignment: go.Spot.MiddleLeft 
          , defaultSeparatorPadding: 0
          , padding: 0 // around table
          , itemTemplate: cellTemplate} // points to above template for use in cells
    
              ,new go.Binding('itemArray', 'cells')  ) // tells gojs make one cell panel for each model.node.cells[] item
         )// end table template
    
    var templmap=new go.Map("string", go.Node);
    templmap.add("table", tableTemplate);    
    myDiagram.nodeTemplateMap=templmap    // link the map to the diagram
    
    var nodeDataArray=[
      { key: "table1"
       , category: "table"
       , id: 'table'
       , name: 'table'
       , cells: [] 
      }
    ];
    
    // Add 10k cells into the model - faster when done as a JS object, likely because there is no change-monitor processing overhead 
    //  as there is when using the gojs addArrayItem() function (which is sensible)
        for (var i = 0; i < 100; i=i+1) {
          for (var j = 0; j < 100; j=j+1) {
    
            var newCell =   {id: 'c' + i + '-' + j, row: i, column: j, name: 'c' + i + '-' + j}
            nodeDataArray[0].cells.push(newCell)      
    
          }
        }
    
    myDiagram.model=new go.GraphLinksModel(nodeDataArray, []) // finally hand the initial node info to the diagram
    
// This function called after diagram drawn first time via 'InitialLayoutCompleted' listener
function runTests() {
  
  console.clear()
 
  // find the table node
  var tbl = findTable("table1", "LIST")
  console.log('table=' + tbl)
  
// read the cell itemArray to make an associative map.
  console.log("Map grid panels into assoc array")
  var map=[]
  var a =   tbl.itemArray
  console.log('cell cnt=' + a.length)
    for (var i = 0; i < a.length; i=i+1) {
      var c = a[i]
      map[c.id] = c     
    }
    
// finally use the map to find a couple of specific nodes with row & cell coords, just to prove that we can.
  var mappedCell=map['c2-2']
  console.log('c2-2=' + mappedCell.id + ' at (' + mappedCell.row + ', ' + mappedCell.column +')')

  var mappedCell=map['c81-89']
  console.log('c81-89=' + mappedCell.id + ' at (' + mappedCell.row + ', ' + mappedCell.column +')')
    
}

// function to get the part that contains the cells of a table.
  function findTable(nodeName, partName) {
    var nd = myDiagram.findPartForKey(nodeName)
    var tbl = nd.findObject(partName)
    return tbl
  }

OK, we’ll add a Panel.findItemPanelForData method in version 1.6.

But that might not address your scenario, which might be to find all of the GraphObjects that are at a particular row/column cell in a Table Panel. Not only can there be multiple GraphObjects with the same row and column values, but other GraphObjects might span over that cell across rows and/or columns.

And you would still need your map associating identifiers with item objects. GoJS cannot make any such assumptions about the properties and structure of the item data.

Thanks, sounds like a useful extension.

I agree that finding a part in a given row & column is only the start of a story, however in my case now that I have an approach I can modify my model to contain sufficient attributes to identify the parts I need to focus on should there be multiple parts with same row & column values. That said, I appreciate that GoJS cannot make such assumptions as you point out, and I need to write some code if only to impress my boss ;-)

Thanks for your assistance - all very useful.

Addendum: I just realised that the solution code above only gets me a map to access the elements of the panel.itemArray - not what I actually wanted which was the diagram parts that are the items constructed for the itemArray members.

I therefore modified the map process to iterate the cell panels. I have a filter to reduce the mapped cell panels to those that I need. The basis of the iteration was copied from the docs entry on collections GoJS Collections -- Northwoods Software

var tbl = <get the panel that is the container of the cell panels>
var mapGantt=[] // the fast lookup map to hold references to cell panels.
for (var it = tbl.elements; it.next(); ) {
    var cell = it.value;  // elt is now a GraphObject that is an immediate child of the Panel

    if (cell instanceof go.Panel && cell.row === 2) { // filter for part type and row position. Adjust as required

        mapGantt[ cell.data.key] = cell // key to whatever value you have for a key: attribute you put into the originating model for this part .
    }
}