GAS: Modify Chart Vertical Axis Min and Max Values - google-apps-script

Thanks to #iansedano for roundup() and rounddown() equivalents, and #Tanaike for past posts, I have a function that will calculate Minimum and Maximum Values for the Vertical Axis of a chart. However, there is an error when I try to Modify the chart:
Exception: The parameters (String) don't match the method signature for SpreadsheetApp.Sheet.updateChart
For Modify, I used the docs example at:
Class EmbeddedChart
The Options I found at:
Chart configuration options
I'm not sure, but the issue might be that there can be two Vertical axes, and Left or Right have to be specified somehow?
function test(){
c_ScaleVerticalAxis('Chart', 'Stock SMA3', 'C6:C1263');
}
function c_ScaleVerticalAxis(sheetName, chartTitle, rangeA1) {
// Author: Max Hugen
// Date: 2021-08-09
// Purpose: Modify MinValue & MaxValue of Vert.Axis to suit Value Data
// Params: rangeA1 of Values, eg "C6:C"
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(sheetName);
const range = sheet.getRange(rangeA1);
// Ref: #Tanaike, https://stackoverflow.com/a/64887850/190925
var chart = c_GetChartByTitle(sheetName, chartTitle);
var margin = 0.05; // a margin below/above the min/max data vals
var decPlaces = 0;
// Ref: #Tanaike, https://stackoverflow.com/a/45203334/190925
var ar = Array.prototype.concat.apply([], range.getValues());
var minVal = Math.min.apply(null, ar);
Logger.log('Data minVal: ' + minVal);
if (minVal > 100000) { decPlaces = -5; } else
if (minVal > 10000) { decPlaces = -4; } else
if (minVal > 1000) { decPlaces = -3; } else
if (minVal > 100) { decPlaces = -2; } else
{ decPlaces = -1; }
// Ref: #iansedano, https://stackoverflow.com/a/68710322/190925
minVal = rounddown(minVal*(1-margin), decPlaces);
Logger.log('Chart minVal: ' + minVal);
var maxVal = Math.max.apply(null, ar);
Logger.log('Data maxVal: ' + maxVal);
if (maxVal > 100000) { decPlaces = -5; } else
if (maxVal > 10000) { decPlaces = -4; } else
if (maxVal > 1000) { decPlaces = -3; } else
if (maxVal > 100) { decPlaces = -2; } else
{ decPlaces = -1; }
maxVal = roundup(maxVal*(1+margin), decPlaces );
Logger.log('Chart maxVal: ' + maxVal);
chart = chart.modify()
.setOption('vAxis.minValue',minVal)
.setOption('vAxis.maxValue',maxVal)
.build;
sheet.updateChart(chart);
}

It seems that vAxis.minValue and vAxis.maxValue only works for Continuous Charts.
To set min and max vertical axis for Discrete Charts, use vAxis.viewWindow.min and vAxis.viewWindow.max respectively.
As stated in the configuration options.
Sample Discrete Chart (data column type of string)
Sample Code:
function updateChart(){
var sheet = SpreadsheetApp.getActiveSheet();
var chart = sheet.getCharts()[0];
var newChart = chart.modify().setOption("vAxis.viewWindow.max", 20).setOption("vAxis.viewWindow.min", 5).build();
sheet.updateChart(newChart);
}
Output:
Sample Continuous Chart (data column type to one of: number, date, datetime or timeofday)
Sample Code:
function updateChart1(){
var sheet = SpreadsheetApp.getActiveSheet();
Logger.log(sheet.getName());
var chart = sheet.getCharts()[0];
var newChart = chart.modify().setOption("vAxis.maxValue", 30).setOption("vAxis.minValue", 0).build();
sheet.updateChart(newChart);
}
Output:

Related

How can I parse html table inside a script respecting merged cells

