Search code examples
javascriptchartsgoogle-visualization

Google Charts Event listener - first click after filter fails


I have a table which behaves correctly until I use the Category Picker.

Correct Behavior:

  1. Filter for a "Gender"
  2. Click a row in the table - receive a result
  3. Sort any column like gender in the table
  4. Click on a row in the table - receive a result

Problematic Behavior: I would like to be able to select a row and receive a result without having to sort the gender column first.

  1. Filter for a "Gender"
  2. Click on a row in the table - FAIL - No results returned
  3. Sort any column like gender in the table
  4. Click on a row in the table - SUCCESS - you now get a result

How can I get success without having to sort a column first?

Grazie mille!

   google.charts.load('current', {
        'packages': ['corechart', 'table', 'gauge', 'controls', 'charteditor']
    });

    $(document).ready(function () {
        //console.log("ready!");
        renderChart_onPageLoad();
    });

    function renderChart_onPageLoad() {
        google.charts.setOnLoadCallback(function () {
            //console.log("renderChart_onPageLoad");
            drawDashboard();
        });
    }

    function drawDashboard() {
        //console.log("drawDashboard");

        var data = google.visualization.arrayToDataTable([
            ['Name', 'RoolNumber', 'Gender', 'Age', 'DonutsEaten'],
            ['Michael', 1, 'Male', 12, 5],
            ['Elisa', 2, 'Female', 20, 7],
            ['Robert', 3, 'Male', 7, 3],
            ['John', 4, 'Male', 54, 2],
            ['Jessica', 5, 'Female', 22, 6],
            ['Aaron', 6, 'Male', 3, 1],
            ['Margareth', 7, 'Female', 42, 8],
            ['Miranda', 8, 'Female', 33, 6]
        ]);

        var dashboard = new google.visualization.Dashboard(document.getElementById('dashboard'));

        var categoryPicker = new google.visualization.ControlWrapper({
            controlType: 'CategoryFilter',
            containerId: 'categoryPicker',
            options: {
                filterColumnLabel: 'Gender',
                ui: {
                    labelStacking: 'vertical',
                    allowTyping: false,
                    allowMultiple: false
                }
            }
        });

        var proxyTable = new google.visualization.ChartWrapper({
            chartType: 'Table',
            containerId: 'div_proxyTable',
            options: {
                width: '500px'
            }
        });

        var table = new google.visualization.ChartWrapper({
            chartType: 'Table',
            containerId: 'div_table',
            options: {
                sort: 'event', // <-- set sort to 'event' for table totaling
                width: '500px',
                allowHtml: true,
                page: 'enable',
                pageSize: '5',
            }
        });

        //This json contains my settings for later
        let json = {
            "tableChart": {
                "hasTable": true,
                "dataView": {
                    "columns": [
                        { "id": "Name" },
                        { "id": "RoolNumber" },
                        { "id": "Gender" },
                        { "id": "Age" },
                        { "id": "DonutsEaten" }
                    ]
                },
                "groupView": {
                    "hasGroupView": false                    
                },
                "totalRow": {
                    "hasTotalRow": true,
                    "labelCol": "Total",
                    "labelColIndex": 0,//This is the column where the words "Grand Total" are stored.  It must be text column.
                    "totalColumns": [
                        { "id": "RoolNumber", "type": "number", "function": "sum" }                       
                    ]
                },
                "conditionalFormat": {
                    "hasConditionalFormat": false
                },
                "options": {},
                "hasCSV": false,
                "clickGetFunc": null
            },
        };

        google.visualization.events.addOneTimeListener(proxyTable, 'ready', function () {
            console.log(".addOneTimeListener(proxyTable, 'ready' - sort");

            google.visualization.events.addOneTimeListener(table, 'ready', function () {
                console.log(".addOneTimeListener(table, 'ready' - sort");

                //#region table - sort: 'event'
                google.visualization.events.addListener(table.getChart(), 'sort', function (sender) {
                    console.log(".addListener(table.getChart(), 'sort' - sorted");

                    //// update table --> options must include (sort: 'event') for total row to work properly
                    //// update var grandTotal = tableData.getFilteredRows([{column: 0,
                    //// update .draw() 'table' references to new name if using for a different table variable

                    // sort data table according to sort properties
                    var tableData = table.getDataTable();
                    var sortIndexes = tableData.getSortedRows({
                        column: sender.column,
                        desc: !sender.ascending
                    });

                    //#region reposition total row - if required
                    let totalRow = (json.tableChart.totalRow !== undefined) ? json.tableChart.totalRow : [];
                    let hasTotalRow = totalRow.hasTotalRow;
                    if (hasTotalRow) {
                        // find grand total row
                        var grandTotal = tableData.getFilteredRows([{
                            column: json.tableChart.totalRow.labelColIndex, //must be placed in a column which is of type string.
                            value: json.tableChart.totalRow.labelCol
                        }]);
                        if (grandTotal.length > 0) {
                            // find grand total in sort
                            var grandTotalSort = sortIndexes.indexOf(grandTotal[0]);

                            // remove grand total from sort
                            sortIndexes.splice(grandTotalSort, 1);

                            // add grand total as first index
                            sortIndexes.unshift(grandTotal[0]);
                        }
                    }
                    //#endregion

                    // set table sort arrow
                    table.setOption('sortAscending', sender.ascending);
                    table.setOption('sortColumn', sender.column);

                    // set table view & re-draw table
                    table.setView({ rows: sortIndexes });
                    table.draw();

                    //Table ready then fires
                });
                //#endregion

            });
        });

        google.visualization.events.addOneTimeListener(proxyTable, 'ready', function () {
            console.log(".addOneTimeListener(proxyTable, 'ready' - select");

            google.visualization.events.addOneTimeListener(table, 'ready', function () {
                console.log(".addOneTimeListener(table, 'ready' - select");
                test_gcharts_selectedRowCol(table, json.tableChart.clickGetFunc);//(wrapperName, callback)
            });
        });

        google.visualization.events.addListener(proxyTable, 'ready', function () {
            console.log(".addListener(proxyTable, 'ready' - redrawTable()");
            redrawTable(json.tableChart);
        });

        dashboard.bind([categoryPicker], [proxyTable]);
        dashboard.draw(data);


        //This is a table builder which uses json from above.  This is working ok.  Contains no listeners.
        function redrawTable(tableChart) {
            console.log('redrawTable()');

            // set defaults for any undefined settings
            let dataView = (tableChart.dataView !== undefined) ? tableChart.dataView : [];
            let groupView = (tableChart.groupView !== undefined) ? tableChart.groupView : [];
            let totalRow = (tableChart.totalRow !== undefined) ? tableChart.totalRow : [];
            let conditionalFormat = (tableChart.conditionalFormat !== undefined) ? tableChart.conditionalFormat : [];

            // update .draw() 'table' or 'chart' references when using a different or additional chart name

            var sourceData = proxyTable.getDataTable().toDataTable().clone();
            //console.log('sourceData', sourceData);

            //#region create data view - this is used as basis for dataResults
            let view = new google.visualization.DataView(sourceData);

            //#region create group view - if required
            let dataResults_forTable;
            let hasGroupView = groupView.hasGroupView;
            if (hasGroupView) {

                // create keys for grouping
                const groupKeys = [];
                for (let i = 0; i < groupView.keys.length; i++) {
                    groupKeys.push(
                        groupKey_default(view, groupView.keys[i]),
                    );
                };
                // create columns for aggregating
                const groupColumns = [];
                for (let i = 0; i < groupView.columns.length; i++) {
                    groupColumns.push(
                        groupColumn_default(view, groupView.columns[i]),
                    );
                };

                // create data aggregation
                let group = google.visualization.data.group(view, groupKeys, groupColumns);
                //console.log('group'); console.log(group);

                dataResults_forTable = group.clone();
            }
            else {
                dataResults_forTable = view.toDataTable().clone();
            }//END if (hasGroupView) {
            //console.log('dataResults_forTable', dataResults_forTable);

            //#endregion

            //#region create total row - if required
            let hasTotalRow = totalRow.hasTotalRow;
            if (hasTotalRow) {

                let labelCol = totalRow.labelCol;
                let labelColIndex = totalRow.labelColIndex;
                let totalColumns = totalRow.totalColumns;

                //Create groupColumns for total row aggregation calculations
                const groupColumns = [];
                for (let i = 0; i < totalColumns.length; i++) {
                    const column = { column: dataResults_forTable.getColumnIndex(totalColumns[i].id), type: 'number' }
                    switch (totalColumns[i].function) {
                        case 'sum': column.aggregation = google.visualization.data.sum; break;
                        case 'count': column.aggregation = google.visualization.data.count; break;
                        case 'average': column.aggregation = google.visualization.data.avg; break;
                        case 'min': column.aggregation = google.visualization.data.min; break;
                        case 'max': column.aggregation = google.visualization.data.avg; break;
                        default: column.aggregation = google.visualization.data.sum;
                    }
                    groupColumns.push(column);
                }

                let groupTotal = google.visualization.data.group(dataResults_forTable,
                    // need key column to group on, so we want all rows grouped into 1, then it needs a constant value
                    [{ column: 0, type: "number", modifier: function () { return 1; } }], groupColumns);

                // this code block will run if the filter results in rows available to total.  Otherwise the table will present no rows.
                if (groupTotal.getNumberOfRows() !== 0) {

                    let formatDecimal = new google.visualization.NumberFormat({ pattern: '#,###.##' });
                    for (let i = 1; i < groupTotal.getNumberOfColumns(); i++) { formatDecimal.format(groupTotal, i); }

                    // create Grand Total row from colToTotal and groupTotal
                    const gtRow = [];
                    for (let i = 0; i < dataResults_forTable.getNumberOfColumns(); i++) {
                        //Build GT Row to match length of dataResults_forTable
                        gtRow.push(null);
                    }

                    //Push words "Grand Total" into it's set position in gtRow - It must go into a column of type string.
                    gtRow[labelColIndex] = labelCol;

                    for (let i = 0; i < totalColumns.length; i++) {
                        //Loop through groupColumns, test setting type for string.

                        if (totalColumns[i].type === 'string') {
                            //Convert to number from groupTotal result to string to match the column it's being pushed into
                            gtRow[dataResults_forTable.getColumnIndex(totalColumns[i].id)] = String(groupTotal.getValue(0, i + 1));
                        }
                        else {
                            //Otherwise push in the number value
                            gtRow[dataResults_forTable.getColumnIndex(totalColumns[i].id)] = groupTotal.getValue(0, i + 1);
                        }
                    }
                    //console.log('insertRow', insertRow);

                    // insert complete gtRow with values into row position 0
                    dataResults_forTable.insertRows(0, [gtRow]);

                    // add formatting for grand total row to highlight && justify to right if of type number
                    for (let j = 0; j < dataResults_forTable.getNumberOfColumns(); j++) {
                        //if statement on column type for left right justification
                        if (dataResults_forTable.getColumnType(j) === 'number') {
                            dataResults_forTable.setColumnProperty(j, 'className', 'googleTableTextRight');
                            dataResults_forTable.setProperty(0, j, 'className', 'googleTableTotalRow googleTableTextRight');//stored in css file
                        } else {
                            dataResults_forTable.setProperty(0, j, 'className', 'googleTableTotalRow');//stored in css file
                        }
                    }
                    //console.log('dataResults_forTable', dataResults_forTable);
                }//END (groupTotal.getNumberOfRows() !== 0) {
            }//END if (hasTotalRow) {
            //#endregion

            //#region conditional formatting - if required
            let hasConditionalFormat = conditionalFormat.hasConditionalFormat;
            if (hasConditionalFormat) {
                dataResults_forTable = conditionalFormatting_default(dataResults_forTable, conditionalFormat);
            }//END if (hasConditionalFormat) {
            //#endregion

            var finalView_forTable = new google.visualization.DataView(dataResults_forTable);
            //console.log('finalView_forTable', finalView_forTable);

            // set reset sorting, set dataTable & draw chart
            table.setView(null); // reset in case sorting has been used via user click
            table.setDataTable(finalView_forTable);//includes any total row
            table.draw();

        }//END redrawChart()

    }

    function test_gcharts_selectedRowCol(wrapperName, callback) {
        //console.log('wrapperName', wrapperName); console.log('callback', callback)

        //NEW - Works with paging active
        // initialize page number and size
        var page = 0;
        var pageSize = 10;
        if (wrapperName.getOption('page') === 'enable') {
            page = wrapperName.getOption('startPage');
            pageSize = wrapperName.getOption('pageSize');
        }
        test_enableCoordinates(callback);

        // page event
        google.visualization.events.addListener(wrapperName.getChart(), 'page', function (sender) {
            console.log(".addListener(wrapperName.getChart(), 'page' - paged - test_enableCoordinates called");
            page = sender.page; // save current page
            test_enableCoordinates(callback);
        });

        // sort event
        google.visualization.events.addListener(wrapperName.getChart(), 'sort', function () {
            console.log(".addListener(wrapperName.getChart(), 'sort' - sorted - test_enableCoordinates called ")
            page = 0; // reset back to first page
            test_enableCoordinates(callback);
        });

        function test_enableCoordinates(callback) {
            //console.log('callback enableCoordinates', callback);

            //remove event listeners
            var container = document.getElementById(wrapperName.getContainerId());
            Array.prototype.forEach.call(container.getElementsByTagName('td'), function (cell) {
                cell.removeEventListener("click", test_selectCell, false);
            });

            //add event listeners
            var container = document.getElementById(wrapperName.getContainerId());
            Array.prototype.forEach.call(container.getElementsByTagName('td'), function (cell) {
                cell.addEventListener('click', test_selectCell, false);
            });
        }

        function test_selectCell(sender) {

            var cell = sender.target;
            var row = cell.closest('tr');

            var wrapperDataTable = wrapperName.getDataTable();

            var selectedRow = row.rowIndex - 1; // adjust for header row (-1)
            selectedRow = (page * pageSize) + selectedRow;  // adjust for page number

            // Original from whitehat - save sorted info
            // This version does not work after a user clicks a column to sort
            //var sortInfo = wrapperName.getChart().getSortInfo();
            //if (sortInfo.sortedIndexes !== null) {
            //    selectedRow = sortInfo.sortedIndexes[selectedRow];
            //}

            // Replaced code to which finds the view row order by .getView then taking the selected row
            // and returning the number which is in the .getView result
            var sortInfo = wrapperName.getView();  // save sorted info
            if (sortInfo !== null) {
                selectedRow = sortInfo.rows[selectedRow];
            }
            var selectedCol = cell.cellIndex;

            //var result = "selectedRow: " + selectedRow + " selectedCol: " + selectedCol;
            //var ul = document.getElementById("demo");
            //var li = document.createElement("li");
            //li.innerHTML = result;
            //ul.appendChild(li);

            var selectedValue = wrapperDataTable.getValue(selectedRow, selectedCol);
            var colID = wrapperDataTable.getColumnId(selectedCol);
            var colLabel = wrapperDataTable.getColumnLabel(selectedCol);

            var result = //removed array brackets
            {
                "selectedRow": selectedRow,
                "selectedCol": selectedCol,
                "selectedValue": selectedValue,
                "colID": colID,
                "colLabel": colLabel
            };

            //callback(result, wrapperDataTable);
            console.log('test_selectCell', result);
            return result;
        }
    }
