I'm using handsontable as more or less a gui for a separate backend datastore that uses lokijs. My lokijs datastore has the row, column, the datum, and some metadata.
The user can take a few actions that adjust the db and reloads the page. I have a function that converts the lokijs store to a 2d array and calls loadData on it, which refreshes the page the users see.
The problem: users can mark a cell as 'invalid', which turns the cell red and updates the lokijs db with an 'invalid = true' boolean. Users can also hide columns. On the backend this refreshes handsontable by creating a new 2d array that excludes the selected column and calls loaddata.
However, hiding cells won't play nice with column colors. The way I color cells is like so:
cells: function (row, col, prop) {
var cellProperties = {};
var cell = cells.findOne({ "col": {"$eq": col}, "row": {"$eq": row}});
if (cell.invalid === true) {
cellProperties.renderer = highlightRowRenderer; //colors red
}
return cellProperties;
You may be able to see the problem here -- if a column is hidden, there's an off by one error on highlighting, so a different cell gets hidden on the highlight.
Now a way to fix this is on load data, pass highlighting information in as metadata. Unfortunately I can't figure out how to do this. I'd imagine it looks something like this:
var data =
[
[ { data: 5, color: red }, { data: 7, color: blue} ],
[ { data: 3, color: green}, { data: 2 } ]
];
hot.loadData(data);
but I'm not sure what it is exactly. There appears to be a similar concern addressed here but it only seems to handle non-dynamically sized tables.
Any suggestions would be very much appreciated!
As of 1/2/18, this is not possible :(
Related
This is more of an architectural issue, but basically, I have a server-side React app that renders a bunch of charts and tables with page breaks in-between, so that a puppeteer instance can open the page then print, and send that printed report back to the user in another app.
I need to be able to take some data that is normally rendered into a table format on this app, but make it printable so that the data extends as far as possible before a page break is required, then renders a new table past the page break (so it appears on a new page when printing), and continues until all of the data is rendered into the report. Essentially, I need pagination on a table, without the user interaction that pagination usually comes with.
The thing I'm struggling with is that the length of the data is dynamic, and so are the widths and heights of the rows.
Any suggestions on how to tackle this? The only thing I can think of so far is to basically hide the table, and measure the height of it after every row is attached, and compare that to the max height (the height of a standard letter size in pixels), and if it exceeds it, remove the row, add a page break, then start a new table.
Thanks in advance.
EDIT:
FYI, The solution mentioned here doesn't apply: How to apply CSS page-break to print a table with lots of rows?
This needs to be an entirely new table because I have custom headers and footers that are going above and below it (showing metadata like the name of the chart, how many rows are shown out of how many total, etc.), so it can't just be one continuous table that's split.
Here's a codepen with a shell of what I'm trying to do. If you open in debug view, and print it, you'll see in the print preview that the table is split up across two pages, but the footer I created will only be on the second page (where it needs to be on both pages, after the table). Additionally, the footer needs to display the dynamic count of rows that were able to fit on the page, so it can't be a static part of the table as a tfoot element. https://codepen.io/nicholaswilson/pen/GRWNzMa
So I'm trying to figure out now if I can mount the table to the DOM, but hide it, and calculate the height as I add rows to it so I can try my original method above. But I'm also open to other suggestions.
Alright, I think I got it. Still needs some tweaking (and there are probably more performant ways to do it) but this is my concept: https://codepen.io/nicholaswilson/pen/abJpLYE. Currently I'm splitting the tables after they've exceeded the height, but I'll be fixing that later. The concept is here.
Basically, the idea is to build a 4D array to represent the instances of tables that need to be rendered. Then in componentDidMount() and componentDidUpdate(), I can add new tables to the state as needed:
componentDidMount() {
const { tableData } = this.props;
if (this.state.currentRowIndex === 0) {
// just starting out
this.setState((state, props) => {
const tables = state.tables;
tables.push([tableData.data[state.currentRowIndex]]); // push first new table and first row
return {
tables,
currentRowIndex: state.currentRowIndex + 1,
currentTableIndex: 0
};
});
}
}
componentDidUpdate() {
const { tableData } = this.props;
if (this.state.currentRowIndex < tableData.data.length) {
this.setState((state, props) => {
const tables = state.tables;
const currentTableHeight = this.tableRefs[this.state.currentTableIndex]
.clientHeight;
console.log(
`Table ${this.state.currentTableIndex} height: ${currentTableHeight}`
);
if (currentTableHeight > MAX_TABLE_HEIGHT_IN_PIXELS) {
// push a new table instead based on this condition
tables.push([tableData.data[state.currentRowIndex]]);
return {
tables,
currentRowIndex: state.currentRowIndex + 1,
currentTableIndex: state.currentTableIndex + 1
};
} else {
tables[state.currentTableIndex].push(
tableData.data[state.currentRowIndex]
); // push new row to existing table
return {
tables,
currentRowIndex: state.currentRowIndex + 1,
currentTableIndex: state.currentTableIndex
};
}
});
}
}
See the codepen for the rest of the implementation.
I have a vlookup formula looking for names and returning the average for that person. I record these averages throughout the week, and identify them as W1, W2, W3, etc. in different columns. The problem is, the data I import only has ONE average on it, meaning that if I import it, it will override what I had already put in a few days ago using the same report, just an older version.
My question is, is there a way for me to stop the formula for W1 from updating when the data it is calling on changes? Essentially, to freeze the values? That way I don't have to keep adding new tabs to import the new data in an effort to save the history. The data is robust, and I am gathering more info than just averages, so I need the whole thing.
I wouldn't mind scripting something too that would help solve my issue, I just need some guidance.
Edit: thoughts on scripting a menu button that would copy, paste values only of a selected range that I could trigger right before I update the data?
I want to be able to select a range in my sheet, select the menu "Save Info", have it copy what I selected, and paste it right back where it already was. This will remove the formulas that would otherwise cause my values to change upon the new data import, and leave my history intact.
The following is what I have come up with so far, but I receive this error:
"TypeError: sourceSheet.getDataRange is not a function".
function saveInfo(){
var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var sourceSheet = spreadSheet.getActiveSheet
var sourceRange = sourceSheet.getDataRange();
var targetSheet = spreadSheet.getActiveSheet();
sourceRange.copyTo(targetSheet.getActiveRange());
}
function updateMenu() {
SpreadsheetApp.getActiveSpreadsheet().updateMenu('Save Info', generateMenu())
};
function onOpen() {
SpreadsheetApp.getActiveSpreadsheet().addMenu('Save Info', generateMenu());
};
function generateMenu() {
var entries = [{
name: "Save Data Before Update",
functionName: "saveInfo"
}];
return entries;
}
Any ideas??
Thanks!
Here you go:
function saveInfo() {
var range = SpreadsheetApp.getActiveSheet().getActiveRange();
var values = range.getDisplayValues();
range.setValues(values);
}
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Save Info')
.addItem('Save Data Before Update', 'saveInfo')
.addToUi();
}
The script converts data inside selected cells into a plain text.
But actually you don't need any script. You can just select the cells, press Ctrl+C and Ctrl+Shift+V.
Weird, inconsistent problem I am running into when using Chrome's built in search function. I have some 250 lines of data to be rendered in a handsontable, more than can be displayed on your screen without scrolling or zooming out.
http://jsfiddle.net/hU6Kz/3723/
var myData = [
["", "Kia", "Nissan", "Toyota", "Honda"],
["lots of data begins here"],
];
$("#exampleGrid").handsontable({
data: myData,
startRows: 5,
startCols: 5,
minSpareCols: 1,
minSpareRows: 1,
rowHeaders: true,
colHeaders: true,
contextMenu: true
});
Observe that when you first pull up the page, you can scroll down and all the data is rendered in the handsontable.
Now hit control + f to pull up the Chrome's built in search function. Search for any character in the handsontable. Much of the data in the handsontable is no longer rendered! Occasionally the data will get rendered again if I search for something else, but it seems inconsistent and I can't find a common cause..
This does not seem to be a problem in firefox, but my company is decidedly in the Chrome camp. Help me, o wizards of the internet.
This is because Handsontable uses a wonderful technique called "Virtual Rendering" which only renders the rows you are currently looking at plus a few more. It makes it possible to display "infinitely" many rows. The problem with ctrl+f is that it searches the HTML text so you won't be able to search using this.
This is why there is a search plugin available which returns to you a list of all matching cells. From there you can do many things like on enter, scrollTo the next available matching cell (search). Another very famous application is to filter rows by recreating the table with less data (filter).
Here there is a working demo using the search feature of handsontable.
In the search input write Test and then hit Enter.
This is the method that does what I described above.
var searchField = document.getElementById('search_field');
Handsontable.Dom.addEvent(searchField, 'keyup', function (event) {
if (event.keyCode == 13) {
var queryResult = hot.search.query(this.value);
hot.selectCell(queryResult[0].row, queryResult[0].col);
}
});
http://jsfiddle.net/pdivasto/hp8ge8kk/
I have a grid and a store. The store gets populated with some data - country - while the rest is empty. Once I receive more inputs I fill up the store with the inputs.
Assume that I have only 1 country in the store after the initial load.
Store:
feedTheGrid = new Ext.data.JsonStore({
autoDestroy : true,
autoSave : false,
autoLoad : true,
idProperty: 'country',
root: 'root',
totalProperty : 'totalcount',
fields : ['country', 'towns'],
proxy : new Ext.data.HttpProxy(new Ext.data.Connection({
url : "country/searchCountries.action",
method : 'POST',
timeout : 300000
}))
});
So the user inputs towns. I save a town in the array towns using push():
listeners: {
'change': function() {
if (feedTheGrid.getAt(0).towns === undefined) {
feedTheGrid.getAt(0).towns = [];
}
feedTheGrid.getAt(0).towns.push(this.getValue());
gridMonster.getView().refresh();
}
}
I want the grid to update every time a new town is put in.
This is part of the column model I use for gridMonster:
{ header: "Town 1", width: 150, dataIndex: 'towns',
renderer: function(val) {
if (val[0] != undefined) {
return val[0];
}
}, sortable: true},
{ header: "Town 2", width: 150, dataIndex: 'towns',
renderer: function(val) {
if (val[1] != undefined) {
return val[1];
}
}, sortable: true},
{ header: "Town 3", width: 150, dataIndex: 'towns',
renderer: function(val) {
if (val[2] != undefined) {
return val[2];
}
}, sortable: true}
The issue is that after a town is put in the grid never refreshes.
Some things I noticed so far:
The store gets updated with the new value when I check through Firebug. Also it seems the val that is given in renderer is always empty String, even after I push the new value into the array and refresh the grid view.
I am using ExtJS 3.3.1
I know its old so I am open to solutions that would work for 4.0+ and I can try to apply it to 3.3.1.
EDIT: Found the issue
Very subtle problem, spent hours trying to find what is the problem only to find out I was looking in the wrong place. The issue was in this line:
feedTheGrid.getAt(0).towns.push(this.getValue());
This line actually creates a new field towns as part of the record that you get using getAt(0). Using firebug to see if getAt(0).towns[0] is populated actually returns the value you pushed which is misleading.
ExtJS is not aware of this new field and it shouldn't be.
This line should be:
feedTheGrid.getAt(0).data.towns.push(this.getValue());
The trick is to point at the right place: .data. Javascript/ExtJS allows you to add new field without any warning because technically you are not doing anything wrong. Logically it creates a new field that does nothing.
#Lorenz Meyer I am able to display elements of the array using renderer while pointing at towns array in the column model.
There is a major misunderstanding about what a store and a grid are in ExtJs.
Both are just a representation of tabular data. A store is the representation in memory of a table, in order to get fast access to records filtering and more features. A grid is the visual representation of a table in the browser window.
Such a table could look like :
Id | Country | City
-------------------
1 | US | NYC
2 | France | Macon
3 | US | Macon
First of all on one table row, you can only have one city. In tables, you cannot have an array in a single field. If you have more than one city, this is what rows are for.
Then, your idProperty cannot be the country. If it were, this would mean that countries are unique. Obviously, they aren't. There can be more than one city in a country. (cities cannot be the idProperty neither, because there exists more than one city with the same).
Now you certainly begin to understand, that feedTheGrid.getAt(0).towns.push(this.getValue()); makes no sense whatever. The towns column cannot be an array. You insert a new row with feedTheGrid.add({country: 'US', towns: 'LA'}).
At best the towns column can be a comma separated value, in which case, you update the store with
var record = feedTheGrid.getAt(0),
currentTowns = record.get('towns');
record.set('towns', currentTowns + ', ' + value)
The method set of the record does all the magic: It passes the update event to the grid in order to rerender it. Your push in addition to be wrong will not trigger the events necessary to update the view.
About the grid: The columns of your grid should be:
{ header: "Id", width: 150, dataIndex: 'id', sortable: true},
{ header: "Country", width: 150, dataIndex: 'country', sortable: true},
{ header: "Town(s)", width: 150, dataIndex: 'towns', sortable: true}
Notes:
That listeners: looks really strange to me, but maybe it's just because the bigger picture is missing.
feedTheGrid is a strange name for a store. The store does not feed the data. It is tha data.
A renderer alway must return a value, else the cell will remain blank. Therefore your renderers cannot work.
In your renderers the val can not be an array. It's a simple variable. That's why your grid is not refreshing.
Very subtle problem, spent hours trying to find what is the problem only to find out I was looking in the wrong place. The issue was in this line:
feedTheGrid.getAt(0).towns.push(this.getValue());
This line actually creates a new field towns as part of the record that you get using getAt(0). Using firebug to see if getAt(0).towns[0] is populated actually returns the value you pushed which is misleading.
ExtJS is not aware of this new field and it shouldn't be.
This line should be:
feedTheGrid.getAt(0).data.towns.push(this.getValue());
The trick is to point at the right place: .data. Javascript/ExtJS allows you to add new field without any warning because technically you are not doing anything wrong. Logically it creates a new field that does nothing.
#Lorenz Meyer I am able to display elements of the array using renderer while pointing at towns array in the column model.
I'm setting up a way to be able to enlarge small-ish charts on my webpage by un-hiding a page 'overlay' div and creating a secondary, larger chart inside it with the same data as the chart which was clicked on.
function DrawSmallChart() {
chart_data = [1, 2, 3, 4, 5];
LargeChartOptions = {
series: [{
name: 'Data',
data: chart_data
}],
};
SmallChartOptions = {
series: [{
name: 'Data',
data: chart_data
}],
events: {
click = function (e) { DrawLargeChart(LargeChartOptions); }
}
};
$('#smallchart-div-container').highcharts(SmallChartOptions);
}
function DrawLargeChart(options) {
chart_container_div = document.getElementById("graph-div-id");
chart_container_div.style.display = ""
$('#graph-div-id').off();
$("#graph-overlay-graph").highcharts('StockChart', options);
}
I have another function that hides this div when I click a button.
The first time I click the small graph when the page loads, the big graph shows up fine with all the data. The second time I click it, the graph shows but with no data.
I've used the debugger to flick through what is happening and I've found exactly what the problem is, but I can't figure out how to solve it.
The first time I click the graph, the DrawLargeChart function is called with options.series = <Array containing my series object with chart_data>. The second time, DrawLargeChart is called with options.series = null.
When I refresh the page it is the same - first click works, subsequent clicks don't. I suspect it has something to do with the chart_data variable...
Any help would be greatly appreciated
EDIT 1:
After some more debugging, it is clear that the options object which is passed to DrawLargeChart() is not the same in the first click versus subsequent clicks. There is nothing in my code which is changing the LargeChartOptions structure
EDIT 2:
I figured out that this is a pass by value / pass by reference error. LargeChartOptions is being passed in by reference which no longer exists after the first click. Is there a way to pass it by value? I'd rather not have to type out the LargeChartOptions (much bigger than I've typed up here) into the function parameter in case I change anything in future
EDIT 3 // I've figured it out:
I figured out what the problem is. The $(target).highcharts(options) function actually modifies the options object and sets options.series = null
The solution
I modified the DrawLargeChart function to create a local copy of options by using options_buffer = $.extend(true,{},options);
function DrawLargeChart(options) {
chart_container_div = document.getElementById("graph-div-id");
options_buffer = $(target).highcharts(options);
chart_container_div.style.display = ""
$('#graph-div-id').off();
$("#graph-overlay-graph").highcharts('StockChart', options_buffer);
}
By creating options_buffer, the highcharts function cannot modify LargeChartOptions (because options is just a reference to that variable - yay Javascript)
I found the answer (in original post but copied here):
I modified the DrawLargeChart function to create a local copy of options by using options_buffer = $.extend(true,{},options);
function DrawLargeChart(options) {
chart_container_div = document.getElementById("graph-div-id");
options_buffer = $(target).highcharts(options);
chart_container_div.style.display = ""
$('#graph-div-id').off();
$("#graph-overlay-graph").highcharts('StockChart', options_buffer);
}
By creating options_buffer, the highcharts function cannot modify LargeChartOptions (because options is just a reference to that variable - yay Javascript)