nsa
nsa

Reputation: 565

How can I control the plotting order of points in a google scatter chart?

I'm working with multi-series data yielding dense scatter plots. Sometimes if two or more series produce a lot of points at more or less the same regions in the plot, it becomes difficult to interpret the plot due to google charts plotting all points of a series first, then the other and so on. This means all points of a series always end up behind or above all the points of other series. My ideal plot would have points randomly drawn independent from their series.

I made a simplified example to illustrate what I mean:

google.charts.load('current', {
  packages: ['corechart']
}).then(function () {
    
  var data = {
        cols: [
          {id: "x", label: "xValues", type: "number", role: "domain"},
          {id: "y0", label: "y0Values", type: "number", role: "data"},
          {id: "y1", label: "y1Values", type: "number", role: "data"}
        ],
        rows: [
          {c: [{v: 0.1}, {v: 0.5}, {}]},
          {c: [{v: 0.1}, {}, {v: 0.5}]},
          {c: [{v: 0.6}, {v: 0.1}, {}]},
          {c: [{v: 0.6}, {}, {v: 0.1}]}
        ]
      };
  
  var dt = new google.visualization.DataTable(data);
  var options = {seriesType: 'scatter'};
  var chart = new google.visualization.ComboChart(document.getElementById('container'));
  chart.draw(dt, options);
});
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="container"></div>

In this example we have 4 data points, 2 for each of the 2 series in the data. What I want to do is to plot the point (0.1, 0.5) from series y0, then plot (0.1, 0.5) from y1, then plot (0.6, 0.1) from y1 and finally (0.6, 0.1) from y0. The result would be that only y1 is visible at (0.1, 0.5) and only y0 is visible at (0.6, 0.1). Right now only y1 is visible at both locations. I think it is possible to manipulate the order of point plotting by series (i.e. plot ALL points of a series first, last, etc.) but I want to control the plotting order in a point-by-point basis.

Note that the order in which the rows appear in the data does not actually make a difference in how the points are plotted (the example has the points for each series alternated). This was the first thing I tried changing but it does not work. There are other things one can do to help the problem of dense scatters (using point alpha, changing to a hollow shape or implementing some interactivity on the chart to hide or show points) but I would still benefit from controlling the order of point plotting if possible.

Upvotes: 1

Views: 161

Answers (1)

WhiteHat
WhiteHat

Reputation: 61222

when the chart is drawn, each series is drawn in order.
series 1 first, then series 2, etc...
changing the order of the rows will not affect this sequence.

SVG will display the last drawn element on top.
see this answer for more --> How to use z-index in svg elements?

in order to control which points are displayed on top,
you will need to modify the SVG on the chart's 'ready' event.

we can take a couple of approaches, based on the referenced answer.

  1. The <use> element takes nodes from within the SVG document, and duplicates them somewhere else.
    we can use the <use> element to duplicate a point and add it after the other points.
    the drawback to this approach seems to be that the duplicated point is not interactive.

see following working snippet...

google.charts.load('current', {
  packages: ['corechart']
}).then(function () {
  var data = {
    cols: [
      {id: "x", label: "xValues", type: "number", role: "domain"},
      {id: "y0", label: "y0Values", type: "number", role: "data"},
      {id: "y1", label: "y1Values", type: "number", role: "data"}
    ],
    rows: [
      {c: [{v: 0.1}, null, {v: 0.5}]},
      {c: [{v: 0.6}, null, {v: 0.1}]},
      {c: [{v: 0.1}, {v: 0.5}, null]},
      {c: [{v: 0.6}, {v: 0.1}, null]},
    ]
  };

  var dt = new google.visualization.DataTable(data);
  var options = {seriesType: 'scatter'};
  var chart = new google.visualization.ComboChart(document.getElementById('container'));

  google.visualization.events.addListener(chart, 'ready', function () {
    // points parent
    var parent;

    // svg namespace
    var svgNS = chart.getContainer().getElementsByTagName('svg')[0].namespaceURI;

    // find points
    var circles = chart.getContainer().getElementsByTagName('circle');
    Array.prototype.forEach.call(circles, function(circle, index) {
      // skip legend circles
      var rowIndex = (index - (dt.getNumberOfColumns() - 1));
      if (rowIndex >= 0) {
        // set element id
        circle.id = 'circle' + rowIndex;
        parent = circle.parentNode;
      }
    });

    // add first point to end of order
    var order = document.createElementNS(svgNS, 'use');
    order.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#circle0');
    parent.appendChild(order);
  });

  chart.draw(dt, options);
});
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="container"></div>

  1. manually move the point elements, thus changing the order.
    the only drawback to this approach, if you want to call it that, is the point underneath is still interactive, around the border of the displayed point.

see following working snippet...

google.charts.load('current', {
  packages: ['corechart']
}).then(function () {
  var data = {
    cols: [
      {id: "x", label: "xValues", type: "number", role: "domain"},
      {id: "y0", label: "y0Values", type: "number", role: "data"},
      {id: "y1", label: "y1Values", type: "number", role: "data"}
    ],
    rows: [
      {c: [{v: 0.1}, null, {v: 0.5}]},
      {c: [{v: 0.6}, null, {v: 0.1}]},
      {c: [{v: 0.1}, {v: 0.5}, null]},
      {c: [{v: 0.6}, {v: 0.1}, null]},
    ]
  };

  var dt = new google.visualization.DataTable(data);
  var options = {seriesType: 'scatter'};
  var chart = new google.visualization.ComboChart(document.getElementById('container'));

  google.visualization.events.addListener(chart, 'ready', function () {
    // find points
    var circles = chart.getContainer().getElementsByTagName('circle');
    Array.prototype.forEach.call(circles, function(circle, index) {
      // skip legend circles
      var rowIndex = (index - (dt.getNumberOfColumns() - 1));

      // move first point to end
      if (rowIndex === 0) {
        circle.parentNode.appendChild(circle);
      }
    });
  });

  chart.draw(dt, options);
});
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="container"></div>

other options would be to change the pointSize of each series, so at least some of the point is visible.
or simply not draw the point if it is going to cover another...

Upvotes: 1

Related Questions