首先,请原谅下面的小说,但这很难描述。
我有一个相当复杂的应用程序,我目前正在通过GAS应用程序的HtmlService使用gviz。
这更像是客户端gviz API问题。
我通过查询方法加载2个数据表,并通过data.join组合它们。生成的dataTable是数据视图的源。
然后,该视图用于生成包含多个类别过滤器和单个tableChart的仪表板。我有一个按钮,可以在显示的视图行和列的两个过滤器集之间切换。这个概念是加载所有记录并在挂起和完成之间切换,向用户显示相关列。
这部分实际上运作得很好。我遇到的问题是数据更新问题。每次可视化绘制时,我都会遍历生成的表并将操作附加到各个列。
一个这样的操作打开一个带有行值的jQueryUI对话框,允许用户进行修改并提交给服务器。当它提交,而不是重新查询我的数据时,我的方法是在AJAX成功时更新dataTable,这要快得多。
在实现连接之前,这是基于单个dataTable,当我更新dataTable时,基于它的dataView也会更新,并且在chartWrapper上调用draw()会像预期的那样更新UI。
麻烦的是,这并不适用于已加入的dataTable。我已经在客户端的开发人员工具中确认dataTable实际上是像dataView那样更新值,但是这从未传播到tableChart。
这是一个非常奇怪的部分。如果在开发工具中使用chartWrapper的.getDataTable方法,我可以确认有问题的记录已更新!虽然生成的可视化中的值不是。
我尝试在我的AJAX成功中重新绘制chartWrapper无济于事。它会在滚动级别更改时重绘(chartWrapper),但数据不会更改。我尝试在dev工具中手动重绘chartWrapper和dashboard,甚至将视图作为dataTable传递。两者都实际上是重绘,但都没有更新数据。
我已尝试重新绘制chartWrapper上的getDataTable表,但这基本上与我的仪表板和样式无关,并绘制了不是我需要的整个数据集。
更改类别过滤器后,过滤回来后不会更新tableChart。即使将不同的过滤器应用于dataView然后返回也不会更新它。我发现工作的唯一方法是重新查询整个过程需要5-10秒。每一次改变都是不可接受的延迟。这在第一次加载时很好,但理想情况下,每个用户会话应该只发生一次。
我花了好几个小时调试和搜索(可怕的)文档,我所看到的一切似乎都表明它应该有效。
我开始怀疑这可能是某种缓存问题,或者我是否遗漏了某些未在文档中解释过的技巧。
此特定页面的客户端代码为1100行,因此很难完整发布。以下是我对所包含关键部件所做的极其简化的版本。非常感谢任何建议或意见。
注意,我省略了很多与生成操作对话框或按钮相关的部分,但它们大多不相关。
var loaded = false;
var ready = {pro: false, sec: false};
var data = {};
// Ajax load gviz api
$.ajax({
url: 'https://www.google.com/jsapi?callback',
cache: true,
dataType: 'script',
success: function(){
google.load('visualization', '1', {packages:['controls'], 'callback' : sendQuery
});
return true;
}
});
function sendQuery() {
console.log('query setting');
var opts = {sendMethod: 'auto'};
var urlPro = 'Google Spreadsheet source 1';
queryPro = new google.visualization.Query(urlPro, opts);
queryPro.setQuery('select A,B,C,D,E where(G = \'No\')');//...15k+ rows 30+ cols in reality
var urlSec = 'Google Spreadsheet source 2';
querySec = new google.visualization.Query(urlSec, opts);
querySec.setQuery('select A,B,C,D');//...~200+ rows 30+ cols in reality, rows created with app
queryPro.send(function(response){
console.log('query Pro returned');
if (response.isError()) {
alert('Error in query Pro: ' + response.getMessage() + ' ' + response.getDetailedMessage());
return;
}
ready.pro = true;
data.pro = response.getDataTable();
if (ready.pro && ready.sec) {
drawDashboard(false);
}
});
querySec.send(function(response){
if (response.isError()) {
alert('Error in query Sec: ' + response.getMessage() + ' ' + response.getDetailedMessage());
return;
}
console.log('query Sec returned');
ready.sec = true;
data.sec = response.getDataTable();
if (ready.pro && ready.sec) {
drawDashboard(false);
}
});
}
function drawDashboard(complete) {
var pendingCols = [0,2,3,4];
var completeCols = [1,2,3,4,5,7,8];
if (!loaded) {
console.log('first load of data');
var joinProCols=[1,2,3,4];
var joinSecCols=[1,2,3];
joined = new google.visualization.data.join(data.pro, data.sec, 'left', [[0, 0]], joinProCols, joinSecCols);
viewActive = new google.visualization.DataView(joined);
var numCols = joined.getNumberOfColumns();
for (var i=0; i<numCols; i++){
ogColName = joined.getColumnLabel(i).replace(/\W/g, '');
if (empTable.hasOwnProperty(ogColName)){ //passed from server; ommitted def in this example
joined.setColumnLabel(i, empTable[ogColName][lang]);
}
joined.setColumnProperty(i, 'ident', ogColName);
}
}
var pendingRows = joined.getFilteredRows([{column: 4, value: null}]);
var completeRows = joined.getFilteredRows([{column: 4, minValue: ''}]);
if (complete) {
viewActive.setColumns(completeCols);
viewActive.setRows(completeRows);
} else {
viewActive.setColumns(pendingCols);
viewActive.setRows(pendingRows);
}
dashboard = new google.visualization.Dashboard(document.getElementById('dashboard-div'));
// options for displayed table
var tableOpts = {
width: '1500px',
height: '100%',
page: 'enable',
pageSize: 40,
cssClassNames: {
headerCell: 'gviz header',
headerRow: 'gviz header',
oddTableRow: 'gviz odd',
tableRow: 'gviz even',
selectedTableRow: 'gviz selected',
hoverTableRow: 'gviz hover',
rowNumberCell: 'gviz rowNum'
}
};
tableChart = new google.visualization.ChartWrapper({
'chartType': 'Table',
'containerId': 'table-div',
'options': tableOpts
});
var picker1 = new google.visualization.ControlWrapper({
'controlType': 'CategoryFilter',
'containerId': 'sel-pick1',
'options': {
'filterColumnLabel': 'Picker1',
'ui': {
'labelStacking': 'vertical',
'selectedValuesLayout': 'belowWrapping',
'caption': 'Picker1'
}
}
});
var picker2 = new google.visualization.ControlWrapper({
'controlType': 'CategoryFilter',
'containerId': 'sel-pick2',
'options': {
'filterColumnLabel': 'Picker2',
'ui': {
'labelStacking': 'vertical',
'selectedValuesLayout': 'belowWrapping',
'caption': 'Picker2'
}
}
});
//...quite a few more pickers in real code
if (complete) {
var picker3 = new google.visualization.ControlWrapper({
'controlType': 'CategoryFilter',
'containerId': 'sel-pick3',
'options': {
'filterColumnLabel': 'picker3',
'ui': {
'labelStacking': 'vertical',
'selectedValuesLayout': 'belowWrapping',
'caption': 'picker3'
}
}
});
//...A couple more pickers in real app here too
}
// Set up dependencies between controls and charts
dashboard.bind(picker1, picker2);
//...bindings for other pickers
if (complete) {
dashboard.bind(picker2, picker3);
dashboard.bind([picker1, picker2, picker3], tableChart);
} else {
dashboard.bind([picker1, picker2], tableChart);
}
// Draw all visualization components of the dashboard and add listeners
google.visualization.events.addListener(tableChart, 'ready', function(){
google.visualization.events.addListener(tableChart.getChart(), 'page', addFields);
google.visualization.events.addListener(tableChart.getChart(), 'sort', addFields);
google.visualization.events.addListener(tableChart.getChart(), 'select', function(e){
//cancel selection here as it won't be useful
tableChart.getChart().setSelection('');
});
//grab the table and inject our custom actions into it
if (complete) {
addFields(true);
} else {
addFields();
}
// show containers
$('.processing.page').hide();
$('#dashboard-div').show();
loaded = true;
});
dashboard.draw(viewActive);
}
//........................................
$('#dataForm').submit(function( event ) {
if ($('#dataForm').valid()){
$('#dataForm').hide();
$('.processing.data.form').show();
google.script.run.withSuccessHandler(onSuccessUpdate).recordData(this);
}
event.preventDefault();
});
//...................................................
function onSuccessUpdate(response){
var numCols = joined.getNumberOfColumns();
for (var i=0; i<numCols; i++){
for (var key in response.form){
if (!response.form.hasOwnProperty(key)) continue;
if (joined.getColumnProperty(i, 'ident') == key){
joined.setValue(response.row, i, response.form[key]);
}
}
}
tableChart.draw();//This should be updating the values!
$('.message.success').html(response.message).show();
$('#dataForm').show();
$('.processing.data.form').hide();
$('#userProfileDialog').dialog('close');
}
提前感谢任何见解。这个让我发疯了。
答案 0 :(得分:1)
事实证明,一切都正常运作,问题实际上是我错过了一个关于如何运作的关键方面。
在循环中更新连接的dataTable值时,我正在使用setValue()方法更新'value'。
但是,渲染图表实际读取的“值”是可通过setFormattedValue()方法访问的“格式化值”。以编程方式更新值时,似乎没有自动更新formattedValue。我可能错误地认为,或者更可能的是,由于我没有使用它,因此对它的那一部分完全无知。
最初,在使用联接之前,当我只使用单个数据源且一切正常时,我将'options no_format'参数传递给查询。在这种情况下,似乎API会使用值而不是formatValue作为实际的表格图。
一旦我意识到我的错误,我第一次尝试修复就是在我的两个数据查询中添加'options no_format'参数。这似乎不起作用。看起来join方法会自动生成格式化的值,即使它的源dataTables没有。
因此,对我原始代码的更正是除了setValues方法之外还使用setFormattedValue方法,如下所示:
function onSuccessUpdate(response){
var numCols = joined.getNumberOfColumns();
for (var i=0; i<numCols; i++){
for (var key in response.form){
if (!response.form.hasOwnProperty(key)) continue;
if (joined.getColumnProperty(i, 'ident') == key){
joined.setValue(response.row, i, response.form[key]);
//Need to also set formatted value
joined.setFormattedValue(response.row, i, response.form[key]);
}
}
}
tableChart.draw();//This should be updating the values!
$('.message.success').html(response.message).show();
$('#dataForm').show();
$('.processing.data.form').hide();
$('#userProfileDialog').dialog('close');
更新值和格式化值后,图表会在绘制时按预期更新。
所以最后,这只是我的一个错误,但我希望有这个示例代码的人可能有一些价值,以及join方法生成的dataTable的这种特殊行为。