Skip to content
50 changes: 49 additions & 1 deletion dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io
import json
from functools import wraps
import pandas as pd
from . import exceptions

logger = logging.getLogger()
Expand All @@ -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):
Expand Down Expand Up @@ -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
24 changes: 19 additions & 5 deletions dash/dash-renderer/src/actions/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 44 additions & 3 deletions dash/dash-renderer/src/reducers/layout.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
split_callback_id,
strip_relative_path,
to_json,
serializer,
)
from . import _callback
from . import _dash_renderer
Expand Down Expand Up @@ -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", []
)
Expand Down