I put together a script for Google Sheets which resets the filters on each sheet (YR5, YR6, YR7 and YR8) and then re-orders the sheets by 3 columns. It's needed as users often leave the sheets filtered and then mistakes are made when entering data as rows are hidden.
The script works as expected, but is there a way to optimise this so it will run any faster? I'm very new to Google Apps script and have put this together from various other people's work.
Thanks :)
This is the script:
function clearFilter(sheet) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var YRS = ['YR5','YR6','YR7','YR8'];
var pos = 0;
for (var i = 0; i <=3; i = i + 1) {
var sheet = ss.getSheetByName(YRS[pos]);
var filter = sheet.getFilter();
var range = filter.getRange();
filter.remove();
range.createFilter();
var filter = sheet.getFilter();
filter.sort(1, true);
filter.sort(4, true);
filter.sort(6, true);
pos = pos + 1
}
SpreadsheetApp.getUi().alert("Ready to edit");
}
I think your best bet is to try checking if the filters have been changed before recreating them. This wouldn't help in cases where every sheet's filter has been changed, though. Using filter.getColumnFilterCriteria(columnPosition) looks helpful for this.
Instead of removing the filter, is it enough to only re-sort the first, fourth, and sixth columns? This could make a reasonable dent in performance. But you'd probably want to call filter.removeColumnFilterCriteria(columnPosition) for each column before re-sorting, though, and I don't know if doing that will be slower than just removing and re-creating a filter.
(Other methods on the filter object)
Finally, I'll mention a couple very minor optimizations that probably won't make a dent, which are removing the 'sheet' parameter and making it a local variable, and removing the pos variable and using i instead for accessing the YRS array.
If the problem is that users leave a sheet in a filtered state, you can avoid the whole issue by instructing them to use filter views instead of a regular filter. This feature lets multiple simultaneous users sort, filter and edit the sheet without disturbing each other.
For additional ease of use, you can insert links in the frozen section of the sheet to easily switch between filter views, instead of having to go to Data > Filter views to switch. See the Filter views example spreadsheet for an illustration.
Related
I'm very new to writing script for Google Sheets, and I'm attempting to create a spreadsheet that will only display a dropdown in a column ("Provisional Notes") if the value in a column ("Certified or Provisional" is "Provisional." If it is "Certified," the user is be able to enter data freely. I'm also wanting to remove the validation if the value changes from "Provisional". I also need the solution to run on the Google Sheets App, as this spreadsheet will be run on an iPad and/or a smartphone.
I've done quite a bit of searching, and only seem to find dropdowns that depend on other dropdowns, rather than leaving a cell blank if the initial dropdown is a certain value.
What I have come up with so far only runs once (even though I've attempted to have it occur on every edit?) Also, if the value changes from Provisional to Certified, the validation does not get removed.
For my practice, I've applied it to only 2 specific cells, rather than the entire 2 columns.
function onEdit2(e){
var dropdownCell = SpreadsheetApp.getActive().getRange('P2');
var certCell = SpreadsheetApp.getActive().getRange('J2');
var rule = SpreadsheetApp.newDataValidation().requireValueInList(['Test 1', 'Test 2'], true).build();
function insertDropdown(){
if(e.certCell.getValue() === "P"){
dropdownCell.setDataValidation(rule);
}
}
}
I greatly appreciate all suggestions!
To delete a data validation you just need to use the method setDataValidation() and set its value to null to delete the data validation present on that cell.
In the following code example, depending on the value inserted I am adding or deleting the data validation. This code example has self explanatory comments:
function onEdit(e) {
// get sheet
var sheet = SpreadsheetApp.getActive().getSheetByName('Sheet1');
// if modifications happen in A
if(e.range.getColumn()==1){
// check inserted value
if(e.range.getValue()=='Provisional'){
// create rule from a list range and set it to the same row in column B
var rule = SpreadsheetApp.newDataValidation().requireValueInList(['A','B']).setAllowInvalid(false).build();
sheet.getRange(e.range.getRow(),2).setDataValidation(rule);
}
// if it is set to certified
if(e.range.getValue()=='Certified'){
// delete any data validation that may have been in its adjacent cell
sheet.getRange(e.range.getRow(),2).setDataValidation(null);
}
}
}
HandsonTable is not rendering all rows - it loads only part of all rows. But when I do Ctrl+A and paste into Excel I see all the rows. Why is Handsontable not displaying all the rows?
<hot-table col-headers="true" row-headers="true" datarows="data" context-menu width="1080">
<hot-column ng-repeat="column in columns" data="{{column.data}}"></hot-column>
</hot-table>
To render all rows, just set renderAllRows: true
The current answer does not answer the original question.
Handsontable does not render all cells at once because it is designed to be efficient for very large data sets. It does this using virtual rendering, dynamically modifying the DOM to include only the cells at the scroll position.
The rows virtual rendering can be disabled by setting renderAllRows: true, as described in the docs: "If typed true then virtual rendering mechanism for handsontable will be disabled." Although it will then not be as efficient for large data sets.
You can also change the number of pre-rendered rows and columns instead of rendering them all. From the performance tips,
You can explicitly specify the number of rows and columns to be rendered outside of the visible part of the table. In some cases you can achieve better results by setting a lower number (as less elements get rendered), but sometimes setting a larger number may also work well (as less operations are being made on each scroll event). Tweaking these settings and finding the sweet spot may improve the feeling of your Handsontable implementation.
This is done by setting viewportRowRenderingOffset and viewportColumnRenderingOffset in the handsontable options. These are by default set to auto which lets handsontable try to find the best value, but may be provided an integer value (e.g. viewportRowRenderingOffset: 70, viewportColumnRenderingOffset: 70).
I had the same problem (using HandsOnTable 6.2.1 and the old AngularJS) and customers would start complaining about not being sure if they were at the end of the table or not.
I was able to create two buttons linked to the functions 'scrollToBeginning' and 'scrollToEnd'. This way the user is sure to be at the last line. Three things specific about my answer:
I expose the functions to the DOM using $scope;
I have an object 'goToLine' holding 3 properties (scrollingToEnd: boolean, row: number, col: number), it is used in other functions not posted here;
I have a list of ID referencing HandsOnTable objects stored in $scope.hots.
Here is my raw solution, feel free to adapt / enhance:
$scope.stopScrollingToEnd = function () {
$scope.goToLine.scrollingToEnd = false;
};
$scope.scrollToBeginning = function (goToLine) {
$scope.stopScrollingToEnd();
const hot = $scope.hots[goToLine.id];
hot.scrollViewportTo(0, 0);
};
/**
* Scroll to the end of the List Element.
* We need this functionality because of a bug in HandsOnTable related to its Virtualization process.
* In some cases (complex table), when manually scrolling, the max row is wrong, hence causing major confusion for the user.
* #param {*} goToLine
* #returns
*/
$scope.scrollToEnd = function (goToLine) {
// We scroll to the first line before going to the last to avoid the bug and being sure we get to the last line
$scope.scrollToBeginning(goToLine);
const hot = $scope.hots[goToLine.id];
var numberOfRows = hot.countRows();
// This variable is used to repeat the scrollViewportTo command.
// It is built using the length of `numberOfRows`.
var repeat = numberOfRows ? 1 * Math.ceil(Math.log10(numberOfRows + 1)) : 1;
// Used in other goTo function to avoid conflict.
$scope.goToLine.scrollingToEnd = true;
// FIXME : not supposed to call scrollViewportTo several times... => fixed in recent versions of HandsOnTable ?
for (let n = 0; n < repeat; n++) {
if (!$scope.goToLine.scrollingToEnd) {
return;
}
setTimeout(function () {
if (!$scope.goToLine.scrollingToEnd) {
return;
}
hot.scrollViewportTo(numberOfRows - 1, 0);
}, 500);
}
};
At our marketing company/agency, we're using a master tracker in Google Sheets to keep track of all paid advertising campaigns that we are handling for our clients. The document is getting longer and longer, and the variety of conditional formatting rules we are using is getting heavy and slow upon any change made to the document.
Five employees are using the document at any given time, making changes to the "STATUS" column upon any change to the campaign – if it is ready to upload, if it is LIVE, if it is paused etc. The conditional formatting simply changes the color of each line based on the value in the "STATUS" column. It also looks at the start/end dates and marks the line red if there is an issue. Etc.
How can I speed up processing using this document? I have created a minified version of our tracker with one line for each conditional formatting rule to make it easy for you to have a look.
I'm sure there are smarter ways to consolidate the rules and/or build a script that can handle the task more easily and more efficiently.
This answer uses a script to change the background color of a row whenever the Status is changed (works for "READY", "LIVE" and "DONE").
Live demo:
https://docs.google.com/spreadsheets/d/1bVwM1wSBVlZTmz5S95RXSrRQxlTKWWN_Hl4PZ81sbGI/edit?usp=sharing
The script is viewable under the "Tools - Script Editor..." menu. It is activated by an "onEdit" trigger (see Is it possible to automate Google Spreadsheets Scripts (e.g. without an event to trigger them)?).
Here is the script itself:
function onEdit(e) {
var STATUS_COL = 18;
var MAX_COLS = 18;
var COLOR_READY = "grey";
var COLOR_LIVE = "#512da8";
var COLOR_DONE = "green";
var activeSheet = SpreadsheetApp.getActiveSheet();
var cell = activeSheet.getActiveSelection();
var val = cell.getValues()[0][0];
var color = null;
switch (val) {
case "READY":
color = COLOR_READY;
break;
case "LIVE":
color = COLOR_LIVE;
break;
case "DONE":
color = COLOR_DONE;
break;
}
if (color != null) {
var row = activeSheet.getRange(cell.getRow(), 1, 1, MAX_COLS);
row.setBackgroundColor(color);
}
}
I had whole rows changing colors depending on some conditions. So I extracted complex formulas from conditional formatting panel into columns on sheet (I got "TRUE" or "FALSE") and referenced those columns in conditional formatting rules. For some reason calculation of conditional formatting formulas is much slower than same calculation inside cells.
I am using data validation rules on a Google Spreadsheet.
In my scenario, I need users to entry only valid values. I use the 'Reject input' to force them to write only validated content.
However, the 'Reject input' option works for manually entried data only, but it does not work if the user pastes content into the cell from a different source (e.g. a MS Excel document). In that case, a warning is still shown but the invalid value is written.
In other words, I need the 'Reject input' option to work also with pasted content.
OR... another approach would be to programmatically check the validity of the value according the Datavalidation rule for that cell.
Any ideas?
Thank you in advance.
I had a little play with this.
I had inconsistent behavior from google.
On occasion when I ctrl-c and ctrl-p, the target cell lost its data validation!
To do this programmatically
Write myfunction(e)
Set it to run when the spreadsheet is edited, do this by Resources>Current Project's Triggers
Query e to see what has happened.
Use the following to gather parameters
var sheet = e.source.getActiveSheet();
var sheetname = sheet.getSheetName();
var a_range = sheet.getActiveRange();
var activecell = e.source.getActiveCell();
var col = activecell.getColumn();
var row = activecell.getRow();
You may wish to check a_range to make sure they have not copied and pasted multiple cells.
Find out if the edit happened in an area that you have data validated;
if (sheetname == "mySheet") {
// checking they have not just cleared the cell
if (col == # && activecell.getValue() != "") {
THIS IS WHERE YOU CHECK THE activecell.getValue() AGAINST YOUR DATA VALIDATION
AND USE
activecell.setValue("") ;
to clear the cell if you want to reject the value
}
}
The obvious problem with this is that it is essentially repeating programmatically what the data validation should already be doing.
So you have to keep two sets of validation rules synchronized. You could just delete the in sheet data validation but I find that useful for providing the user feedback. Also is the data validation you are using provides content it is practical to leave it in place.
It would be great if there was a way of detecting that ctrl-p had been used or one of the paste-special options and only run the script in those cases. I would really like to know the answer to that. Can't find anything to help you there.
Note also that if someone inserts a row, this will not trigger any data validation and the onEdit() trigger will not detect it. It only works when the sheet is edited and by this I think it means there is a content change.
onChange() should detect insertion, it is described as;
Specifies a trigger that will fire when the spreadsheet's content or
structure is changed.
I am posting another answer because this is a programmatic solution.
It has a lot of problems and is pretty slow but I am demonstrating the process not the efficiency of the code.
It is slow. It will be possible to make this run faster.
It assumes that a single cell is pasted.
It does not cater for inserting of rows or columns.
This is what I noticed
The onEdit(event) has certain properties that are accessible. I could not be sure I got a full listing and one would be appreciated. Look at the Spreadsheet Edit Events section here.
The property of interest is "e.value".
I noticed that if you typed into a cell e.value = "value types" but if you pasted or Paste->special then e.value = undefined. This is also true for if you delete a cell content, I am not sure of other cases.
This is a solution
Here is a spreadsheet and script that detects if the user has typed, pasted or deleted into a specific cell. It also detects a user select from Data validation.
Type, paste or delete into the gold cell C3 or select the dropdown green cell C4.
You will need to request access, if you can't wait just copy & paste the code, set a trigger and play with it.
Example
Code
Set the trigger onEdit() to call this or rename it to onEdit(event)
You can attach it to a blank sheet and it will write to cells(5,3) and (6,3).
function detectPaste(event) {
var sheet = event.source.getActiveSheet();
var input_type =" ";
if (event.value == undefined) { // paste has occured
var activecell = event.source.getActiveCell();
if (activecell.getValue() == "") { // either delete or paste of empty cell
sheet.getRange(5,3).setValue("What a good User you are!");
sheet.getRange(6,3).setValue(event.value);
input_type = "delete"
}
else {
activecell.setValue("");
sheet.getRange(5,3).setValue("You pasted so I removed it");
sheet.getRange(6,3).setValue(event.value);
input_type = "paste";
}
}
else { // valid input
sheet.getRange(5,3).setValue("What a good User you are!");
sheet.getRange(6,3).setValue(event.value);
input_type = "type or select";
}
SpreadsheetApp.getActiveSpreadsheet().toast('Input type = ' + input_type, 'User Input detected ', 3);
}
I have 6 textboxes at the top of the screen that update an entire column(one textbox per column) based on any changes. I was selecting the columns based on their class (.l#). Here is the code (issues to follow):
function UpdateField() {
var ctrl = this;
var id = parseInt(ctrl.id.replace("item", ""), 10) - 1;
var bound = [".l1", ".l7", ".l8", ".l9"];
var fields = $(bound[id]);
for (var i = 0; i < fields.length; i++)
{
fields[i].innerHTML = $(ctrl).val();
}
};
which is bound to the keyup event for the text areas. Issues are:
1) initially fields.length was -1 as I didn't want to put data in the "add new
row" section at the bottom. However, when running it, I noticed the
final "real" record wasn't being populated. Also, when stepping through, I
noticed that the "new row" field was before the "last row" field.
2) when doing it this way, it is purely superficial: if I double click the field,
the real data hasn't been changed.
so in the grand scheme of things, I know that I was doing it wrong. I'm assuming it involves updating the data and then forcing a render, but I'm not certain.
Figured out how to do it. Modified the original code this way:
function UpdateField() {
var ctrl = this;
var id = parseInt(ctrl.id.replace("item", ""), 10) - 1;
var bound = ['title1', 'title2', 'title3', 'title4'];
var field = bound[id];
for (var i = 0; i < dataView.getLength(); i++)
{
var item = dataView.getItem(i);
item[field] = $(ctrl).val();
dataView.updateItem(i, item);
}
grid.invalidate();
};
I have 6 textboxes (item1-item6) that "bind" to fields in the sense that if I change data in a textbox, it updates all of the rows and any new rows added also have this data.
Parts where the two issues can be explained this way:
1) to work around that, though still it would be a presentational fix and not a real updating of the underlying data, one could force it to ignore if it had the active class attached. Extra work, and not in the "real" direction one is going for (masking the field).
2) It was pretty obvious with the original implementation (though it was all I could figure out via Chrome Dev Tools that I could modify at the time) that it was merely updating a div's content and not actually interacting with the data underneath. Would look nice, and perhaps one could just pull data from the item1-item6 boxes in place of the column if it is submitted, but if someone attempts to modify the cell, they'll be looking at the real data again.