I am struggling with Dash Clientside callbacks. I am looking to create a smooth animation, so I need the clientside callback to have the fast update rate. I have an example that seems to replicate the problem; I have a normal callback and that works as expected. When I convert the same callback to clientside, it no longer works. However, when I do a JSON.stringify to the clientside return, I see the data field updating. I do not understand the issue, though I expect it is an issue with my js. I do not know how to debug on the clientisde, so any advice for error recording would also be appreciated.
Here is the working 'normal' callback:
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input,Output,State
fig_test={
'data': [
{'x': [1, 2, 3], 'y': [4, 1, 2], 'type': 'bar', 'name': 'SF'},
{'x': [1, 2, 3], 'y': [2, 4, 5], 'type': 'bar', 'name': u'Montréal'},
],
'layout': {
'title': 'Dash Data Visualization'
}
}
app = dash.Dash(__name__)
app.layout = html.Div([
html.Button("Button 1", id="btn1"),
dcc.Graph(id="graph", figure=fig_test),
dcc.Slider(
id='interval-component',
min=0,
max=36,
step=1,
value=10,
),
html.Div(id="log"),
html.Pre(
id='structure',
style={
'border': 'thin lightgrey solid',
'overflowY': 'scroll',
'height': '275px'
}
)
])
#app.callback(
Output("graph", "figure"),
Input('interval-component','value'),Input("graph", "figure"),Input("btn1", "n_clicks"))
def display_structure(value, figure, btn1):
figure['data'][0]['y'][1] = value
return {'data': figure['data'], 'layout':figure['layout']}
app.run_server(debug=False)
Here is the same callback implemented through clientside:
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input,Output,State
fig_test={
'data': [
{'x': [1, 2, 3], 'y': [4, 1, 2], 'type': 'bar', 'name': 'SF'},
{'x': [1, 2, 3], 'y': [2, 4, 5], 'type': 'bar', 'name': u'Montréal'},
],
'layout': {
'title': 'Dash Data Visualization'
}
}
app = dash.Dash(__name__)
app.layout = html.Div([
html.Button("Button 1", id="btn1"),
dcc.Graph(id="graph", figure=fig_test),
dcc.Slider(
id='interval-component',
min=0,
max=36,
step=1,
value=10,
),
html.Div(id="log"),
html.Pre(
id='structure',
style={
'border': 'thin lightgrey solid',
'overflowY': 'scroll',
'height': '275px'
}
)
])
app.clientside_callback(
"""
function(value, figure, btn1){
figure['data'][0]['y'][1] = value
return {'data': figure['data'], 'layout':figure['layout']};
}
""", Output("graph", "figure"), [Input('interval-component','value'),Input("graph", "figure"),Input("btn1", "n_clicks")])
app.run_server(debug=False)
If I implement the clientside to jsonify the output like this:
app.clientside_callback(
"""
function(value, figure, btn1){
figure['data'][0]['y'][1] = value
return JSON.stringify({'data': figure['data'], 'layout':figure['layout']});
}
""", Output("log", "children"), [Input('interval-component','value'),Input("graph", "figure"),Input("btn1", "n_clicks")])
I can see the value being updated, so I do not know what the issue is.
So I figured out the 'smooth animation' for layout updates, wherein 'extend data' is not possible and so the solution in this answer: Plotly/Dash display real time data in smooth animation
is not applicable. Further it allows smooth animations of live-updated data without being dependent on the 'animate' api. This is not an exact answer to the question I asked, but addresses the concept.
If you are unfamiliar with Frames, or otherwise unsure how to setup a figure, see plotly's example here: https://plotly.com/python/animations/
Psuedocode for setup:
## setup figure,
for data in some_data:
##do something
fig['frames'].append(frame)
fig1 = go.Figure(fig)
setup a store for your frames, and a store for the frames to be passed to a clientside callback. As I was setting up a process to simulate live data acquisition, I had a timer for 'polling' and a secondary one for the animation. If you don't want a timer to as a trigger, the main concept is still the same; have some 'animate' trigger, in this case 'interval-component', to kick off the quickly refreshing secondary timer.
app.layout = html.Div([
dcc.Store(id='frames-stored', data=fig1['frames']),
dcc.Store(id='frames'),
dcc.Interval(
id='interval-component',
interval=1*500, # in milliseconds
n_intervals=0
),
dcc.Interval(id='graph-refresher',
interval=1*25,
n_intervals=0,
max_intervals=50,
disabled=True),
dcc.Graph(id="graph", figure=fig1),
])
now a callback to catch your 'animation' trigger and pass frames to your clientside callback:
#app.callback(
Output("frames", "data"),Output("graph-refresher", "disabled"),
Output("graph-refresher", "max_intervals"),Output('graph-refresher','n_intervals'),
Input("interval-component", "n_intervals"),State("frames-stored", "data"))
def data_smoother(n_intervals,frames):
## can do whatever here as long as a list of frames are passed to the store
selected_frames = frames[n_intervals]
return selected_frames,False,'some_max',0
This callback turns on the timer for the clientside callback, and resets the max_intervals with 'some_max'. This is going to be dependent on whatever you are doing.
Now the clientside callback that handles the 'animation'.
app.clientside_callback(
"""
function(n_intervals, frames){
return {'data':frames[parseInt(n_intervals)]['data'], 'layout':frames[parseInt(n_intervals)]['layout']};
}
""",Output("graph", "figure"),Input('graph-refresher','n_intervals'), State("frames", "data"))
I hope this is useful for someone!
Related
I want to use the eCharts to draw the candletick picture. I get the data from flask using Axios:
request.post('/investor').then(res=>{
var arr=Object.keys(res.close)
for(let i=0;i<arr.length;i++){
this.stockdata[i]=[]
for(let j=0;j<4;j++){
var temp=[]
temp[0]=res.open[i]
temp[1]=res.close[i]
temp[2]=res.low[i]
temp[3]=res.high[i]
this.stockdata[i][j]=temp[j]
}
}
})
this my data:
The 2d array
but it cannot render the website
then i create the data using the method below(assign data manually):
this.stockdata= [
[20, 34, 10, 38],
[40, 35, 30, 50],
[31, 38, 33, 44],
[38, 15, 5, 42]
]
it successfully draw the picture,i don't know why.
and this my main function:
mounted(){
request.post('/investor').then(res=>{
var arr=Object.keys(res.close)
for(let i=0;i<arr.length;i++){
this.stockdata[i]=[]
for(let j=0;j<4;j++){
var temp=[]
temp[0]=res.open[i]
temp[1]=res.close[i]
temp[2]=res.low[i]
temp[3]=res.high[i]
this.stockdata[i][j]=temp[j]
}
}
})
console.log(this.stockdata)
var echarts = require('echarts');
var option = {
xAxis: {
data: ['2017-10-24', '2017-10-25', '2017-10-26', '2017-10-27']
},
yAxis: {},
tooltip: {
trigger: 'axis'
},
series: [
{
type: 'candlestick',
data:this.stockdata
}
]
};
var charts = echarts.init(this.$refs.myChart);
charts.setOption(option);
}
}
There is not enough information, but I can still guess.
I am going to assume that your Object.keys double for loop with i and j works fine. To be sure of that, please add console.log(this.stockdata) after both loops end running, to make sure you are building your data from the response the right way.
The error is probably because in your code you call the async function, which is calling your API.
// this *.then* happens after the initial render, and after a delay it gets a value,
// which is too late
request.post('/investor').then(res=>{
This causes that at the exact frame the App is being rendered, your response is not yet there, causing your App to break because this.stockdata is still undefined. One way to fix it is the following:
// in render function:
{this.stockdata && <YourComponent dataToRender={this.stockdata} />}
This will prevent rendering YourComponent until the API response is there
I'm struggling to make uPlot work with svelte. I can't find any minimal working example, so I'm crafting one myself and it does not render. Repro goes as follows:
npm create svelte repro
# y
# skeleton
# yes TypeScript
# yes ESLing
# yes Prettier
# yes Playwright
npm install
npm i uplot
npm run dev -- --open
And then I modify index.svelte to contain the following (best I could come up with, thanks to this answer In SvelteKit, how do you conditionally include a `<script>` block in `app.html`?)
<script lang="ts">
import { browser } from '$app/env';
import { onMount } from 'svelte';
import "uplot/dist/uPlot.min.css"
let uPlot;
function redraw(uPlot) {
if(!uPlot) return;
let data = [new Float32Array([1, 2, 3, 4, 5]), new Float32Array([1, 3, 2, 5, 4])];
const opts = {
width: 600,
height: 300,
scales: {x: {time: false}},
series: [{label: "x"}, {label: "y", stroke: "red"}],
};
new uPlot(opts, data, document.getElementById("uPlot"));
}
onMount(async () => {
if (browser) {
const uplotModule = await import('uplot');
uPlot = uplotModule.default;
console.log("uplot loaded", uplotModule, uPlot);
}
})
$: redraw(uPlot)
</script>
<h1>Welcome to SvelteKit</h1>
<div id="uPlot"></div>
It does not render the plot :( What am I missing?
There are several things to fix or improve:
uPlot does not take typed arrays as data
The reactive statement is a bit pointless, as it only triggers in a meaningful once after uPlot has been loaded
One should not query the DOM in Svelte but use bind:this or events instead
onMount already executes only in the browser
<script lang="ts">
import { onMount } from 'svelte';
import 'uplot/dist/uPlot.min.css';
let plotContainer;
function redraw(uPlot) {
let data = [[1, 2, 3, 4, 5], [1, 3, 2, 5, 4]];
const opts = {
width: 600,
height: 300,
scales: {x: {time: false}},
series: [{label: "x"}, {label: "y", stroke: "red"}],
};
new uPlot(opts, data, plotContainer);
}
onMount(async () => {
const uplotModule = await import('uplot');
const uPlot = uplotModule.default;
redraw(uPlot);
})
</script>
<div bind:this={plotContainer}></div>
REPL equivalent
I'm struggling to get 2 traces on a plotly graph when updating the data from a javascript clientside callback.
I have this clientside callback:
app.clientside_callback(
ClientsideFunction(
namespace='clientside',
function_name='update_rms_graph'
),
Output('rmstrace-graph', 'extendData'),
(
Input('client-rms-data', 'data'),
Input('client-rms-2-data', 'data'),
Input('timer-refresh-rms-fft', 'n_intervals')
)
)
Which is defined as such:
window.dash_clientside = Object.assign({}, window.dash_clientside, {
clientside: {
update_rms_graph: function(data, data_rms, n_intervals) {
console.log(data.rms.length, data_rms.rms.length);
return [
{y: [data.rms], x: [data.t]},
//{y: [data_rms.rms], x: [data_rms.t]}, // This is where i'd like to add my new trace
[0],
data.max_points
]
}
}
)
The 'rmstrace-graph' is defined as such:
html.Div([
dcc.Graph(id='rmstrace-graph', figure=fig_rmstrace),
], className='six columns')
And the corresponding figure used for the dash graph is the following:
fig_rmstrace = go.Figure(data=go.Scattergl(y=[], x=[]))
How should the 'return' from the clientside callback be wrote if I like to get a second trace from the same dash graph ?
return [
{
y: [rms.rms, averaged_rms.rms],
x: [rms.t, averaged_rms.t],
}
[0, 1],
rms.max_points
]
This may be in the series of dumb questions, but when I look at https://deck.gl/docs/api-reference/geo-layers/mvt-layer, I do not understand how to make a MVTLayer that fetches self-hosted tiles without React pieces. Can someone help? This would feel to be even large interest now that buildless is also becoming a thing in web programming.
What I would like to achieve is a simple HTML (e.g. index.html) file that uses a script tag like <script src="https://unpkg.com/deck.gl#8.4.5/dist.min.js"></script> and the example from the aforementioned Deck.gl that looks like (I changed the URL)
import DeckGL from '#deck.gl/react';
import {MVTLayer} from '#deck.gl/geo-layers';
function App({viewState}) {
const layer = new MVTLayer({
data: `https://<selfhostedurl>/{z}/{x}/{y}.pbf`,
minZoom: 0,
maxZoom: 23,
getLineColor: [192, 192, 192],
getFillColor: [140, 170, 180],
getLineWidth: f => {
switch (f.properties.class) {
case 'street':
return 6;
case 'motorway':
return 10;
default:
return 1;
}
},
lineWidthMinPixels: 1
});
return <DeckGL viewState={viewState} layers={[layer]} />;
}
but instead make this a without React. I see it requires a bit more code on how to define a canvas HTML element and use it. Maplibre example would be OK too. :) There is one Maplibre example at https://codepen.io/snickell/pen/dypOWzj.
You can use the Scripting API for more 'simple' examples, here you have an example of using MVTLayer.
Deck.gl offers a standalone bundled version of the library - a native JavaScript scripting interface like that of d3.js.
As simple as
const deckgl = new deck.DeckGL({
container: 'map',
mapStyle: 'https://maps-api-v2.us.carto.com/user/public/carto/sql/{z}/{x}/{y}?source=SELECT * FROM ne_10m_railroads_public&api_key=default_public&format=mvt',
initialViewState: {
latitude: 41.4,
longitude: 2.18,
zoom: 5,
},
controller: true,
layers: [
new deck.MVTLayer({
data: 'https://d25uarhxywzl1j.cloudfront.net/v0.1/{z}/{x}/{y}.mvt',
getLineColor: [192, 192, 192],
lineWidthMinPixels: 1
})
]
});
JavaScript client sending request like this:
$.ajax({
url: 'http://localhost:7973/test',
type: 'GET',
data: {'host': 'mike', 'guests': {'name': ['car', 'ball'], 'age': [6, 10, 7]}},
success: function(result){alert(result)},
error: function(error){alert(error)}
});
Python server handling request using tornado:
import tornado.ioloop
import tornado.web
class TestHandler(tornado.web.RequestHandler):
def get(self):
host = self.get_argument('host')
print(host)
guests = self.get_argument('guests')
print(guests)
def make_app():
return tornado.web.Application([
(r'/test', TestHandler)
])
if __name__ == "__main__":
app = make_app()
port = 7973
app.listen(port)
print('-' * 100)
print('server started, listening to ', port, '...\n')
tornado.ioloop.IOLoop.current().start()
The outputs on the server side is as below. Apparently, the 'host' argument is successfully got, but I have no clue how to get a argument whose value is a complex object itself (say an array or a dictionary). Please explain to me the mechanism of these casts and dumps between data structures and their string representation? I read the tornado document, but I'm not able to find the answer.
mike
WARNING:tornado.general:400 GET
/test?host=mike&guests%5Bname%5D%5B%5D=car&guests%5Bname%5D%5B%5D=ball&guests%5Bage%5D%5B%5D=6&guests%5Bage%5D%5B%5D=10&guests%5Bage%5D%5B%5D=7
(::1): Missing argument guests
WARNING:tornado.access:400 GET
/test?host=mike&guests%5Bname%5D%5B%5D=car&guests%5Bname%5D%5B%5D=ball&guests%5Bage%5D%5B%5D=6&guests%5Bage%5D%5B%5D=10&guests%5Bage%5D%5B%5D=7
(::1) 1.99ms
You can convert your json object to a json string.
change
data: {'host': 'mike', 'guests': {'name': ['car', 'ball'], 'age': [6, 10, 7]}},
to
data: JASON.stringify({'host': 'mike',
'guests': {'name': ['car', 'ball'],
'age': [6, 10, 7]}}),
and then on the server side you can do:
guests_string = self.get_argument('guests')
guests = json.loads(guests_string)
guests should be a dictionary that you can do whatever with in Python.