I am working on a tool for manual classification which changes the property of certain dot(color in my case) chosen in a scatter plot by bokeh. I changed the source data in callback by s.data = d2 and s.change.emit() but both failed. I thought such operation will change source.data, but when I print source.data, actually nothing happens.
The dots' color in the plot changes as expected though.
Here is my related code:
DF = pd.read_csv(csv_path)
s = ColumnDataSource(DF_file)
p = figure(plot_width=500, plot_height=500, tooltips=TOOLTIPS,tools="lasso_select, tap", title="manual classification")
circles = p.circle('x', 'y', color='color', size=10, source=s, line_alpha=0.6,fill_alpha=0.6)
s.callback = CustomJS(args=dict(s1=s), code="""
var inds = cb_obj.selected.indices;
var d1 = s1.data;
for (var i = 0; i < inds.length; i++)
{d1['color'][inds[i]] = 'green';}
s1.change.emit();
""")
Both print(s.data) and the csv file saved from s.to_csv(xxx) shows no change to the original input data.
Also, I wonder how does callback work to change the plot's data while leave the data in python unchanged when the data in python is the data passed to it in args=(s1=s).
I have searched for some possible methods and found this answer in https://discourse.bokeh.org/t/getting-selected-glyph-data-properties-via-customjs/4311/5?u=1113
when the Bokeh JS object is instantiated it uses the Python objects as sources for data but then is essentially disconnected from them - so updates to the JS model are not propagated back to their Python parents.
While the discussion in this webpage also proposed a workaround using IPython.notebook.kernel.execute to create or overwrite a variable in the Python. It can only be used in a notebook frontend(I found this workaround only works when use output_notebook() in the code).
Then here is my new code to change the original data in python:
s.callback = CustomJS(args=dict(s1=s1), code="""
var inds = s1.selected.indices;
var d1 = s1.data;
for (var i = 0; i < inds.length; i++)
{
d1['color'][inds[i]] = 'red';
var index = inds[i];
var command = "s1.data['color'][" + index + "] = red";
var kernel = IPython.notebook.kernel;
kernel.execute(command);
}
s1.change.emit();;
""")
Related
I've been using this function to erase certain rows depending on the name of a cell in a column, for some reason it works, but after it finishes it just gives me an error message.
Exception: Service Spreadsheets failed while accessing document with id #################################################.
function erase() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var range = 'J:J';
var object = 'Delete';
var name = 'Roster';
var sheet = spreadsheet.getSheetByName(name);
var row = sheet.getRange(range);
var value = row.getValues();
for (var i = value.length - 1; i >= 0; i--)
if (value[0, i] == object)
sheet.deleteRow(i + 1);
}
This looks to be a known issue from Google's side which has been reported on Issue Tracker. I suggest you star the issue from here and eventually add a comment saying that you are affected by it.
As a possible workaround, you can try adding SpreadsheetApp.flush() before this line here var row = sheet.getRange(range);.
I'm not familiar with google sheets specifically, but just with general syntax knowledge, I feel like the issue would surround, var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();. Are you sure you arn't supposed to put anything inside the getActiveSreadshee(-here-)??
I created a chart with the help of amCharts that shows temperature from the examples on their site. The chart displayed correctly.
Now I am getting the temperature from a database with C# and I am trying to pass the value to the function where temperature was hard coded, so I get dynamic values. However, I just get the chart and the needle is still at 0 and does not show the temperature.
I have tried 3 ways so far:
I used a hidden field, assigned the value to hidden field in C# and called the JavaScript function showing the charts. It only shows chart. The needle does not change.
I used script manager and Web API, (I don't know Web API and just used code on the Internet), it is the same result however the function in c# keeps on being continuously called.
I put the entire amCharts code in a JavaScript function . I got values in c# and then used
String script = "window.onload = function() { UpdateTemp('" + dt.Rows[0][0].ToString()+ "'); };";
ClientScript.RegisterStartupScript(this.GetType(), "UpdateTemp", script, true);
which again shows same result, map is shown needle stays on 0.
This is my code for the 3rd approach:
ASPX page, JavaScript function
<script>
function UpdateTemp(temp) {
am4core.ready(function() {
// Themes begin
am4core.useTheme(am4themes_animated);
// Themes end
// create chart
var chart = am4core.create("chartdiv", am4charts.GaugeChart);
chart.hiddenState.properties.opacity = 0; // this makes initial fade in effect
chart.innerRadius = -25;
var axis = chart.xAxes.push(new am4charts.ValueAxis());
axis.min = 0;
axis.max = 100;
axis.strictMinMax = true;
axis.renderer.grid.template.stroke = new am4core.InterfaceColorSet().getFor("background");
axis.renderer.grid.template.strokeOpacity = 0.3;
var colorSet = new am4core.ColorSet();
var range0 = axis.axisRanges.create();
range0.value = 0;
range0.endValue = 50;
range0.axisFill.fillOpacity = 1;
range0.axisFill.fill = colorSet.getIndex(0);
range0.axisFill.zIndex = - 1;
var range1 = axis.axisRanges.create();
range1.value = 50;
range1.endValue = 80;
range1.axisFill.fillOpacity = 1;
range1.axisFill.fill = colorSet.getIndex(2);
range1.axisFill.zIndex = -1;
var range2 = axis.axisRanges.create();
range2.value = 80;
range2.endValue = 100;
range2.axisFill.fillOpacity = 1;
range2.axisFill.fill = colorSet.getIndex(4);
range2.axisFill.zIndex = -1;
var hand = chart.hands.push(new am4charts.ClockHand());
// using chart.setTimeout method as the timeout will be disposed together with a chart
chart.setTimeout(randomValue, 2000);
function randomValue(temp) {
hand.showValue(temp, 1000, am4core.ease.cubicOut);
chart.setTimeout(randomValue, 2000);
}
}); // end am4core.ready()
};
</script>
C# function to get temperature and call JavaScript function
public void BindGrid(String charttype)
{
string constring = "Data Source=********.DOMAIN.ORG01;Initial Catalog=Temperature;Integrated Security=SSPI;";
using (SqlConnection con = new SqlConnection(constring))
{
using (SqlCommand cmd = new SqlCommand("SELECT Temperature,HighestPoint,LowestPoint FROM Temperature", con))
{
cmd.CommandType = CommandType.Text;
con.Open();
DataTable dt = new DataTable();
dt.Load(cmd.ExecuteReader());
temp1.Value = dt.Rows[0][0].ToString();
con.Close();
String script = "window.onload = function() { UpdateTemp('" + dt.Rows[0][0].ToString()+ "'); };";
ClientScript.RegisterStartupScript(this.GetType(), "UpdateTemp", script, true);
}
}
}
The quality code should be revised but I managed to make it work.
I made 2 changes:
1 : I used the method "PageLoad" instead of "BindGrid" (do you really need this one ?)
2 : I filled the raw temperature value directly as a parameter (as it is a float on my side), you englobed your datarow value with simple quote, that's interpreted as string by javascript and the method "showValue" from "hand" seems to not tolerate it.
Code:
Result:
Some ideas to improve your code:
Put your connection string in configuration file (never in code !)
If you use the "using" do not matter calling the method "close" of your resource, it's automatically called by it. (behind the using it's in fact a try-catch-finally, the close is called anyway in the finally ;) )
If possible separate the data access from the rendering page, create a separate class that manages it, it will improve readability and evolution of your code.
If the variable "temp1" is not used, just remove it.
When you get a data (no matter the source, could be a webservice or database or else), check always if it's null before access it and log it, it doesn't cost a lot and you avoid escalation of exceptions.
We are in 2019, you could use Dapper to get directly object as result from your queries instead of a generic datarow. (check the website if interested they have a lot of interesting tutorials).
Kr,
Ali
I have looking into this issue for a while, but have yet to find a suitable answer (most involve switching to setChoiceValues() rather than addressing the "Cannot convert Array to Choice[]" issue with setChoices([])).
While attempting to generate form sections and questions via Google Script, I ran into the issue of not getting my answer selections to go to specific pages based on the user's answer. This appears to be the difference between setChoiceValues() and setChoices([]), with the latter allowing for page navigation as best as I can tell.
However, when attempting to put my array of new choices into setChoices([]), I get the error message "Cannot convert Array to Choice[]". My code works fine otherwise, but I need to use setChoices([]) (it seems) in order to get the page navigation that I want.
How can I loop values into an array or other container and be able to make them appear as a Choices[] object? How can I make something like this work? It seems like it should be much easier than it is, but I cannot see the solution.
Below is a segment of my code that is causing the issue:
//Form - globally accessible
var f = FormApp.openById(f_id);
//Date Iterator
var curr_date = 0;
//Time Iterator
var curr_time = 0;
//Array of Times
var Tchoices = [];
//Setting Time choices per date
while(curr_date < dates.length)
{
Tchoices = [];
curr_time = 0;
//dates is an array of objects with both d's (single date) and t's
// (array of times for that date)
var d = dates[curr_date].d;
var end_break = f.addPageBreakItem().setTitle("Times for " + d);
var f_time = f.addMultipleChoiceItem().setTitle(d);
while(curr_time < dates[curr_date].t.length)
{
end_break = end_break.setGoToPage(FormApp.PageNavigationType.SUBMIT);
Tchoices.push(f_time.createChoice(dates[curr_date].t[curr_time], end_break).getValue());
curr_time++;
}
f_time.setChoices([Tchoices]);
}
There was some minor issues with the building of your MultipleChoise object:
//Form - globally accessible
var f = FormApp.openById('someID');
//Date Iterator
var curr_date = 0;
//Time Iterator
var curr_time = 0;
//Array of Times
var Tchoices = [];
//Setting Time choices per date
while(curr_date < dates.length)
{
Tchoices = [];
curr_time = 0;
//dates is an array of objects with both d's (single date) and t's
// (array of times for that date)
var d = dates[curr_date].d;
var end_break = f.addPageBreakItem().setTitle("Times for " + d);
var f_time = f.addMultipleChoiceItem();
f.addMultipleChoiceItem().setTitle(d);
while(curr_time < dates[curr_date].t.length) //verify this while not sure what you have inside your dates array
{
end_break = end_break.setGoToPage(FormApp.PageNavigationType.SUBMIT);
Tchoices.push(f_time.createChoice(dates[curr_date].t[curr_time])); //You cannot add a pagebreak inside the elements of a choiseItem array
curr_time++;
}
Logger.log(Tchoices);
f_time.setChoices(Tchoices);
}
Check the values for dates[curr_date].t.length inside the Loop I'm not sure how you constructed the array.
You cannot add a pagebreak inside the elements of a choiseItem array
I have data with three dimensions x, y and z. I display them as a bokeh gridplot with three separate graphs below each other. The x-axes are linked so that if I pan in one of the graphs, the other ones are updated:
time = np.arange(len(data))
labels = ["x","y","z"]
plots = []
for i in range(3):
if i != 0:
fig = figure(plot_width=900, plot_height=150, x_range=first.x_range)
else:
fig = figure(plot_width=900, plot_height=150)
# First figure is saved as other figures are linked to this one
first = fig
source = ColumnDataSource({'x': time, 'y': data[:,i]})
fig.line('x','y', source=source)
fig.yaxis.axis_label = labels[i]
plots.append([fig])
p = gridplot(([plots[0], plots[1], plots[2]]))
show(p)
I also implemented a callback for the x-axes ranges which updates the graph each time the x-range changes so that the plot fills the complete graph on the y-axis:
fig.x_range.callback = CustomJS(args=dict(source=source, xrange=fig.x_range,
yrange=fig.y_range), code=adjust_y_range_js)
In this code, the range objects are set with the newly calculated ranges, which triggers an update in the graph from which the callback originated. I.e. if I zoom in on the first of the three graphs, the y-range of the first graph will be updated so that it fills the plot. Due to the link with the other plots, the x-ranges of the other two plots will change as well. However, their x_ranges won't trigger the callback. I tried passing the other ranges in the callback JS code and updating them in the following manner, but unsuccessfully:
xrange_other.trigger('change');
This will make the box selection tool freeze for some reason, i.e. it will permanently try to make a selection. Any ideas how to update the whole plot in another way?
The code for the callback is the following (taken from someone's github, but can't find the original source unfortunately):
adjust_y_range_js = """
function isNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
var data = source.get('data');
var start = yrange.get('start');
var end = yrange.get('end');
var time_start = xrange.get('start');
var time_end = xrange.get('end');
var pre_max_old = end;
var pre_min_old = start;
var time = data['x'];
var pre = data['y'];
t_idx_start = time.filter(function(st){return st>=time_start})[0];
t_idx_start = time.indexOf(t_idx_start);
t_idx_end = time.filter(function(st){return st>=time_end})[0];
t_idx_end = time.indexOf(t_idx_end);
var pre_interval = pre.slice(t_idx_start, t_idx_end);
pre_interval = pre_interval.filter(function(st){return !isNaN(st)});
var pre_max = Math.max.apply(null, pre_interval);
var pre_min = Math.min.apply(null, pre_interval);
var ten_percent = (pre_max-pre_min)*0.1;
pre_max = pre_max + ten_percent;
pre_min = pre_min - ten_percent;
if((!isNumeric(pre_max)) || (!isNumeric(pre_min))) {
pre_max = pre_max_old;
pre_min = pre_min_old;
}
yrange.set('start', pre_min);
yrange.set('end', pre_max);
// This is what I tried. When using these variables, I passed them in the CustomJS args dict, of course.
//yrange_other1.trigger('change');
//yrange_other2.trigger('change');
//xrange_other1.trigger('change');
//xrange_other2.trigger('change');
//soure_other1.trigger('change');
//soure_other2.trigger('change');
"""
I'm using python 2.7.10 and Bokeh version 0.12.3.
I'm having an issue pulling the correct values out of a for loop in Google Sheets.
Here's my code:
Note: this is a snippet from a larger function
function sendEmails() {
var trackOriginSheet = SpreadsheetApp.getActiveSpreadsheet().getName();
var getMirSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Miranda");
//Set a new object to hold conditional data
var holdingData = new Object();
//Create function to get values from origin sheet
var returnedValues = function (trackOriginSheet) {
//Load dynamic variables into an object via returnedValues()
if (trackOriginSheet === getMirSheet) {
var startMirRow = 2; // First row of data to process
var numRowsMir = 506; // Number of rows to process
// Fetch the range of cells A2:Z506
var dataRangeMir = getMirSheet.getRange(startMirRow, 1, numRowsMir, 26);
// Fetch values for each cell in the Range.
var dataMir = dataRangeMir.getValues();
for (var k in dataMir) {
var secondRowMir = dataMir[k];
var intRefDescMir = secondRowMir[3];
var intAdminActionsMir = secondRowMir[4];
//Push returned data to holdingData Object
holdingData.selectedData = secondRowMir;
holdingData.refDesc = intRefDescMir;
holdingData.adminActions = intAdminActionsMir;
}
}
}
Here's a copy of the sheet I'm working on
What I need to have happened here first, is track the origin sheet, then create an object to hold data returned from the returnedValues() function. Later, I'll call the properties of this object into a send email function.
The problem is that I need to be able to pull data from the selected sheet dynamically (the "Miranda" sheet in this case.) In other words, when a user selects the "Yes" option in column I of the Miranda sheet, the first thing this script needs to do is pull the values of the variables at the top of the for loop within the same row that the user selected "Yes." Then, I'm pushing that data to a custom object to be called later.
It's apparent to me, that I'm doing it wrong. There's, at least, something wrong with my loop. What have I done? :)
EDIT:
After reviewing the suggestion by VyTautas, here's my attempt at a working loop:
for (var k = 0; k < dataMir.length; k++) {
var mirColI = dataMir[k][8];
var mirRefDesc = dataMir[k][2];
var mirAdminActions = dataMir[k][3];
var mirDates = dataMir[k][4];
if (mirColI === "Yes") {
var activeRowMir = mirColI.getActiveSelection.getRowIndex();
//Pull selected values from the active row when Yes is selected
var mirRefDescRange = getMirSheet.getRange(activeRowMir, mirRefDesc);
var mirRefDescValues = mirRefDescRange.getValues();
var mirAdminActionsRange = getMirSheet.getRange(activeRowMir, mirAdminActions);
var mirAdminActionsValues = mirAdminActionsRange.getValues();
var mirDatesRange = getMirSheet.getRange(activeRowMir, mirDates);
var mirDatesValues = mirAdminActionsRange.getValues();
var mirHoldingArray = [mirRefDescValues, mirAdminActionsValues, mirDatesValues];
//Push mirHoldingArray values to holdingData
holdingData.refDesc = mirHoldingArray[0];
holdingData.adminActions = mirHoldingArray[1];
holdingData.dates = mirHoldingArray[2];
}
}
Where did all that whitespace go in the actual script editor? :D
You already correctly use .getValues() to pull the entire table into an array. What you need to do now is have a for loop go through dataMir[k][8] and simply fetch the data if dataMir[k][8] === 'Yes'. I also feel that it's not quite necessary to use for (var k in dataMir) as for (var k = 0; k < dataMir.length; k++) is a lot cleaner and you have a for loop that guarantees control (though that's probably more a preference thing).
You can also reduce the number of variables you use by having
holdingData.selectedData = mirData[k]
holdingData.refDesc = mirData[k][2] //I assume you want the 3rd column for this variable, not the 4th
holdingData.adminActions = mirData[k][3] //same as above
remember, that the array starts with 0, so if you mirData[k][0] is column A, mirData[k][1] is column B and so on.
EDIT: what you wrote in your edits seems like doubling down on the code. You already have the data, but you are trying to pull it again and some variables you use should give you an error. I will cut the code from the if, although I don't really see why you need to both get the active sheet and sheet by name. If you know the name will be constant, then just always get the correct sheet by name (or index) thus eliminating the possibility of working with the wrong sheet.
var titleMirRows = 1; // First row of data to process
var numRowsMir = getMirSheet.getLastRow(); // Number of rows to process
// Fetch the range of cells A2:Z506
var dataRangeMir = getMirSheet.getRange(titleMirRows + 1, 1, numRowsMir - titleMirRows, 26); // might need adjusting but now it will only get as many rows as there is data, you can do the same for columns too
// Fetch values for each cell in the Range.
var dataMir = dataRangeMir.getValues();
for (var k = 0; k < dataMir.length; k++) {
if (dataMir[k][7] === 'Yes') { //I assume you meant column i
holdingData.refDesc = dataMir[k] //this will store the entire row
holdingData.adminActions = dataMir[k][3] //this stores column D
holdingData.dates = dataMir[k][4] //stores column E
}
}
Double check if the columns I have added to those variables are what you want. As I understood the object stores the entire row array, the value in column called Administrative Actions and the value in column Dates/Periods if Applicable. If not please adjust accordingly, but as you can see, we minimize the work we do with the sheet itself by simply manipulating the entire data array. Always make as few calls to Google Services as possible.