This header is contained in a js file https://www.portaldefinancas.com/js-tx-ctb/th-cdib.js
document.write(""),document.write('</p></caption><thead><tr><th rowspan="4">Mês de<br>Referência</th><th colspan="7">Taxas - %</th></tr><tr> <th rowspan="3">Mensal</th><th colspan="4">Anualizada</th><th colspan="2">Acumulada</th></tr><tr> <th colspan="2">Ano de<br>252 dias<br> úteis</th><th colspan="2">Ano de<br>365/366 dias<br>corridos</th><th rowspan="2">No ano</th><th rowspan="2">Em <br>12 meses</th></tr><tr><th>Dias</th><th> Taxa</th><th>Dias</th><th> Taxa</th></tr></thead><tbody>');
How can I parse the headers respecting the merged rows and merged columns. The script I use today is
function getHeaders(url) {
var source = UrlFetchApp.fetch(url).getContentText()
source = source.split('document')[2]
var table = '<table><tr><th ' + source.match(/(?<=<th ).*(?=th>)/g) + 'th></tr></table>'
table=table.replace(/ê/g,'ê').replace(/ú/g,'ú').replace(/<br>/g,'\n')
var doc = XmlService.parse(table);
var rows = doc.getDescendants().filter(function(c) {
var element = c.asElement();
return element && element.getName() == "tr";
});
var data = rows.slice(0).map(function(row) {
return row.getChildren("th").map(function(cell) {
return cell.getValue();
});
});
return data;
}
but it doesn't respect merged areas. Thanks for any help !
Since intellectual exercises is my my drug of choice... I can't help it. Here is the possible solution. It works to a degree but it shows little the traits of lofty style of coding:
function main() {
var sheet = SpreadsheetApp.getActiveSheet();
var data = getHeaders();
data = handle_rowspans(data);
sheet.getRange(1, 1, data.length, data[0].length).setValues(data);
}
function getHeaders(url) {
// var source = UrlFetchApp.fetch(url).getContentText()
// source = source.split('document')[2]
var source = `<thead><tr><th rowspan="4">Mês de<br>Referência</th><th colspan="7">Taxas - %</th></tr><tr> <th rowspan="3">Mensal</th><th colspan="4">Anualizada</th><th colspan="2">Acumulada</th></tr><tr> <th colspan="2">Ano de<br>252 dias<br> úteis</th><th colspan="2">Ano de<br>365/366 dias<br>corridos</th><th rowspan="2">No ano</th><th rowspan="2">Em <br>12 meses</th></tr><tr><th>Dias</th><th> Taxa</th><th>Dias</th><th> Taxa</th></tr></thead><tbody>`;
source = handle_colspans(source);
table = '<table><tr><th ' + source.match(/(?<=<th ).*(?=th>)/g) + 'th></tr></table>';
table = table.replace(/ê/g, 'ê').replace(/ú/g, 'ú').replace(/<br>/g, '\n');
var doc = XmlService.parse(table);
var rows = doc.getDescendants().filter(function (c) {
var element = c.asElement();
return element && element.getName() == "tr";
});
var data = rows.slice(0).map(function (row) {
return row.getChildren("th").map(function (cell) {
return cell.getValue();
});
});
return data;
}
function handle_colspans(table) {
return table.split('</tr>').map(r => add_cells_in_row(r)).join('</tr>');
function add_cells_in_row(row) {
var cells = row.split('</th>');
for (var i in cells) {
if (/colspan/.test(cells[i])) {
var colspan = cells[i].replace(/.*colspan="(\d+).*/, '$1');
cells[i] += '{col' + colspan + '}';
cells[i] = [cells[i], ...(new Array(+colspan - 1).fill('<th>'))];
}
if (/rowspan/.test(cells[i])) {
var rowspan = cells[i].replace(/.*rowspan="(\d+).*/, '$1');
cells[i] += '{row' + rowspan + '}';
}
}
return cells.flat().join('</th>')
}
}
function handle_rowspans(array) {
for (var row in array) {
for (var col in array[row]) {
if (/\{row/.test(array[row][col])) {
var rowspan = array[row][col].replace(/.*\{row(\d+).*/s, '$1');
for (var r = 1; r < rowspan; r++) array[+row + r].splice(col, 0, '')
}
}
}
return array;
}
It will get you the table like this:
Whrere {row#} and {col#} means how many cells or rows to the left or to the bottom you need to join to the current cell to recreate the original design. It could be the next dose of the intellectual exercises. :)
A solution, example with url = https://www.portaldefinancas.com/js-tx-ctb/th-cdib.js
function getHeaders(url) {
var source = UrlFetchApp.fetch(url).getContentText()
source = source.split('document')[2]
var table = '<table><tr><th ' + source.match(/(?<=<th ).*(?=th>)/g) + 'th></tr></table>'
table=table.replace(/ê/g,'ê').replace(/ú/g,'ú').replace(/<br>/g,'\n')
var doc = XmlService.parse(table);
var rows = doc.getDescendants().filter(function(c) {
var element = c.asElement();
return element && element.getName() == "tr";
});
var n=0
var data=[]
rows.slice(0).map(function(row) {data[n++]=[]})
n=0
rows.slice(0).map(function(row) {
row.getChildren("th").map(function(cell) {
try{nbcols = cell.getAttribute('colspan').getValue()}catch(e){nbcols = 1}
try{nbrows = cell.getAttribute('rowspan').getValue()}catch(e){nbrows = 1}
var value = cell.getValue()
r=0
var free=0
while(r<nbrows*1){
c=0
while(c<nbcols*1){
while(data[n+r][free]!=null){free++}
data[n+r][free]=(value)
value=''
c++
}
r++
}
});
n++
});
return (data);
}

Google Charts - Hide series label too wide

With Google Apps Script I created a stacked bar chart. This is the result:
https://drive.google.com/file/d/1DZ2ZtSu2A81OAMc9ds9A4y-bS0l_oftL/view?usp=sharing
I would like to hide the labels on the bars when they are too wide compared to the available space. Following the instructions I found at this address https://developers.google.com/chart/interactive/docs/reference#DataView_setColumns I tried to use a custom function instead of "stringify" in the "annotationObj" object ( see the code) to create a label of zero length, but my function is not recognized when I try to create the chart (error message: Unknown function "getValueAt").
This is my code:
function CHARTS_002() { //
var ABCarray = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','AA','AB','AC','AD','AE','AF','AG','AH','AI','AJ','AK','AL','AM','AN','AO','AP','AQ','AR','AS','AT','AU','AV','AW','AX','AY','AZ','BA','BB','BC','BD','BE','BF','BG','BH','BI','BJ','BK','BL','BM','BN','BO','BP','BQ','BR','BS','BT','BU','BV','BW','BX','BY','BZ','CA','CB','CC','CD','CE','CF','CG','CH','CI','CJ','CK','CL','CM','CN','CO','CP','CQ','CR','CS','CT','CU','CV','CW','CX','CY','CZ'];
var ssId = '1KA2BnUsC-Lp64UhxjtN5Gtth2dOiHp3-pRwIQjAYOLI';
var shName = 'TopGrossingFilms';
var aScale = ["Poco d'accordo","Né d’accordo né in disaccordo","Abbastanza d'accordo","Totalmente d'accordo","Media"];
var aRange = [['B',2],['N',12]];
var sheet = SpreadsheetApp.openById(ssId).getSheetByName(shName);
var row = aRange[0][1];
var column = ABCarray.indexOf(aRange[0][0]) + 1;
var numRows = aRange[1][1] - aRange[0][1];
var numColumns = ABCarray.indexOf(aRange[1][0]) - ABCarray.indexOf(aRange[0][0]) + 1;
var sheetV = sheet.getRange(aRange[0][1], ABCarray.indexOf(aRange[0][0]) + 1, numRows, numColumns).getValues();
var sheetT1D = sheetV[0];
var aData = [];
for (var r in sheetV) {
aData.push(sheetV[r])
}
for (var r in aData) {
for (var c in aData[r]) {
if (!isNaN(aData[r][c])) {
aData[r][c] = round(aData[r][c],2);
if (aData[0][c] == 'Media') {
aData[r][c] = 13;
}
}
}
}
var data = Charts.newDataTable();
var annotationObj = { calc: "stringify",
//calc: "getValueAt",
//calc: "function(data, row) { var ret = data[row][§col§]; if (ret < 7) {return '';} else {return JSON.stringify(ret)} }",
sourceColumn: -1,
type: "string",
role: "annotation"
}
var aAnnotation = [];
for (var r in aData) {
if (r < 1) { continue; }
if (r == 1) {
for (var c in aData[r]) {
aAnnotation.push(c);
if (isNaN(aData[r][c])) {
data.addColumn(Charts.ColumnType.STRING, aData[0][c]);
} else {
data.addColumn(Charts.ColumnType.NUMBER, aData[0][c]);
if (aScale.indexOf(aData[0][c]) != -1) {
var myObj = JSON.parse(JSON.stringify(annotationObj));
var myCol = Number(c);
if (aData[0][c] == 'Media') {
myCol = Number(c) + 1;
}
myObj.sourceColumn = myCol;
myObj.calc = myObj.calc.replace("§col§",myCol)
aAnnotation.push(myObj);
}
}
}
}
data.addRow(aData[r]);
}
var dataViewDefinition = Charts.newDataViewDefinition().setColumns(aAnnotation);
var aTicks = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
var chartBuilder = Charts.newBarChart()
.setDimensions(1200, 700)
.setOption("hAxis", { ticks: aTicks})
.setOption('legend',{ position:'top', maxLines:3 })
.setOption('chartArea',{ left:650 })
.setOption('series',{
6: {type:'line', color:'00FF00', lineWidth:3, visibleInLegend: false}
})
.setDataTable(data)
.setDataViewDefinition(dataViewDefinition)
.setOption('bar', { groupWidth: "80%" })
.setStacked()
.setColors(['#C10000','#F1C12A','#BFBFBF','#0070C1','#244062','00FF00']);
var chart = chartBuilder.build();
var chartImage = chart.getAs('image/png').copyBlob();
DriveApp.createFile(chartImage).setName('NewBarChart.png');
}
function getValueAt(column, dataTable, row) {
var value = dataTable(row, column);
var ret = '';
if (value > 7) { ret = value.toString()}
return ret;
}
function round(value, exp) {
var funcName = 'round';
if (typeof exp === 'undefined' || +exp === 0)
return Math.round(value);
value = +value;
exp = +exp;
if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0))
return NaN;
// Shift
value = value.toString().split('e');
value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp)));
// Shift back
value = value.toString().split('e');
return +(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp));
}
The chart that is produced can be seen in this public folder: https://drive.google.com/file/d/1DZ2ZtSu2A81OAMc9ds9A4y-bS0l_oftL/view?usp=sharing
Does anyone know how to get the result I would like to get?
Thanks in advance.

