I have a interactive Google Line Chart that displays historical sea level data between two different years as selected by the user. The chart also displays both a linear and polynomial trendline for the selected period. The available historical data ranges between the years 1904 to 2018 inclusive. However the user can select any start year and end year from 1904 to 2120 inclusive. If a end year beyond 2018 is selected, the chart displays the available historical data up to 2018 and then extends both trendlines to show the predicted sea level up to the year selected by the user.
This seems to work ok until both selected years exceed 2018, i.e. 2020 and 2056 when an error is thrown ("Cannot read property 'top' of null") because it can't calculate and draw a trendline from a period containing no observed data. At the moment I'm getting around this with an error handler that picks up when this happens and shows an alert message advising the user they can't select a start year greater than 2018. The page then reloads and the number range filter defaults back to the start and end year of 1904 and 2018 respectively, which is not ideal. What I would like to do is restrict the user from selecting a start year that doesn't exceed 2018, but there doesn't seem to be an option/setting in the number range filter control to do this. Any ideas?
My Code:
<html>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
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 data into 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: {
maxValue:2120,
filterColumnLabel: 'Year',
ui: {
cssClass: 'filter-date',
format: {pattern: '0000'},
labelStacking: 'vertical',
allowTyping: false,
allowMultiple: false
}
},
"state": {"lowValue": 1904, "highValue": 2018},
});
google.visualization.events.addListener(YearPicker, 'statechange', filterChange);
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'},
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);
try {
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);
}
catch (error) {alert("Please choose a start year less than or equal to 2018");
window.location.reload(false);
exit;
}
var state = YearPicker.getState();
var EndYear = state.highValue;
// re-draw chart
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);
}
var dashboard = new google.visualization.Dashboard(
document.getElementById('dashboard_div')
).bind(YearPicker, MSLChart).draw(data);
}
</script>
<div id="dashboard_div">
<div id="chart_div"></div>
<div id="filter_div"></div>
</div>
</html>
to prevent the error, in the filterChange
function,
change the do...while
statement,
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);
to just a while
statement
while (boundsLine !== null) {
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);
};
do...while
assumed there would always be at least one line drawn.
we cannot prevent the user from selecting a start year exceeding 2018,
but we can immediately reset the start year when they do.
google.visualization.events.addListener(YearPicker, 'statechange', function () {
var state = YearPicker.getState();
state.lowValue = Math.min(2018, state.lowValue);
YearPicker.setState({
lowValue: state.lowValue,
highValue: state.highValue
});
YearPicker.draw();
filterChange();
});
see following working snippet...
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 data into 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: {
maxValue: 2120,
filterColumnLabel: 'Year',
ui: {
cssClass: 'filter-date',
format: {pattern: '0000'},
labelStacking: 'vertical',
allowTyping: false,
allowMultiple: false
}
},
state: {lowValue: 1904, highValue: 2018},
});
google.visualization.events.addListener(YearPicker, 'statechange', function () {
var state = YearPicker.getState();
state.lowValue = Math.min(2018, state.lowValue);
YearPicker.setState({
lowValue: state.lowValue,
highValue: state.highValue
});
YearPicker.draw();
filterChange();
});
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'},
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);
while (boundsLine !== null) {
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);
};
var state = YearPicker.getState();
var EndYear = state.highValue;
// re-draw chart
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);
}
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>
EDIT
there are no options for modifying the tooltip for the trendline,
but we can change it manually, during the 'onmouseover'
event.
first, we need to use html tooltips, they are svg by default.
add this option...
tooltip: {
isHtml: true
},
then add the 'onmouseover'
event to the chart,
we can do this in the 'ready'
event of the wrapper.
google.visualization.events.addOneTimeListener(MSLChart, 'ready', function () {
google.visualization.events.addListener(MSLChart.getChart(), 'onmouseover', function (props) {
// ensure trendline tooltip
if ((props.column === 0) && (props.row !== null)) {
// get year value
var year = MSLChart.getDataTable().getValue(props.row, 0);
// get tooltip, remove width
var tooltip = MSLChart.getChart().getContainer().getElementsByTagName('ul');
tooltip[0].parentNode.style.width = null;
// get tooltip labels
var tooltipLabels = MSLChart.getChart().getContainer().getElementsByTagName('span');
// set year
tooltipLabels[0].innerHTML = year;
// remove formula
tooltipLabels[1].innerHTML = '';
// set height value
var height = parseFloat(tooltipLabels[2].innerHTML.split(' ')[2].replace(',', '')).toFixed(0);
tooltipLabels[2].innerHTML = height + ' mm';
}
});
});
see following working snippet...
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 data into 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: {
maxValue: 2120,
filterColumnLabel: 'Year',
ui: {
cssClass: 'filter-date',
format: {pattern: '0000'},
labelStacking: 'vertical',
allowTyping: false,
allowMultiple: false
}
},
state: {lowValue: 1904, highValue: 2018},
});
google.visualization.events.addListener(YearPicker, 'statechange', function () {
var state = YearPicker.getState();
state.lowValue = Math.min(2018, state.lowValue);
YearPicker.setState({
lowValue: state.lowValue,
highValue: state.highValue
});
YearPicker.draw();
filterChange();
});
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'},
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'],
tooltip: {
isHtml: true
},
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);
while (boundsLine !== null) {
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);
};
var state = YearPicker.getState();
var EndYear = state.highValue;
// re-draw chart
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);
}
google.visualization.events.addOneTimeListener(MSLChart, 'ready', function () {
google.visualization.events.addListener(MSLChart.getChart(), 'onmouseover', function (props) {
// ensure trendline tooltip
if ((props.column === 0) && (props.row !== null)) {
var year = MSLChart.getDataTable().getValue(props.row, 0);
// get tooltip, remove width
var tooltip = MSLChart.getChart().getContainer().getElementsByTagName('ul');
tooltip[0].parentNode.style.width = null;
// get tooltip labels
var tooltipLabels = MSLChart.getChart().getContainer().getElementsByTagName('span');
// set year
tooltipLabels[0].innerHTML = year;
// remove formula
tooltipLabels[1].innerHTML = '';
// set height
var height = parseFloat(tooltipLabels[2].innerHTML.split(' ')[2].replace(',', '')).toFixed(0);
tooltipLabels[2].innerHTML = height + ' mm';
}
});
});
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>