2

I'm new to coding but over the last several months I've managed to fumble my way through creating a web site that utilises a Google Line Chart and embedded linear trendline to display historical Mean Sea Level and the rate of Mean Sea Level rise for various locations around New Zealand and the Pacific. Each location has it's own Google Line Chart with a linear trendline to show the rate of Mean Sea Level Change for a user selected period. I now want to extend the functionality of each Google Line Chart such that both a linear and polynomial trendline extend to the year 2120 (they currently only show up to the year 2018) even though the available data from which they are calculated uses observed data up to the year 2018. This will allow the user to predict the sea level height up to the year 2020. I realise this explanation may be confusing, so please see my web site www.sealevel.nz to see the existing charts which I hope will aid in understanding my problem.

Below is the code for the extended version of the chart that shows both a linear and second degree polynomial trendline with the x axis of the Google Line Chart now showing up the year 2120. My problem is that I need the y axis to adjust dynamically to show the entirety of both trendlines no matter which time period the user selects. For example if you select the years 1971 and 2018 from the date range slider, then both trendlines are cut off at the years 2017 (linear) and 2031 (polynomial) respectively. I need to be able to see both trendlines and their values up to the year 2120.

Please excuse my novice coding skills. My Code:

<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script src="https://unpkg.com/mathjs/dist/math.min.js"></script>
<script type="text/javascript">