<script src="https://www.gstatic.com/charts/loader.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js"></script>

<div id="dashboard">
    <div id="categoryPicker"></div><br />
    Proxy Table<br />
    <div id="div_proxyTable" style="display:none;"></div><br />
    Table<br />
    <div id="div_table"></div><br /><br />
</div>

================MULTIPLE DASHBOARDS SCENARIO=====================

The solution provided works for a single drawDashboard() scenerio. However it breaks down in a multiple drawDashboard() scenerio which I would like to discuss next.

I will be adding additional drawDashboard() functions to this page.
They also require the same functionality and remain independent from each other.

Is there a way to accommodate multiple drawDashboard() function without global variables?

Do I move listenerPage and listenerSort inside a specific drawDashboard() function to self contain?

I wish to maintain only one library function test_gcharts_selectedRowCol() and not duplicate it for a additional drawDashboard() additions?

https://jsfiddle.net/cmill/pvj23nfk/

On run, drawDashboard_A() begins with listenerPage and listenerSort = null. It will then execute the .addListener routine for each.

Then as drawDashboard_B() runs listenerPage and listenerSort are no longer null but have been set by the pass made by drawDashboard_A().

The sorting and click events work perfectly and as needed and return results.

Bad Behavior:

  1. Click page 2 in drawDashboard_A()

  2. Click a row - FAIL - no result.

  3. click page 2 in drawDashboard_B()

  4. click a row - PASS - receive a result.


