Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ Expose [plotly dash](https://plot.ly/products/dash/) apps as django tags.
See the source for this project here:
<https://github.com/GibbsConsulting/django-plotly-dash>

Online documentation can be found here:
This README file provides a short guide to installing and using the package, and also
outlines how to run the demonstration application.

More detailed information
can be found in the online documentation at
<https://readthedocs.org/projects/django-plotly-dash>


## Installation

First, install the package. This will also install plotly and some dash packages if they are not already present.
Expand All @@ -18,7 +23,7 @@ Then, just add `django_plotly_dash` to `INSTALLED_APPS` in your Django `settings

INSTALLED_APPS = [
...
'django_plotly_dash',
'django_plotly_dash.apps.DjangoPlotlyDashConfig',
...
]

Expand Down Expand Up @@ -110,6 +115,6 @@ templates:
```

The registration code needs to be in a location
that will be imported into the Django process before any template tag attempts to use it. The example Django application
that will be imported into the Django process before any model or template tag attempts to use it. The example Django application
in the demo subdirectory achieves this through an import in the main urls.py file; any views.py would also be sufficient.

17 changes: 17 additions & 0 deletions demo/configdb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python
#
# Create a new superuser
from django.contrib.auth import get_user_model

UserModel = get_user_model()

name="admin"
password="admin"

try:
UserModel.objects.get(username=name)
except:
su = UserModel.objects.create_user(name,password=password)
su.is_staff=True
su.is_superuser=True
su.save()
2 changes: 1 addition & 1 deletion demo/demo/plotly_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def callback_size(dropdown_color, dropdown_size):
html.Div(id="output-one")
])

@a2.callback(
@a2.expanded_callback(
dash.dependencies.Output('output-one','children'),
[dash.dependencies.Input('dropdown-one','value')]
)
Expand Down
2 changes: 1 addition & 1 deletion demo/demo/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
'django.contrib.messages',
'django.contrib.staticfiles',

'django_plotly_dash',
'django_plotly_dash.apps.DjangoPlotlyDashConfig',
]

MIDDLEWARE = [
Expand Down
2 changes: 1 addition & 1 deletion demo/demo/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<body>
<div>
Content here
{%plotly_item "SimpleExample"%}
{%plotly_item "simpleexample-1" ratio=0.2 %}
</div>
<div>
Content here
Expand Down
1 change: 0 additions & 1 deletion dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ dash-html-components==0.10.1
dash-renderer==0.12.1
decorator==4.3.0
Django==2.0.5
-e git+https://github.com/delsim/django-plotly-dash.git@293b454ee8965422155a971d9e417878dc89ce82#egg=django_plotly_dash
docopt==0.6.2
docutils==0.14
Flask==1.0.2
Expand Down
2 changes: 1 addition & 1 deletion django_plotly_dash/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#

__version__ = "0.0.3"
__version__ = "0.0.4"

from .dash_wrapper import DelayedDash

5 changes: 4 additions & 1 deletion django_plotly_dash/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from django.contrib import admin

# Register your models here.
from .models import DashApp, DashAppAdmin

admin.site.register(DashApp, DashAppAdmin)

2 changes: 2 additions & 0 deletions django_plotly_dash/app_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
app_name = "the_django_plotly_dash"
main_view_label = "main"
1 change: 1 addition & 0 deletions django_plotly_dash/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

class DjangoPlotlyDashConfig(AppConfig):
name = 'django_plotly_dash'
verbose_name = "Django Plotly Dash"
200 changes: 173 additions & 27 deletions django_plotly_dash/dash_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,63 @@
from flask import Flask

from django.urls import reverse
from django.http import HttpResponse

import json

from plotly.utils import PlotlyJSONEncoder

from .app_name import app_name, main_view_label

uid_counter = 0

usable_apps = {}
app_instances = {}
nd_apps = {}

def add_usable_app(name, app):
global usable_apps
usable_apps[name] = app

def add_instance(id, instance):
global nd_apps
nd_apps[id] = instance

def get_app_by_name(name):
'''
Locate a registered dash app by name, and return a DelayedDash instance encapsulating the app.
'''
return usable_apps.get(name,None)

def get_app_instance_by_id(id):
'''
Locate an instance of a dash app by identifier, or return None if one does not exist
'''
return nd_apps.get(id,None)

def clear_app_instance(id):
try:
del nd_apps[id]
except:
pass

def get_or_form_app(id, name, **kwargs):
'''
Locate an instance of a dash app by identifier, loading or creating a new instance if needed
'''
app = get_app_instance_by_id(id)
if app:
return app
dd = get_app_by_name(name)
return dd.form_dash_instance()

class Holder:
def __init__(self):
self.items = []
def append_css(self, stylesheet):
self.items.append(stylesheet)
def append_script(self, script):
self.items.append(script)

class DelayedDash:
def __init__(self, name=None, **kwargs):
if name is None:
Expand All @@ -24,27 +68,32 @@ def __init__(self, name=None, **kwargs):
else:
self._uid = name
self.layout = None
self._rep_dash = None
self._callback_sets = []

global usable_apps
usable_apps[self._uid] = self
self.css = Holder()
self.scripts = Holder()

add_usable_app(self._uid,
self)

def _RepDash(self):
if self._rep_dash is None:
self._rep_dash = self._form_repdash()
return self._rep_dash
self._expanded_callbacks = False

def _form_repdash(self):
def form_dash_instance(self, replacements=None, specific_identifier=None):
rd = NotDash(name_root=self._uid,
app_pathname="django_plotly_dash:main")
app_pathname="%s:%s" % (app_name, main_view_label),
expanded_callbacks = self._expanded_callbacks,
replacements = replacements,
specific_identifier = specific_identifier)
rd.layout = self.layout