How to change the Height of Multiple rows at Once?

How do I change the height of multiple rows at once via Google Apps Script?
Attempt:
function resizeHeight() {
  var s = SpreadsheetApp,
      ui = s.getUi(),
  sh = s.getActiveSheet(),
curntRow = s.getActiveRange().getRow(),
rowsMany = s.getActiveRange().getNumRows(),
autoRangeRows = s.getActiveRange().getA1Notation().replace(/[a-z]/gi,""),
  getVal = ui.prompt('⿱⇕', 'Change the height of row❓   Cancel. Back to default❗  (21)', ui.ButtonSet.OK_CANCEL),
  Btn = getVal.getSelectedButton(), Txt = getVal.getResponseText();
for (var i=curntRow; i<=rowsMany+1; i++) {
if (Btn == 'OK') {
if (autoRangeRows.search(":") == -1) {sh.setRowHeight(curntRow, Txt);}
else {sh.setRowHeight(i, Txt);}
}
if (Btn == 'CANCEL') {
if (autoRangeRows.search(":") == -1) {sh.setRowHeight(curntRow, 21);}
else {sh.setRowHeight(i, 21);}
}
}
}
Problem:
The script is not very effective they only work on the part or a few rows but not on all rows can be implemented.
This function works: You just have to pick an active range to select the rows that you want to change the height of and then give it a height in pixels in the prompt.
function changeRowHeight()
{
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sht=ss.getActiveSheet()
var rng=sht.getActiveRange();
var row=rng.getRow();
var numrows=rng.getNumRows();
var resp=SpreadsheetApp.getUi().prompt('Get Row Height', 'Enter Row Height in Pixels', SpreadsheetApp.getUi().ButtonSet.OK);
var height = Number(resp.getResponseText());
for(var i=0;i<numrows;i++)
{
sht.setRowHeight(row + i, height)
}
}

