6 | {% endfor %}
7 | {% endmacro %}
8 |
--------------------------------------------------------------------------------
/diva/utilities.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 | from .widgets import parse_widget_form_data, validate_widget_form_data, widgets_template_data
3 | from functools import singledispatch
4 | from flask import send_file, jsonify
5 | import base64
6 |
7 | def file_response(name, filepath):
8 | """
9 | name: when the client downloads the file, it will be called this (ex. "my_file.csv")
10 |
11 | filepath: path to the file that should be sent to the client
12 | """
13 | with open(filepath, 'rb') as content_file:
14 | file_bytes = content_file.read()
15 | encoded_bytes = base64.b64encode(file_bytes)
16 | response = {
17 | 'filename': name,
18 | 'content': encoded_bytes.decode('utf-8')
19 | }
20 | return jsonify(response)
21 |
22 | # map from type to list of utils for that type
23 | type_utils = {}
24 |
25 | def label_utility(ui_name):
26 | """
27 | Get a label utility with the given name
28 | """
29 | def gen_html(val):
30 | return render_template('utility_label.html', name=ui_name)
31 | # this will never be called
32 | def apply(val, form_data):
33 | pass
34 | return {'generate_html': gen_html, 'apply': apply}
35 |
36 | def register_simple_util(ui_name, some_type, widgets=[]):
37 | """
38 | Helper function for register_widget_util.
39 |
40 | widgets: a list of widgets. The values of these widgets are passed to
41 | the decorated function like ``your_func(val, *widget_values)``
42 |
43 | This is meant to decorate a function that takes the view value as its first
44 | argument, followed by a list of arguments that are given by widgets. It returns
45 | the result of a call to ``file_response``
46 | """
47 | def decorator(user_func):
48 | """
49 | user_func must be like appy_func followed by widget-set args
50 | """
51 | register_widget_util(ui_name, some_type, lambda val: widgets, user_func)
52 | return user_func
53 |
54 | return decorator
55 |
56 | def register_widget_util(ui_name, some_type, gen_widgets, apply_with_params):
57 | """
58 | ui_name: the name of this utility in the UI
59 |
60 | some_type: this utility will appear in the sidebar whenever your view function
61 | returns a value of type ``some_type``
62 |
63 | gen_widgets(val): a function that takes the report value (of the specified type), and
64 | returns a list of widgets. These widget values will be passed like:
65 | ``apply_with_params(val, *widget_values)``.
66 |
67 | apply_with_params: a function that takes the report value (of the specified type) as
68 | its first parameter, followed by a list of arguments that are given by widgets. The function must
69 | return the result of a call to ``file_response``
70 | """
71 | def gen_html(val):
72 | widgets = gen_widgets(val)
73 | widget_data = widgets_template_data(widgets)
74 | return render_template('utility_button.html', name=ui_name, widgets=widget_data)
75 |
76 | def apply_util(val, data):
77 | widgets = gen_widgets(val)
78 | validate_widget_form_data(widgets, data)
79 | inputs = parse_widget_form_data(widgets, data)
80 | return apply_with_params(val, *inputs)
81 |
82 | register_util_for_type(some_type, gen_html, apply_util)
83 |
84 | def register_util_for_type(my_type, gen_html, apply_util):
85 | """
86 | gen_html: func that takes a value and returns the html for the utility
87 | that works with that value
88 |
89 | apply_util: func that takes a value and form_data dict. returns whatever the
90 | flask server should return to the browser
91 | """
92 | if my_type not in type_utils:
93 | type_utils[my_type] = []
94 | util = {'generate_html': gen_html, 'apply': apply_util}
95 | type_utils[my_type].append(util)
96 |
97 | @singledispatch
98 | def get_utilities_for_value(val):
99 | return type_utils.get(type(val), [])
100 |
--------------------------------------------------------------------------------
/diva/utils.py:
--------------------------------------------------------------------------------
1 | from jinja2 import Environment, PackageLoader
2 |
3 | # Allow Jinja use without Flask
4 | env = Environment(loader=PackageLoader('diva', 'templates'))
5 |
6 | def render_template(filename, **variables):
7 | return env.get_template(filename).render(variables)
8 |
9 |
10 |
--------------------------------------------------------------------------------
/diva/widgets.py:
--------------------------------------------------------------------------------
1 | from .exceptions import ValidationError
2 | from flask import render_template
3 | from datetime import date, time, datetime, timedelta
4 | from dateutil.relativedelta import *
5 | from functools import singledispatch
6 | from jsonschema import validate
7 |
8 | class Widget():
9 | def generateHTML(self, formid):
10 | return ''
11 |
12 | def parseForm(self, formData):
13 | return formData
14 |
15 | def default_value(self):
16 | return self.default
17 |
18 | class InputTagWidget(Widget):
19 | def generateHTML(self, formid):
20 | return render_template('input_tag_widget.html',
21 | description=self.description,
22 | attributes=self.attributes)
23 |
24 | class Skip(Widget):
25 | """
26 | A non-interactive widget that just provides a label/header.
27 | Useful for clarifying long widget forms
28 | """
29 | def __init__(self, description):
30 | self.description = description
31 |
32 | def generateHTML(self, formid):
33 | return render_template('label_widget.html', description=self.description)
34 |
35 | def parseForm(self, form_data):
36 | """
37 | This widget should be skipped when parsing form data
38 | see the should_skip and parse_widget_form_data functions below
39 | """
40 | pass
41 |
42 | def validate_input(self, form_data):
43 | pass
44 |
45 | class String(InputTagWidget):
46 | """
47 | Output: str
48 | """
49 | def __init__(self, description, default=""):
50 | """
51 | description(str): the text for this widget's label.
52 | """
53 | self.description = description
54 | self.default = default
55 | self.attributes = {'type': 'text', 'value': default}
56 |
57 | def validate_input(self, formData):
58 | schema = {'type': 'string'}
59 | validate(formData, schema)
60 |
61 | # a helper for validation of numberical types
62 | def set_schema_bounds(schema, min_val, max_val):
63 | if min_val is not None:
64 | schema['minimum'] = min_val
65 | if max_val is not None:
66 | schema['maximum'] = max_val
67 |
68 | def validate_bounds(num, min_val, max_val):
69 | below_min = min_val is not None and num < min_val
70 | above_max = max_val is not None and max_val < num
71 | if below_min or above_max:
72 | raise ValueError("num is outside bounds")
73 |
74 | class Float(InputTagWidget):
75 | """
76 | Output: float
77 | """
78 | def __init__(self, description, default=0, minVal=None, maxVal=None,
79 | step=0.001):
80 | """
81 | step: the interval between allowable values
82 | """
83 | self.description = description
84 | self.default = default
85 | self.minVal = minVal
86 | self.maxVal = maxVal
87 | self.attributes = {'type': 'number', 'value': default,
88 | 'min': minVal, 'max': maxVal, 'step': step}
89 |
90 | def validate_input(self, formData):
91 | num = float(formData)
92 | validate_bounds(num, self.minVal, self.maxVal)
93 |
94 | def parseForm(self, formData):
95 | return float(formData)
96 |
97 | class Int(Float):
98 | """
99 | Output: int
100 | """
101 | def __init__(self, description, default=0, minVal=None, maxVal=None):
102 | super().__init__(description, default, minVal, maxVal, step=1)
103 |
104 | def validate_input(self, formData):
105 | num = int(formData)
106 | validate_bounds(num, self.minVal, self.maxVal)
107 |
108 | def parseForm(self, formData):
109 | return int(formData)
110 |
111 | class Bool(Widget):
112 | """
113 | Output: bool
114 | """
115 | def __init__(self, description, default=False):
116 | self.description = description
117 | self.default = default
118 | self.attributes = {'type': 'checkbox'}
119 |
120 | def generateHTML(self, formid):
121 | return render_template('checkbox_widget.html',
122 | description=self.description,
123 | attributes=self.attributes,
124 | checked=self.default)
125 |
126 | def validate_input(self, formData):
127 | schema = {'type': 'boolean'}
128 | validate(formData, schema)
129 |
130 | def parseForm(self, formData):
131 | return bool(formData)
132 |
133 | class SelectOne(Widget):
134 | """
135 | Output: the str that the user selected
136 | """
137 |
138 | # default is index into the choices array
139 | def __init__(self, description, choices, default=None):
140 | """
141 | * choices: a list of strings.
142 | * default: a string in ``choices``. If not specified,
143 | the default will be the first string in ``choices``.
144 | """
145 | self.description = description
146 | self.choices = choices
147 | if default is None:
148 | self.default = choices[0]
149 | else:
150 | self.default = default
151 |
152 | def generateHTML(self, formid):
153 | return render_template('radio_widget.html',
154 | formid=formid,
155 | description=self.description,
156 | choices=self.choices,
157 | defaultChoice=self.default)
158 |
159 | def validate_input(self, formData):
160 | schema = {
161 | 'type': 'string',
162 | 'enum': self.choices
163 | }
164 | validate(formData, schema)
165 |
166 | class SelectSubset(Widget):
167 | """
168 | Output: A list of all the strings that the user selection.
169 | It may be empty.
170 | """
171 | def __init__(self, description, choices, default=[]):
172 | """
173 | * choices: a list of strings
174 | * default: a list of strings in ``choices`` that will be
175 | selected by default.
176 | """
177 | self.description = description
178 | self.choices = choices
179 | self.default = default
180 |
181 | def generateHTML(self, formid):
182 | return render_template('checklist_widget.html',
183 | description=self.description,
184 | choices=self.choices,
185 | default=self.default)
186 |
187 | def validate_input(self, formData):
188 | has_duplicates = len(set(formData)) < len(formData)
189 | is_subset = set(formData).issubset(set(self.choices))
190 | if has_duplicates or (not is_subset):
191 | raise ValidationError("{} is not a subset of {}", formData, self.choices)
192 |
193 | # TODO: keep in hexadecimal for now
194 | # can later use the more effective:
195 | # return as RGB triple (r, g, b) with values in [0, 1]
196 | class Color(InputTagWidget):
197 | """
198 | Output: a hexadecimal string in the format #RRGGBB
199 | """
200 | def __init__(self, description, default='#000000'):
201 | self.description=description
202 | self.default = default
203 | self.attributes = {'type': 'color', 'value': default}
204 |
205 | def validate_input(self, formData):
206 | schema = {
207 | 'type': 'string',
208 | 'pattern': '^#([A-Fa-f0-9]{6})$'
209 | }
210 | validate(formData, schema)
211 |
212 | # TODO: this is for rgb triple
213 | # def validate_input(self, formData):
214 | # schema = {
215 | # 'type': 'array',
216 | # 'items': {
217 | # 'type': 'number',
218 | # 'minimum': 0,
219 | # 'maximum': 1
220 | # },
221 | # 'minItems': 3,
222 | # 'maxItems': 3
223 | # }
224 | # validate(formData, schema)
225 |
226 | class Slider(Widget):
227 | """
228 | Slider has the same function as Float, the only difference is the UI
229 | Output: float
230 | """
231 | def __init__(self, description, default=1, valRange=(0, 1), numDecimals=4):
232 | """
233 | * valRange: (min, max) where min and max are floats
234 | * numDecimals: the number of decimal places to display in the UI
235 | """
236 | self.description = description
237 | self.valRange = valRange
238 | self.default = default
239 | self.numDecimals = numDecimals
240 | step = 1 / (10 ** numDecimals);
241 | self.attributes = {'type': 'range', 'min': self.valRange[0],
242 | 'max': self.valRange[1], 'step': step, 'value': self.default}
243 |
244 | def generateHTML(self, formid):
245 | return render_template('slider_widget.html',
246 | description=self.description,
247 | default=('{:.{}f}').format(self.default, self.numDecimals),
248 | attributes=self.attributes)
249 |
250 | def validate_input(self, formData):
251 | num = float(formData)
252 | min_val, max_val = self.valRange
253 | validate_bounds(num, min_val, max_val)
254 |
255 | def parseForm(self, formData):
256 | return Float.parseForm(self, formData)
257 |
258 | # classes and helpers for internally working with date ranges
259 |
260 | def iso_to_date(isoStr):
261 | dt = datetime.strptime(isoStr, '%Y-%m-%d')
262 | return dt.date()
263 |
264 | # the date can either by specified as absolute or relative
265 | # to the current date
266 |
267 | class DateModel():
268 | def value(self):
269 | pass
270 | def iso(self):
271 | return self.value().isoformat()
272 |
273 | class AbsoluteDate(DateModel):
274 | def __init__(self, date):
275 | self.date = date
276 |
277 | def value(self):
278 | return self.date
279 |
280 | class RelativeDate(DateModel):
281 | def __init__(self, duration):
282 | self.duration = duration
283 |
284 | def value(self):
285 | return date.today() - self.duration
286 |
287 | # convert the specified date to a date model
288 | @singledispatch
289 | def to_date_model(date):
290 | raise ValueError("given date must be: a) ISO format string, b) datetime.date object, c) datetime.timedelta object, or d) dateutil.relativedelta object")
291 |
292 | # date obj converts to absolute date
293 | @to_date_model.register(date)
294 | def date_to_model(date):
295 | return AbsoluteDate(date)
296 |
297 | # string is assumed to be an absolute date in ISO format
298 | @to_date_model.register(str)
299 | def iso_to_model(date_str):
300 | return AbsoluteDate(iso_to_date(date_str))
301 |
302 | # delta objects convert to dates relative to the current date
303 | # A positive delta is an offset into the past, not the future
304 | # ex. a delta of 1 day means yesterday, not tomorrow
305 | @to_date_model.register(timedelta)
306 | @to_date_model.register(relativedelta)
307 | def delta_to_model(date_delta):
308 | return RelativeDate(date_delta)
309 |
310 | class Date(InputTagWidget):
311 | """
312 | Output: datetime.date
313 | """
314 | def __init__(self, description, default=relativedelta()):
315 | """
316 | default: may either be provided as a:
317 |
318 | * datetime.date object
319 | * string in ISO format (YYYY-mm-dd)
320 | * datetime.timedelta object. The date will be current - delta
321 | * dateutil.relativedelta object. The date will be current - delta
322 |
323 | If not specified, it will be the current date.
324 | Note that dateutil is not in the Python standard library. It provides a simpler
325 | API to specify a duration in days, weeks, months, etc. You can install it with pip.
326 | """
327 | self.description = description
328 | self.default = to_date_model(default)
329 | # see Bootstrap date picker docs for options
330 | # https://bootstrap-datepicker.readthedocs.io/en/stable/#
331 | self.attributes = {
332 | 'data-date-format': 'yyyy-mm-dd',
333 | 'data-date-orientation': 'left bottom',
334 | 'data-date-autoclose': 'true',
335 | 'value': self.default.iso(),
336 | }
337 |
338 | def default_value(self):
339 | return self.default.value()
340 |
341 | def validate_input(self, formData):
342 | schema = {'type': 'string'}
343 | validate(formData, schema)
344 | # throws ValueError if incorrect format
345 | iso_to_date(formData)
346 |
347 | def parseForm(self, formData):
348 | return iso_to_date(formData)
349 |
350 | class DateRange(InputTagWidget):
351 | """
352 | Output: (start_date, end_date) of type (datetime.date, datetime.date)
353 | """
354 |
355 | def __init__(self, description, start=relativedelta(), end=relativedelta()):
356 | """
357 | ``start`` and ``end`` follow the same rules as ``default`` for ``Date``
358 | """
359 | self.description = description
360 | self.start_date = to_date_model(start)
361 | self.end_date = to_date_model(end)
362 | date = '{} to {}'.format(self.start_date.iso(),
363 | self.end_date.iso())
364 | self.attributes = {'type': 'text',
365 | 'value': date,
366 | 'size': len(date),
367 | 'data-startdate': self.start_date.iso(),
368 | 'data-enddate': self.end_date.iso()}
369 |
370 | def default_value(self):
371 | return (self.start_date.value(), self.end_date.value())
372 |
373 | def validate_input(self, formData):
374 | schema = {
375 | 'type': 'array',
376 | 'minItems': 2,
377 | 'maxItems': 2,
378 | 'items': {
379 | 'type': 'string',
380 | }
381 | }
382 | validate(formData, schema)
383 | # strptime throws a value error if does not match the format
384 | for dateStr in formData:
385 | iso_to_date(dateStr)
386 |
387 | def parseForm(self, formData):
388 | start = iso_to_date(formData[0])
389 | end = iso_to_date(formData[1])
390 | return (start, end)
391 |
392 | # TODO: use the same lazy eval as done with datetime.date objects
393 | # NB: times are in 24hr format
394 | class Time(InputTagWidget):
395 | """
396 | Output: datetime.time object
397 | """
398 | def __init__(self, description, default=time()):
399 | """
400 | default: datetime.time object
401 | """
402 | self.description = description
403 | self.default = default
404 | time_str = self.default.strftime('%H:%M')
405 | self.attributes = {
406 | 'value': time_str
407 | }
408 |
409 | def validate_input(self, formData):
410 | schema = {'type': 'string'}
411 | validate(formData, schema)
412 | # throws ValueError on invalid format
413 | datetime.strptime(formData, '%H:%M')
414 |
415 | def parseForm(self, formData):
416 | dt = datetime.strptime(formData, '%H:%M')
417 | return dt.time()
418 |
419 | def widgets_template_data(widgets):
420 | """
421 | The return value of this func should be the input to the widgetform
422 | macro. This generates a widgets form for the given list of widgets.
423 | """
424 | widget_data = []
425 | for index, widget in enumerate(widgets):
426 | # the widget type is the name of its python class
427 | # the JS setup func is Reports.Widgets.setupMap[widget_type]
428 | widget_type = type(widget).__name__
429 | html = widget.generateHTML(index)
430 | data = {'type': widget_type, 'html': html}
431 | widget_data.append(data)
432 | return widget_data
433 |
434 | def should_skip(widget):
435 | """
436 | True if should skip this widget when parsing values, false ow
437 | """
438 | return type(widget).__name__ == 'Skip'
439 |
440 | def validate_widget_form_data(widgets, inputs):
441 | try:
442 | # validate all of the given widget values in 'widgetValues'
443 | if type(inputs) is not list:
444 | raise ValueError("inputs must be an array")
445 | if len(inputs) != len(widgets):
446 | raise ValueError("the inputs array has an incorrect number of items")
447 | for wid, value in zip(widgets, inputs):
448 | wid.validate_input(value)
449 | except Exception as e:
450 | raise ValidationError(str(e))
451 |
452 | def parse_widget_form_data(widgets, form_data):
453 | """
454 | Given list of widgets and a list of form data, with one item
455 | for each widget, return a list of parsed form data
456 | """
457 | inputs = [wid.parseForm(data) for wid, data in
458 | zip(widgets, form_data) if not should_skip(wid)]
459 | return inputs
460 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = python3 -msphinx
7 | SPHINXPROJ = diva
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/about.rst:
--------------------------------------------------------------------------------
1 | About
2 | *******
3 |
4 | I made this library as a hobby project, and I no longer maintain it. There are plenty of alternatives, such as plotly.
5 |
6 | Matthew Riley
7 |
8 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # diva documentation build configuration file, created by
5 | # sphinx-quickstart on Thu Aug 10 16:35:43 2017.
6 | #
7 | # This file is execfile()d with the current directory set to its
8 | # containing dir.
9 | #
10 | # Note that not all possible configuration values are present in this
11 | # autogenerated file.
12 | #
13 | # All configuration values have a default; values that are commented out
14 | # serve to show the default.
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 |
20 | import os
21 | import sys
22 | sys.path.insert(0, os.path.abspath('../diva'))
23 |
24 |
25 | # -- General configuration ------------------------------------------------
26 |
27 | # If your documentation needs a minimal Sphinx version, state it here.
28 | #
29 | # needs_sphinx = '1.0'
30 |
31 | # Add any Sphinx extension module names here, as strings. They can be
32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
33 | # ones.
34 | extensions = ['sphinx.ext.autodoc']
35 |
36 | # autodoc configuration
37 | autoclass_content = 'both' # both class doc and init doc
38 | autodoc_member_order = 'bysource'
39 |
40 | # Add any paths that contain templates here, relative to this directory.
41 | templates_path = ['_templates']
42 |
43 | # The suffix(es) of source filenames.
44 | # You can specify multiple suffix as a list of string:
45 | #
46 | # source_suffix = ['.rst', '.md']
47 | source_suffix = '.rst'
48 |
49 | # The master toctree document.
50 | master_doc = 'index'
51 |
52 | # General information about the project.
53 | project = 'diva'
54 | copyright = '2017, Matthew Riley'
55 | author = 'Matthew Riley'
56 |
57 | # The version info for the project you're documenting, acts as replacement for
58 | # |version| and |release|, also used in various other places throughout the
59 | # built documents.
60 | #
61 | # The short X.Y version.
62 | version = '0.1.0'
63 | # The full version, including alpha/beta/rc tags.
64 | release = '0.1.0'
65 |
66 | # The language for content autogenerated by Sphinx. Refer to documentation
67 | # for a list of supported languages.
68 | #
69 | # This is also used if you do content translation via gettext catalogs.
70 | # Usually you set "language" from the command line for these cases.
71 | language = None
72 |
73 | # List of patterns, relative to source directory, that match files and
74 | # directories to ignore when looking for source files.
75 | # This patterns also effect to html_static_path and html_extra_path
76 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
77 |
78 | # The name of the Pygments (syntax highlighting) style to use.
79 | pygments_style = 'sphinx'
80 |
81 | # If true, `todo` and `todoList` produce output, else they produce nothing.
82 | todo_include_todos = False
83 |
84 |
85 | # -- Options for HTML output ----------------------------------------------
86 |
87 | # The theme to use for HTML and HTML Help pages. See the documentation for
88 | # a list of builtin themes.
89 | #
90 | html_theme = 'alabaster'
91 |
92 | # Theme options are theme-specific and customize the look and feel of a theme
93 | # further. For a list of options available for each theme, see the
94 | # documentation.
95 |
96 | html_theme_options = {
97 | 'description': 'An analytics dashboard library',
98 | 'github_user': 'mgriley',
99 | 'github_repo': 'diva',
100 | # 'github_button': False,
101 | 'github_count': False,
102 | 'analytics_id': 'UA-87495443-6',
103 | 'extra_nav_links': {
104 | 'diva @ github': 'https://github.com/mgriley/diva',
105 | 'diva @ PyPi': 'https://pypi.python.org/pypi/diva'
106 | }
107 | }
108 |
109 | # Add any paths that contain custom static files (such as style sheets) here,
110 | # relative to this directory. They are copied after the builtin static files,
111 | # so a file named "default.css" will overwrite the builtin "default.css".
112 | html_static_path = ['_static']
113 |
114 | # Custom sidebar templates, must be a dictionary that maps document names
115 | # to template names.
116 | #
117 | # This is required for the alabaster theme
118 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
119 | html_sidebars = {
120 | '**': [
121 | 'about.html',
122 | 'navigation.html',
123 | 'relations.html', # needs 'show_related': True theme option to display
124 | 'searchbox.html',
125 | 'donate.html',
126 | ]
127 | }
128 |
129 |
130 | # -- Options for HTMLHelp output ------------------------------------------
131 |
132 | # Output file base name for HTML help builder.
133 | htmlhelp_basename = 'divadoc'
134 |
135 |
136 | # -- Options for LaTeX output ---------------------------------------------
137 |
138 | latex_elements = {
139 | # The paper size ('letterpaper' or 'a4paper').
140 | #
141 | # 'papersize': 'letterpaper',
142 |
143 | # The font size ('10pt', '11pt' or '12pt').
144 | #
145 | # 'pointsize': '10pt',
146 |
147 | # Additional stuff for the LaTeX preamble.
148 | #
149 | # 'preamble': '',
150 |
151 | # Latex figure (float) alignment
152 | #
153 | # 'figure_align': 'htbp',
154 | }
155 |
156 | # Grouping the document tree into LaTeX files. List of tuples
157 | # (source start file, target name, title,
158 | # author, documentclass [howto, manual, or own class]).
159 | latex_documents = [
160 | (master_doc, 'diva.tex', 'diva Documentation',
161 | 'Matthew Riley', 'manual'),
162 | ]
163 |
164 |
165 | # -- Options for manual page output ---------------------------------------
166 |
167 | # One entry per manual page. List of tuples
168 | # (source start file, name, description, authors, manual section).
169 | man_pages = [
170 | (master_doc, 'diva', 'diva Documentation',
171 | [author], 1)
172 | ]
173 |
174 |
175 | # -- Options for Texinfo output -------------------------------------------
176 |
177 | # Grouping the document tree into Texinfo files. List of tuples
178 | # (source start file, target name, title, author,
179 | # dir menu entry, description, category)
180 | texinfo_documents = [
181 | (master_doc, 'diva', 'diva Documentation',
182 | author, 'diva', 'One line description of project.',
183 | 'Miscellaneous'),
184 | ]
185 |
--------------------------------------------------------------------------------
/docs/developers_guide.rst:
--------------------------------------------------------------------------------
1 | Developer's Guide
2 | ******************
3 |
4 | In case you'd like to contribute, here is an explanation of how the parts fit together.
5 |
6 | First, see the file diva/diva/reporter.py. Notice the constructor creates a Flask object ``self.server``, and an array to store the user's decorated functions. Calling the ``view`` decorator stores a reference to the user's function in a map, along with the widget values. Next, see ``run``, which is simply delegated to the underlying Flask object.
7 |
8 | The index endpoint (/) is the HTML that is returned when you go the root of the site. This produces the menu of reports, widgets for each report, and all other HTML. The index.html template refers to diva/diva/templates/index.html. Next, there is the update endpoint. Whenever the user reloads a report, the widgets values are gathered into a JSON object and sent to this endpoint. This function calls ``generate_figure_html``, which converts the JSON to the relevant Python datatypes, calls your function (remember that ``view`` stores a reference to it), and converts its result to HTML using ``convert_to_html``. ``convert_to_html`` is defined in diva/diva/converters.py file, alongside all of the supported types that can be converted.
9 |
10 | Before we get to the Javascript, let's look at the widgets. A widget must be able to:
11 |
12 | #. generate HTML allowing the user to select its value
13 | #. retrieve the current value of the widget from the HTML DOM
14 | #. convert this retrieved value into the python object that the user expects
15 | #. for convenience, provide a default value. The user can restore a report to its default values.
16 |
17 | Parts 1, 3, and 4 are done in diva/diva/widgets.py. You will see that parts 3 and 4 are straightforward. The ``generateHTML`` functions render templates in the diva/diva/templates folder. Many of the widgets are thin wrappers around HTML input tags, so these widgets extend the ``InputTagWidget`` class. Part 2 ventures into Javascript: through diva/static/reports.js and diva/static/widgets.js.
18 |
19 | Let's look at diva/diva/templates/index.html. The contains a bunch of Bootstrap components, which are used for layout and to replace unsupported input tags (like type date and time). Some parts of the may seem strange, like the fact that the links in the dropdown menu don't link anywhere. This is because the webpage is tabbed. When you change the report, the current report is just hidden in case you want to see it again later. In some cases the loop index of a templating loop is used as an id. This is used to refer to specific reports later. Note that the ``{{ widget.html | safe }}`` statement is where the HTML from ``generateHTML`` is transplanted into the page.
20 |
21 | The Javascript files are found in diva/diva/static. JQuery is heavily used. The file report.js creates a global Reports object. It has a list of Report objects (created by the newReport function), a property Widgets that contains ``setupMap``, and functions for adding new report objects to the global state. Each report maintains a list of its widgets. The most important function for a report is ``object.update``, given in the ``newReport`` function. ``update`` is where the Flask server's update endpoint is called, and the DOM is updated with the HTML received. As you can see, the JSON sent to the server requires collecting the values of the report's widgets (via ``obj.widgets.getValues()``). Scrolling up to ``newFigureWidgets``, you can see that this just calls ``getCurrentValue`` on each widget object. These functions are defined in widgets.js (alongside the a ``resetToDefault`` function, which is less important). In widget.js you will see an entry in Reports.Widgets.setupMap for every widgets class in diva/diva/widget.py. Finally, in script.js we iterate through relevant sections of the index.html DOM, and use the data passed to the index.html Jinja template from the server side (see ``generate_widget_data`` in reporter.py, for ex.) to build the following structure:
22 |
23 | * Reports
24 | * Report 0
25 | * Widget a
26 | * Widget b
27 | * ...
28 | * ...
29 |
--------------------------------------------------------------------------------
/docs/images/example_screenshot_a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgriley/diva/5a9e8144f690d02eca0d221544c675ce682ed548/docs/images/example_screenshot_a.png
--------------------------------------------------------------------------------
/docs/images/example_screenshot_b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgriley/diva/5a9e8144f690d02eca0d221544c675ce682ed548/docs/images/example_screenshot_b.png
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. diva documentation master file, created by
2 | sphinx-quickstart on Thu Aug 10 16:35:43 2017.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Diva
7 | ================================
8 |
9 | Diva is a Python library for creating interactive dashboards. It supports analytics libraries like matplotlib, pandas, and bokeh. MIT Licensed.
10 |
11 | The example below will serve a webpage where you can interact with the decorated functions ``foo`` and ``bar``. In this case, the pandas Series objects are converted to HTML tables. You can see a demo video here: https://vimeo.com/351814466. Please see the User's Guide for more details.
12 |
13 | .. literalinclude:: ../examples/minimal_example.py
14 |
15 | .. toctree::
16 | :maxdepth: 2
17 | :caption: Contents:
18 |
19 | users_guide
20 | developers_guide
21 | about
22 |
23 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=python -msphinx
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 | set SPHINXPROJ=diva
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed,
20 | echo.then set the SPHINXBUILD environment variable to point to the full
21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the
22 | echo.Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgriley/diva/5a9e8144f690d02eca0d221544c675ce682ed548/docs/requirements.txt
--------------------------------------------------------------------------------
/docs/to_note.txt:
--------------------------------------------------------------------------------
1 | This dir was generated with: sphinx-quickstart
2 | To build the docs: make html
3 |
--------------------------------------------------------------------------------
/docs/users_guide.rst:
--------------------------------------------------------------------------------
1 | User's Guide
2 | *************
3 |
4 | If you are restless, you can scroll down to the Jumbo Example section. Otherwise, here are the instructions:
5 |
6 | Setup
7 | ============
8 |
9 | This library requires Python 3.4-3.6. Assuming you already have Python 3 and pip installed, you can setup your project like this::
10 |
11 | $ mkdir myproj
12 | $ cd myproj
13 | $ python3 -m venv myvenv
14 | $ source myvenv/bin/activate
15 | $ pip install diva
16 |
17 | (you can check your version of Python 3 with ``python3 --version``)
18 |
19 | The command ``python3 -m venv myvenv`` creates a directory called ``myvenv`` to handle your project's virtual environment. A virtual environment is a mechanism that gives the illusion (hence "virtual") that your project's version of the Python interpreter and any required libraries are installed locally. This isolates your project from other projects that may use different versions of Python (and thus different library versions). Virtual environments prevent conflicts of the form: Project A uses Python 2 and Project B uses Python3, and both depend on ``somelibrary``, which is installed globally. Project A is broken because it thinks it should use the latest installed version of ``somelibrary``, which only works for Python 3.
20 |
21 | When you start working on your project, you must activate the environment with ``$ source myenv/bin/activate`` (which should prepend the environment name to your prompt like ``(myvenv) ... $``), and you should deactivate it when you're done using ``$ deactivate``.
22 |
23 | Introduction
24 | =============
25 |
26 | Let's start with a simple example. You'll need to install diva and pandas from pip.
27 |
28 | .. literalinclude:: ../examples/minimal_example.py
29 |
30 | You can run the example like::
31 |
32 | $ python3 minimal_example.py
33 | * Running on http://127.0.0.1:5000/ (Press Ctrl+C to quit)
34 | ...
35 |
36 | Going to the given address in your browser should display:
37 |
38 | .. image:: images/example_screenshot_a.png
39 |
40 | You should be able to change the report, and play with the widget values.
41 |
42 | .. image:: images/example_screenshot_b.png
43 |
44 | First, we create a ``Diva`` object. Next, we use python's `decorator syntax `_ to register our analytics functions ``foo`` and ``bar`` with our ``Diva`` object. The ``view`` decorator *does not modify the underlying function* (``view`` just stores a reference to it in the ``Diva`` object). You can call ``foo`` or ``bar`` elsewhere in your code as if you weren't using diva at all. Finally, we call ``app.run()``, which serves the website linked above. The site contains a report for every function we register with our ``Diva`` object.
45 |
46 | You can pass a list of widgets to ``view``. The ``bar`` function takes an integer and a float, so we pass the ``Int`` and ``Float`` objects to ``view``. As you can see, the webserver generates appropriate HTML widgets. When we reload the ``bar`` report, the values of these widgets are sent to the server, passed to ``bar``, and the result of ``bar`` is sent back to the browser (converted to HTML).
47 |
48 | Basic API
49 | ==========
50 |
51 | .. function:: Diva()
52 |
53 | This internally creates ``self.server``, a Flask object, which is is started by ``run``. More complex uses of Diva may require directly modifying this Flask object.
54 |
55 | .. function:: Diva.view(name, widgets=[], short=None)
56 |
57 | Meant to be used with decorator syntax. ``name`` is what the view will be called in the web interface. ``widgets`` is an optionally empty list of ``diva.widgets.Widget`` objects. ``short`` allows you to give a short name that you can use to refer to the report later (see ``compose_view``). It will be set to ``name`` by default. Your decorated function is called like, ``your_func(*widget_values)``, where ``widget_values`` the list of values of the given widgets. Please see the Widgets section for a list of available widgets and what values they pass to the underlying function.
58 |
59 | .. function:: Diva.compose_view(name, view_names, layout=None, short=None)
60 |
61 | Creates a view by composing existing views. ``name`` is the name of the new view, ``view_names`` is a list of names (its ``short`` name if one is given, otherwise its UI ``name``) of the desired reports, ``layout`` is a Dashboard layout (please see the Dashboard section), and ``short`` is a short name to give to the newly created report (this works the same as ``short`` from ``view``). Note that this function can only be called after you've registered all of the views named in ``view_names``.
62 |
63 | .. function:: Diva.run(host=None, port=None, debug=None, **options)
64 |
65 | ``run`` internally looks like this::
66 |
67 | # self.server is a Flask object
68 | self.server.run(host, port, debug, **options)
69 |
70 | Please see the `Flask documentation `_ for an explanation of ``run``'s arguments. Briefly, setting ``debug=True`` will open an interactive debugger when an exception occurs, and also attempt to reload the server when the code changes.
71 |
72 | .. warning::
73 |
74 | The interactive debugger allows one to run arbitrary Python code on your server, so don't use ``debug=True`` on a publically accessable site.
75 |
76 | .. warning::
77 |
78 | If you want to make your diva app production ready, follow `these steps `_ to make the underlying Flask server production ready. Also see the Security section below.
79 |
80 | .. function:: Diva.__call__(environ, start_response)
81 |
82 | This is likely only relevant to you if you'd like to deploy the server, in which case you should first read an article on WSGI servers and also refer to `Flask's documentation `_. The ``Diva`` object is callable as a WSGI entry point. This function passes the args to the Flask server's (``self.server``) WSGI entry point and returns the result. Please see the source directory ``diva/examples/demo_server`` for an example.
83 |
84 | Widgets
85 | ========
86 |
87 | The built-in widgets (available via ``from diva.widgets import *``) are:
88 |
89 | * String
90 | * Float
91 | * Int
92 | * Bool
93 | * SelectOne
94 | * SelectSubset
95 | * Color
96 | * Slider
97 | * Date
98 | * DateRange
99 | * Time
100 |
101 | The first argument passed to every widget constructor is the description of the widget in the web interface (such as, "choose a scale").
102 |
103 | .. automodule:: diva.widgets
104 | :members:
105 | :exclude-members: Skip, should_skip, validate_widget_form_data, parse_widget_form_data, widgets_template_data
106 |
107 | Converters
108 | ===========
109 |
110 | Diva attempts to convert the return value of your functions to HTML. The following conversions are supported:
111 |
112 | * string: the string is assumed to be HTML.
113 | * Dashboard: a diva.Dashboard object, see the Dashboard section below
114 | * matplotlib.figure.Figure (using the mpld3 library)
115 | * pandas.DataFrame & pandas.Series
116 | * bokeh.plotting.figure.Figure
117 | * *other*: the value is converted to a string and wrapped in HTML
118 |
119 | Conversion internally uses the `single dispatch decorator from functools `_, so you can add your own converter like this:
120 |
121 | .. literalinclude:: ../examples/custom_converter.py
122 |
123 | Dashboards
124 | ===========
125 |
126 | The ``diva.Dashboard`` class and the ``diva.compose_view`` function allow you to create views that arrange plots, tables, etc. in a grid layout.
127 |
128 | .. function:: diva.Dashboard(convertable_list, layout=None)::
129 |
130 | ``convertable_list`` is a list of objects that can be converted to HTML (see the Converters section), such as ``[my_figure, my_table, my_custom_html]`` (you can even include other Dashboard objects). ``layout`` specifies how the items are sized and positioned in the grid. The most convenient way to create a layout is with ``diva.row_layout``.
131 |
132 | .. function:: diva.row_layout(*num_columns)::
133 |
134 | The ith integer given is the number of items to place in row i. Returns a layout compatible with ``Dashboard`` and ``compose_view``. Examples: ``row_layout(1, 1, 1)`` creates a 3-row layout where there is one item per row. ``row_layout(1, 2)`` creates a 2-row layout where there is one item in the first row and two items in the second row (placed side by side, with the row divided in half).
135 |
136 | If ``row_layout`` is not enough, you can manually specify the ``layout`` argument. It is a list of ``[top_left_x, top_left_y, width, height]`` lists. For a 10 by 10 grid container, the top-left corner is (0, 0) and the bottom-right is (10, 10). For example, ``[0, 1, 2, 3]`` occupies the grid space from (0, 1) to (2, 4) on the grid. When giving your list of panes, you can imagine that your grid is any size you want. It doesn't matter because it is scaled to fit its parent div in HTML. For example, layouts ``[[0, 0, 1, 1], [1, 0, 1, 1]]`` and ``[[0, 0, 2, 2], [2, 0, 2, 2]]`` both give a vertically split layout. The first one is not smaller than the second. Note that ``row_layout(2)`` returns this same layout.
137 |
138 | .. literalinclude:: ../examples/dashboard_example.py
139 |
140 | Utilities
141 | ==========
142 |
143 | Depending on the type that your function returns, utility buttons may be added to the sidebar. If your view function returns a pandas DataFrame, for example, a button will appear in the widgets sidebar allowing you to export it to a .csv file. You can add utilities like this:
144 |
145 | .. literalinclude:: ../examples/custom_utility.py
146 |
147 | As shown in the example, utilities must return the result of a call to ``file_response``. This triggers a file download on the client side.
148 |
149 | .. autofunction:: diva.utilities.register_widget_util
150 |
151 | .. autofunction:: diva.utilities.register_simple_util
152 |
153 | .. autofunction:: diva.utilities.file_response
154 |
155 | Splitting into multiple files
156 | ==============================
157 |
158 | You split your project into multiple files using the extend function:
159 |
160 | .. automethod:: diva.reporter.Diva.extend
161 |
162 | Here is an example, where foo.py, bar.py, and main.py are in the same directory:
163 |
164 | foo.py
165 |
166 | .. literalinclude:: ../examples/multiple_files/foo.py
167 |
168 | bar.py
169 |
170 | .. literalinclude:: ../examples/multiple_files/bar.py
171 |
172 | main.py
173 |
174 | .. literalinclude:: ../examples/multiple_files/main.py
175 |
176 | To show all views, run ``python3 main.py``. If you want to focus your work on the views in foo.py,
177 | just run ``python3 foo.py``.
178 |
179 | Security
180 | =========
181 |
182 | **Input Sanitation**
183 |
184 | If you are allowing public access to your site, you are responsible for sanitizing user input. Diva performs some trivial sanitation, like ensuring the value of a string widget is actually passed to your function as a string and not an int. However, if your underlying functions are accessing sensitive information, take heed.
185 |
186 | **Password Protection**
187 |
188 | Diva currently doesn't support password management. It may support simple password protection in the future, but likely not a full user access system.
189 |
190 | However, you can modify the underlying Flask object to add your authentication code like this::
191 |
192 | app = Diva()
193 |
194 | # create some views like normal
195 |
196 | flask_server = app.server
197 |
198 | # Modify flask_server to add your auth code
199 |
200 | # this is the same as flask_server.run()
201 | app.run()
202 |
203 | You can modify the Flask object's view functions (`docs here `_) to add your auth code. See the function ``setup_server`` from the diva source file ``diva/diva/reporter.py`` to see what endpoints diva uses.
204 |
205 | More Examples
206 | ================
207 |
208 | You can find many examples in the ``diva/examples`` folder on Github.
209 |
210 | Jumbo Example
211 |
212 | .. literalinclude:: ../examples/jumbo_example.py
213 |
214 |
215 | matplotlib examples:
216 |
217 | .. literalinclude:: ../examples/matplotlib_examples.py
218 |
219 | kwargs example:
220 |
221 | If your function takes ``**kwargs``, you must suffer this mild inconvenience:
222 |
223 | .. literalinclude:: ../examples/other_examples.py
224 | :pyobject: baz
225 |
226 | .. literalinclude:: ../examples/other_examples.py
227 | :pyobject: baz_shim
228 |
229 | Alternatives
230 | =============
231 |
232 | Jupyter has its own widget library, and `you can interact with functions like this `_. To share a Jupyter notebook, you can archive the .ipynb file in your GitHub, then use the tools nbviewer or mybinder to give others access to your notebook. You can also take a look at `IPython Dashboards `_.
233 |
234 |
--------------------------------------------------------------------------------
/examples/compose.py:
--------------------------------------------------------------------------------
1 | # TODO: import reports
2 | import matplotlib
3 | # use the Agg backend, which is non-interactivate (just for PNGs)
4 | # this way, a separate script isn't started by matplotlib
5 | matplotlib.use('Agg')
6 | import matplotlib.pyplot as plt
7 | import numpy as np
8 | import pandas as pd
9 | from datetime import datetime
10 | from diva import Diva, row_layout
11 | from diva.widgets import *
12 | from bokeh.plotting import figure
13 | from functools import singledispatch
14 |
15 | reporter = Diva()
16 |
17 | @reporter.view('convert: pandas.DataFrame', [Int('l'), Int('w')], short='dat')
18 | def pandas_df(a, b):
19 | df = pd.DataFrame(np.random.randn(a, b))
20 | return df;
21 |
22 | @reporter.view('convert: pandas.Series', [Int('c'), Int('d')], short='ser')
23 | def pandas_series(a, b):
24 | s = pd.Series([p for p in range(a * b)])
25 | return s
26 |
27 | @reporter.view('convert: bokeh.plotting.figure.Figure', [Float('e'), Float('f')], short='bok')
28 | def bokeh_fig(a, b):
29 | x = [1, 2, 3, 4, a]
30 | y = [6, 7, 2, 4, b]
31 | plot = figure(title="bokeh example", x_axis_label='x', y_axis_label='y')
32 | plot.line(x, y, legend="Temp", line_width=2)
33 | return plot
34 |
35 | reporter.compose_view('composition view', ['dat', 'ser', 'bok'], short='c1')
36 | reporter.compose_view('compose b', ['bok', 'ser'], short='c2')
37 | reporter.compose_view('compose d', ['bok', 'ser', 'dat'], row_layout(1, 2))
38 |
39 | # compose-ception
40 | reporter.compose_view('compose c', ['c1', 'c2'], short='c4')
41 |
42 | if __name__ == "__main__":
43 | reporter.run(debug=True)
44 |
--------------------------------------------------------------------------------
/examples/custom_converter.py:
--------------------------------------------------------------------------------
1 | from diva import Diva
2 | from diva.converters import convert_to_html
3 | from datetime import date
4 |
5 | @convert_to_html.register(date)
6 | def my_converter(d):
7 | return '
year: {}, month: {}, day: {}
'.format(d.year, d.month, d.day)
8 |
9 | app = Diva()
10 |
11 | @app.view('my sample view')
12 | def foo():
13 | return date(2017, 8, 11)
14 |
15 | app.run()
16 |
--------------------------------------------------------------------------------
/examples/custom_utility.py:
--------------------------------------------------------------------------------
1 | from diva import Diva
2 | from diva.widgets import *
3 | from diva.utilities import register_simple_util, register_widget_util, file_response
4 | import pandas as pd
5 | import tempfile
6 |
7 | # if your utility has options that depend on the currently displayed value,
8 | # of the figure, then use register_widget_util
9 |
10 | def my_util_widgets(val):
11 | """
12 | Allow the user to select which of the table's columns to export
13 | """
14 | column_names = [str(name) for name in list(val)]
15 | return [SelectSubset('select the columns you want', column_names)]
16 |
17 | def my_util_apply(val, chosen_columns):
18 | """
19 | Export only the selected columns to csv
20 | """
21 | # convert the subset to a list of bools, with True for cols to include
22 | # and False ow
23 | all_col_names = [str(name) for name in list(val)]
24 | col_bools = [e in chosen_columns for e in all_col_names]
25 | my_file = tempfile.NamedTemporaryFile()
26 | val.to_csv(my_file.name, columns=col_bools)
27 | return file_response('your_file.csv', my_file.name)
28 |
29 | register_widget_util('export columns', pd.DataFrame, my_util_widgets, my_util_apply)
30 |
31 | # if, on the other hand, your utility does not depend on the currently displayed
32 | # value, you can use register_simple_util, which is a wrapper around the above method
33 | @register_simple_util('export with separator', pd.DataFrame, [String('enter a separator', ',')])
34 | def another_util_apply(val, sep):
35 | my_file = tempfile.NamedTemporaryFile()
36 | val.to_csv(my_file.name, sep=sep)
37 | return file_response('your_file.csv', my_file.name)
38 |
39 | app = Diva()
40 |
41 | @app.view('my sample view')
42 | def foo():
43 | return pd.DataFrame({'a': [1, 2], 'b': [3, 4], 'c': [5, 6]})
44 |
45 | app.run()
46 |
--------------------------------------------------------------------------------
/examples/dashboard_example.py:
--------------------------------------------------------------------------------
1 | from diva import Diva, Dashboard, row_layout
2 | from diva.widgets import *
3 | import pandas as pd
4 | import numpy as np
5 |
6 | app = Diva()
7 |
8 | @app.view('convert: Dashboard')
9 | def dashboard_view():
10 | a = pd.DataFrame(np.random.randn(10, 10))
11 | b = pd.DataFrame(np.random.randn(10, 10))
12 | c = pd.DataFrame(np.random.randn(10, 10))
13 | # will arrange the views such that a takes up the full first row
14 | # and the second row is split between b and c
15 | return Dashboard([a, b, c], row_layout(1, 2))
16 |
17 | @app.view('convert: another Dashboard')
18 | def dashboard_view():
19 | a = pd.DataFrame(np.random.randn(20, 5))
20 | b = pd.DataFrame(np.random.randn(10, 10))
21 | c = pd.DataFrame(np.random.randn(10, 10))
22 | # this uses a custom layout instead of row_layout
23 | # a will take up the left half of the view, and the right half
24 | # will be horizontally split, with b on top of c
25 | return Dashboard([a, b, c], [[0, 0, 1, 2], [1, 0, 1, 1], [1, 1, 1, 1]])
26 |
27 | """
28 | You can create a dashboard view by composing existing views
29 | """
30 |
31 | @app.view('view a', [Int('enter num', 5)])
32 | def view_a(foo):
33 | return pd.Series([foo for i in range(10)])
34 |
35 | @app.view('some very long and tedious name', [String('enter name', 'foo')], short='view b')
36 | def view_b(bar):
37 | return pd.DataFrame([bar for i in range(10)])
38 |
39 | # provide a list of the names of the views you want to compose.
40 | # If a short name is provided for the view, you must use that name
41 | app.compose_view('composed view', ['view a', 'view b'], row_layout(2))
42 |
43 | app.run(debug=True)
44 |
--------------------------------------------------------------------------------
/examples/demo_server/demo_server.py:
--------------------------------------------------------------------------------
1 | import matplotlib
2 | matplotlib.use('Agg')
3 | import matplotlib.pyplot as plt
4 | import numpy as np
5 | import pandas as pd
6 | from datetime import datetime
7 | from diva import Diva, Dashboard, row_layout
8 | from diva.widgets import *
9 | from bokeh.plotting import figure
10 | from functools import singledispatch
11 |
12 | reporter = Diva()
13 |
14 | @singledispatch
15 | def type_to_str(val):
16 | # strip off the tags or the HTML will not work
17 | s = str(type(val))
18 | return s[1:-1]
19 |
20 | # helper for printing the types that widgets output:
21 | def type_of_iterable(val):
22 | s = ''
23 | for item in val:
24 | s += type_to_str(item) + ', '
25 | return s
26 |
27 | @type_to_str.register(tuple)
28 | def tuple_type(val):
29 | return '({})'.format(type_of_iterable(val))
30 |
31 | @type_to_str.register(list)
32 | def list_type(val):
33 | return '[{}]'.format(type_of_iterable(val))
34 |
35 | # Provide an overview pages for all of the available widgets
36 | all_widgets = [
37 | String('some text', 'hello'),
38 | Float('a float', 1.5),
39 | Int('an integer', 2),
40 | Bool('a bool', True),
41 | SelectOne('pick a name', ['foo', 'bar', 'baz'], 'bar'),
42 | SelectSubset('pick names', ['foo', 'bar', 'baz'], ['foo', 'baz']),
43 | Color('pick a color', '#ff0000'),
44 | Slider('a float'),
45 | Date('pick a date'),
46 | Time('pick a time'),
47 | DateRange('pick a date range')
48 | ]
49 | @reporter.view('all widgets', all_widgets)
50 | def widgets_test(wstr, wflo, wint, wbool, wso, wss, wcol, wsli, wdate, wtime, wdaterange):
51 | args = [wstr, wflo, wint, wbool, wso, wss, wcol, wsli, wdate, wtime, wdaterange]
52 | formats = ['{}', '{}', '{}', '{}', '{}', '{}', '{}', '{:f}', '{}', '{}', '{}']
53 | body = ''
54 | for w, arg, f in zip(all_widgets, args, formats):
55 | arg_type = type_to_str(arg)
56 | class_name = w.__class__.__name__
57 | body += "widget class: {} type: {} value: {}
If a string is returned, it is assumed to be raw HTML
'
63 |
64 | @reporter.view('convert: matplotlib.figure.Figure')
65 | def matplot_fig():
66 | plt.figure()
67 | plt.plot([3,1,4,1,20], 'ks-', mec='w', mew=5, ms=20)
68 | return plt.gcf()
69 |
70 | @reporter.view('convert: pandas.DataFrame')
71 | def pandas_df():
72 | df = pd.DataFrame(np.random.randn(20, 20))
73 | return df;
74 |
75 | @reporter.view('convert: pandas.Series')
76 | def pandas_series():
77 | s = pd.Series([p for p in range(100)])
78 | return s
79 |
80 | @reporter.view('convert: bokeh.plotting.figure.Figure')
81 | def bokeh_fig():
82 | x = [1, 2, 3, 4, 5]
83 | y = [6, 7, 2, 4, 5]
84 | plot = figure(title="bokeh example", x_axis_label='x', y_axis_label='y')
85 | plot.line(x, y, legend="Temp", line_width=2)
86 | return plot
87 |
88 | @reporter.view('convert: Dashboard', [Int('enter some num', 5)])
89 | def dashboard_view(x):
90 | a = pd.DataFrame(np.random.randn(x, x))
91 | b = pd.DataFrame(np.random.randn(x, x))
92 | c = pd.DataFrame(np.random.randn(x, x))
93 | # will arrange the views such that a takes up the full first row
94 | # and the second row is split between b and c
95 | return Dashboard([a, b, c], row_layout(1, 2))
96 |
97 | @reporter.view('convert: none of the above (ex. datetime.time)')
98 | def na():
99 | return datetime.now()
100 |
101 | if __name__ == "__main__":
102 | reporter.run()
103 |
--------------------------------------------------------------------------------
/examples/demo_server/wsgi.py:
--------------------------------------------------------------------------------
1 | from demo_server import reporter
2 |
3 | if __name__ == "__main__":
4 | reporter.run()
5 |
--------------------------------------------------------------------------------
/examples/jumbo_example.py:
--------------------------------------------------------------------------------
1 | from diva import Diva, Dashboard, row_layout
2 | from diva.widgets import *
3 | # only required if adding your own converter:
4 | from diva.converters import convert_to_html
5 |
6 | import numpy as np
7 | import pandas as pd
8 | from bokeh.plotting import figure
9 | from datetime import *
10 |
11 | app = Diva()
12 |
13 | """
14 | No Widgets
15 | Reloading will always give the same figure
16 | """
17 | @app.view('no widgets')
18 | def no_widgets():
19 | return pd.Series([x for x in range(20)])
20 |
21 | """
22 | Simple Widgets
23 | Reloading passes the values of the widgets to the func
24 | """
25 | @app.view('simple widgets', [String('enter name'), Int('enter age')])
26 | def simple_widgets(name, age):
27 | return name, age
28 |
29 | """
30 | Many widgets
31 | """
32 | @app.view('many widgets', [
33 | String('some text'),
34 | Float('a float'),
35 | Int('an int'),
36 | Bool('a bool'),
37 | SelectOne('choose one', ['a', 'b', 'c']),
38 | SelectSubset('select many', ['foo', 'baz', 'baz'], ['foo']),
39 | Color('pick a color'),
40 | Slider('a float'),
41 | Date('a date'),
42 | Time('a time'),
43 | DateRange('a date range')
44 | ])
45 | def many_widgets(*widget_values):
46 | return widget_values
47 |
48 | """
49 | Date Widgets
50 | There are many ways to specify the defaults, see the docs for details
51 | """
52 | @app.view('date widgets', [
53 | # this defaults to: the exact date in ISO format
54 | Date('date a', '2017-08-21'),
55 | # defaults to: 7 days ago
56 | Date('date b', relativedelta(weeks=1)),
57 | # defaults to: the range between the exact dates in ISO format
58 | DateRange('range a', '2017-08-21', '2017-08-26'),
59 | # you can also use relative dates
60 | # defaults to: the last week
61 | DateRange('range b', relativedelta(days=7), relativedelta()),
62 | # or a combination of exact and relative
63 | # defaults to: exact date to present
64 | DateRange('range c', '2017-07-15', relativedelta())
65 | ])
66 | def date_widgets(date_a, date_b, range_a, range_b, range_c):
67 | return date_a, date_b, range_a, range_b, range_c
68 |
69 | """
70 | Converter Examples:
71 | An example of using each type that can be converted to HTML
72 | is given.
73 | See the matplotlib example for the matplotlib.figure.Figure converter
74 | """
75 |
76 | """
77 | A string is assumed to be raw HTML
78 | """
79 | @app.view('convert: str')
80 | def raw_html():
81 | return '
Raw HTML
If a string is returned, it is assumed to be raw HTML
'
82 |
83 | @app.view('convert: pandas.DataFrame')
84 | def pandas_df():
85 | df = pd.DataFrame(np.random.randn(20, 20))
86 | return df;
87 |
88 | @app.view('convert: pandas.Series')
89 | def pandas_series():
90 | s = pd.Series([p for p in range(100)])
91 | return s
92 |
93 | @app.view('convert: bokeh.plotting.figure.Figure')
94 | def bokeh_fig():
95 | x = [1, 2, 3, 4, 5]
96 | y = [6, 7, 2, 4, 5]
97 | plot = figure(title="bokeh example", x_axis_label='x', y_axis_label='y')
98 | plot.line(x, y, legend="Temp", line_width=2)
99 | return plot
100 |
101 | """
102 | If Diva does not support the type, it's string representation is
103 | converted to HTML
104 | """
105 | @app.view('convert: none of the above (ex. array of ints)')
106 | def na():
107 | return [i for i in range(10)]
108 |
109 | @app.view('convert: Dashboard')
110 | def dashboard_view():
111 | a = pd.DataFrame(np.random.randn(10, 10))
112 | b = pd.DataFrame(np.random.randn(10, 10))
113 | c = pd.DataFrame(np.random.randn(10, 10))
114 | # will arrange the views such that a takes up the full first row
115 | # and the second row is split between b and c
116 | return Dashboard([a, b, c], row_layout(1, 2))
117 |
118 | @app.view('convert: another Dashboard')
119 | def dashboard_view():
120 | a = pd.DataFrame(np.random.randn(20, 5))
121 | b = pd.DataFrame(np.random.randn(10, 10))
122 | c = pd.DataFrame(np.random.randn(10, 10))
123 | # this uses a custom layout instead of row_layout
124 | # a will take up the left half of the view, and the right half
125 | # will be horizontally split, with b on top of c
126 | return Dashboard([a, b, c], [[0, 0, 1, 2], [1, 0, 1, 1], [1, 1, 1, 1]])
127 |
128 | """
129 | You can create a dashboard view by composing existing views
130 | """
131 |
132 | @app.view('view a', [Int('enter num', 5)])
133 | def view_a(foo):
134 | return pd.Series([foo for i in range(10)])
135 |
136 | @app.view('some very long and tedious name', [String('enter name', 'foo')], short='view b')
137 | def view_b(bar):
138 | return pd.DataFrame([bar for i in range(10)])
139 |
140 | # provide a list of the names of the views you want to compose.
141 | # If a short name is provided for the view, you must use that name
142 | app.compose_view('composed view', ['view a', 'view b'], row_layout(2))
143 |
144 | """
145 | Register a new converter.
146 | Now if you register a view that returns a datetime.date object, it will
147 | return the HTML from this function
148 | """
149 | @convert_to_html.register(date)
150 | def my_converter(d):
151 | return '
year: {}, month: {}, day: {}
'.format(d.year, d.month, d.day)
152 |
153 | @app.view('my sample view')
154 | def foo():
155 | # this will use the new converter
156 | return date(2017, 8, 11)
157 |
158 | # Setting debug=True will allow live code reload and display a debugger (via Flask)
159 | # if an exception is thrown.
160 | app.run(debug=True)
161 |
--------------------------------------------------------------------------------
/examples/matplotlib_examples.py:
--------------------------------------------------------------------------------
1 | # You should use the 'Agg' backend (for PNGs) when importing matplotlib
2 | # b/c otherwise a matplotlib will attempt to open a GUI app
3 | import matplotlib
4 | matplotlib.use('Agg')
5 | import matplotlib.pyplot as plt
6 | import numpy as np
7 | import pandas as pd
8 | from diva import Diva, Dashboard
9 | from diva.widgets import *
10 |
11 | """
12 | As shown, you should use the object-oriented matplotlib functions.
13 | Otherwise, two different functions may unintentionally be modifying
14 | the axes of the same figure, which can cause confusion.
15 |
16 | Since matplotlib maintains internal references to all figures you create,
17 | they will not actually be garbage collected until you explicitly close them!
18 | This is not shown here b/c I intend to make some kind of workaround,
19 | something like:
20 | https://stackoverflow.com/questions/16334588/create-a-figure-that-is-reference-counted/16337909#16337909
21 |
22 | These examples are adapted from:
23 | https://matplotlib.org/users/pyplot_tutorial.html
24 | """
25 |
26 | app = Diva()
27 |
28 | @app.view('simple figure', [Int('x', 3)])
29 | def matplot_fig(x):
30 | # make a new figure
31 | fig, ax = plt.subplots()
32 | ax.plot([3,1,4,1,x], 'ks-', mec='w', mew=5, ms=20)
33 | return fig
34 |
35 | """
36 | There is some subtle error here. Only updates upon
37 | increasing x
38 | May have to do with overwriting fig 1?
39 | """
40 | @app.view('subplots', [Float('x', 5.0)])
41 | def subplots(x):
42 | def f(t):
43 | return np.exp(-t) * np.cos(2*np.pi*t)
44 |
45 | t1 = np.arange(0.0, x, 0.1)
46 | t2 = np.arange(0.0, x, 0.02)
47 |
48 | # Use the object-oriented matplotlib functions
49 | # for subplots,
50 | fig, axes = plt.subplots(2, 1)
51 | axes[0].plot(t1, f(t1), 'bo', t2, f(t2), 'k')
52 | axes[1].plot(t2, np.cos(2*np.pi*t2), 'r--')
53 |
54 | return fig
55 |
56 | @app.view('matplotlib mutliple figures')
57 | def multiple_figures():
58 | fig_a, ax_a = plt.subplots()
59 | ax_a.plot([1, 2, 3, 4, 5], 'ks-', mec='w', mew=5, ms=20)
60 | fig_b, ax_b = plt.subplots()
61 | ax_b.plot([6, 7, 8, 9, 10], 'ks-', mec='w', mew=5, ms=20)
62 | return Dashboard([fig_a, fig_b])
63 |
64 | @app.view('matplotlib larger figure', [Int('x', 3)])
65 | def matplot_fig(x):
66 | fig_a, ax_a = plt.subplots(figsize=(4, 4))
67 | ax_a.plot([1, 2, 3, 4, 5], 'ks-', mec='w', mew=5, ms=20)
68 | # figsize allows you to set the size of the figure in inches
69 | fig_b, ax_b = plt.subplots(figsize=(8, 8))
70 | ax_b.plot([6, 7, 8, 9, 10], 'ks-', mec='w', mew=5, ms=20)
71 | return Dashboard([fig_a, fig_b])
72 |
73 | app.run(debug=True)
74 |
--------------------------------------------------------------------------------
/examples/minimal_example.py:
--------------------------------------------------------------------------------
1 | from diva import Diva
2 | from diva.widgets import *
3 | import pandas as pd
4 |
5 | app = Diva()
6 |
7 | @app.view('my sample view')
8 | def foo():
9 | data = [p * 1.5 for p in range(20)]
10 | return pd.Series(data)
11 |
12 | @app.view('my sample view with widgets', [
13 | Int('choose a size', 20),
14 | Float('choose a factor', 1.5)
15 | ])
16 | def bar(size, factor):
17 | data = [p * factor for p in range(size)]
18 | return pd.Series(data)
19 |
20 | app.run(debug=True)
21 |
--------------------------------------------------------------------------------
/examples/multiple_files/bar.py:
--------------------------------------------------------------------------------
1 | from diva import Diva
2 |
3 | app = Diva()
4 |
5 | @app.view('bar')
6 | def bar():
7 | return [4, 5, 6]
8 |
9 | @app.view('bar_2')
10 | def bar_2():
11 | return [7, 8, 9]
12 |
13 | if __name__ == '__main__':
14 | app.run(debug=True)
15 |
--------------------------------------------------------------------------------
/examples/multiple_files/foo.py:
--------------------------------------------------------------------------------
1 | from diva import Diva
2 | app = Diva()
3 |
4 | @app.view('foo')
5 | def foo():
6 | return [1, 2, 3]
7 |
8 | if __name__ == '__main__':
9 | app.run(debug=True)
10 |
--------------------------------------------------------------------------------
/examples/multiple_files/main.py:
--------------------------------------------------------------------------------
1 | from diva import Diva
2 | from foo import app as foo_app
3 | from bar import app as bar_app
4 |
5 | app = Diva()
6 |
7 | # adds all of the views from foo_app and bar_app
8 | # to this app
9 | app.extend(foo_app)
10 | app.extend(bar_app)
11 |
12 | app.run(debug=True)
13 |
14 |
--------------------------------------------------------------------------------
/examples/other_examples.py:
--------------------------------------------------------------------------------
1 | from diva import Diva
2 | from diva.widgets import *
3 | import pandas as pd
4 |
5 | app = Diva()
6 |
7 | def baz(a, b, *args, **kwargs):
8 | return '
{} {} {} {}
'.format(a, b, args, kwargs)
9 |
10 | @app.view('shim example', [
11 | Int('choose an int'),
12 | Float('choose a float'),
13 | String('choose a string'),
14 | Bool('choose a bool')])
15 | def baz_shim(my_int, my_float, my_str, my_bool):
16 | # in baz: a=my_int, b=my_float, args=(my_str), kwargs={'hi': my_bool}
17 | return baz(my_int, my_float, my_str, hi=my_bool)
18 |
19 | app.run()
20 |
--------------------------------------------------------------------------------
/examples/rough_examples/double_ha.py:
--------------------------------------------------------------------------------
1 | # TODO: import reports
2 | import matplotlib
3 | # use the Agg backend, which is non-interactivate (just for PNGs)
4 | # this way, a separate script isn't started by matplotlib
5 | matplotlib.use('Agg')
6 | import matplotlib.pyplot as plt
7 | import numpy as np
8 | import pandas as pd
9 | from datetime import *
10 | from diva import Diva
11 | from diva.widgets import *
12 | from bokeh.plotting import figure
13 |
14 | reporter = Diva()
15 |
16 | # generates an assertion error (unit test this later)
17 | # def figure_too_few():
18 | # return '