diff --git a/dash/_utils.py b/dash/_utils.py index 1c147065c4..e496357cae 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -9,6 +9,7 @@ import io import json from functools import wraps +import pandas as pd from . import exceptions logger = logging.getLogger() @@ -18,7 +19,7 @@ def to_json(value): # pylint: disable=import-outside-toplevel from plotly.io.json import to_json_plotly - return to_json_plotly(value) + return to_json_plotly(serializer.serialize_tree(value)) def interpolate_str(template, **data): @@ -247,3 +248,50 @@ def _wrapper(*args, **kwargs): return _wrapper return wrapper + + +class serializer: + @classmethod + def serialize(cls, prop): + if isinstance(prop, pd.DataFrame): + return {"__type": "DataFrame", "__value": prop.to_dict("records")} + + return prop + + @classmethod + def serialize_tree(cls, obj): + if isinstance(obj, pd.DataFrame): + return cls.serialize(obj) + + # Plotly + try: + obj = obj.to_plotly_json() + except AttributeError: + pass + + if isinstance(obj, (list, tuple)): + if obj: + # Must process list recursively even though it may be slow + return [cls.serialize_tree(v) for v in obj] + + # Recurse into lists and dictionaries + if isinstance(obj, dict): + return {k: cls.serialize_tree(v) for k, v in obj.items()} + + return obj + + @classmethod + def unserialize(cls, prop): + if not (isinstance(prop, dict) and "__type" in prop): + return prop + if prop["__type"] == "DataFrame": + return pd.DataFrame(prop["__value"]) + + return prop["__value"] + + @classmethod + def unserialize_input(cls, cb_input): + if "value" in cb_input: + return {**cb_input, "value": cls.unserialize(cb_input["value"])} + + return cb_input diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 2769d4c5eb..0997400c60 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -145,11 +145,25 @@ function fillVals( const inputVals = getter(paths).map((inputList: any, i: number) => { const [inputs, inputError] = unwrapIfNotMulti( paths, - inputList.map(({id, property, path: path_}: any) => ({ - id, - property, - value: (path(path_, layout) as any).props[property] - })), + inputList.map(({id, property, path: path_}: any) => { + const type = (path(path_, layout) as any).propertyTypes?.[ + property + ]; + let value = (path(path_, layout) as any).props[property]; + + if (type) { + value = { + __type: type, + __value: value + }; + } + + return { + id, + property, + value + }; + }), specs[i], cb.anyVals, depType diff --git a/dash/dash-renderer/src/reducers/layout.js b/dash/dash-renderer/src/reducers/layout.js index 6c082f313c..c5056a35de 100644 --- a/dash/dash-renderer/src/reducers/layout.js +++ b/dash/dash-renderer/src/reducers/layout.js @@ -1,10 +1,44 @@ -import {append, assocPath, includes, lensPath, mergeRight, view} from 'ramda'; +import { + append, + assocPath, + includes, + lensPath, + mergeRight, + view, + type +} from 'ramda'; import {getAction} from '../actions/constants'; +const processProperties = node => { + const updatedNode = {...node, propertyTypes: {...node.propertyTypes}}; + const { + props, + props: {children} + } = node; + + if (type(children) === 'Array') { + updatedNode.props.children = children.map(child => + processProperties(child) + ); + } else if (type(children) === 'Object' && !children.__type) { + updatedNode.props.children = processProperties(children); + } + + Object.entries(props).forEach(([key, value]) => { + updatedNode.props[key] = value?.__value || value; + updatedNode.propertyTypes[key] = + value?.__type || updatedNode.propertyTypes[key] || null; + }); + + return updatedNode; +}; + const layout = (state = {}, action) => { if (action.type === getAction('SET_LAYOUT')) { - return action.payload; + const updatedState = processProperties(action.payload); + + return updatedState; } else if ( includes(action.type, [ 'UNDO_PROP_CHANGE', @@ -13,9 +47,16 @@ const layout = (state = {}, action) => { ]) ) { const propPath = append('props', action.payload.itempath); + const existingProps = view(lensPath(propPath), state); const mergedProps = mergeRight(existingProps, action.payload.props); - return assocPath(propPath, mergedProps, state); + + let updatedState = assocPath(propPath, mergedProps, state); + if (action.payload.source === 'response') { + updatedState = processProperties(updatedState); + } + + return updatedState; } return state; diff --git a/dash/dash.py b/dash/dash.py index b567e5b9d9..196a63e3ba 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -51,6 +51,7 @@ split_callback_id, strip_relative_path, to_json, + serializer, ) from . import _callback from . import _dash_renderer @@ -1266,9 +1267,12 @@ def callback(_triggers, user_store_data, user_callback_args): def dispatch(self): body = flask.request.get_json() + flask.g.inputs_list = inputs = body.get( # pylint: disable=assigning-non-slot "inputs", [] ) + inputs = [serializer.unserialize_input(v) for v in inputs] + flask.g.states_list = state = body.get( # pylint: disable=assigning-non-slot "state", [] )