How can I check if a range is in a named range?

I need to check if the edited cell in an onEdit function is part of a named range. Something like this in Excel VBA:
If Not Intersect(Edited Cell, Range("NamedRange")) Is Nothing Then ...
Any ideas? Thanks in advance!
Something fairly rudimentary, at least, will give you ideas:
function _check_rows(range, namedRange) {
var _result = false,
_range_row_begin,
_range_row_end,
_namedRange_row_begin,
_namedRange_row_end;
if (range && namedRange) {
_range_row_begin = range.getRow(),
_range_row_end = range.getLastRow(),
_namedRange_row_begin = namedRange.getRow(),
_namedRange_row_end = namedRange.getLastRow(),
_result = _range_row_begin >= _namedRange_row_begin && _range_row_end <= _namedRange_row_end;
}
return _result;
}
function _check_columns(range, namedRange) {
var _result = false,
_range_column_begin,
_range_column_end,
_namedRange_column_begin,
_namedRange_column_end;
if (range && namedRange) {
_range_column_begin = range.getColumn(),
_range_column_end = range.getLastColumn(),
_namedRange_column_begin = namedRange.getColumn(),
_namedRange_column_end = namedRange.getLastColumn(),
_result = _range_column_begin >= _namedRange_column_begin && _range_column_end <= _namedRange_column_end;
}
return _result;
}
function _setNamedRange(name) {
var _ss = SpreadsheetApp.getActiveSpreadsheet();
_ss.setNamedRange(name, _ss.getRange('Sheet 1!C3:D6'));
}
function Intersect(range, namedRange) {
var _result = false,
_ss,
_sheet,
_range,
_namedRange;
if (range && namedRange) {
_ss = SpreadsheetApp.getActiveSpreadsheet(),
_sheet = _ss.getActiveSheet(),
_namedRange = _ss.getRangeByName([_sheet.getName(), '!', namedRange].join('')),
_range = _sheet.getRange(range),
_result = _check_rows(_range, _namedRange) && _check_columns(_range, _namedRange);
}
return _result;
}
function test_Intersect() {
var _currentCell = 'C4',
_namedRange = 'NamedRange'; // NamedRange = 'Sheet 1!C3:D6';
_setNamedRange(_namedRange);
if (!Intersect(_currentCell, _namedRange))
Logger.log('TRUE');
else
Logger.log('FALSE');
}
This function will return the indices of a cell in a named range or otherwise return -1s as the indices. Notice that it returns an array with two values representing the row and column indices in the named range.
const indexOfCellInNamedRange = (cell, range_name) => {
const namedRange = SpreadsheetApp.getActiveSpreadsheet().getRangeByName(range_name);
if(cell.getSheet().getName() !== namedRange.getSheet().getName()) return [-1, -1]
const start_row = namedRange.getRow();
const end_row = start_row + namedRange.getNumRows() - 1;
const start_col = namedRange.getColumn();
const end_col = start_col + namedRange.getNumColumns() - 1;
const row_index = Math.max( cell.getRow() - start_row, -1);
const col_index = Math.max(cell.getColumn() - start_col, -1);
if(row_index > end_row)return [-1, -1]
if(col_index > end_col)return [-1, -1]
return [row_index, col_index]
}