for cb, func in self._callback_sets:
rd.callback(**cb)(func)
return rd
for s in self.css.items:
rd.css.append_css(s)
for s in self.scripts.items:
rd.scripts.append_script(s)

def base_url(self):
return self._RepDash().base_url()
return rd

def callback(self, output, inputs=[], state=[], events=[]):
callback_set = {'output':output,
Expand All @@ -56,6 +105,10 @@ def wrap_func(func,callback_set=callback_set,callback_sets=self._callback_sets):
return func
return wrap_func

def expanded_callback(self, output, inputs=[], state=[], events=[]):
self._expanded_callbacks = True
return self.callback(output, inputs, state, events)

class NotFlask:
def __init__(self):
self.config = {}
Expand All @@ -74,30 +127,89 @@ def run(self,*args,**kwargs):
pass

class NotDash(Dash):
def __init__(self, name_root, app_pathname, **kwargs):

global app_instances
current_instances = app_instances.get(name_root,None)
def __init__(self, name_root, app_pathname=None, replacements = None, specific_identifier=None, expanded_callbacks=False, **kwargs):

if current_instances is not None:
self._uid = "%s-%i" % (name_root,len(current_instances)+1)
current_instances.append(self)
if specific_identifier is not None:
self._uid = specific_identifier
else:
self._uid = name_root
app_instances[name_root] = [self,]

add_instance(self._uid, self)

self._flask_app = Flask(self._uid)
self._notflask = NotFlask()
self._base_pathname = reverse(app_pathname,kwargs={'id':self._uid})

kwargs['url_base_pathname'] = self._base_pathname
kwargs['server'] = self._notflask

super(NotDash, self).__init__(**kwargs)
global nd_apps
nd_apps[self._uid] = self
if False: # True for some debug info and a load of errors...
self.css.config.serve_locally = True
self.scripts.config.serve_locally = True

self._adjust_id = False
self._dash_dispatch = not expanded_callbacks
if replacements:
self._replacements = replacements
else:
self._replacements = dict()
self._use_dash_layout = len(self._replacements) < 1

def use_dash_dispatch(self):
return self._dash_dispatch

def use_dash_layout(self):
return self._use_dash_layout

def augment_initial_layout(self, base_response):
if self.use_dash_layout() and False:
return HttpResponse(base_response.data,
content_type=base_response.mimetype)
# Adjust the base layout response
baseDataInBytes = base_response.data
baseData = json.loads(baseDataInBytes.decode('utf-8'))
# Walk tree. If at any point we have an element whose id matches, then replace any named values at this level
reworked_data = self.walk_tree_and_replace(baseData)
response_data = json.dumps(reworked_data,
cls=PlotlyJSONEncoder)
return HttpResponse(response_data,
content_type=base_response.mimetype)

def walk_tree_and_extract(self, data, target):
if isinstance(data, dict):
for key in ['children','props',]:
self.walk_tree_and_extract(data.get(key,None),target)
ident = data.get('id', None)
if ident is not None:
idVals = target.get(ident,{})
for key, value in data.items():
if key not in ['props','options','children','id']:
idVals[key] = value
if len(idVals) > 0:
target[ident] = idVals
if isinstance(data, list):
for element in data:
self.walk_tree_and_extract(element, target)

def walk_tree_and_replace(self, data):
# Walk the tree. Rely on json decoding to insert instances of dict and list
# ie we use a dna test for anatine, rather than our eyes and ears...
if isinstance(data,dict):
response = {}
replacements = {}
# look for id entry
thisID = data.get('id',None)
if thisID is not None:
replacements = self._replacements.get(thisID,{})
# walk all keys and replace if needed
for k, v in data.items():
r = replacements.get(k,None)
if r is None:
r = self.walk_tree_and_replace(v)
response[k] = r
return response
if isinstance(data,list):
# process each entry in turn and return
return [self.walk_tree_and_replace(x) for x in data]
return data

def flask_app(self):
return self._flask_app
Expand All @@ -123,10 +235,13 @@ def locate_endpoint_function(self, name=None):

@Dash.layout.setter
def layout(self, value):
self._fix_component_id(value)

if self._adjust_id:
self._fix_component_id(value)
return Dash.layout.fset(self, value)

def _fix_component_id(self, component):

theID = getattr(component,"id",None)
if theID is not None:
setattr(component,"id",self._fix_id(theID))
Expand All @@ -137,6 +252,8 @@ def _fix_component_id(self, component):
pass

def _fix_id(self, name):
if not self._adjust_id:
return name
return "%s_-_%s" %(self._uid,
name)

Expand All @@ -150,3 +267,32 @@ def callback(self, output, inputs=[], state=[], events=[]):
[self._fix_callback_item(x) for x in state],
[self._fix_callback_item(x) for x in events])

def dispatch(self):
import flask
body = flask.request.get_json()
return self. dispatch_with_args(body, argMap=dict())

def dispatch_with_args(self, body, argMap):
inputs = body.get('inputs', [])
state = body.get('state', [])
output = body['output']

target_id = '{}.{}'.format(output['id'], output['property'])
args = []
for component_registration in self.callback_map[target_id]['inputs']:
args.append([
c.get('value', None) for c in inputs if
c['property'] == component_registration['property'] and
c['id'] == component_registration['id']
][0])

for component_registration in self.callback_map[target_id]['state']:
args.append([
c.get('value', None) for c in state if
c['property'] == component_registration['property'] and
c['id'] == component_registration['id']
][0])

return self.callback_map[target_id]['callback'](*args,**argMap)


Loading