google.load('visualization', 'current', {'packages':['controls','corechart']});
google.setOnLoadCallback(initialize);
function initialize() {
  var query = new google.visualization.Query('https://docs.google.com/spreadsheets/d/1vn1iuhsG33XzFrC4QwkTdUnxOGdcPQOj-cuaEZeX-eA/edit#gid=0');
  query.send(drawDashboard);
}
function drawDashboard(response) {
  var data = response.getDataTable();
//Asign units of 'mm' to data.
    var formatMS = new google.visualization.NumberFormat({
    pattern: '# mm'
  });
  // format into data mm..
  for (var colIndex = 1; colIndex < data.getNumberOfColumns(); colIndex++) {
    formatMS.format(data, colIndex);
  }
 var YearPicker = new google.visualization.ControlWrapper({
    'controlType': 'NumberRangeFilter',
    'containerId': 'filter_div',
    'options': {
      'filterColumnLabel': 'Year',
        'ui': {
       cssClass: 'filter-date',
          'format': { pattern: '0000' },
      'labelStacking': 'vertical',
      'allowTyping': false,
      'allowMultiple': false    
      }
    },
  });
  var MSLChart = new google.visualization.ChartWrapper({
    'chartType': 'LineChart',
    'containerId': 'chart_div',
    'options': {  
    'fontSize': '14', 
    'title': 'Timbucktoo Annual Mean Sea Level Summary',
        hAxis: {title: 'Year', format:'0000', maxValue: 2120},
        vAxis: {title: 'Height above Chart Datum (mm)', format:'0000'},
        'height': 600,
    chartArea: {height: '81%', width: '85%', left: 100},
    'legend': {'position': 'in', 'alignment':'end', textStyle: {fontSize: 13} },
    colors: ['blue'],
    trendlines: {
            0: {
                type: 'polynomial',
                degree: 2,
                color: 'green',
                visibleInLegend: true,
            },
            1: {
                type: 'linear',
                color: 'black',
                visibleInLegend: true,
            },
        },
        series: {
            0: { visibleInLegend: true },
            1: { visibleInLegend: false },
        },    
    },
    'view': {'columns': [0,1,2]}
  });

  var dashboard = new google.visualization.Dashboard(document.getElementById('dashboard_div')).
    bind(YearPicker, MSLChart).

  draw(data)
 </script>
Bryan Ward
  • 53
  • 1
  • 6

1 Answers1

2

first, I'm not sure why the chart would draw a trend line that isn't visible
which makes this a bit tricky, because we first have to draw the chart,
in order to find the min & max y-axis values.

but there are chart methods we can use to find the max value.

first, we get the chart's layout interface.

var chartLayout = MSLChart.getChart().getChartLayoutInterface();

since we're using a ChartWrapper, we have to get the chart from the wrapper (MSLChart.getChart()).

next, we use method getBoundingBox to find the min & max values of each line.

var yAxisCoords = {min: null, max: null};
var lineIndex = 0;
var boundsLine = chartLayout.getBoundingBox('line#' + lineIndex);
do {
  yAxisCoords.max = yAxisCoords.max || boundsLine.top;
  yAxisCoords.max = Math.min(yAxisCoords.max, boundsLine.top);
  yAxisCoords.min = yAxisCoords.min || (boundsLine.top + boundsLine.height);
  yAxisCoords.min = Math.max(yAxisCoords.min, (boundsLine.top + boundsLine.height));
  lineIndex++;
  boundsLine = chartLayout.getBoundingBox('line#' + lineIndex);
} while (boundsLine !== null);

then we use method getVAxisValue to determine what each y-axis value should be,
set the viewWindow on the y-axis, and re-draw the chart.

MSLChart.setOption('vAxis.viewWindow.max', chartLayout.getVAxisValue(yAxisCoords.max));
MSLChart.setOption('vAxis.viewWindow.min', chartLayout.getVAxisValue(yAxisCoords.min));
MSLChart.draw();

we do all this in a function.
we use a one time 'ready' event on the chart wrapper for the first calculation.
then again, on the chart.

google.visualization.events.addOneTimeListener(MSLChart, 'ready', filterChange);

function filterChange() {
  // get chart layout
  var chartLayout = MSLChart.getChart().getChartLayoutInterface();

  // get y-axis bounds
  var yAxisCoords = {min: null, max: null};
  var lineIndex = 0;
  var boundsLine = chartLayout.getBoundingBox('line#' + lineIndex);
  do {
    yAxisCoords.max = yAxisCoords.max || boundsLine.top;
    yAxisCoords.max = Math.min(yAxisCoords.max, boundsLine.top);
    yAxisCoords.min = yAxisCoords.min || (boundsLine.top + boundsLine.height);
    yAxisCoords.min = Math.max(yAxisCoords.min, (boundsLine.top + boundsLine.height));
    lineIndex++;
    boundsLine = chartLayout.getBoundingBox('line#' + lineIndex);
  } while (boundsLine !== null);

  // re-draw chart
  MSLChart.setOption('vAxis.viewWindow.max', chartLayout.getVAxisValue(yAxisCoords.max));
  MSLChart.setOption('vAxis.viewWindow.min', chartLayout.getVAxisValue(yAxisCoords.min));
  MSLChart.draw();
  google.visualization.events.addOneTimeListener(MSLChart.getChart(), 'ready', filterChange);
}

see following working snippet...
(when you run the snippet, click "full page" at the top right)

google.charts.load('current', {
  packages: ['controls']
}).then(initialize);

function initialize() {
  var query = new google.visualization.Query('https://docs.google.com/spreadsheets/d/1vn1iuhsG33XzFrC4QwkTdUnxOGdcPQOj-cuaEZeX-eA/edit#gid=0');
  query.send(drawDashboard);
}

function drawDashboard(response) {
  var data = response.getDataTable();

  //Asign units of 'mm' to data.
  var formatMS = new google.visualization.NumberFormat({
    pattern: '# mm'
  });

  // format into data mm..
  for (var colIndex = 1; colIndex < data.getNumberOfColumns(); colIndex++) {
    formatMS.format(data, colIndex);
  }

  var YearPicker = new google.visualization.ControlWrapper({
    controlType: 'NumberRangeFilter',
    containerId: 'filter_div',
    options: {
      filterColumnLabel: 'Year',
      ui: {
        cssClass: 'filter-date',
        format: {pattern: '0000'},
        labelStacking: 'vertical',
        allowTyping: false,
        allowMultiple: false
      }
    },
  });

  var MSLChart = new google.visualization.ChartWrapper({
    chartType: 'LineChart',
    containerId: 'chart_div',
    dataTable: data,
    options: {
      fontSize: '14',
      title: 'Timbucktoo Annual Mean Sea Level Summary',
      hAxis: {title: 'Year', format: '0000', maxValue: 2120},
      vAxis: {title: 'Height above Chart Datum (mm)', format:'###0'},
      height: 600,
      chartArea: {height: '81%', width: '85%', left: 100},
      legend: {position: 'in', alignment: 'end', textStyle: {fontSize: 13}},
      colors: ['blue'],
      trendlines: {
        0: {
          type: 'polynomial',
          degree: 2,
          color: 'green',
          visibleInLegend: true,
        },
        1: {
          type: 'linear',
          color: 'black',
          visibleInLegend: true,
        },
      },
      series: {
        0: { visibleInLegend: true },
        1: { visibleInLegend: false },
      },
    },
    view: {columns: [0,1,2]}
  });

  google.visualization.events.addOneTimeListener(MSLChart, 'ready', filterChange);

  function filterChange() {
    // get chart layout
    var chartLayout = MSLChart.getChart().getChartLayoutInterface();

    // get y-axis bounds
    var yAxisCoords = {min: null, max: null};
    var lineIndex = 0;
    var boundsLine = chartLayout.getBoundingBox('line#' + lineIndex);
    do {
      yAxisCoords.max = yAxisCoords.max || boundsLine.top;
      yAxisCoords.max = Math.min(yAxisCoords.max, boundsLine.top);
      yAxisCoords.min = yAxisCoords.min || (boundsLine.top + boundsLine.height);
      yAxisCoords.min = Math.max(yAxisCoords.min, (boundsLine.top + boundsLine.height));
      lineIndex++;
      boundsLine = chartLayout.getBoundingBox('line#' + lineIndex);
    } while (boundsLine !== null);

    // re-draw chart
    MSLChart.setOption('vAxis.viewWindow.max', chartLayout.getVAxisValue(yAxisCoords.max));
    MSLChart.setOption('vAxis.viewWindow.min', chartLayout.getVAxisValue(yAxisCoords.min));
    MSLChart.draw();
    google.visualization.events.addOneTimeListener(MSLChart.getChart(), 'ready', filterChange);
  }

  var dashboard = new google.visualization.Dashboard(
    document.getElementById('dashboard_div')
  ).bind(YearPicker, MSLChart).draw(data);
}
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="dashboard_div">
  <div id="chart_div"></div>
  <div id="filter_div"></div>
</div>

note: it appears you're using an old load statement, to load google chart.
see above snippet for update...

WhiteHat
  • 59,912
  • 7
  • 51
  • 133
  • 1
    Thank you so much @WhiteHat. I'm gobsmacked with what you've come up with. If you're ever in New Zealand I'd be more than happy to buy you a beer or two. I do have a couple of questions in separate comments as follows: – Bryan Ward Nov 06 '19 at 04:23
  • 1. The trend does turn negative for certain pairs of years. For example 1932 and 1990 the polynomial trendline turns negative while the linear trendline remains positive. So in attempting to calculate the minimum y-axis value for such a scenario I've returned the left, width and height properties for both trendlines and then used the getVAxisValue method to return the corresponding y-axis value as you've pointed out, but I only seem to get nonsense values. – Bryan Ward Nov 06 '19 at 04:25
  • For example: `var minTrend = Math.min(trendLine0.left, trendLine1.left); var yCoord = chartLayout.getVAxisValue(minTrend); console.log(yCoord);` Or `var minTrend = Math.min(trendLine0.width, trendLine1.width); var yCoord = chartLayout.getVAxisValue(minTrend); console.log(yCoord);` Or `var minTrend = Math.min(trendLine0.height, trendLine1.height); var yCoord = chartLayout.getVAxisValue(minTrend); console.log(yCoord);` – Bryan Ward Nov 06 '19 at 04:27
  • All the above seem to return a value that isn't the minimum y axis value. So is there another property other than 'left', 'width', 'top' and 'height' of the trendline object that will return the minimum value? – Bryan Ward Nov 06 '19 at 04:29
  • 2. What do the initial 'trendLine0.top', 'trendLine1.top' values refer to before they are converted into corresponding y-axis values using the getVAxisValue method? – Bryan Ward Nov 06 '19 at 04:29
  • No problem. Take your time. I'm in no rush. – Bryan Ward Nov 06 '19 at 21:26
  • 1
    changed answer above, note: I noticed when the min axis value dropped below 1000, the option `minValue` would ignore our value, and drop down to zero. using `viewWindow` instead, corrected the issue... – WhiteHat Nov 07 '19 at 13:07
  • 1. Why is `yAxisCoords.min = boundsLine.top + boundsLine.height;` (when lineIndex=0) and instead not equal to `boundsLine.top - boundsLine.height;`? – Bryan Ward Nov 11 '19 at 19:20
  • 2. Can you remind me again why yAxisCoordsMax is equal to the minimum of yAxisCoords.max and boundsLine.top and not the maximum of these two values? I remember you mentioning in your earlier response that it was something to do with the largest value being negative if it was too large to be shown on the existing y axis of the rendered chart.. – Bryan Ward Nov 11 '19 at 19:21
  • 3. I want to make a change to the chart and numberRangeFilter such that it defaults to the years for which there is available observed data (the blue line) when the chart is first loaded, i.e. in this case 1904 and 2018. Once loaded, the user can then choose a year beyond which there is available observed data up to 2120 and see both trendlines up to the year they've chosen to get prediction for what the sea level will be at some point in the future.. – Bryan Ward Nov 11 '19 at 19:28
  • To achieve this I inserted `'maxValue': 2120,` into the options and `"state": {"lowValue": 1904, "highValue": 2018},` into the configuration options of the numberRangeFilter of the ControlWrapper and removed `maxValue: 2120` from the options in the ChartWrapper. However when I then reload the chart and select the years 1904 and 2120 for example, the chart fails to redraw. In fact it fails to redraw for any year selected past 2018? – Bryan Ward Nov 11 '19 at 19:30
  • 4. In order to show two trendlines for the the same series of data I've had to create two data series containing identical data and then show a linear trendline for one series and a polynomial trendline for the other. As it appears that you cannot show two different trendlines for the same series of data. Is this correct? – Bryan Ward Nov 11 '19 at 19:31
  • Ok, so I've added the following to the `filterChange()` function such that it redraws the x-axis correctly when a year beyond 2018 is selected which seems to work: – Bryan Ward Nov 12 '19 at 02:57
  • `google.visualization.events.addListener(YearPicker, 'statechange', function () { var state = YearPicker.getState(); var EndYear = state.highValue; MSLChart.setOption('vAxis.viewWindow.max', chartLayout.getVAxisValue(yAxisCoords.max)); MSLChart.setOption('vAxis.viewWindow.min', chartLayout.getVAxisValue(yAxisCoords.min)); MSLChart.setOption('hAxis.viewWindow.max', EndYear); MSLChart.draw(); google.visualization.events.addOneTimeListener(MSLChart.getChart(), 'ready', filterChange); });` – Bryan Ward Nov 12 '19 at 02:57
  • However it still cuts off one or both trendlines when certain years are selected. So I know I need to fire the `filterChange` function whenever the `YearPicker` numberRangeFilter changes state. However I've tried inserting `google.visualization.events.addOneTimeListener(YearPicker, 'statechange', filterChange);`in various places to trigger the `filterChange` function with no luck. I've also tried changing the value of `function()` in the above listener in my previous comment to `filterChange` also with no success. – Bryan Ward Nov 12 '19 at 03:08
  • 1
    it may be easier to ask another question, I'm having trouble following all the comments. I'll try to answer a few. 1) for the y-axis min, we need to find the bottom most point of the line, so we start with the top, and add the height. 2) think of the top left corner of the chart area as location 0, 0. and it increases to the left and down. if the line is above the visible chart area, then the y coordinate will be a negative value. as for 3) another question would be best -- let's call this one done – WhiteHat Nov 13 '19 at 12:23
  • No problem @WhiteHat. Really appreciate your help. Have just posted the above as another question titled "Set Maximum Value for Lower Range of Google Number Range Filter". – Bryan Ward Nov 14 '19 at 02:59