Solution

  • to correct the issue with the table cell select event,
    use addListener instead of addOneTimeListener, here...

        google.visualization.events.addOneTimeListener(proxyTable, 'ready', function () {
            console.log(".addOneTimeListener(proxyTable, 'ready' - select");
    
       // USE addListener HERE
    
            google.visualization.events.addListener(table, 'ready', function () {
                console.log(".addOneTimeListener(table, 'ready' - select");
                test_gcharts_selectedRowCol(table, json.tableChart.clickGetFunc);//(wrapperName, callback)
            });
        });
    

    then to ensure we don't get multiple sort and page events,
    save a reference to the listener handles when adding,
    and remove before adding again.

    we can use an object to store the handlers,
    and the container id of the chart wrapper as the key...

    // save handles to event listeners
    var listenerPage = {};
    var listenerSort = {};
    
    function test_gcharts_selectedRowCol(wrapperName, callback) {
      //console.log('wrapperName', wrapperName); console.log('callback', callback)
    
      //NEW - Works with paging active
      // initialize page number and size
      var page = 0;
      var pageSize = 10;
      if (wrapperName.getOption('page') === 'enable') {
        page = wrapperName.getOption('startPage');
        pageSize = wrapperName.getOption('pageSize');
      }
      test_enableCoordinates(callback);
    
      // remove previous event handlers
      if (listenerPage.hasOwnProperty(wrapperName.getContainerId())) {
        google.visualization.events.removeListener(listenerPage[wrapperName.getContainerId()]);
        delete listenerPage[wrapperName.getContainerId()];
      }
      if (listenerSort.hasOwnProperty(wrapperName.getContainerId())) {
        google.visualization.events.removeListener(listenerSort[wrapperName.getContainerId()]);
        delete listenerSort[wrapperName.getContainerId()];
      }
    
      // page event
      listenerPage[wrapperName.getContainerId()] = google.visualization.events.addListener(wrapperName.getChart(), 'page', function(sender) {
        console.log(".addListener(wrapperName.getChart(), 'page' - paged - test_enableCoordinates called");
        page = sender.page; // save current page
        test_enableCoordinates(callback);
      });
    
      // sort event
      listenerSort[wrapperName.getContainerId()] = google.visualization.events.addListener(wrapperName.getChart(), 'sort', function() {
        console.log(".addListener(wrapperName.getChart(), 'sort' - sorted - test_enableCoordinates called ")
        page = 0; // reset back to first page
        test_enableCoordinates(callback);
      });
    

    see following working snippet...

    google.charts.load('current', {
      'packages': ['corechart', 'table', 'gauge', 'controls', 'charteditor']
    }).then(drawDashboard);
    
    function drawDashboard() {
      //console.log("drawDashboard");
    
      var data = google.visualization.arrayToDataTable([
        ['Name', 'RoolNumber', 'Gender', 'Age', 'DonutsEaten'],
        ['Michael', 1, 'Male', 12, 5],
        ['Elisa', 2, 'Female', 20, 7],
        ['Robert', 3, 'Male', 7, 3],
        ['John', 4, 'Male', 54, 2],
        ['Jessica', 5, 'Female', 22, 6],
        ['Aaron', 6, 'Male', 3, 1],
        ['Margareth', 7, 'Female', 42, 8],
        ['Miranda', 8, 'Female', 33, 6]
      ]);
    
      var dashboard = new google.visualization.Dashboard(document.getElementById('dashboard'));
    
      var categoryPicker = new google.visualization.ControlWrapper({
        controlType: 'CategoryFilter',
        containerId: 'categoryPicker',
        options: {
          filterColumnLabel: 'Gender',
          ui: {
            labelStacking: 'vertical',
            allowTyping: false,
            allowMultiple: false
          }
        }
      });
    
      var proxyTable = new google.visualization.ChartWrapper({
        chartType: 'Table',
        containerId: 'div_proxyTable',
        options: {
          width: '500px'
        }
      });
    
      var table = new google.visualization.ChartWrapper({
        chartType: 'Table',
        containerId: 'div_table',
        options: {
          sort: 'event', // <-- set sort to 'event' for table totaling
          width: '500px',
          allowHtml: true,
          page: 'enable',
          pageSize: '5',
        }
      });
    
      //This json contains my settings for later
      let json = {
        "tableChart": {
          "hasTable": true,
          "dataView": {
            "columns": [{
                "id": "Name"
              },
              {
                "id": "RoolNumber"
              },
              {
                "id": "Gender"
              },
              {
                "id": "Age"
              },
              {
                "id": "DonutsEaten"
              }
            ]
          },
          "groupView": {
            "hasGroupView": false
          },
          "totalRow": {
            "hasTotalRow": true,
            "labelCol": "Total",
            "labelColIndex": 0, //This is the column where the words "Grand Total" are stored.  It must be text column.
            "totalColumns": [{
              "id": "RoolNumber",
              "type": "number",
              "function": "sum"
            }]
          },
          "conditionalFormat": {
            "hasConditionalFormat": false
          },
          "options": {},
          "hasCSV": false,
          "clickGetFunc": null
        },
      };
    
      google.visualization.events.addOneTimeListener(proxyTable, 'ready', function() {
        console.log(".addOneTimeListener(proxyTable, 'ready' - sort");
    
        google.visualization.events.addOneTimeListener(table, 'ready', function() {
          console.log(".addOneTimeListener(table, 'ready' - sort");
    
          //#region table - sort: 'event'
          google.visualization.events.addListener(table.getChart(), 'sort', function(sender) {
            console.log(".addListener(table.getChart(), 'sort' - sorted");
    
            //// update table --> options must include (sort: 'event') for total row to work properly
            //// update var grandTotal = tableData.getFilteredRows([{column: 0,
            //// update .draw() 'table' references to new name if using for a different table variable
    
            // sort data table according to sort properties
            var tableData = table.getDataTable();
            var sortIndexes = tableData.getSortedRows({
              column: sender.column,
              desc: !sender.ascending
            });
    
            //#region reposition total row - if required
            let totalRow = (json.tableChart.totalRow !== undefined) ? json.tableChart.totalRow : [];
            let hasTotalRow = totalRow.hasTotalRow;
            if (hasTotalRow) {
              // find grand total row
              var grandTotal = tableData.getFilteredRows([{
                column: json.tableChart.totalRow.labelColIndex, //must be placed in a column which is of type string.
                value: json.tableChart.totalRow.labelCol
              }]);
              if (grandTotal.length > 0) {
                // find grand total in sort
                var grandTotalSort = sortIndexes.indexOf(grandTotal[0]);
    
                // remove grand total from sort
                sortIndexes.splice(grandTotalSort, 1);
    
                // add grand total as first index
                sortIndexes.unshift(grandTotal[0]);
              }
            }
            //#endregion
    
            // set table sort arrow
            table.setOption('sortAscending', sender.ascending);
            table.setOption('sortColumn', sender.column);
    
            // set table view & re-draw table
            table.setView({
              rows: sortIndexes
            });
            table.draw();
    
            //Table ready then fires
          });
          //#endregion
    
        });
      });
    
      google.visualization.events.addOneTimeListener(proxyTable, 'ready', function() {
        console.log(".addOneTimeListener(proxyTable, 'ready' - select");
    
        google.visualization.events.addListener(table, 'ready', function() {
          console.log(".addOneTimeListener(table, 'ready' - select");
          test_gcharts_selectedRowCol(table, json.tableChart.clickGetFunc); //(wrapperName, callback)
        });
      });
    
      google.visualization.events.addListener(proxyTable, 'ready', function() {
        console.log(".addListener(proxyTable, 'ready' - redrawTable()");
        redrawTable(json.tableChart);
      });
    
      dashboard.bind([categoryPicker], [proxyTable]);
      dashboard.draw(data);
    
    
      //This is a table builder which uses json from above.  This is working ok.  Contains no listeners.
      function redrawTable(tableChart) {
        console.log('redrawTable()');
    
        // set defaults for any undefined settings
        let dataView = (tableChart.dataView !== undefined) ? tableChart.dataView : [];
        let groupView = (tableChart.groupView !== undefined) ? tableChart.groupView : [];
        let totalRow = (tableChart.totalRow !== undefined) ? tableChart.totalRow : [];
        let conditionalFormat = (tableChart.conditionalFormat !== undefined) ? tableChart.conditionalFormat : [];
    
        // update .draw() 'table' or 'chart' references when using a different or additional chart name
    
        var sourceData = proxyTable.getDataTable().toDataTable().clone();
        //console.log('sourceData', sourceData);
    
        //#region create data view - this is used as basis for dataResults
        let view = new google.visualization.DataView(sourceData);
    
        //#region create group view - if required
        let dataResults_forTable;
        let hasGroupView = groupView.hasGroupView;
        if (hasGroupView) {
    
          // create keys for grouping
          const groupKeys = [];
          for (let i = 0; i < groupView.keys.length; i++) {
            groupKeys.push(
              groupKey_default(view, groupView.keys[i]),
            );
          };
          // create columns for aggregating
          const groupColumns = [];
          for (let i = 0; i < groupView.columns.length; i++) {
            groupColumns.push(
              groupColumn_default(view, groupView.columns[i]),
            );
          };
    
          // create data aggregation
          let group = google.visualization.data.group(view, groupKeys, groupColumns);
          //console.log('group'); console.log(group);
    
          dataResults_forTable = group.clone();
        } else {
          dataResults_forTable = view.toDataTable().clone();
        } //END if (hasGroupView) {
        //console.log('dataResults_forTable', dataResults_forTable);
    
        //#endregion
    
        //#region create total row - if required
        let hasTotalRow = totalRow.hasTotalRow;
        if (hasTotalRow) {
    
          let labelCol = totalRow.labelCol;
          let labelColIndex = totalRow.labelColIndex;
          let totalColumns = totalRow.totalColumns;
    
          //Create groupColumns for total row aggregation calculations
          const groupColumns = [];
          for (let i = 0; i < totalColumns.length; i++) {
            const column = {
              column: dataResults_forTable.getColumnIndex(totalColumns[i].id),
              type: 'number'
            }
            switch (totalColumns[i].function) {
              case 'sum':
                column.aggregation = google.visualization.data.sum;
                break;
              case 'count':
                column.aggregation = google.visualization.data.count;
                break;
              case 'average':
                column.aggregation = google.visualization.data.avg;
                break;
              case 'min':
                column.aggregation = google.visualization.data.min;
                break;
              case 'max':
                column.aggregation = google.visualization.data.avg;
                break;
              default:
                column.aggregation = google.visualization.data.sum;
            }
            groupColumns.push(column);
          }
    
          let groupTotal = google.visualization.data.group(dataResults_forTable,
            // need key column to group on, so we want all rows grouped into 1, then it needs a constant value
            [{
              column: 0,
              type: "number",
              modifier: function() {
                return 1;
              }
            }], groupColumns);
    
          // this code block will run if the filter results in rows available to total.  Otherwise the table will present no rows.
          if (groupTotal.getNumberOfRows() !== 0) {
    
            let formatDecimal = new google.visualization.NumberFormat({
              pattern: '#,###.##'
            });
            for (let i = 1; i < groupTotal.getNumberOfColumns(); i++) {
              formatDecimal.format(groupTotal, i);
            }
    
            // create Grand Total row from colToTotal and groupTotal
            const gtRow = [];
            for (let i = 0; i < dataResults_forTable.getNumberOfColumns(); i++) {
              //Build GT Row to match length of dataResults_forTable
              gtRow.push(null);
            }
    
            //Push words "Grand Total" into it's set position in gtRow - It must go into a column of type string.
            gtRow[labelColIndex] = labelCol;
    
            for (let i = 0; i < totalColumns.length; i++) {
              //Loop through groupColumns, test setting type for string.
    
              if (totalColumns[i].type === 'string') {
                //Convert to number from groupTotal result to string to match the column it's being pushed into
                gtRow[dataResults_forTable.getColumnIndex(totalColumns[i].id)] = String(groupTotal.getValue(0, i + 1));
              } else {
                //Otherwise push in the number value
                gtRow[dataResults_forTable.getColumnIndex(totalColumns[i].id)] = groupTotal.getValue(0, i + 1);
              }
            }
            //console.log('insertRow', insertRow);
    
            // insert complete gtRow with values into row position 0
            dataResults_forTable.insertRows(0, [gtRow]);
    
            // add formatting for grand total row to highlight && justify to right if of type number
            for (let j = 0; j < dataResults_forTable.getNumberOfColumns(); j++) {
              //if statement on column type for left right justification
              if (dataResults_forTable.getColumnType(j) === 'number') {
                dataResults_forTable.setColumnProperty(j, 'className', 'googleTableTextRight');
                dataResults_forTable.setProperty(0, j, 'className', 'googleTableTotalRow googleTableTextRight'); //stored in css file
              } else {
                dataResults_forTable.setProperty(0, j, 'className', 'googleTableTotalRow'); //stored in css file
              }
            }
            //console.log('dataResults_forTable', dataResults_forTable);
          } //END (groupTotal.getNumberOfRows() !== 0) {
        } //END if (hasTotalRow) {
        //#endregion
    
        //#region conditional formatting - if required
        let hasConditionalFormat = conditionalFormat.hasConditionalFormat;
        if (hasConditionalFormat) {
          dataResults_forTable = conditionalFormatting_default(dataResults_forTable, conditionalFormat);
        } //END if (hasConditionalFormat) {
        //#endregion
    
        var finalView_forTable = new google.visualization.DataView(dataResults_forTable);
        //console.log('finalView_forTable', finalView_forTable);
    
        // set reset sorting, set dataTable & draw chart
        table.setView(null); // reset in case sorting has been used via user click
        table.setDataTable(finalView_forTable); //includes any total row
        table.draw();
    
      } //END redrawChart()
    
    }
    
    // save handles to event listeners
    var listenerPage = {};
    var listenerSort = {};
    
    function test_gcharts_selectedRowCol(wrapperName, callback) {
      //console.log('wrapperName', wrapperName); console.log('callback', callback)
    
      //NEW - Works with paging active
      // initialize page number and size
      var page = 0;
      var pageSize = 10;
      if (wrapperName.getOption('page') === 'enable') {
        page = wrapperName.getOption('startPage');
        pageSize = wrapperName.getOption('pageSize');
      }
      test_enableCoordinates(callback);
    
      // remove previous event handlers
      if (listenerPage.hasOwnProperty(wrapperName.getContainerId())) {
        google.visualization.events.removeListener(listenerPage[wrapperName.getContainerId()]);
        delete listenerPage[wrapperName.getContainerId()];
      }
      if (listenerSort.hasOwnProperty(wrapperName.getContainerId())) {
        google.visualization.events.removeListener(listenerSort[wrapperName.getContainerId()]);
        delete listenerSort[wrapperName.getContainerId()];
      }
    
      // page event
      listenerPage[wrapperName.getContainerId()] = google.visualization.events.addListener(wrapperName.getChart(), 'page', function(sender) {
        console.log(".addListener(wrapperName.getChart(), 'page' - paged - test_enableCoordinates called");
        page = sender.page; // save current page
        test_enableCoordinates(callback);
      });
    
      // sort event
      listenerSort[wrapperName.getContainerId()] = google.visualization.events.addListener(wrapperName.getChart(), 'sort', function() {
        console.log(".addListener(wrapperName.getChart(), 'sort' - sorted - test_enableCoordinates called ")
        page = 0; // reset back to first page
        test_enableCoordinates(callback);
      });
    
      function test_enableCoordinates(callback) {
        //console.log('callback enableCoordinates', callback);
    
        //remove event listeners
        var container = document.getElementById(wrapperName.getContainerId());
        Array.prototype.forEach.call(container.getElementsByTagName('td'), function(cell) {
          cell.removeEventListener("click", test_selectCell, false);
        });
    
        //add event listeners
        var container = document.getElementById(wrapperName.getContainerId());
        Array.prototype.forEach.call(container.getElementsByTagName('td'), function(cell) {
          cell.addEventListener('click', test_selectCell, false);
        });
      }
    
      function test_selectCell(sender) {
    
        var cell = sender.target;
        var row = cell.closest('tr');
    
        var wrapperDataTable = wrapperName.getDataTable();
    
        var selectedRow = row.rowIndex - 1; // adjust for header row (-1)
        selectedRow = (page * pageSize) + selectedRow; // adjust for page number
    
        // Original from whitehat - save sorted info
        // This version does not work after a user clicks a column to sort
        //var sortInfo = wrapperName.getChart().getSortInfo();
        //if (sortInfo.sortedIndexes !== null) {
        //    selectedRow = sortInfo.sortedIndexes[selectedRow];
        //}
    
        // Replaced code to which finds the view row order by .getView then taking the selected row
        // and returning the number which is in the .getView result
        var sortInfo = wrapperName.getView(); // save sorted info
        if (sortInfo !== null) {
          selectedRow = sortInfo.rows[selectedRow];
        }
        var selectedCol = cell.cellIndex;
    
        //var result = "selectedRow: " + selectedRow + " selectedCol: " + selectedCol;
        //var ul = document.getElementById("demo");
        //var li = document.createElement("li");
        //li.innerHTML = result;
        //ul.appendChild(li);
    
        var selectedValue = wrapperDataTable.getValue(selectedRow, selectedCol);
        var colID = wrapperDataTable.getColumnId(selectedCol);
        var colLabel = wrapperDataTable.getColumnLabel(selectedCol);
    
        var result = //removed array brackets
          {
            "selectedRow": selectedRow,
            "selectedCol": selectedCol,
            "selectedValue": selectedValue,
            "colID": colID,
            "colLabel": colLabel
          };
    
        //callback(result, wrapperDataTable);
        console.log('test_selectCell', result);
        return result;
      }
    }
    <script src="https://www.gstatic.com/charts/loader.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js"></script>
    
    <div id="dashboard">
        <div id="categoryPicker"></div><br />
        Proxy Table<br />
        <div id="div_proxyTable" style="display:none;"></div><br />
        Table<br />
        <div id="div_table"></div><br /><br />
    </div>