Progressbar in angular

I want to make Progressbar in Angular.js in decimal format, simple format, times based Progressbar. Someone could pls help !
E.g.
Start Timer {{ counter }}/{{ max }} = {{ (counter/max)*100 }}%
Start Timer 20/30 = 66.66666666666666%
Here is example.js:
angular.module('plunker', ['ui.bootstrap']);
var ProgressDemoCtrl = function ($scope) {
$scope.max = 200;
$scope.random = function () {
var value = Math.floor((Math.random() * 100) + 1);
var type;
if (value < 25) {
type = 'success';
} else if (value < 50) {
type = 'info';
} else if (value < 75) {
type = 'warning';
} else {
type = 'danger';
}
$scope.showWarning = (type === 'danger' || type === 'warning');
$scope.dynamic = value;
$scope.type = type;
};
var app = angular.module('plunker', ['ui.bootstrap']);
app.controller('ProgressDemoCtrl', function ($scope) {
$scope.value = 40;
$scope.state = "progress-bar-success";
$scope.myStyle = {width: $scope.value + '%'};
});
$scope.random();
$scope.randomStacked = function() { $scope.stacked = [];
var types = ['success', 'info', 'warning', 'danger']; for (var i = 0, n = Math.floor((Math.random() * 4) + 1); i < n; i++) { var index = Math.floor((Math.random() * 4));
$scope.stacked.push({ value: Math.floor((Math.random() * 30) + 1),
type: types[index] }); } }; $scope.randomStacked(); };
var app = angular.module('progressApp',['nprogress']); var MainCtrl = function($scope,ngProgress){ }
I use this round progress ba directive, works pretty well:
http://www.youtube.com/watch?v=w2qrYL0Le24
https://github.com/angular-directives/angular-round-progress-directive
If you need a rectangular one give me a buzz I, have a custom directive implemented.
If you need two decimal numbers you only have to adjust the font size.
Test with two decimals:
Code to change (configuring ang:roundprogress directive)
data-round-progress-label-font="80pt Arial"
Whole markup
<div ang:round:progress data-round-progress-model="roundProgressData"
data-round-progress-width="500"
data-round-progress-height="500"
data-round-progress-outer-circle-width="40"
data-round-progress-inner-circle-width="10"
data-round-progress-outer-circle-radius="200"
data-round-progress-inner-circle-radius="140"
data-round-progress-label-font="80pt Arial"
data-round-progress-outer-circle-background-color="#505769"
data-round-progress-outer-circle-foreground-color="#12eeb9"
data-round-progress-inner-circle-color="#505769"
data-round-progress-label-color="#fff"></div>