Closed josefkorbel closed 5 years ago
Interesting. I'm not a super expert in Javascript, but I've played with your code and noticed some things:
You use the the {% for row in data %}
Jinja templating syntax, which treats the data
variable as python object to iterate over and converts each element to a string. However your data was already converted to a string by the json.dumps
part in the render
function here:
return Template(head + self.template).render(
div_id=div_id.replace(" ", "_"),
data=json.dumps(data, indent=4).replace("'", "\\'").replace('"', "'"),
chart_type=chart_type,
chart_package=chart_package,
options=json.dumps(options, indent=4).replace("'", "\\'").replace('"', "'"),
)
When I used the plotter.save
and looked at the html file, I see many new Date objects with empty values and brackets, because Jinja was iterating over the jsonified string form of the data with bunch of newlines etc, not a python list of lists. It embeds the data into the javascript like this:
data.addRow([new Date( , , ), ]);
data.addRow([new Date( , , ), ]);
data.addRow([new Date( , , ), ]);
data.addRow([new Date(2, , ), ]);
data.addRow([new Date(0, , ), ]);
data.addRow([new Date(1, , ), ]);
data.addRow([new Date(8, , ), ]);
So, I'm trying to figure out how you got it to work at all in #11 as I can't replicate your basic example.
It's a bit confusing with a name collision on data
being set as a javascript variable var data = new google.visualization.DataTable();
and iterating over the python string object named data
{% for row in data %}
data.addRow([new Date({{row[0]}}, {{row[1]}}, {{row[2]}}), {{row[3]}}]);
{% endfor %}
You reference your selectHandler
listener here google.visualization.events.addListener(chart, 'select', selectHandler);
before it is defined just below it. Not sure if this is a deal-breaker in javascript but it would be in python.
The plots are being rendered in Iframes, which are isolated from the global notebook Javascript context so, it makes sense that the IPython kernel is not available. I don't think this libary lends itself to easily sending messages back to the Python runtime from the chart. Maybe writing some more custom javascript to work with them embedded data would meet your needs.
I was able to get the even listener to work though for click events logged to the console with this complete example. Also note var tableRow = [new Date(row[0], row[1]-1, row[2]), row[3]]
has a row[2]-1
because Javascript Date objects have month ranges from 0-11 instead of 1-12.
from iplotter import GCPlotter
plotter = GCPlotter()
data = [[2018, 8, 30, 0],
[2018, 8, 29, 0],
[2018, 8, 28, 0],
[2018, 8, 27, 10],
[2018, 8, 26, 23],
[2018, 8, 25, 0],
[2018, 8, 24, 0],
[2018, 8, 23, 0],
[2018, 8, 22, 0],
[2018, 8, 21, 0],
[2018, 8, 20, 1],
[2018, 8, 19, 1],
[2018, 8, 18, 1]]
template = """
<div id={{div_id}} style='width: 100%; height: 100%' ></div>
<script type='text/javascript'>
google.charts.load('current', {'packages':['{{ chart_package}}']});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
var json_data = {{data}}
var table = []
var dataTable = new google.visualization.DataTable();
dataTable.addColumn({ type: 'date', id: json_data[0][0] });
dataTable.addColumn({ type: 'number', id: json_data[0][1]});
for (i=1;i<json_data.length;i++) {
var row = json_data[i];
var tableRow = [new Date(row[0], row[1]-1, row[2]), row[3]]
table.push(tableRow);
}
console.log(table);
dataTable.addRows(table);
var chart = new google.visualization.Calendar(document.getElementById('chart'));
chart.draw(dataTable, {{options}});
// Click handler
function selectHandler(e) {
// Get item clicked
var info = chart.getSelection();
// Log to console
console.log('Clicked timestamp: ' + new Date(info[0].date));
}
google.visualization.events.addListener(chart, 'select', selectHandler);
}
</script>
"""
options = {
"width": 600,
"height": 400
}
plotter.template = template
plotter.plot(data, chart_type="Calendar",chart_package='calendar', options=options)
Hello @niloch thanks again for your reply and time.
I am aware of the fact that the charts are rendered to an IFrame, and I see that there could simply not be a way to achieve this, but I might need to switch back plotly or seaborn again because I need this callback feature, was just loving the fact that it is easily replicatable on frontend.
However your data was already converted to a string by the json.dumps part in the render function
I actually rewrote the return statement to this, so I am no longer dumping the data to json:
return Template(self.head + self.template).render(
div_id=div_id.replace(" ", "_"),
data=data,
chart_type=chart_type,
chart_package=chart_package,
options=json.dumps(
options, indent=4).replace("'", "\\'").replace('"', "'"))
It's a bit confusing with a name collision on data being set as a javascript variable var data = new google.visualization.DataTable(); and iterating over the python string object named data
That is true, however I guess the Jinja is smart enough to treat those values diferently as they are in double curly brackets, might try.
You reference your selectHandler listener here google.visualization.events.addListener(chart, 'select', selectHandler); before it is defined just below it.
The same is actually happening here, so I dont think this might be an issue.
google.charts.setOnLoadCallback(drawChart);
function drawChart() {}
The plots are being rendered in Iframes, which are isolated from the global notebook Javascript context
Yes that is what im scared of, maybe I could take a look at something like window.postMessage()
Javascript Date objects have month ranges from 0-11 instead of 1-12.
I am decreasing the month value by one already in my python code, but after second look I see that it makes no sense and should be placed inside the JS as this is JS-specific thing.
Will investigate further and will let you know, thanks for colaborating !
Horaay, I managed to get data back to python using window.postMessage()
API.
Edited event handler:
function selectHandler(e) {
// Get item clicked
var info = chart.getSelection();
if (info[0] !== undefined) {
// Call python callback function with this date
var unix_ts = info[0].date;
// Sending Data to parent !
window.top.postMessage(unix_ts, '*')
// Log to console
console.log('Clicked timestamp: ' + new Date(info[0].date));
}
}
Edited part of iplotter:
def link_callback(self, callback):
""" Function for linking IFrame and parent jupyter window using PostMessage API
"""
js = \
"""
var kernel = IPython.notebook.kernel;
window.onmessage = function(e){{
var command = "{callback}(" + e.data + ")";
console.log(command);
kernel.execute(command);
}}
""".format(callback=callback.__name__)
display(Javascript(js))
So now, before plotting, I can pass any function to link_callback
.
Example callback:
clicks = []
def callback_without_stdout(x):
clicks.append(x)
Usage
plotter.link_callback(callback_with_stdout)
After clicking to few days in the Calendar
char from GoogleCharts, this is the clicks
list, which contains unix times of days that I've clicked on ! Miracle ! :dancer:
In [6] : clicks
Out [6] :
[1529020800000,
1525996800000,
1525132800000,
1520467200000,
1519776000000,
1520640000000,
1518825600000,
1517616000000,
1516406400000]
Now working on version of callback with stdout (printing to notebook) using ipywidgets.widgets.Output()
Cheers!
Wow Awesome! If you think there is an elegant and reuseble way to add this capability to other chart libraries, I'd be happy to accept a pull request! Thanks!
Hello!
I am back again with another issue I would love to solve. After I solved Google Calendar Chart (#11) I was trying to make a callback function when user clicks on given day.
This is how I altered the template in order to achieve this.
This works well for logging to JS console. But when I want to execute the python command, it shows a red rectangle on top of the chart saying
IPython is not defined
. However when I open developer console in google chrome, theIPython
objects exist and is accessible. Any ideas how this could be solved?