├── .gitignore ├── .readthedocs.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── assets └── favicon.svg ├── diva ├── __init__.py ├── analytics_converters.py ├── converters.py ├── dashboard.py ├── exceptions.py ├── reporter.py ├── static │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── crossdomain.xml │ ├── favicon.ico │ ├── humans.txt │ ├── main.css │ ├── normalize.css │ ├── plugins.js │ ├── reports.js │ ├── robots.txt │ ├── script.js │ ├── style.css │ ├── style.scss │ ├── tile-wide.png │ ├── tile.png │ ├── utilities.js │ ├── vendor │ │ ├── FileSaver.min.js │ │ ├── bootstrap-timepicker.min.css │ │ ├── bootstrap-timepicker.min.js │ │ └── modernizr-2.8.3.min.js │ └── widgets.js ├── templates │ ├── .index.html.swp │ ├── .label_widget.html.swp │ ├── checkbox_widget.html │ ├── checklist_widget.html │ ├── dashboard.html │ ├── index.html │ ├── input_tag_widget.html │ ├── label_widget.html │ ├── radio_widget.html │ ├── slider_widget.html │ ├── utility_button.html │ ├── utility_form.html │ ├── utility_label.html │ └── widgetform.html ├── utilities.py ├── utils.py └── widgets.py ├── docs ├── Makefile ├── about.rst ├── conf.py ├── developers_guide.rst ├── images │ ├── example_screenshot_a.png │ └── example_screenshot_b.png ├── index.rst ├── make.bat ├── requirements.txt ├── to_note.txt └── users_guide.rst ├── examples ├── compose.py ├── custom_converter.py ├── custom_utility.py ├── dashboard_example.py ├── demo_server │ ├── demo_server.py │ └── wsgi.py ├── jumbo_example.py ├── matplotlib_examples.py ├── minimal_example.py ├── multiple_files │ ├── bar.py │ ├── foo.py │ └── main.py ├── other_examples.py ├── rough_examples │ ├── double_ha.py │ ├── my_demo.py │ ├── sample_export.png │ └── tester.py ├── some_reports.py └── widget_tests.py ├── note.txt ├── requirements.txt ├── setup.py ├── static ├── test_upload.sh ├── tests ├── dashboard_test.py └── main_test.py ├── tox.ini └── upload.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # not sure why this dir was created in the first place, the package is 2 | # in diva/diva 3 | src 4 | 5 | # from the python packaging guide 6 | *.pyc 7 | /dist/ 8 | /*.egg-info 9 | 10 | # From Github's Python gitignore 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | .hypothesis/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | 112 | # for Sass 113 | .sass-cache/ 114 | *.css.map 115 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # readthedocs configuration file 2 | 3 | version: 2 4 | 5 | sphinx: 6 | configuration: docs/conf.py 7 | 8 | formats: all 9 | 10 | python: 11 | version: 3.5 12 | install: 13 | - requirements: docs/requirements.txt 14 | 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matthew Riley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE.txt 2 | recursive-include diva/static * 3 | recursive-include diva/templates * 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Diva 2 | ===== 3 | 4 | This project is no longer maintained. But here is more info for those who are interested: 5 | 6 | Project Homepage and Docs: http://diva.readthedocs.io/en/latest/index.html 7 | 8 | PyPi Link: https://pypi.python.org/pypi/diva 9 | 10 | MIT Licensed 11 | 12 | -------------------------------------------------------------------------------- /assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 45 | 49 | 53 | 54 | 56 | 57 | 59 | image/svg+xml 60 | 62 | 63 | 64 | 65 | 66 | 71 | 78 | 89 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /diva/__init__.py: -------------------------------------------------------------------------------- 1 | # expose for convenience when importing 2 | from .reporter import Diva 3 | from .dashboard import Dashboard, row_layout 4 | 5 | # register all converters 6 | import diva.analytics_converters 7 | import diva.dashboard 8 | -------------------------------------------------------------------------------- /diva/analytics_converters.py: -------------------------------------------------------------------------------- 1 | # use matplotlib with the Agg backend to avoid opening an app 2 | # to view the matplotlib figures 3 | from .converters import convert_to_html 4 | from .utilities import register_simple_util, register_widget_util, file_response 5 | from .widgets import * 6 | import matplotlib 7 | matplotlib.use('Agg') 8 | import matplotlib.pyplot as plt, mpld3 9 | import numpy as np 10 | import pandas as pd 11 | from bokeh.plotting.figure import Figure 12 | from bokeh.resources import CDN 13 | from bokeh.embed import components 14 | import tempfile 15 | 16 | @convert_to_html.register(matplotlib.figure.Figure) 17 | def fig_to_html(fig): 18 | return mpld3.fig_to_html(fig) 19 | 20 | @register_simple_util( 21 | 'export', 22 | matplotlib.figure.Figure, 23 | [SelectOne('format: ', ['png', 'pdf', 'svg'])]) 24 | def export_matplot_fig(fig, file_format): 25 | my_file = tempfile.NamedTemporaryFile() 26 | fig.savefig(my_file.name, bbox_inches='tight', format=file_format) 27 | return file_response('your_file.{}'.format(file_format), my_file.name) 28 | 29 | @convert_to_html.register(pd.DataFrame) 30 | def dataframe_to_html(df): 31 | # Bootstrap table classes 32 | css_classes = ['table', 'table-bordered', 'table-hover', 'table-sm'] 33 | return df.to_html(classes=css_classes) 34 | 35 | @convert_to_html.register(pd.Series) 36 | def series_to_html(series): 37 | return dataframe_to_html(series.to_frame()) 38 | 39 | @register_simple_util('export to csv', pd.DataFrame) 40 | @register_simple_util('export to csv', pd.Series) 41 | def df_to_csv(p): 42 | my_file = tempfile.NamedTemporaryFile() 43 | p.to_csv(my_file.name) 44 | return file_response('your_file.csv', my_file.name) 45 | 46 | # Keep in mind: 47 | # Inserting a script tag into the DOM using innerHTML does not 48 | # execute any script tags in the transplanted HTML. 49 | # see: http://bokeh.pydata.org/en/latest/docs/user_guide/embed.html 50 | @convert_to_html.register(Figure) 51 | def bokeh_figure_to_html(figure): 52 | # NB: cannot just use file_html due to the issue mentioned above 53 | # scale up the plot to its container size 54 | # TODO: scale_both is desired, but does not work 55 | # scale_width makes the figure far too high to fit in the window 56 | # figure.sizing_mode = 'scale_width' 57 | script, div = components(figure) 58 | return '{}{}'.format(div, script) 59 | 60 | -------------------------------------------------------------------------------- /diva/converters.py: -------------------------------------------------------------------------------- 1 | from functools import singledispatch 2 | 3 | @singledispatch 4 | def convert_to_html(val): 5 | # default is to just display the value as a string 6 | return '

{}

'.format(str(val)) 7 | 8 | # assume a raw string is valid HTML 9 | @convert_to_html.register(str) 10 | def str_to_html(html_string): 11 | return html_string 12 | 13 | -------------------------------------------------------------------------------- /diva/dashboard.py: -------------------------------------------------------------------------------- 1 | from .converters import convert_to_html 2 | from .utilities import get_utilities_for_value, label_utility 3 | from .utils import render_template 4 | from functools import partial 5 | 6 | # A 'layout' is a list of [x, y, width, height] items, 1 for each panel 7 | # where x, y is the position of the top-left corner of the panel, and 8 | # the parent container has (0, 0) in the top-left with +y downwards 9 | 10 | # helpers for defining layouts 11 | def row_layout(*row_sizes): 12 | # calc a common-multiple so that can define all columns 13 | # (not actually the LCM using this lazy way) 14 | lcm = 1 15 | for size in row_sizes: 16 | lcm *= size 17 | layout = [] 18 | for row_num, num_cols in enumerate(row_sizes): 19 | col_width = int(lcm / num_cols) 20 | for i in range(num_cols): 21 | layout.append([i * col_width, row_num, col_width, 1]) 22 | 23 | return layout 24 | 25 | class Dashboard(): 26 | # default layout is a vertical list of items 27 | def __init__(self, items, layout=None): 28 | self.items = items 29 | if layout is None: 30 | self.layout = [[0, i, 1, 1] for i in range(len(items))] 31 | else: 32 | self.layout = layout 33 | 34 | def get_grid_size(layout): 35 | max_x = max([x + w for x, y, w, h in layout]) 36 | max_y = max([y + h for x, y, w, h in layout]) 37 | return (max_x, max_y) 38 | 39 | def get_grid(dash): 40 | grid_size = get_grid_size(dash.layout) 41 | 42 | # create a list of panes 43 | panes = [] 44 | for index, (item, coord) in enumerate(zip(dash.items, dash.layout)): 45 | pane = {} 46 | # add 1 to point b/c CSS grids start at (1, 1) not (0, 0) 47 | css_coord = coord[0] + 1, coord[1] + 1, coord[2], coord[3] 48 | pane['x'], pane['y'], pane['w'], pane['h'] = css_coord 49 | pane['html'] = convert_to_html(item) 50 | pane['name'] = 'report {}'.format(index) 51 | panes.append(pane) 52 | return {'size': grid_size, 'panes': panes} 53 | 54 | @convert_to_html.register(Dashboard) 55 | def dashboard_to_html(dash): 56 | grid = get_grid(dash) 57 | return render_template('dashboard.html', grid=grid) 58 | 59 | @get_utilities_for_value.register(Dashboard) 60 | def dashboard_utilities(dash): 61 | 62 | def gen_html_for_item(dash, util, item_index): 63 | return util['generate_html'](dash.items[item_index]) 64 | 65 | def apply_for_item(dash, form_data, util, item_index): 66 | return util['apply'](dash.items[item_index], form_data) 67 | 68 | # compose the utilities of the child elements 69 | all_utils = [] 70 | for index, child in enumerate(dash.items): 71 | child_utilities = get_utilities_for_value(child) 72 | if len(child_utilities) > 0: 73 | # separate the utilities of different children with a label 74 | label = label_utility('report {}'.format(index)) 75 | all_utils.append(label) 76 | for child_util in child_utilities: 77 | # partially apply the util and the child index to the child util 78 | # so that the resulting util takes a dashboard object and delegrates 79 | # it to the correct util of the correct child 80 | util = {} 81 | util['generate_html'] = partial(gen_html_for_item, util=child_util, item_index=index) 82 | util['apply'] = partial(apply_for_item, util=child_util, item_index=index) 83 | all_utils.append(util) 84 | return all_utils 85 | 86 | -------------------------------------------------------------------------------- /diva/exceptions.py: -------------------------------------------------------------------------------- 1 | class ValidationError(Exception): 2 | pass 3 | 4 | class WidgetsError(Exception): 5 | pass 6 | 7 | class WidgetInputError(Exception): 8 | def __init__(self, user_func, inputs): 9 | self.message = """ 10 | A TypeError was raised when Diva called your function like: {}(**kwargs), where kwargs = {}. 11 | Please double-check that you have a valid number of widgets, 12 | and see the Diva docs for help. 13 | """.format(user_func.__name__, inputs) 14 | -------------------------------------------------------------------------------- /diva/reporter.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from jsonschema import validate 3 | from collections import OrderedDict 4 | from .converters import convert_to_html 5 | from .widgets import (widgets_template_data, validate_widget_form_data, 6 | parse_widget_form_data, Skip, should_skip) 7 | from .utilities import get_utilities_for_value 8 | from .dashboard import Dashboard 9 | from .exceptions import * 10 | from flask import Flask, render_template, request, abort, jsonify 11 | 12 | class Diva(): 13 | def __init__(self): 14 | """ 15 | Sample doc string for testing autodocs 16 | """ 17 | self.reports = [] 18 | self.setup_server() 19 | 20 | def setup_server(self): 21 | """ 22 | Sets up an internal Flask server 23 | """ 24 | self.server = Flask(__name__) 25 | 26 | @self.server.route('/') 27 | def index(): 28 | return self.get_index() 29 | 30 | @self.server.route('/update', methods=['POST']) 31 | def update_figure(): 32 | body = request.get_json() 33 | report = self.get_report_from_body(body) 34 | self.validate_widget_values(report, body) 35 | response_data = self.update_report(report, body['widgetValues']) 36 | return jsonify(response_data) 37 | 38 | @self.server.route('/utility', methods=['POST']) 39 | def apply_utility(): 40 | body = request.get_json() 41 | report = self.get_report_from_body(body) 42 | self.validate_utility_data(report, body) 43 | # get the utility 44 | utility_index = body['utilityIndex'] 45 | utility = report['utilities'][utility_index] 46 | # apply the utility to the report's current value and 47 | # any user-input sent with the post (for options) 48 | current_value = report['current_value'] 49 | form_data = body['data'] 50 | return utility['apply'](current_value, form_data) 51 | 52 | def __call__(self, environ, start_response): 53 | """ 54 | This allows a Diva object to act as a wsgi entry point. 55 | Just delegates the wsgi callable to the underlying flask server 56 | """ 57 | return self.server.wsgi_app(environ, start_response) 58 | 59 | def run(self, host=None, port=None, debug=None, **options): 60 | self.server.run(host, port, debug, **options) 61 | 62 | def view(self, name, user_widgets=[], short=None): 63 | """ 64 | name: the name that will appear in the UI 65 | user_widgets: list of Widget objects whose values 66 | will be given the user's functon 67 | short: a short name, for easy reference to this report later. 68 | Defaults to the actual name 69 | """ 70 | if short is None: 71 | short = name 72 | def real_decorator(user_func): 73 | # save a ref to the user's func and widgets 74 | self.reports.append({ 75 | 'id': short, 76 | 'name': name, 77 | 'user_func': user_func, 78 | 'widgets': user_widgets 79 | }) 80 | # the func is not modified 81 | return user_func 82 | return real_decorator 83 | 84 | def compose_view(self, name, id_list, layout=None, short=None): 85 | """ 86 | Creates a view that composes the specified views into a single view, 87 | arranged according to the given layout. 88 | 89 | id_list: list of IDs/short names of the reports to combine 90 | layout: a list of coordinate lists, in the same format as specified in 91 | the Dashboard constructor 92 | """ 93 | # get all reports with matching names 94 | report_list = [report for report in self.reports if report['id'] in id_list] 95 | if len(report_list) < len(id_list): 96 | raise ValueError("one of the given reports was not found: {}".format(id_list)) 97 | 98 | # concatenate the widgets of the reports 99 | # also add labels between the widget groups 100 | all_widgets = [] 101 | for index, r in enumerate(report_list): 102 | all_widgets.append(Skip('report {}'.format(index))) 103 | all_widgets.extend(r['widgets']) 104 | 105 | # register a view that takes the list of all widgets, and gives the 106 | # correct arguments to the correct reports 107 | @self.view(name, all_widgets, short) 108 | def view_func(*widget_args): 109 | # regenerate all reports, passing in the values from the relevant widgets 110 | remaining_args = widget_args 111 | results = [] 112 | for r in report_list: 113 | # get the number of widgets that pass their value to the underlying 114 | # function (that is, the number that shouldn't be skipped) 115 | active_widgets = [w for w in r['widgets'] if not should_skip(w)] 116 | num_widgets = len(active_widgets) 117 | # pop this report's widget args from the list 118 | # note that the Skip/label widgets are not passed to the view func 119 | # so there is no need to account for them 120 | arg_list = remaining_args[:num_widgets] 121 | remaining_args = remaining_args[num_widgets:] 122 | # generate the figure 123 | output = r['user_func'](*arg_list) 124 | results.append(output) 125 | return Dashboard(results, layout) 126 | 127 | def extend(self, other_diva): 128 | """ 129 | Add all of the views of the given Diva object to this one. 130 | This is useful for mutli-file projects 131 | """ 132 | self.reports.extend(other_diva.reports) 133 | 134 | # func that generates the figure by passing the user func the 135 | # parsed form data, then converting the func's output to HTML 136 | def update_report(self, report, form_data={}): 137 | inputs = parse_widget_form_data(report['widgets'], form_data) 138 | user_func = report['user_func'] 139 | try: 140 | output = user_func(*inputs) 141 | except TypeError as err: 142 | raise WidgetInputError(user_func, inputs) 143 | utilities = get_utilities_for_value(output) 144 | # update the report 145 | report['current_value'] = output 146 | report['utilities'] = utilities 147 | # generate the HTML required to update the UI 148 | figure_html = convert_to_html(output) 149 | utilityHTML = [util['generate_html'](output) for util in utilities] 150 | response = {'figureHTML': figure_html, 'utilityHTML': utilityHTML} 151 | return response 152 | 153 | def get_index(self): 154 | report_data = [] 155 | for report in self.reports: 156 | widgets = widgets_template_data(report['widgets']) 157 | report_data.append({'name': report['name'], 'widgets': widgets}) 158 | return render_template( 159 | 'index.html', 160 | reports=report_data) 161 | 162 | def validate_widget_values(self, report, json): 163 | inputs = json.get('widgetValues', []) 164 | validate_widget_form_data(report['widgets'], inputs) 165 | 166 | def validate_utility_data(self, report, json): 167 | try: 168 | schema = { 169 | 'type': 'object', 170 | 'properties': { 171 | 'utilityIndex': { 172 | 'type': 'integer', 173 | 'minimum': 0, 174 | 'maximum': len(report['utilities']) - 1 175 | }, 176 | 'data': { 177 | } 178 | }, 179 | 'required': ['utilityIndex', 'data'] 180 | } 181 | validate(json, schema) 182 | except Exception as e: 183 | raise ValidationError(str(e)) 184 | 185 | def get_report_from_body(self, body): 186 | self.validate_request(body) 187 | report_index = body['reportIndex'] 188 | return self.reports[report_index] 189 | 190 | def validate_request(self, json): 191 | try: 192 | schema = { 193 | 'type': 'object', 194 | 'properties': { 195 | 'reportIndex': { 196 | 'type': 'integer', 197 | 'minimum': 0, 198 | 'maximum': len(self.reports) - 1 199 | } 200 | }, 201 | 'required': ['reportIndex'], 202 | } 203 | validate(json, schema) 204 | except Exception as e: 205 | raise ValidationError(str(e)) 206 | 207 | -------------------------------------------------------------------------------- /diva/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgriley/diva/5a9e8144f690d02eca0d221544c675ce682ed548/diva/static/apple-touch-icon.png -------------------------------------------------------------------------------- /diva/static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /diva/static/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /diva/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgriley/diva/5a9e8144f690d02eca0d221544c675ce682ed548/diva/static/favicon.ico -------------------------------------------------------------------------------- /diva/static/humans.txt: -------------------------------------------------------------------------------- 1 | # humanstxt.org/ 2 | # The humans responsible & technology colophon 3 | 4 | # TEAM 5 | 6 | -- -- 7 | 8 | # THANKS 9 | 10 | 11 | 12 | # TECHNOLOGY COLOPHON 13 | 14 | CSS3, HTML5 15 | Apache Server Configs, jQuery, Modernizr, Normalize.css 16 | -------------------------------------------------------------------------------- /diva/static/main.css: -------------------------------------------------------------------------------- 1 | /*! HTML5 Boilerplate v5.3.0 | MIT License | https://html5boilerplate.com/ */ 2 | 3 | /* 4 | * What follows is the result of much research on cross-browser styling. 5 | * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, 6 | * Kroc Camen, and the H5BP dev community and team. 7 | */ 8 | 9 | /* ========================================================================== 10 | Base styles: opinionated defaults 11 | ========================================================================== */ 12 | 13 | html { 14 | color: #222; 15 | font-size: 1em; 16 | line-height: 1.4; 17 | } 18 | 19 | /* 20 | * Remove text-shadow in selection highlight: 21 | * https://twitter.com/miketaylr/status/12228805301 22 | * 23 | * These selection rule sets have to be separate. 24 | * Customize the background color to match your design. 25 | */ 26 | 27 | ::-moz-selection { 28 | background: #b3d4fc; 29 | text-shadow: none; 30 | } 31 | 32 | ::selection { 33 | background: #b3d4fc; 34 | text-shadow: none; 35 | } 36 | 37 | /* 38 | * A better looking default horizontal rule 39 | */ 40 | 41 | hr { 42 | display: block; 43 | height: 1px; 44 | border: 0; 45 | border-top: 1px solid #ccc; 46 | margin: 1em 0; 47 | padding: 0; 48 | } 49 | 50 | /* 51 | * Remove the gap between audio, canvas, iframes, 52 | * images, videos and the bottom of their containers: 53 | * https://github.com/h5bp/html5-boilerplate/issues/440 54 | */ 55 | 56 | audio, 57 | canvas, 58 | iframe, 59 | img, 60 | svg, 61 | video { 62 | vertical-align: middle; 63 | } 64 | 65 | /* 66 | * Remove default fieldset styles. 67 | */ 68 | 69 | fieldset { 70 | border: 0; 71 | margin: 0; 72 | padding: 0; 73 | } 74 | 75 | /* 76 | * Allow only vertical resizing of textareas. 77 | */ 78 | 79 | textarea { 80 | resize: vertical; 81 | } 82 | 83 | /* ========================================================================== 84 | Browser Upgrade Prompt 85 | ========================================================================== */ 86 | 87 | .browserupgrade { 88 | margin: 0.2em 0; 89 | background: #ccc; 90 | color: #000; 91 | padding: 0.2em 0; 92 | } 93 | 94 | /* ========================================================================== 95 | Author's custom styles 96 | ========================================================================== */ 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | /* ========================================================================== 115 | Helper classes 116 | ========================================================================== */ 117 | 118 | /* 119 | * Hide visually and from screen readers 120 | */ 121 | 122 | .hidden { 123 | display: none !important; 124 | } 125 | 126 | /* 127 | * Hide only visually, but have it available for screen readers: 128 | * http://snook.ca/archives/html_and_css/hiding-content-for-accessibility 129 | */ 130 | 131 | .visuallyhidden { 132 | border: 0; 133 | clip: rect(0 0 0 0); 134 | height: 1px; 135 | margin: -1px; 136 | overflow: hidden; 137 | padding: 0; 138 | position: absolute; 139 | width: 1px; 140 | } 141 | 142 | /* 143 | * Extends the .visuallyhidden class to allow the element 144 | * to be focusable when navigated to via the keyboard: 145 | * https://www.drupal.org/node/897638 146 | */ 147 | 148 | .visuallyhidden.focusable:active, 149 | .visuallyhidden.focusable:focus { 150 | clip: auto; 151 | height: auto; 152 | margin: 0; 153 | overflow: visible; 154 | position: static; 155 | width: auto; 156 | } 157 | 158 | /* 159 | * Hide visually and from screen readers, but maintain layout 160 | */ 161 | 162 | .invisible { 163 | visibility: hidden; 164 | } 165 | 166 | /* 167 | * Clearfix: contain floats 168 | * 169 | * For modern browsers 170 | * 1. The space content is one way to avoid an Opera bug when the 171 | * `contenteditable` attribute is included anywhere else in the document. 172 | * Otherwise it causes space to appear at the top and bottom of elements 173 | * that receive the `clearfix` class. 174 | * 2. The use of `table` rather than `block` is only necessary if using 175 | * `:before` to contain the top-margins of child elements. 176 | */ 177 | 178 | .clearfix:before, 179 | .clearfix:after { 180 | content: " "; /* 1 */ 181 | display: table; /* 2 */ 182 | } 183 | 184 | .clearfix:after { 185 | clear: both; 186 | } 187 | 188 | /* ========================================================================== 189 | EXAMPLE Media Queries for Responsive Design. 190 | These examples override the primary ('mobile first') styles. 191 | Modify as content requires. 192 | ========================================================================== */ 193 | 194 | @media only screen and (min-width: 35em) { 195 | /* Style adjustments for viewports that meet the condition */ 196 | } 197 | 198 | @media print, 199 | (-webkit-min-device-pixel-ratio: 1.25), 200 | (min-resolution: 1.25dppx), 201 | (min-resolution: 120dpi) { 202 | /* Style adjustments for high resolution devices */ 203 | } 204 | 205 | /* ========================================================================== 206 | Print styles. 207 | Inlined to avoid the additional HTTP request: 208 | http://www.phpied.com/delay-loading-your-print-css/ 209 | ========================================================================== */ 210 | 211 | @media print { 212 | *, 213 | *:before, 214 | *:after, 215 | *:first-letter, 216 | *:first-line { 217 | background: transparent !important; 218 | color: #000 !important; /* Black prints faster: 219 | http://www.sanbeiji.com/archives/953 */ 220 | box-shadow: none !important; 221 | text-shadow: none !important; 222 | } 223 | 224 | a, 225 | a:visited { 226 | text-decoration: underline; 227 | } 228 | 229 | a[href]:after { 230 | content: " (" attr(href) ")"; 231 | } 232 | 233 | abbr[title]:after { 234 | content: " (" attr(title) ")"; 235 | } 236 | 237 | /* 238 | * Don't show links that are fragment identifiers, 239 | * or use the `javascript:` pseudo protocol 240 | */ 241 | 242 | a[href^="#"]:after, 243 | a[href^="javascript:"]:after { 244 | content: ""; 245 | } 246 | 247 | pre, 248 | blockquote { 249 | border: 1px solid #999; 250 | page-break-inside: avoid; 251 | } 252 | 253 | /* 254 | * Printing Tables: 255 | * http://css-discuss.incutio.com/wiki/Printing_Tables 256 | */ 257 | 258 | thead { 259 | display: table-header-group; 260 | } 261 | 262 | tr, 263 | img { 264 | page-break-inside: avoid; 265 | } 266 | 267 | img { 268 | max-width: 100% !important; 269 | } 270 | 271 | p, 272 | h2, 273 | h3 { 274 | orphans: 3; 275 | widows: 3; 276 | } 277 | 278 | h2, 279 | h3 { 280 | page-break-after: avoid; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /diva/static/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS and IE text size adjust after device orientation change, 6 | * without disabling user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability of focused elements when they are also in an 95 | * active/hover state. 96 | */ 97 | 98 | a:active, 99 | a:hover { 100 | outline: 0; 101 | } 102 | 103 | /* Text-level semantics 104 | ========================================================================== */ 105 | 106 | /** 107 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 108 | */ 109 | 110 | abbr[title] { 111 | border-bottom: 1px dotted; 112 | } 113 | 114 | /** 115 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 116 | */ 117 | 118 | b, 119 | strong { 120 | font-weight: bold; 121 | } 122 | 123 | /** 124 | * Address styling not present in Safari and Chrome. 125 | */ 126 | 127 | dfn { 128 | font-style: italic; 129 | } 130 | 131 | /** 132 | * Address variable `h1` font-size and margin within `section` and `article` 133 | * contexts in Firefox 4+, Safari, and Chrome. 134 | */ 135 | 136 | h1 { 137 | font-size: 2em; 138 | margin: 0.67em 0; 139 | } 140 | 141 | /** 142 | * Address styling not present in IE 8/9. 143 | */ 144 | 145 | mark { 146 | background: #ff0; 147 | color: #000; 148 | } 149 | 150 | /** 151 | * Address inconsistent and variable font size in all browsers. 152 | */ 153 | 154 | small { 155 | font-size: 80%; 156 | } 157 | 158 | /** 159 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 160 | */ 161 | 162 | sub, 163 | sup { 164 | font-size: 75%; 165 | line-height: 0; 166 | position: relative; 167 | vertical-align: baseline; 168 | } 169 | 170 | sup { 171 | top: -0.5em; 172 | } 173 | 174 | sub { 175 | bottom: -0.25em; 176 | } 177 | 178 | /* Embedded content 179 | ========================================================================== */ 180 | 181 | /** 182 | * Remove border when inside `a` element in IE 8/9/10. 183 | */ 184 | 185 | img { 186 | border: 0; 187 | } 188 | 189 | /** 190 | * Correct overflow not hidden in IE 9/10/11. 191 | */ 192 | 193 | svg:not(:root) { 194 | overflow: hidden; 195 | } 196 | 197 | /* Grouping content 198 | ========================================================================== */ 199 | 200 | /** 201 | * Address margin not present in IE 8/9 and Safari. 202 | */ 203 | 204 | figure { 205 | margin: 1em 40px; 206 | } 207 | 208 | /** 209 | * Address differences between Firefox and other browsers. 210 | */ 211 | 212 | hr { 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. 354 | */ 355 | 356 | input[type="search"] { 357 | -webkit-appearance: textfield; /* 1 */ 358 | box-sizing: content-box; /* 2 */ 359 | } 360 | 361 | /** 362 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 363 | * Safari (but not Chrome) clips the cancel button when the search input has 364 | * padding (and `textfield` appearance). 365 | */ 366 | 367 | input[type="search"]::-webkit-search-cancel-button, 368 | input[type="search"]::-webkit-search-decoration { 369 | -webkit-appearance: none; 370 | } 371 | 372 | /** 373 | * Define consistent border, margin, and padding. 374 | */ 375 | 376 | fieldset { 377 | border: 1px solid #c0c0c0; 378 | margin: 0 2px; 379 | padding: 0.35em 0.625em 0.75em; 380 | } 381 | 382 | /** 383 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 384 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 385 | */ 386 | 387 | legend { 388 | border: 0; /* 1 */ 389 | padding: 0; /* 2 */ 390 | } 391 | 392 | /** 393 | * Remove default vertical scrollbar in IE 8/9/10/11. 394 | */ 395 | 396 | textarea { 397 | overflow: auto; 398 | } 399 | 400 | /** 401 | * Don't inherit the `font-weight` (applied by a rule above). 402 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 403 | */ 404 | 405 | optgroup { 406 | font-weight: bold; 407 | } 408 | 409 | /* Tables 410 | ========================================================================== */ 411 | 412 | /** 413 | * Remove most spacing between table cells. 414 | */ 415 | 416 | table { 417 | border-collapse: collapse; 418 | border-spacing: 0; 419 | } 420 | 421 | td, 422 | th { 423 | padding: 0; 424 | } 425 | -------------------------------------------------------------------------------- /diva/static/plugins.js: -------------------------------------------------------------------------------- 1 | // Avoid `console` errors in browsers that lack a console. 2 | (function() { 3 | var method; 4 | var noop = function () {}; 5 | var methods = [ 6 | 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 7 | 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 8 | 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 9 | 'timeline', 'timelineEnd', 'timeStamp', 'trace', 'warn' 10 | ]; 11 | var length = methods.length; 12 | var console = (window.console = window.console || {}); 13 | 14 | while (length--) { 15 | method = methods[length]; 16 | 17 | // Only stub undefined methods. 18 | if (!console[method]) { 19 | console[method] = noop; 20 | } 21 | } 22 | }()); 23 | 24 | // Place any jQuery/helper plugins in here. 25 | -------------------------------------------------------------------------------- /diva/static/reports.js: -------------------------------------------------------------------------------- 1 | function newFigureWidgets() { 2 | var obj = {}; 3 | 4 | // List of all Widget objects, which are 5 | // the objects returned by the function in setupMap 6 | // for a widget of its type 7 | obj.widgetList = []; 8 | 9 | /* 10 | widget is the Widget object returned from the function in the setupMap for 11 | a widget of this type. 12 | */ 13 | obj.add = function(widget) { 14 | obj.widgetList.push(widget); 15 | }; 16 | 17 | /* 18 | Get a list of widget values from list of widgets 19 | */ 20 | obj.getValues = function() { 21 | var values = []; 22 | for (var i = 0; i < obj.widgetList.length; ++i) { 23 | values.push(obj.widgetList[i].getCurrentValue()); 24 | } 25 | return values; 26 | }; 27 | 28 | obj.resetToDefaults = function() { 29 | for (var i = 0; i < obj.widgetList.length; ++i) { 30 | obj.widgetList[i].resetToDefault(); 31 | } 32 | }; 33 | 34 | return obj; 35 | }; 36 | 37 | /* 38 | Return a Report object. There is one Report for every function 39 | decorated/registered with the Diva object. 40 | */ 41 | function newReport(reportIndex) { 42 | var obj = { 43 | reportIndex: reportIndex, 44 | // a Widgets object, as defined above 45 | widgets: newFigureWidgets() 46 | }; 47 | 48 | // true until the report is opened/viewed 49 | obj.notYetSeen = true; 50 | 51 | /* 52 | Use the current values of the widgets to update the HTML displayed 53 | for this report. 54 | */ 55 | obj.update = function() { 56 | // get the current values of the widgets 57 | valueArray = obj.widgets.getValues(); 58 | var updateRequest = { 59 | reportIndex: obj.reportIndex, 60 | widgetValues: valueArray 61 | }; 62 | //console.log('form values: ' + JSON.stringify(updateRequest)); 63 | var currentPath = window.location.pathname; 64 | 65 | // on success, replace the report's HTML with the response 66 | // TODO: ensure that any JS included in the returned HTML (via script tags) 67 | // is always run 68 | var callback = function(data) { 69 | //console.log(data) 70 | // update the figure's HTML 71 | var figureId = '#figure-' + obj.reportIndex; 72 | var figureHTML = data['figureHTML'] 73 | $(figureId).html(figureHTML); 74 | 75 | // add the utilities to the sidebars 76 | // this should also run the script tags that set them up 77 | var widgetFormId = '#widgetform-' + obj.reportIndex; 78 | htmlArray = data['utilityHTML']; 79 | if (htmlArray.length > 0) { 80 | var utilityHTML = htmlArray.join(''); 81 | var utilityTag = $(widgetFormId).find('.utilities'); 82 | utilityTag.html(utilityHTML); 83 | 84 | // setup all of the utilities 85 | var utilities = utilityTag.children('.utility'); 86 | utilities.each(function(utilityIndex) { 87 | var util = $(this) 88 | var utilType = util.data('type'); 89 | var setupFunc = Reports.Utilities.setupMap[utilType]; 90 | setupFunc(obj.reportIndex, utilityIndex, util); 91 | }); 92 | } 93 | } 94 | $.ajax({ 95 | url: currentPath + 'update', 96 | type: "POST", 97 | data: JSON.stringify(updateRequest), 98 | contentType: "application/json", 99 | success: callback 100 | }); 101 | }; 102 | 103 | // the id of the widget form for this Report 104 | var widgetFormId = '#widgetform-' + obj.reportIndex; 105 | 106 | // update the figure/HTML on submit 107 | $(widgetFormId).on("submit", function(formEvent) { 108 | // prevent default get request 109 | //console.log('submitting'); 110 | formEvent.preventDefault(); 111 | obj.update(); 112 | }); 113 | 114 | // Reset the widget form and the displayed figure to the default 115 | // values of the widgets 116 | // NA if no user-defined widgets 117 | $(widgetFormId).on("reset", function(formEvent) { 118 | //console.log('resetting'); 119 | formEvent.preventDefault(); 120 | obj.widgets.resetToDefaults(); 121 | obj.update(); 122 | }); 123 | 124 | return obj; 125 | } 126 | 127 | // Global state 128 | var Reports = {}; 129 | (function(obj) { 130 | // list of Report objects 131 | obj.reportList = []; 132 | 133 | obj.Widgets = { 134 | /* 135 | Map from widget type (which is the class name in widgets.py) to setup function 136 | A setup function takes the widget's parent container (see class widgetcontainer in index.html) 137 | as a JQuery object, and returns an object of the following form: 138 | { 139 | resetToDefault: noargs function that resets the widget to its default value 140 | getCurrentValue: noargs function that returns the current value of the widget 141 | } 142 | The functions in the returned object keep a reference to the given JQuery object (closure) 143 | */ 144 | setupMap: {}, 145 | 146 | // Takes a JQuery div containing all of the widgets (via the widgetform macro) 147 | // Returns a 'widgets' object (the result of newFigureWidgets) 148 | setupForm: function(parentDiv) { 149 | var widgetsObj = newFigureWidgets(); 150 | var widgetElements = parentDiv.children('.widgetcontainer'); 151 | // setup all of the widgets, adding them to the widgetsObj 152 | widgetElements.each(function() { 153 | // extract name and type from the widget's outer div 154 | // , which is of class widgetcontainer (see index.html) 155 | var element = $(this); 156 | var widgetType = element.data('widget-type'); 157 | // setup a widget of the requested type, and add to report 158 | var setupFunc = Reports.Widgets.setupMap[widgetType]; 159 | var widget = setupFunc(element); 160 | widgetsObj.add(widget); 161 | }); 162 | return widgetsObj; 163 | } 164 | }; 165 | 166 | obj.Utilities = { 167 | /* 168 | Setup map is a dict from utility type (which is specified as data-type in the 169 | div of class utility) to function that takes that parent div and returns nothing. 170 | The function is responsible for registering any callbacks that should be called 171 | when the user interacts the the util's HTML 172 | */ 173 | setupMap: {} 174 | }; 175 | 176 | /* 177 | Create a new Report object, and add it to the list 178 | */ 179 | obj.create = function() { 180 | var reportIndex = obj.reportList.length; 181 | var report = newReport(reportIndex); 182 | obj.reportList.push(report); 183 | return report; 184 | }; 185 | })(Reports); 186 | -------------------------------------------------------------------------------- /diva/static/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /diva/static/script.js: -------------------------------------------------------------------------------- 1 | // reportId is a string like "report-0", "report-1", ... 2 | function changeTab(reportId) { 3 | // hide all report tabs 4 | $('.report-tab').css('display', 'none'); 5 | 6 | // display the components for the desired tab 7 | $('.' + reportId).css('display', 'block'); 8 | 9 | // if the report can not yet been opened, load its default 10 | var index = parseInt(reportId.split('-')[1]) 11 | var report = Reports.reportList[index] 12 | if (report.notYetSeen) { 13 | report.notYetSeen = false; 14 | report.update() 15 | } 16 | } 17 | 18 | $(document).ready(function() { 19 | // setup the buttons in report dropdown menu 20 | $('.report-option').on('click', function() { 21 | var reportId = $(this).attr('value'); 22 | changeTab(reportId); 23 | }); 24 | 25 | // init all reports 26 | var reportElements = $('.report'); 27 | reportElements.each(function(index) { 28 | var reportElement = $(this); 29 | var report = Reports.create(); 30 | 31 | // setup the report's user-defined widgets 32 | var widgetformParent = $('#widgetform-' + index).find('.user-widgets'); 33 | report.widgets = Reports.Widgets.setupForm(widgetformParent); 34 | }); 35 | 36 | // open the first tab 37 | // this must be called after the Reports state is configured 38 | $('.report-option').first().trigger('click'); 39 | }); 40 | -------------------------------------------------------------------------------- /diva/static/style.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | border-radius: 0; } 3 | 4 | html, body { 5 | margin: 0; 6 | padding: 0; 7 | background: #fff; 8 | color: #333; 9 | height: 100vh; } 10 | 11 | .wrapper { 12 | margin: 0; 13 | padding: 0; 14 | display: grid; 15 | grid-template-columns: 250px 1fr; 16 | height: 100%; 17 | overflow: hidden; } 18 | 19 | .sidebar { 20 | grid-column: 1; 21 | background-color: #eee; 22 | height: 100%; } 23 | 24 | .reports-button { 25 | width: 100%; 26 | border: none; 27 | border-radius: 0; 28 | background-color: #aaa; 29 | padding: 0.5em; 30 | height: 40px; } 31 | 32 | .reports-list { 33 | border-radius: 0; 34 | margin: 0; 35 | padding: 0; 36 | width: 100%; 37 | height: calc(100vh - 40px); 38 | overflow: auto; } 39 | 40 | .reports-list li a { 41 | white-space: normal; } 42 | 43 | .report-details { 44 | height: calc(100vh - 40px); 45 | overflow: auto; } 46 | 47 | .widgetform { 48 | padding: 1em; } 49 | 50 | .report-title { 51 | margin-bottom: 1em; 52 | font-size: 1.5em; } 53 | 54 | .report-tab { 55 | display: none; } 56 | 57 | .main-pane { 58 | grid-column: 2; 59 | overflow: auto; 60 | padding: 1em; 61 | height: 100vh; } 62 | 63 | .figure { 64 | margin: auto; } 65 | 66 | .figure-placeholder { 67 | margin: 0 auto; 68 | text-align: center; 69 | padding: 1em; } 70 | 71 | .widgetcontainer { 72 | margin-bottom: 0.5em; } 73 | 74 | .update-buttons { 75 | margin-top: 1em; } 76 | 77 | .label-widget { 78 | font-size: 1.25em; 79 | margin-top: 1em; } 80 | 81 | .utilities-pane { 82 | margin-top: 2em; } 83 | 84 | .utility-label { 85 | font-size: 1.25em; } 86 | 87 | .utility { 88 | margin-bottom: 1em; } 89 | 90 | .dashboard { 91 | display: grid; 92 | width: 100%; 93 | grid-gap: 2px; } 94 | 95 | .dashboard-pane { 96 | border-style: solid; 97 | border-color: black; 98 | border-width: 1px; 99 | padding: 0.5em; 100 | overflow: auto; } 101 | 102 | .pane-name { 103 | font-size: 1.5em; } 104 | 105 | /*# sourceMappingURL=style.css.map */ 106 | -------------------------------------------------------------------------------- /diva/static/style.scss: -------------------------------------------------------------------------------- 1 | $sidebar-color: #333; 2 | $sidebar-text-color: #ddd; 3 | $report-background-color: #fff; 4 | $upper-button-height: 40px; 5 | 6 | // bootstrap overloads: 7 | .btn { 8 | border-radius: 0; 9 | } 10 | 11 | html, body { 12 | margin: 0; 13 | padding: 0; 14 | background: $report-background-color; 15 | color: $sidebar-color; 16 | //font-family: "Lato", sans-serif; 17 | height: 100vh; 18 | } 19 | 20 | .wrapper { 21 | margin: 0; 22 | padding: 0; 23 | display: grid; 24 | grid-template-columns: 250px 1fr; 25 | height: 100%; 26 | overflow: hidden; 27 | } 28 | 29 | .sidebar { 30 | grid-column: 1; 31 | background-color: #eee; 32 | height: 100%; 33 | } 34 | 35 | // date pickers 36 | 37 | .datepicker { 38 | } 39 | 40 | // reports dropdown menu 41 | 42 | .reports-button { 43 | width: 100%; 44 | border: none; 45 | border-radius: 0; 46 | background-color: #aaa; 47 | padding: 0.5em; 48 | height: $upper-button-height; 49 | } 50 | 51 | .reports-list { 52 | border-radius: 0; 53 | margin: 0; 54 | padding: 0; 55 | width: 100%; 56 | height: calc(100vh - #{$upper-button-height}); 57 | overflow: auto; 58 | } 59 | 60 | .reports-list li a { 61 | white-space: normal; 62 | } 63 | 64 | // tab-specific content 65 | 66 | .report-details { 67 | height: calc(100vh - #{$upper-button-height}); 68 | overflow: auto; 69 | } 70 | 71 | .widgetform { 72 | padding: 1em; 73 | } 74 | 75 | .report-title { 76 | margin-bottom: 1em; 77 | font-size: 1.5em; 78 | } 79 | 80 | // hide all tabbed DOM by default 81 | .report-tab { 82 | display: none; 83 | } 84 | 85 | .main-pane { 86 | grid-column: 2; 87 | overflow: auto; 88 | padding: 1em; 89 | height: 100vh; 90 | } 91 | 92 | .figure { 93 | margin: auto; 94 | } 95 | .figure-placeholder { 96 | margin: 0 auto; 97 | text-align: center; 98 | padding: 1em; 99 | } 100 | 101 | .widgetcontainer { 102 | // spacing b/w adjacent widgets 103 | margin-bottom: 0.5em; 104 | } 105 | 106 | .update-buttons { 107 | margin-top: 1em; 108 | } 109 | 110 | .label-widget { 111 | font-size: 1.25em; 112 | margin-top: 1em; 113 | } 114 | 115 | // for utilities: 116 | 117 | .utilities-pane { 118 | margin-top: 2em; 119 | } 120 | 121 | .utility-label { 122 | font-size: 1.25em; 123 | } 124 | 125 | .utility { 126 | margin-bottom: 1em; 127 | } 128 | 129 | // For HTML converters 130 | 131 | .dashboard { 132 | display: grid; 133 | width: 100%; 134 | grid-gap: 2px; 135 | } 136 | 137 | .dashboard-pane { 138 | border-style: solid; 139 | border-color: black; 140 | border-width: 1px; 141 | padding: 0.5em; 142 | overflow: auto; 143 | } 144 | 145 | .pane-name { 146 | font-size: 1.5em; 147 | } 148 | -------------------------------------------------------------------------------- /diva/static/tile-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgriley/diva/5a9e8144f690d02eca0d221544c675ce682ed548/diva/static/tile-wide.png -------------------------------------------------------------------------------- /diva/static/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgriley/diva/5a9e8144f690d02eca0d221544c675ce682ed548/diva/static/tile.png -------------------------------------------------------------------------------- /diva/static/utilities.js: -------------------------------------------------------------------------------- 1 | /* 2 | Must convert strings to array buffers before passing to Blob b/c array 3 | buffers are for arbitrary binary data. 4 | see: https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript 5 | */ 6 | var strToArrayBuffer = function(str) { 7 | var byteNums = new Array(str.length); 8 | for (var i = 0; i < str.length; ++i) { 9 | byteNums[i] = str.charCodeAt(i); 10 | } 11 | return new Uint8Array(byteNums); 12 | }; 13 | 14 | // the 'utility' arg is the container div tag (class utility) as a JQuery object 15 | Reports.Utilities.setupMap['basic'] = function(reportIndex, utilityIndex, utility) { 16 | 17 | // helper 18 | var submitData = function(data) { 19 | var onSuccess = function(responseData) { 20 | // the 'content' field of the response is the string of the 21 | // file data encoded in base64 22 | var contentStr = window.atob(responseData['content']); 23 | var arrayBuffer = strToArrayBuffer(contentStr); 24 | var filename = responseData['filename']; 25 | var blob = new Blob([arrayBuffer]); 26 | saveAs(blob, filename); 27 | }; 28 | var currentPath = window.location.pathname; 29 | var requestBody = { 30 | reportIndex: reportIndex, 31 | utilityIndex: utilityIndex, 32 | data: data 33 | }; 34 | $.ajax({ 35 | url: currentPath + 'utility', 36 | type: 'POST', 37 | data: JSON.stringify(requestBody), 38 | contentType: 'application/json', 39 | success: onSuccess 40 | }); 41 | }; 42 | 43 | // setup the modal's widget form 44 | var modal = utility.find('.utility-modal'); 45 | var utilityForm = $(modal).find('.utility-form'); 46 | var widgets = Reports.Widgets.setupForm(utilityForm); 47 | 48 | // if the button takes no widgets, then clicking should submit 49 | // otherwise clicking should open the widget form for submitting 50 | var button = utility.find('.utility-button'); 51 | $(button).on('click', function() { 52 | var requiresInput = widgets.widgetList.length > 0; 53 | if (requiresInput) { 54 | $(modal).modal('show'); 55 | } else { 56 | submitData(widgets.getValues()); 57 | } 58 | }); 59 | 60 | // upon submit, submit the widget values 61 | $(modal).find('.submit').on('click', function() { 62 | var data = widgets.getValues(); 63 | submitData(data); 64 | }); 65 | 66 | // upon reset, reset the widget values 67 | $(modal).find('.reset').on('click', function() { 68 | widgets.resetToDefaults(); 69 | }); 70 | }; 71 | 72 | Reports.Utilities.setupMap['label'] = function() { 73 | // it's just a label, so no need for setup 74 | }; 75 | -------------------------------------------------------------------------------- /diva/static/vendor/FileSaver.min.js: -------------------------------------------------------------------------------- 1 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 2 | var saveAs=saveAs||function(e){"use strict";if(typeof e==="undefined"||typeof navigator!=="undefined"&&/MSIE [1-9]\./.test(navigator.userAgent)){return}var t=e.document,n=function(){return e.URL||e.webkitURL||e},r=t.createElementNS("http://www.w3.org/1999/xhtml","a"),o="download"in r,a=function(e){var t=new MouseEvent("click");e.dispatchEvent(t)},i=/constructor/i.test(e.HTMLElement)||e.safari,f=/CriOS\/[\d]+/.test(navigator.userAgent),u=function(t){(e.setImmediate||e.setTimeout)(function(){throw t},0)},s="application/octet-stream",d=1e3*40,c=function(e){var t=function(){if(typeof e==="string"){n().revokeObjectURL(e)}else{e.remove()}};setTimeout(t,d)},l=function(e,t,n){t=[].concat(t);var r=t.length;while(r--){var o=e["on"+t[r]];if(typeof o==="function"){try{o.call(e,n||e)}catch(a){u(a)}}}},p=function(e){if(/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)){return new Blob([String.fromCharCode(65279),e],{type:e.type})}return e},v=function(t,u,d){if(!d){t=p(t)}var v=this,w=t.type,m=w===s,y,h=function(){l(v,"writestart progress write writeend".split(" "))},S=function(){if((f||m&&i)&&e.FileReader){var r=new FileReader;r.onloadend=function(){var t=f?r.result:r.result.replace(/^data:[^;]*;/,"data:attachment/file;");var n=e.open(t,"_blank");if(!n)e.location.href=t;t=undefined;v.readyState=v.DONE;h()};r.readAsDataURL(t);v.readyState=v.INIT;return}if(!y){y=n().createObjectURL(t)}if(m){e.location.href=y}else{var o=e.open(y,"_blank");if(!o){e.location.href=y}}v.readyState=v.DONE;h();c(y)};v.readyState=v.INIT;if(o){y=n().createObjectURL(t);setTimeout(function(){r.href=y;r.download=u;a(r);h();c(y);v.readyState=v.DONE});return}S()},w=v.prototype,m=function(e,t,n){return new v(e,t||e.name||"download",n)};if(typeof navigator!=="undefined"&&navigator.msSaveOrOpenBlob){return function(e,t,n){t=t||e.name||"download";if(!n){e=p(e)}return navigator.msSaveOrOpenBlob(e,t)}}w.abort=function(){};w.readyState=w.INIT=0;w.WRITING=1;w.DONE=2;w.error=w.onwritestart=w.onprogress=w.onwrite=w.onabort=w.onerror=w.onwriteend=null;return m}(typeof self!=="undefined"&&self||typeof window!=="undefined"&&window||this.content);if(typeof module!=="undefined"&&module.exports){module.exports.saveAs=saveAs}else if(typeof define!=="undefined"&&define!==null&&define.amd!==null){define("FileSaver.js",function(){return saveAs})} 3 | -------------------------------------------------------------------------------- /diva/static/vendor/bootstrap-timepicker.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Timepicker Component for Twitter Bootstrap 3 | * 4 | * Copyright 2013 Joris de Wit 5 | * 6 | * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */.bootstrap-timepicker{position:relative}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu{left:auto;right:0}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:before{left:auto;right:12px}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:after{left:auto;right:13px}.bootstrap-timepicker .input-group-addon{cursor:pointer}.bootstrap-timepicker .input-group-addon i{display:inline-block;width:16px;height:16px}.bootstrap-timepicker-widget.dropdown-menu{padding:4px}.bootstrap-timepicker-widget.dropdown-menu.open{display:inline-block}.bootstrap-timepicker-widget.dropdown-menu:before{border-bottom:7px solid rgba(0,0,0,0.2);border-left:7px solid transparent;border-right:7px solid transparent;content:"";display:inline-block;position:absolute}.bootstrap-timepicker-widget.dropdown-menu:after{border-bottom:6px solid #fff;border-left:6px solid transparent;border-right:6px solid transparent;content:"";display:inline-block;position:absolute}.bootstrap-timepicker-widget.timepicker-orient-left:before{left:6px}.bootstrap-timepicker-widget.timepicker-orient-left:after{left:7px}.bootstrap-timepicker-widget.timepicker-orient-right:before{right:6px}.bootstrap-timepicker-widget.timepicker-orient-right:after{right:7px}.bootstrap-timepicker-widget.timepicker-orient-top:before{top:-7px}.bootstrap-timepicker-widget.timepicker-orient-top:after{top:-6px}.bootstrap-timepicker-widget.timepicker-orient-bottom:before{bottom:-7px;border-bottom:0;border-top:7px solid #999}.bootstrap-timepicker-widget.timepicker-orient-bottom:after{bottom:-6px;border-bottom:0;border-top:6px solid #fff}.bootstrap-timepicker-widget a.btn,.bootstrap-timepicker-widget input{border-radius:4px}.bootstrap-timepicker-widget table{width:100%;margin:0}.bootstrap-timepicker-widget table td{text-align:center;height:30px;margin:0;padding:2px}.bootstrap-timepicker-widget table td:not(.separator){min-width:30px}.bootstrap-timepicker-widget table td span{width:100%}.bootstrap-timepicker-widget table td a{border:1px transparent solid;width:100%;display:inline-block;margin:0;padding:8px 0;outline:0;color:#333}.bootstrap-timepicker-widget table td a:hover{text-decoration:none;background-color:#eee;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border-color:#ddd}.bootstrap-timepicker-widget table td a i{margin-top:2px;font-size:18px}.bootstrap-timepicker-widget table td input{width:25px;margin:0;text-align:center}.bootstrap-timepicker-widget .modal-content{padding:4px}@media(min-width:767px){.bootstrap-timepicker-widget.modal{width:200px;margin-left:-100px}}@media(max-width:767px){.bootstrap-timepicker{width:100%}.bootstrap-timepicker .dropdown-menu{width:100%}} -------------------------------------------------------------------------------- /diva/static/vendor/bootstrap-timepicker.min.js: -------------------------------------------------------------------------------- 1 | /*! bootstrap-timepicker v0.5.2 2 | * http://jdewit.github.com/bootstrap-timepicker 3 | * Copyright (c) 2016 Joris de Wit and bootstrap-timepicker contributors 4 | * MIT License 5 | */!function(a,b,c){"use strict";var d=function(b,c){this.widget="",this.$element=a(b),this.defaultTime=c.defaultTime,this.disableFocus=c.disableFocus,this.disableMousewheel=c.disableMousewheel,this.isOpen=c.isOpen,this.minuteStep=c.minuteStep,this.modalBackdrop=c.modalBackdrop,this.orientation=c.orientation,this.secondStep=c.secondStep,this.snapToStep=c.snapToStep,this.showInputs=c.showInputs,this.showMeridian=c.showMeridian,this.showSeconds=c.showSeconds,this.template=c.template,this.appendWidgetTo=c.appendWidgetTo,this.showWidgetOnAddonClick=c.showWidgetOnAddonClick,this.icons=c.icons,this.maxHours=c.maxHours,this.explicitMode=c.explicitMode,this.handleDocumentClick=function(a){var b=a.data.scope;b.$element.parent().find(a.target).length||b.$widget.is(a.target)||b.$widget.find(a.target).length||b.hideWidget()},this._init()};d.prototype={constructor:d,_init:function(){var b=this;this.showWidgetOnAddonClick&&this.$element.parent().hasClass("input-group")&&this.$element.parent().hasClass("bootstrap-timepicker")?(this.$element.parent(".input-group.bootstrap-timepicker").find(".input-group-addon").on({"click.timepicker":a.proxy(this.showWidget,this)}),this.$element.on({"focus.timepicker":a.proxy(this.highlightUnit,this),"click.timepicker":a.proxy(this.highlightUnit,this),"keydown.timepicker":a.proxy(this.elementKeydown,this),"blur.timepicker":a.proxy(this.blurElement,this),"mousewheel.timepicker DOMMouseScroll.timepicker":a.proxy(this.mousewheel,this)})):this.template?this.$element.on({"focus.timepicker":a.proxy(this.showWidget,this),"click.timepicker":a.proxy(this.showWidget,this),"blur.timepicker":a.proxy(this.blurElement,this),"mousewheel.timepicker DOMMouseScroll.timepicker":a.proxy(this.mousewheel,this)}):this.$element.on({"focus.timepicker":a.proxy(this.highlightUnit,this),"click.timepicker":a.proxy(this.highlightUnit,this),"keydown.timepicker":a.proxy(this.elementKeydown,this),"blur.timepicker":a.proxy(this.blurElement,this),"mousewheel.timepicker DOMMouseScroll.timepicker":a.proxy(this.mousewheel,this)}),this.template!==!1?this.$widget=a(this.getTemplate()).on("click",a.proxy(this.widgetClick,this)):this.$widget=!1,this.showInputs&&this.$widget!==!1&&this.$widget.find("input").each(function(){a(this).on({"click.timepicker":function(){a(this).select()},"keydown.timepicker":a.proxy(b.widgetKeydown,b),"keyup.timepicker":a.proxy(b.widgetKeyup,b)})}),this.setDefaultTime(this.defaultTime)},blurElement:function(){this.highlightedUnit=null,this.updateFromElementVal()},clear:function(){this.hour="",this.minute="",this.second="",this.meridian="",this.$element.val("")},decrementHour:function(){if(this.showMeridian)if(1===this.hour)this.hour=12;else{if(12===this.hour)return this.hour--,this.toggleMeridian();if(0===this.hour)return this.hour=11,this.toggleMeridian();this.hour--}else this.hour<=0?this.hour=this.maxHours-1:this.hour--},decrementMinute:function(a){var b;b=a?this.minute-a:this.minute-this.minuteStep,0>b?(this.decrementHour(),this.minute=b+60):this.minute=b},decrementSecond:function(){var a=this.second-this.secondStep;0>a?(this.decrementMinute(!0),this.second=a+60):this.second=a},elementKeydown:function(a){switch(a.which){case 9:if(a.shiftKey){if("hour"===this.highlightedUnit){this.hideWidget();break}this.highlightPrevUnit()}else{if(this.showMeridian&&"meridian"===this.highlightedUnit||this.showSeconds&&"second"===this.highlightedUnit||!this.showMeridian&&!this.showSeconds&&"minute"===this.highlightedUnit){this.hideWidget();break}this.highlightNextUnit()}a.preventDefault(),this.updateFromElementVal();break;case 27:this.updateFromElementVal();break;case 37:a.preventDefault(),this.highlightPrevUnit(),this.updateFromElementVal();break;case 38:switch(a.preventDefault(),this.highlightedUnit){case"hour":this.incrementHour(),this.highlightHour();break;case"minute":this.incrementMinute(),this.highlightMinute();break;case"second":this.incrementSecond(),this.highlightSecond();break;case"meridian":this.toggleMeridian(),this.highlightMeridian()}this.update();break;case 39:a.preventDefault(),this.highlightNextUnit(),this.updateFromElementVal();break;case 40:switch(a.preventDefault(),this.highlightedUnit){case"hour":this.decrementHour(),this.highlightHour();break;case"minute":this.decrementMinute(),this.highlightMinute();break;case"second":this.decrementSecond(),this.highlightSecond();break;case"meridian":this.toggleMeridian(),this.highlightMeridian()}this.update()}},getCursorPosition:function(){var a=this.$element.get(0);if("selectionStart"in a)return a.selectionStart;if(c.selection){a.focus();var b=c.selection.createRange(),d=c.selection.createRange().text.length;return b.moveStart("character",-a.value.length),b.text.length-d}},getTemplate:function(){var a,b,c,d,e,f;switch(this.showInputs?(b='',c='',d='',e=''):(b='',c='',d='',e=''),f=''+(this.showSeconds?'':"")+(this.showMeridian?'':"")+" "+(this.showSeconds?'":"")+(this.showMeridian?'":"")+''+(this.showSeconds?'':"")+(this.showMeridian?'':"")+"
   
"+b+' :'+c+":'+d+" '+e+"
  
",this.template){case"modal":a='';break;case"dropdown":a='"}return a},getTime:function(){return""===this.hour?"":this.hour+":"+(1===this.minute.toString().length?"0"+this.minute:this.minute)+(this.showSeconds?":"+(1===this.second.toString().length?"0"+this.second:this.second):"")+(this.showMeridian?" "+this.meridian:"")},hideWidget:function(){this.isOpen!==!1&&(this.$element.trigger({type:"hide.timepicker",time:{value:this.getTime(),hours:this.hour,minutes:this.minute,seconds:this.second,meridian:this.meridian}}),"modal"===this.template&&this.$widget.modal?this.$widget.modal("hide"):this.$widget.removeClass("open"),a(c).off("mousedown.timepicker, touchend.timepicker",this.handleDocumentClick),this.isOpen=!1,this.$widget.detach())},highlightUnit:function(){this.position=this.getCursorPosition(),this.position>=0&&this.position<=2?this.highlightHour():this.position>=3&&this.position<=5?this.highlightMinute():this.position>=6&&this.position<=8?this.showSeconds?this.highlightSecond():this.highlightMeridian():this.position>=9&&this.position<=11&&this.highlightMeridian()},highlightNextUnit:function(){switch(this.highlightedUnit){case"hour":this.highlightMinute();break;case"minute":this.showSeconds?this.highlightSecond():this.showMeridian?this.highlightMeridian():this.highlightHour();break;case"second":this.showMeridian?this.highlightMeridian():this.highlightHour();break;case"meridian":this.highlightHour()}},highlightPrevUnit:function(){switch(this.highlightedUnit){case"hour":this.showMeridian?this.highlightMeridian():this.showSeconds?this.highlightSecond():this.highlightMinute();break;case"minute":this.highlightHour();break;case"second":this.highlightMinute();break;case"meridian":this.showSeconds?this.highlightSecond():this.highlightMinute()}},highlightHour:function(){var a=this.$element.get(0),b=this;this.highlightedUnit="hour",a.setSelectionRange&&setTimeout(function(){b.hour<10?a.setSelectionRange(0,1):a.setSelectionRange(0,2)},0)},highlightMinute:function(){var a=this.$element.get(0),b=this;this.highlightedUnit="minute",a.setSelectionRange&&setTimeout(function(){b.hour<10?a.setSelectionRange(2,4):a.setSelectionRange(3,5)},0)},highlightSecond:function(){var a=this.$element.get(0),b=this;this.highlightedUnit="second",a.setSelectionRange&&setTimeout(function(){b.hour<10?a.setSelectionRange(5,7):a.setSelectionRange(6,8)},0)},highlightMeridian:function(){var a=this.$element.get(0),b=this;this.highlightedUnit="meridian",a.setSelectionRange&&(this.showSeconds?setTimeout(function(){b.hour<10?a.setSelectionRange(8,10):a.setSelectionRange(9,11)},0):setTimeout(function(){b.hour<10?a.setSelectionRange(5,7):a.setSelectionRange(6,8)},0))},incrementHour:function(){if(this.showMeridian){if(11===this.hour)return this.hour++,this.toggleMeridian();12===this.hour&&(this.hour=0)}return this.hour===this.maxHours-1?void(this.hour=0):void this.hour++},incrementMinute:function(a){var b;b=a?this.minute+a:this.minute+this.minuteStep-this.minute%this.minuteStep,b>59?(this.incrementHour(),this.minute=b-60):this.minute=b},incrementSecond:function(){var a=this.second+this.secondStep-this.second%this.secondStep;a>59?(this.incrementMinute(!0),this.second=a-60):this.second=a},mousewheel:function(b){if(!this.disableMousewheel){b.preventDefault(),b.stopPropagation();var c=b.originalEvent.wheelDelta||-b.originalEvent.detail,d=null;switch("mousewheel"===b.type?d=-1*b.originalEvent.wheelDelta:"DOMMouseScroll"===b.type&&(d=40*b.originalEvent.detail),d&&(b.preventDefault(),a(this).scrollTop(d+a(this).scrollTop())),this.highlightedUnit){case"minute":c>0?this.incrementMinute():this.decrementMinute(),this.highlightMinute();break;case"second":c>0?this.incrementSecond():this.decrementSecond(),this.highlightSecond();break;case"meridian":this.toggleMeridian(),this.highlightMeridian();break;default:c>0?this.incrementHour():this.decrementHour(),this.highlightHour()}return!1}},changeToNearestStep:function(a,b){return a%b===0?a:Math.round(a%b/b)?(a+(b-a%b))%60:a-a%b},place:function(){if(!this.isInline){var c=this.$widget.outerWidth(),d=this.$widget.outerHeight(),e=10,f=a(b).width(),g=a(b).height(),h=a(b).scrollTop(),i=parseInt(this.$element.parents().filter(function(){return"auto"!==a(this).css("z-index")}).first().css("z-index"),10)+10,j=this.component?this.component.parent().offset():this.$element.offset(),k=this.component?this.component.outerHeight(!0):this.$element.outerHeight(!1),l=this.component?this.component.outerWidth(!0):this.$element.outerWidth(!1),m=j.left,n=j.top;this.$widget.removeClass("timepicker-orient-top timepicker-orient-bottom timepicker-orient-right timepicker-orient-left"),"auto"!==this.orientation.x?(this.$widget.addClass("timepicker-orient-"+this.orientation.x),"right"===this.orientation.x&&(m-=c-l)):(this.$widget.addClass("timepicker-orient-left"),j.left<0?m-=j.left-e:j.left+c>f&&(m=f-c-e));var o,p,q=this.orientation.y;"auto"===q&&(o=-h+j.top-d,p=h+g-(j.top+k+d),q=Math.max(o,p)===p?"top":"bottom"),this.$widget.addClass("timepicker-orient-"+q),"top"===q?n+=k:n-=d+parseInt(this.$widget.css("padding-top"),10),this.$widget.css({top:n,left:m,zIndex:i})}},remove:function(){a("document").off(".timepicker"),this.$widget&&this.$widget.remove(),delete this.$element.data().timepicker},setDefaultTime:function(a){if(this.$element.val())this.updateFromElementVal();else if("current"===a){var b=new Date,c=b.getHours(),d=b.getMinutes(),e=b.getSeconds(),f="AM";0!==e&&(e=Math.ceil(b.getSeconds()/this.secondStep)*this.secondStep,60===e&&(d+=1,e=0)),0!==d&&(d=Math.ceil(b.getMinutes()/this.minuteStep)*this.minuteStep,60===d&&(c+=1,d=0)),this.showMeridian&&(0===c?c=12:c>=12?(c>12&&(c-=12),f="PM"):f="AM"),this.hour=c,this.minute=d,this.second=e,this.meridian=f,this.update()}else a===!1?(this.hour=0,this.minute=0,this.second=0,this.meridian="AM"):this.setTime(a)},setTime:function(a,b){if(!a)return void this.clear();var c,d,e,f,g,h;if("object"==typeof a&&a.getMonth)e=a.getHours(),f=a.getMinutes(),g=a.getSeconds(),this.showMeridian&&(h="AM",e>12&&(h="PM",e%=12),12===e&&(h="PM"));else{if(c=(/a/i.test(a)?1:0)+(/p/i.test(a)?2:0),c>2)return void this.clear();if(d=a.replace(/[^0-9\:]/g,"").split(":"),e=d[0]?d[0].toString():d.toString(),this.explicitMode&&e.length>2&&e.length%2!==0)return void this.clear();f=d[1]?d[1].toString():"",g=d[2]?d[2].toString():"",e.length>4&&(g=e.slice(-2),e=e.slice(0,-2)),e.length>2&&(f=e.slice(-2),e=e.slice(0,-2)),f.length>2&&(g=f.slice(-2),f=f.slice(0,-2)),e=parseInt(e,10),f=parseInt(f,10),g=parseInt(g,10),isNaN(e)&&(e=0),isNaN(f)&&(f=0),isNaN(g)&&(g=0),g>59&&(g=59),f>59&&(f=59),e>=this.maxHours&&(e=this.maxHours-1),this.showMeridian?(e>12&&(c=2,e-=12),c||(c=1),0===e&&(e=12),h=1===c?"AM":"PM"):12>e&&2===c?e+=12:e>=this.maxHours?e=this.maxHours-1:(0>e||12===e&&1===c)&&(e=0)}this.hour=e,this.snapToStep?(this.minute=this.changeToNearestStep(f,this.minuteStep),this.second=this.changeToNearestStep(g,this.secondStep)):(this.minute=f,this.second=g),this.meridian=h,this.update(b)},showWidget:function(){this.isOpen||this.$element.is(":disabled")||(this.$widget.appendTo(this.appendWidgetTo),a(c).on("mousedown.timepicker, touchend.timepicker",{scope:this},this.handleDocumentClick),this.$element.trigger({type:"show.timepicker",time:{value:this.getTime(),hours:this.hour,minutes:this.minute,seconds:this.second,meridian:this.meridian}}),this.place(),this.disableFocus&&this.$element.blur(),""===this.hour&&(this.defaultTime?this.setDefaultTime(this.defaultTime):this.setTime("0:0:0")),"modal"===this.template&&this.$widget.modal?this.$widget.modal("show").on("hidden",a.proxy(this.hideWidget,this)):this.isOpen===!1&&this.$widget.addClass("open"),this.isOpen=!0)},toggleMeridian:function(){this.meridian="AM"===this.meridian?"PM":"AM"},update:function(a){this.updateElement(),a||this.updateWidget(),this.$element.trigger({type:"changeTime.timepicker",time:{value:this.getTime(),hours:this.hour,minutes:this.minute,seconds:this.second,meridian:this.meridian}})},updateElement:function(){this.$element.val(this.getTime()).change()},updateFromElementVal:function(){this.setTime(this.$element.val())},updateWidget:function(){if(this.$widget!==!1){var a=this.hour,b=1===this.minute.toString().length?"0"+this.minute:this.minute,c=1===this.second.toString().length?"0"+this.second:this.second;this.showInputs?(this.$widget.find("input.bootstrap-timepicker-hour").val(a),this.$widget.find("input.bootstrap-timepicker-minute").val(b),this.showSeconds&&this.$widget.find("input.bootstrap-timepicker-second").val(c),this.showMeridian&&this.$widget.find("input.bootstrap-timepicker-meridian").val(this.meridian)):(this.$widget.find("span.bootstrap-timepicker-hour").text(a),this.$widget.find("span.bootstrap-timepicker-minute").text(b),this.showSeconds&&this.$widget.find("span.bootstrap-timepicker-second").text(c),this.showMeridian&&this.$widget.find("span.bootstrap-timepicker-meridian").text(this.meridian))}},updateFromWidgetInputs:function(){if(this.$widget!==!1){var a=this.$widget.find("input.bootstrap-timepicker-hour").val()+":"+this.$widget.find("input.bootstrap-timepicker-minute").val()+(this.showSeconds?":"+this.$widget.find("input.bootstrap-timepicker-second").val():"")+(this.showMeridian?this.$widget.find("input.bootstrap-timepicker-meridian").val():"");this.setTime(a,!0)}},widgetClick:function(b){b.stopPropagation(),b.preventDefault();var c=a(b.target),d=c.closest("a").data("action");d&&this[d](),this.update(),c.is("input")&&c.get(0).setSelectionRange(0,2)},widgetKeydown:function(b){var c=a(b.target),d=c.attr("class").replace("bootstrap-timepicker-","");switch(b.which){case 9:if(b.shiftKey){if("hour"===d)return this.hideWidget()}else if(this.showMeridian&&"meridian"===d||this.showSeconds&&"second"===d||!this.showMeridian&&!this.showSeconds&&"minute"===d)return this.hideWidget();break;case 27:this.hideWidget();break;case 38:switch(b.preventDefault(),d){case"hour":this.incrementHour();break;case"minute":this.incrementMinute();break;case"second":this.incrementSecond();break;case"meridian":this.toggleMeridian()}this.setTime(this.getTime()),c.get(0).setSelectionRange(0,2);break;case 40:switch(b.preventDefault(),d){case"hour":this.decrementHour();break;case"minute":this.decrementMinute();break;case"second":this.decrementSecond();break;case"meridian":this.toggleMeridian()}this.setTime(this.getTime()),c.get(0).setSelectionRange(0,2)}},widgetKeyup:function(a){(65===a.which||77===a.which||80===a.which||46===a.which||8===a.which||a.which>=48&&a.which<=57||a.which>=96&&a.which<=105)&&this.updateFromWidgetInputs()}},a.fn.timepicker=function(b){var c=Array.apply(null,arguments);return c.shift(),this.each(function(){var e=a(this),f=e.data("timepicker"),g="object"==typeof b&&b;f||e.data("timepicker",f=new d(this,a.extend({},a.fn.timepicker.defaults,g,a(this).data()))),"string"==typeof b&&f[b].apply(f,c)})},a.fn.timepicker.defaults={defaultTime:"current",disableFocus:!1,disableMousewheel:!1,isOpen:!1,minuteStep:15,modalBackdrop:!1,orientation:{x:"auto",y:"auto"},secondStep:15,snapToStep:!1,showSeconds:!1,showInputs:!0,showMeridian:!0,template:"dropdown",appendWidgetTo:"body",showWidgetOnAddonClick:!0,icons:{up:"glyphicon glyphicon-chevron-up",down:"glyphicon glyphicon-chevron-down"},maxHours:24,explicitMode:!1},a.fn.timepicker.Constructor=d,a(c).on("focus.timepicker.data-api click.timepicker.data-api",'[data-provide="timepicker"]',function(b){var c=a(this);c.data("timepicker")||(b.preventDefault(),c.timepicker())})}(jQuery,window,document); -------------------------------------------------------------------------------- /diva/static/vendor/modernizr-2.8.3.min.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.8.3 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load 3 | */ 4 | ;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a){var e=a[d];if(!G(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function K(){e.input=function(c){for(var d=0,e=c.length;d',a,""].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b)&&c(b).matches||!1;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return J("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=function(){return J("borderRadius")},s.boxshadow=function(){return J("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return E("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return J("animationName")},s.csscolumns=function(){return J("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return D((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),G(j.backgroundImage,"gradient")},s.cssreflections=function(){return J("boxReflect")},s.csstransforms=function(){return!!J("transform")},s.csstransforms3d=function(){var a=!!J("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return J("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.generatedcontent=function(){var a;return y(["#",h,"{font:0/0 a}#",h,':after{content:"',l,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var L in s)C(s,L)&&(x=L.toLowerCase(),e[x]=s[L](),v.push((e[x]?"":"no-")+x));return e.input||K(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)C(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},D(""),i=k=null,function(a,b){function l(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function m(){var a=s.elements;return typeof a=="string"?a.split(" "):a}function n(a){var b=j[a[h]];return b||(b={},i++,a[h]=i,j[i]=b),b}function o(a,c,d){c||(c=b);if(k)return c.createElement(a);d||(d=n(c));var g;return d.cache[a]?g=d.cache[a].cloneNode():f.test(a)?g=(d.cache[a]=d.createElem(a)).cloneNode():g=d.createElem(a),g.canHaveChildren&&!e.test(a)&&!g.tagUrn?d.frag.appendChild(g):g}function p(a,c){a||(a=b);if(k)return a.createDocumentFragment();c=c||n(a);var d=c.frag.cloneNode(),e=0,f=m(),g=f.length;for(;e",g="hidden"in a,k=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){g=!0,k=!0}})();var s={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:c,shivCSS:d.shivCSS!==!1,supportsUnknownElements:k,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:r,createElement:o,createDocumentFragment:p};a.html5=s,r(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.hasEvent=A,e.testProp=function(a){return H([a])},e.testAllProps=J,e.testStyles=y,e.prefixed=function(a,b,c){return b?J(a,b,c):J(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;fcall setupUtility 18 | div 19 | */ 20 | var setupUtility = function(callback) { 21 | var currentScript = $(document.currentScript); 22 | var parentElement = currentScript.closest('.utility'); 23 | $(parentElement).ready(function() { 24 | callback($(parentElement)); 25 | }); 26 | } 27 | 28 | // Helpers for setup: 29 | 30 | var resetToDefaultChecked = function(element) { 31 | var defaultVal = element.prop('defaultChecked'); 32 | element.prop('checked', defaultVal); 33 | }; 34 | 35 | var resetAllToDefaultChecked = function(elements) { 36 | elements.each(function() { 37 | resetToDefaultChecked($(this)); 38 | }); 39 | } 40 | 41 | // index into the children of a jquery obj and return the result 42 | // as a jquery obj 43 | var getChild = function(parentObj, index) { 44 | return $(parentObj.children().toArray(index)); 45 | } 46 | 47 | // The setup function for a widget that simply wraps an input tag 48 | var setupInputTagWidget = function(widget) { 49 | // the input tag is the first and only child of the widget's 50 | // div parent/container 51 | var input = widget.find(".input-tag-widget"); 52 | return { 53 | resetToDefault: function() { 54 | input.val(input.prop('defaultValue')); 55 | }, 56 | getCurrentValue: function() { 57 | return input.val(); 58 | } 59 | }; 60 | }; 61 | 62 | // Setup functions for the built-in widgets: 63 | // Note: the key value in the setup map must match the name of the 64 | // widget class (as defined in widgets.py) exactly 65 | 66 | // setup all types that use input tags 67 | var inputTagTypes = ['String', 'Float', 'Int', 'Color', 'Date', 'Time'] 68 | for (var i = 0; i < inputTagTypes.length; ++i) { 69 | widgetType = inputTagTypes[i] 70 | Reports.Widgets.setupMap[widgetType] = function(widget) { 71 | return setupInputTagWidget(widget); 72 | }; 73 | } 74 | 75 | Reports.Widgets.setupMap['Skip'] = function(widget) { 76 | return { 77 | getCurrentValue: function() { 78 | return 'this is skipped'; 79 | }, 80 | resetToDefault: function() { 81 | } 82 | }; 83 | }; 84 | 85 | Reports.Widgets.setupMap['Bool'] = function(widget) { 86 | return { 87 | getCurrentValue: function() { 88 | return getChild(widget, 0).is(':checked'); 89 | }, 90 | resetToDefault: function() { 91 | var input = getChild(widget, 0); 92 | resetToDefaultChecked(input); 93 | } 94 | }; 95 | }; 96 | 97 | Reports.Widgets.setupMap['SelectOne'] = function(widget) { 98 | return { 99 | getCurrentValue: function() { 100 | return widget.children().filter('input:checked').val(); 101 | }, 102 | resetToDefault: function() { 103 | var buttons = widget.children().filter('input'); 104 | resetAllToDefaultChecked(buttons); 105 | } 106 | } 107 | }; 108 | 109 | Reports.Widgets.setupMap['SelectSubset'] = function(widget) { 110 | return { 111 | getCurrentValue: function() { 112 | // get a list of the values of each checked input tag 113 | var checkedElems = widget.children().filter(':checked'); 114 | var selectedValues = checkedElems.map(function() { 115 | return $(this).val(); 116 | }).get(); 117 | return selectedValues; 118 | }, 119 | resetToDefault: function() { 120 | var buttons = widget.children(); 121 | resetAllToDefaultChecked(buttons); 122 | } 123 | } 124 | }; 125 | 126 | Reports.Widgets.setupMap['Slider'] = function(widget) { 127 | var inputElement = widget.children().filter('.slider-input'); 128 | var textElement = widget.children().filter('.slider-value'); 129 | // the text element should always display the slider's 130 | // current value 131 | inputElement.on('input', function(changeEvent) { 132 | var currentVal = inputElement.val(); 133 | textElement.text(currentVal); 134 | }); 135 | return { 136 | resetToDefault: function() { 137 | inputElement.val(inputElement.prop('defaultValue')); 138 | // force the text to update 139 | inputElement.trigger('input'); 140 | }, 141 | getCurrentValue: function() { 142 | return inputElement.val(); 143 | } 144 | }; 145 | }; 146 | 147 | Reports.Widgets.setupMap['DateRange'] = function(widget) { 148 | // attach date-range picker to the input tag 149 | var inputTag = widget.find(".input-tag-widget") 150 | pickerOptions = { 151 | "showDropdowns": true, 152 | "ranges": { 153 | 'Today': [moment(), moment()], 154 | 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')], 155 | 'Last 7 Days': [moment().subtract(6, 'days'), moment()], 156 | 'Last 30 Days': [moment().subtract(29, 'days'), moment()], 157 | 'This Month': [moment().startOf('month'), moment().endOf('month')], 158 | 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')] 159 | }, 160 | "locale": { 161 | "format": "YYYY-MM-DD", 162 | "separator": " to ", 163 | "applyLabel": "Apply", 164 | "cancelLabel": "Cancel", 165 | "fromLabel": "From", 166 | "toLabel": "To", 167 | "customRangeLabel": "Custom", 168 | "weekLabel": "W", 169 | "daysOfWeek": [ 170 | "Su", 171 | "Mo", 172 | "Tu", 173 | "We", 174 | "Th", 175 | "Fr", 176 | "Sa" 177 | ], 178 | "monthNames": [ 179 | "January", 180 | "February", 181 | "March", 182 | "April", 183 | "May", 184 | "June", 185 | "July", 186 | "August", 187 | "September", 188 | "October", 189 | "November", 190 | "December" 191 | ], 192 | "firstDay": 1 193 | }, 194 | "linkedCalendars": false, 195 | "alwaysShowCalendars": true, 196 | "startDate": inputTag.data('startdate'), 197 | "endDate": inputTag.data('enddate'), 198 | "drops": "down" 199 | }; 200 | // min and max date not implemented into the data-range widget yet 201 | // It doesn't seem very useful 202 | /* 203 | if (inputTag.dataset.mindate) { 204 | pickerOptions.minDate = inputTag.dataset.mindate; 205 | } 206 | if (inputTag.dataset.maxdate) { 207 | pickerOptions.maxDate = inputTag.dataset.maxdate; 208 | } 209 | */ 210 | inputTag.daterangepicker(pickerOptions, function(start, end, label) { 211 | return start.format('YYYY-MM-DD') + ' to ' + end.format('YYYY-MM-DD'); 212 | }); 213 | return { 214 | resetToDefault: function() { 215 | inputTag.val(inputTag.prop('defaultValue')); 216 | }, 217 | getCurrentValue: function() { 218 | return inputTag.val().split(' to '); 219 | } 220 | }; 221 | }; 222 | 223 | Reports.Widgets.setupMap['Date'] = function(widget) { 224 | // via Bootstrap's datepicker 225 | $(widget).find('.input-tag-widget').datepicker(); 226 | return setupInputTagWidget(widget); 227 | }; 228 | 229 | Reports.Widgets.setupMap['Time'] = function(widget) { 230 | // via Bootstrap timepicker. For options 231 | // see: https://jdewit.github.io/bootstrap-timepicker/ 232 | $(widget).find('.input-tag-widget').timepicker({ 233 | 'defaultTime': false, 234 | // 12 vs 24hr mode 235 | 'showMeridian': false 236 | }); 237 | return setupInputTagWidget(widget); 238 | }; 239 | -------------------------------------------------------------------------------- /diva/templates/.index.html.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgriley/diva/5a9e8144f690d02eca0d221544c675ce682ed548/diva/templates/.index.html.swp -------------------------------------------------------------------------------- /diva/templates/.label_widget.html.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgriley/diva/5a9e8144f690d02eca0d221544c675ce682ed548/diva/templates/.label_widget.html.swp -------------------------------------------------------------------------------- /diva/templates/checkbox_widget.html: -------------------------------------------------------------------------------- 1 | {{description}}
2 | -------------------------------------------------------------------------------- /diva/templates/checklist_widget.html: -------------------------------------------------------------------------------- 1 | {{ description }}
2 | {% for choice in choices %} 3 | {{choice}}
4 | {% endfor %} 5 | -------------------------------------------------------------------------------- /diva/templates/dashboard.html: -------------------------------------------------------------------------------- 1 |
5 | {% for pane in grid.panes %} 6 |
10 |

{{ pane.name }}

11 |
12 | {{ pane.html | safe }} 13 |
14 |
15 | {% endfor %} 16 |
17 | -------------------------------------------------------------------------------- /diva/templates/index.html: -------------------------------------------------------------------------------- 1 | {% from 'widgetform.html' import widgetform %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | diva 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 | 101 |
102 | {% for report in reports %} 103 |
104 |
105 |

106 | loading 107 |

108 |
109 |
110 | {% endfor %} 111 |
112 |
113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /diva/templates/input_tag_widget.html: -------------------------------------------------------------------------------- 1 | {{description}}

2 | -------------------------------------------------------------------------------- /diva/templates/label_widget.html: -------------------------------------------------------------------------------- 1 |

{{description}}

2 | -------------------------------------------------------------------------------- /diva/templates/radio_widget.html: -------------------------------------------------------------------------------- 1 | {{ description }}
2 | {% for choice in choices %} 3 | 4 | {{choice}}
5 | {% endfor %} 6 | -------------------------------------------------------------------------------- /diva/templates/slider_widget.html: -------------------------------------------------------------------------------- 1 | {{description}}. range: [{{attributes.min}}, {{attributes.max}}]
{{default}}
2 |
3 | -------------------------------------------------------------------------------- /diva/templates/utility_button.html: -------------------------------------------------------------------------------- 1 | {% from 'widgetform.html' import widgetform %} 2 |
3 | 7 | 24 |
25 | -------------------------------------------------------------------------------- /diva/templates/utility_form.html: -------------------------------------------------------------------------------- 1 | {% from 'widgetform.html' import widgetform %} 2 | 19 | -------------------------------------------------------------------------------- /diva/templates/utility_label.html: -------------------------------------------------------------------------------- 1 |
2 |

{{name}}

3 |
4 | -------------------------------------------------------------------------------- /diva/templates/widgetform.html: -------------------------------------------------------------------------------- 1 | {% macro widgetform(widgets) %} 2 | {% for widget in widgets %} 3 |
4 | {{ widget.html | safe }} 5 |
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: {}

".format(class_name, arg_type, f.format(arg)) 58 | return '

{}

'.format(body) 59 | 60 | @reporter.view('convert: str') 61 | def raw_html(): 62 | return '

Raw HTML

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 '

dksjalf

' 19 | # register_report('too_few', figure_too_few, [TextWidget('meh')]) 20 | 21 | @reporter.view('basic widget test', 22 | [String('text', 'hello'), 23 | Float('float', 1.5), 24 | Int('integer', 2), 25 | Bool('tis true', True), 26 | SelectOne('pick a name', ['foo', 'bar', 'baz'], 'bar'), 27 | SelectOne('pick another name', ['hi', 'yo', 'hey'], 'yo'), 28 | SelectSubset('pick names', ['foo', 'bar', 'baz'], ['foo', 'baz']), 29 | SelectSubset('pick many', ['a', 'b', 'c']), 30 | Color('my color', '#ff0000'), 31 | Slider('my default slider'), 32 | Slider('my param slider', 0, (-10, 10), 0)]) 33 | def widgets_test(*varargs): 34 | return '

{}

'.format(' '.join(['{}'.format(w) for w in varargs])) 35 | 36 | @reporter.view('datetime widgets',[ 37 | DateRange('default date range'), 38 | DateRange('abs date range', '2017-01-02', '2018-02-03'), 39 | DateRange('last __ range', relativedelta(weeks=3)), 40 | DateRange('rel range', relativedelta(days=7), relativedelta(days=1)), 41 | DateRange('date to present', '2017-01-02', timedelta()), 42 | Date('default date'), 43 | Date('date relative', relativedelta(days=7)), 44 | Date('date absolute', '2017-01-11'), 45 | Time('default'), 46 | Time('param', time(5, 10, 15))] 47 | ) 48 | def date_range_test(a, b, c, d, e, f, g, h, i, j): 49 | return '

{} | {} | {} | {} | {} | {} | {} | {} | {} | {}

'.format(a, b, c, d, e, f, g, h, i, j) 50 | 51 | @reporter.view('unknown type') 52 | def unknown_figure(): 53 | dict = {'foo': 10, 'bar': 47} 54 | return dict 55 | 56 | @reporter.view('invalid HTML string') 57 | def invalid_string(): 58 | return 'this is not valid html' 59 | 60 | @reporter.view('valid HTML string') 61 | def valid_string(): 62 | return '

foo



bar

' 63 | 64 | @reporter.view('too_many', [Float('floating', 6.5)]) 65 | def figure_extra(a, b=6): 66 | return '

{} {}

'.format(a, b) 67 | 68 | @reporter.view('matplotlib figure') 69 | def figure_a(): 70 | plt.figure() 71 | plt.plot([3,1,4,1,20], 'ks-', mec='w', mew=5, ms=20) 72 | return plt.gcf() 73 | 74 | @reporter.view('pd.DataFrame test') 75 | def figure_c(): 76 | df = pd.DataFrame(np.random.randn(20, 20)) 77 | return df; 78 | 79 | @reporter.view('pd.Series test') 80 | def figure_d(): 81 | s = pd.Series([p for p in range(100)]) 82 | return s 83 | 84 | @reporter.view('bokeh figure') 85 | def figure_e(): 86 | x = [1, 2, 3, 4, 5] 87 | y = [6, 7, 2, 4, 5] 88 | plot = figure(title="bokeh example", x_axis_label='x', y_axis_label='y') 89 | plot.line(x, y, legend="Temp", line_width=2) 90 | return plot 91 | 92 | @reporter.view('report with a very longggggggggg name') 93 | def figure_long(): 94 | return 4 95 | 96 | for i in range(100): 97 | @reporter.view('rand report {}'.format(i)) 98 | def foo(): 99 | return 'a' 100 | 101 | reporter.run(debug=True) 102 | -------------------------------------------------------------------------------- /examples/rough_examples/my_demo.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, Dashboard 11 | from diva.dashboard import row_layout 12 | from diva.widgets import * 13 | from bokeh.plotting import figure 14 | from functools import singledispatch 15 | 16 | reporter = Diva() 17 | 18 | @singledispatch 19 | def type_to_str(val): 20 | # strip off the tags or the HTML will not work 21 | s = str(type(val)) 22 | return s[1:-1] 23 | 24 | # helper for printing the types that widgets output: 25 | def type_of_iterable(val): 26 | s = '' 27 | for item in val: 28 | s += type_to_str(item) + ', ' 29 | return s 30 | 31 | @type_to_str.register(tuple) 32 | def tuple_type(val): 33 | return '({})'.format(type_of_iterable(val)) 34 | 35 | @type_to_str.register(list) 36 | def list_type(val): 37 | return '[{}]'.format(type_of_iterable(val)) 38 | 39 | # Provide an overview pages for all of the available widgets 40 | all_widgets = [ 41 | String('some text', 'hello'), 42 | Float('a float', 1.5), 43 | Int('an integer', 2), 44 | Bool('a bool', True), 45 | SelectOne('pick a name', ['foo', 'bar', 'baz'], 'bar'), 46 | SelectSubset('pick names', ['foo', 'bar', 'baz'], ['foo', 'baz']), 47 | Color('pick a color', '#ff0000'), 48 | Slider('a float'), 49 | Date('pick a date'), 50 | Time('pick a time'), 51 | DateRange('pick a date range') 52 | ] 53 | @reporter.view('all widgets', all_widgets) 54 | def widgets_test(wstr, wflo, wint, wbool, wso, wss, wcol, wsli, wdate, wtime, wdaterange): 55 | args = [wstr, wflo, wint, wbool, wso, wss, wcol, wsli, wdate, wtime, wdaterange] 56 | formats = ['{}', '{}', '{}', '{}', '{}', '{}', '{}', '{:f}', '{}', '{}', '{}'] 57 | body = '' 58 | for w, arg, f in zip(all_widgets, args, formats): 59 | arg_type = type_to_str(arg) 60 | class_name = w.__class__.__name__ 61 | body += "widget class: {}
type: {}
value: {}

".format(class_name, arg_type, f.format(arg)) 62 | return '

{}

'.format(body) 63 | 64 | @reporter.view('Skip wid', [Skip('heyhye'), Int('hey'), Skip('barbar')]) 65 | def skip_sample(i): 66 | return i 67 | 68 | @reporter.view('convert: Dashboard') 69 | def dashboard_view(): 70 | a = pd.DataFrame(np.random.randn(20, 20)) 71 | b = pd.DataFrame(np.random.randn(10, 10)) 72 | return Dashboard([a, b], [[0, 0, 1, 1], [1, 0, 1, 1]]) 73 | 74 | @reporter.view('test dash') 75 | def test_dash(): 76 | a = pd.DataFrame(np.random.randn(10, 10)); 77 | b = pd.DataFrame(np.random.randn(10, 10)); 78 | plt.figure() 79 | plt.plot([1, 2, 3, 4], 'ks-', mec='w', mew=5, ms=20) 80 | return Dashboard([a, plt.gcf(), b]) 81 | 82 | @reporter.view('dashboard nice') 83 | def dashboard_b(): 84 | x = [1, 2, 3, 4, 5] 85 | y = [6, 7, 2, 4, 5] 86 | plot = figure(title="bokeh example", x_axis_label='x', y_axis_label='y') 87 | plot.line(x, y, legend="Temp", line_width=2) 88 | a = pd.DataFrame(np.random.randn(20, 20)) 89 | b = pd.DataFrame(np.random.randn(10, 10)) 90 | c = pd.DataFrame(np.random.randn(5, 5)) 91 | d = pd.DataFrame(np.random.randn(1, 1)) 92 | return Dashboard([a, b, plot, c, d]) 93 | 94 | @reporter.view('dashboard b') 95 | def dashboard_b(): 96 | a = pd.DataFrame(np.random.randn(20, 20)) 97 | b = pd.DataFrame(np.random.randn(10, 10)) 98 | c = pd.DataFrame(np.random.randn(5, 5)) 99 | d = pd.DataFrame(np.random.randn(1, 1)) 100 | return Dashboard([a, b, c, d], [[0, 0, 1, 1], [1, 0, 1, 1], [0, 1, 1, 1], [1, 1, 1, 1]]) 101 | 102 | @reporter.view('dashboard c') 103 | def dashboard_c(): 104 | a = pd.DataFrame(np.random.randn(20, 20)) 105 | b = pd.DataFrame(np.random.randn(10, 10)) 106 | c = pd.DataFrame(np.random.randn(5, 5)) 107 | d = pd.DataFrame(np.random.randn(50, 50)) 108 | return Dashboard([a, b, c, d], [[0, 0, 3, 1], [0, 1, 1, 1], [1, 1, 1, 1], [2, 1, 1, 1]]) 109 | 110 | @reporter.view('dashboard d') 111 | def dashboard_d(): 112 | x = [1, 2, 3, 4, 5] 113 | y = [6, 7, 2, 4, 5] 114 | plot = figure(title="bokeh example", x_axis_label='x', y_axis_label='y') 115 | plot.line(x, y, legend="Temp", line_width=2) 116 | a = pd.DataFrame(np.random.randn(20, 20)) 117 | b = pd.DataFrame(np.random.randn(10, 10)) 118 | c = pd.DataFrame(np.random.randn(5, 5)) 119 | d = pd.DataFrame(np.random.randn(50, 50)) 120 | return Dashboard([plot, b, c, d], row_layout(2, 2)) 121 | 122 | @reporter.view('convert: str') 123 | def raw_html(): 124 | return '

Raw HTML

If a string is returned, it is assumed to be raw HTML

' 125 | 126 | @reporter.view('convert: matplotlib.figure.Figure') 127 | def matplot_fig(): 128 | plt.figure() 129 | plt.plot([1, 2, 3, 4], 'ks-', mec='w', mew=5, ms=20) 130 | return plt.gcf() 131 | 132 | @reporter.view('another matplotlib') 133 | def matplot_b(): 134 | plt.figure() 135 | plt.plot([5, 6, 7, 7], 'ks-', mec='w', mew=5, ms=20) 136 | return plt.gcf() 137 | 138 | @reporter.view('convert: pandas.DataFrame') 139 | def pandas_df(): 140 | df = pd.DataFrame(np.random.randn(20, 20)) 141 | return df; 142 | 143 | @reporter.view('convert: pandas.Series') 144 | def pandas_series(): 145 | s = pd.Series([p for p in range(100)]) 146 | return s 147 | 148 | @reporter.view('convert: bokeh.plotting.figure.Figure') 149 | def bokeh_fig(): 150 | x = [1, 2, 3, 4, 5] 151 | y = [6, 7, 2, 4, 5] 152 | plot = figure(title="bokeh example", x_axis_label='x', y_axis_label='y') 153 | plot.line(x, y, legend="Temp", line_width=2) 154 | return plot 155 | 156 | @reporter.view('convert: none of the above (ex. datetime.time)') 157 | def na(): 158 | return datetime.now() 159 | 160 | for i in range(100): 161 | @reporter.view('filler report {}'.format(i)) 162 | def foo(): 163 | return '

hi

' 164 | 165 | if __name__ == "__main__": 166 | reporter.run(debug=True) 167 | -------------------------------------------------------------------------------- /examples/rough_examples/sample_export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgriley/diva/5a9e8144f690d02eca0d221544c675ce682ed548/examples/rough_examples/sample_export.png -------------------------------------------------------------------------------- /examples/rough_examples/tester.py: -------------------------------------------------------------------------------- 1 | from diy_dashboard.reporter import Reporter 2 | from diy_dashboard.widgets import * 3 | import pandas as pd 4 | 5 | reporter = Reporter() 6 | 7 | @reporter.display('report 0') 8 | def foo(): 9 | return pd.Series([p for p in range(10)]) 10 | 11 | @reporter.display('report 1') 12 | def bar(): 13 | return pd.Series([p for p in range(20)]) 14 | 15 | @reporter.display('report 2') 16 | def baz(): 17 | return pd.Series([p for p in range(30)]) 18 | 19 | @reporter.display('report 3', [Float('ye?')]) 20 | def yee(isYe): 21 | return '

{}

'.format(isYe) 22 | 23 | reporter.run(debug=True) 24 | -------------------------------------------------------------------------------- /examples/some_reports.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 '

dksjalf

' 19 | # register_report('too_few', figure_too_few, [TextWidget('meh')]) 20 | 21 | @reporter.view('basic widget test', 22 | [String('text', 'hello'), 23 | Float('float', 1.5), 24 | Int('integer', 2), 25 | Bool('tis true', True), 26 | SelectOne('pick a name', ['foo', 'bar', 'baz'], 'bar'), 27 | SelectSubset('pick names', ['foo', 'bar', 'baz'], ['foo', 'baz']), 28 | Color('my color', '#ff0000'), 29 | Slider('my default slider'), 30 | Slider('my param slider', 0, (-10, 10), 0)]) 31 | def widgets_test(a, b, c, d, e, f, g, h, i): 32 | return '

{} {} {} {} {} {} {} {:f} {:f}

'.format(a, b, c, d, e, f, g, h, i) 33 | 34 | @reporter.view('datetime widgets',[ 35 | DateRange('default date range'), 36 | DateRange('abs date range', '2017-01-02', '2018-02-03'), 37 | DateRange('last __ range', relativedelta(weeks=3)), 38 | DateRange('rel range', relativedelta(days=7), relativedelta(days=1)), 39 | DateRange('date to present', '2017-01-02', timedelta()), 40 | Date('default date'), 41 | Date('date relative', relativedelta(days=7)), 42 | Date('date absolute', '2017-01-11'), 43 | Time('default'), 44 | Time('param', time(5, 10, 15))] 45 | ) 46 | def date_range_test(a, b, c, d, e, f, g, h, i, j): 47 | return '

{} | {} | {} | {} | {} | {} | {} | {} | {} | {}

'.format(a, b, c, d, e, f, g, h, i, j) 48 | 49 | @reporter.view('unknown type') 50 | def unknown_figure(): 51 | dict = {'foo': 10, 'bar': 47} 52 | return dict 53 | 54 | @reporter.view('invalid HTML string') 55 | def invalid_string(): 56 | return 'this is not valid html' 57 | 58 | @reporter.view('valid HTML string') 59 | def valid_string(): 60 | return '

foo



bar

' 61 | 62 | @reporter.view('too_many', [Float('floating', 6.5)]) 63 | def figure_extra(a, b=6): 64 | return '

{} {}

'.format(a, b) 65 | 66 | @reporter.view('matplotlib figure') 67 | def figure_a(): 68 | plt.figure() 69 | plt.plot([3,1,4,1,20], 'ks-', mec='w', mew=5, ms=20) 70 | return plt.gcf() 71 | 72 | @reporter.view('pd.DataFrame test') 73 | def figure_c(): 74 | df = pd.DataFrame(np.random.randn(20, 20)) 75 | return df; 76 | 77 | @reporter.view('pd.Series test') 78 | def figure_d(): 79 | s = pd.Series([p for p in range(100)]) 80 | return s 81 | 82 | @reporter.view('bokeh figure') 83 | def figure_e(): 84 | x = [1, 2, 3, 4, 5] 85 | y = [6, 7, 2, 4, 5] 86 | plot = figure(title="bokeh example", x_axis_label='x', y_axis_label='y', sizing_mode='scale_width') 87 | plot.line(x, y, legend="Temp", line_width=2) 88 | return plot 89 | 90 | @reporter.view('report with a very longggggggggg name') 91 | def figure_long(): 92 | return 4 93 | 94 | for i in range(100): 95 | @reporter.view('rand report {}'.format(i)) 96 | def foo(): 97 | return 'a' 98 | 99 | reporter.run(debug=True) 100 | -------------------------------------------------------------------------------- /examples/widget_tests.py: -------------------------------------------------------------------------------- 1 | from diy_dashboard.reporter import Reporter 2 | from diy_dashboard.widgets import * 3 | 4 | reporter = Reporter() 5 | 6 | @reporter.display('String', [String('string', 'hello')]) 7 | @reporter.display('Float', [Float('float', 1.5, -5, 5, 0.1)]) 8 | @reporter.display('Int', [Int('int', 2, 0, 10)]) 9 | @reporter.display('Bool', [Bool('bool', True)]) 10 | @reporter.display('SelectOne', [SelectOne(['foo', 'bar', 'baz'], 'bar')]) 11 | @reporter.display('SelectSubset', [SelectSubset(['foo', 'bar', 'baz'], ['foo', 'baz'])]) 12 | @reporter.display('Color', [Color('color', '#ff0000')]) 13 | @reporter.display('Date', [Date('default date')]) 14 | @reporter.display('Time', [Time('default time')]) 15 | @reporter.display('DateRange', [DateRange('default date range')]) 16 | def test_a(a): 17 | return '

{}

'.format(a) 18 | 19 | @reporter.display('Slider', [Slider('slider', 0, (-10, 10), 2)]) 20 | def float_test(a): 21 | return '

{:f}

'.format(a) 22 | 23 | reporter.run(debug=True) 24 | -------------------------------------------------------------------------------- /note.txt: -------------------------------------------------------------------------------- 1 | To setup flask: 2 | export FLASK_APP=diy_dashboard (package name, that is) 3 | export FLASK_DEBUG=true 4 | run flask 5 | 6 | SASS: 7 | On mac, run 'sudo gem install sass' 8 | then: sass --watch stylesheet_dir 9 | 10 | Requirements: 11 | pip freeze > requirements.txt 12 | pip install -r requirements.txt 13 | 14 | To create Python 3 virtual env: 15 | python3 -m venv myenv 16 | 17 | To run the examples: 18 | python3 tester.py 19 | 20 | To install the lib and also work on it, run from root: 21 | pip install -e . 22 | 23 | See an article on how WSGI python servers work. 24 | Essentially you must point the WSGI server to a callable that takes a dictionary 25 | of the relevant request info and returns a response. In flask the Flask object is 26 | itself the callable. 27 | 28 | The docs dir must be called "docs" so that readthedocs can build it properly 29 | 30 | Upload instructions: 31 | https://packaging.python.org/tutorials/distributing-packages/ 32 | python3 setup.py sdist 33 | python3 setup.py bdist_wheel 34 | twine upload dist/* 35 | 36 | To pip install from test pypi: 37 | pip install --index-url https://test.pypi.org/simple/ diva 38 | Actually (installs reqs from actual pypi): 39 | pip install -i https://testpypi.python.org/pypi --extra-index-url https://pypi.python.org/pypi diva 40 | 41 | For testing: 42 | test must be named test_*.py or *_test.py 43 | just run "pytest", it will find all test files 44 | 45 | Tox: 46 | To make all the interpreters available, I installed them like this: https://gist.github.com/Bouke/11261620. Then I did "pyenv global ..." (note not local!). Then I ran tox from outside the virtualenv! This may be necessary b/c both pyenv and the virtualenv mess with PATH. 47 | Running "tox" will create a sdist, install it in each venv specified in tox.ini, and run the command (pytest) in each one. 48 | 49 | When building docs: to avoid using outdated version of the repo, delete the @... commit tag from requirements.txt. Must do this after every freeze, sadly. 50 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.10 2 | appnope==0.1.0 3 | attrs==19.1.0 4 | Babel==2.4.0 5 | bkcharts==0.2 6 | bleach==2.0.0 7 | bokeh==0.12.6 8 | certifi==2017.4.17 9 | chardet==3.0.4 10 | click==6.7 11 | cycler==0.10.0 12 | decorator==4.0.11 13 | -e git+https://github.com/mgriley/diva@0b236e85304064d173e31107bdfa60d195ba80e9#egg=diva 14 | docutils==0.14 15 | entrypoints==0.2.2 16 | Flask==0.12.2 17 | gunicorn==19.7.1 18 | html5lib==0.999999999 19 | idna==2.5 20 | imagesize==0.7.1 21 | ipykernel==4.6.1 22 | ipython==6.0.0 23 | ipython-genutils==0.2.0 24 | ipywidgets==6.0.0 25 | itsdangerous==0.24 26 | jedi==0.10.2 27 | Jinja2==2.9.6 28 | jsonschema==2.6.0 29 | jupyter==1.0.0 30 | jupyter-client==5.0.1 31 | jupyter-console==5.1.0 32 | jupyter-core==4.3.0 33 | MarkupSafe==1.0 34 | matplotlib==2.0.2 35 | mistune==0.7.4 36 | mpld3==0.3 37 | nbconvert==5.1.1 38 | nbformat==4.3.0 39 | notebook==5.0.0 40 | numpy==1.17.0 41 | packaging==19.1 42 | pandas==0.20.1 43 | pandocfilters==1.4.1 44 | pexpect==4.2.1 45 | pickleshare==0.7.4 46 | pkginfo==1.4.1 47 | pluggy==0.4.0 48 | prompt-toolkit==1.0.14 49 | ptyprocess==0.5.1 50 | py==1.4.34 51 | Pygments==2.2.0 52 | pyparsing==2.2.0 53 | pytest==3.2.1 54 | python-dateutil==2.6.0 55 | pytz==2017.2 56 | PyYAML==3.12 57 | pyzmq==16.0.2 58 | qtconsole==4.3.0 59 | requests==2.18.1 60 | requests-toolbelt==0.8.0 61 | simplegeneric==0.8.1 62 | six==1.10.0 63 | snowballstemmer==1.2.1 64 | Sphinx==2.1.2 65 | sphinxcontrib-applehelp==1.0.1 66 | sphinxcontrib-devhelp==1.0.1 67 | sphinxcontrib-htmlhelp==1.0.2 68 | sphinxcontrib-jsmath==1.0.1 69 | sphinxcontrib-qthelp==1.0.2 70 | sphinxcontrib-serializinghtml==1.1.3 71 | sphinxcontrib-websupport==1.0.1 72 | strict-rfc3339==0.7 73 | terminado==0.6 74 | testpath==0.3 75 | tornado==4.5.1 76 | tox==2.7.0 77 | tqdm==4.14.0 78 | traitlets==4.3.2 79 | twine==1.9.1 80 | urllib3==1.21.1 81 | virtualenv==15.1.0 82 | wcwidth==0.1.7 83 | webencodings==0.5.1 84 | Werkzeug==0.12.2 85 | widgetsnbextension==2.0.0 86 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | # see https://packaging.python.org/tutorials/distributing-packages/ 4 | 5 | setup( 6 | name='diva', 7 | version='0.1.5', 8 | description='Analytics dashboards made simple', 9 | long_description='create a simple web analytics dashboard', 10 | url='https://github.com/mgriley/diva', 11 | author='Matthew Riley', 12 | author_email='mgriley97@gmail.com', 13 | license='MIT', 14 | classifiers=[ 15 | # How mature is this project? Common values are 16 | # 3 - Alpha 17 | # 4 - Beta 18 | # 5 - Production/Stable 19 | 'Development Status :: 3 - Alpha', 20 | 21 | # Indicate who your project is intended for 22 | 'Intended Audience :: Developers', 23 | 'Topic :: Software Development :: Build Tools', 24 | 25 | # Pick your license as you wish (should match "license" above) 26 | 'License :: OSI Approved :: MIT License', 27 | 28 | # Specify the Python versions you support here. In particular, ensure 29 | # that you indicate whether you support Python 2, Python 3 or both. 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.4', 32 | 'Programming Language :: Python :: 3.5', 33 | 'Programming Language :: Python :: 3.6', 34 | ], 35 | keywords='dashboard analytics plotting', 36 | packages=['diva'], 37 | include_package_data=True, 38 | # minimal project dependencies here 39 | # NB: not the same as requirements.txt b/c requirements.txt includes 40 | # things like wtine, gunicorn, sphinx, etc. 41 | install_requires=[ 42 | 'flask', 43 | 'jsonschema', 44 | 'python-dateutil', 45 | 'matplotlib', 46 | 'mpld3', 47 | 'numpy', 48 | 'pandas', 49 | 'bokeh' 50 | ], 51 | # to install: pip install diva[dev] 52 | extras_require={ 53 | 'dev': [ 54 | 'pytest', 55 | 'tox', 56 | 'sphinx', 57 | 'gunicorn', 58 | 'wheel', 59 | 'twine' 60 | ] 61 | }, 62 | python_requires='>=3.4' 63 | 64 | # can create an automatic script, for use on the console, 65 | # use this! (flask run probably uses this) 66 | # entry_points={} 67 | ) 68 | -------------------------------------------------------------------------------- /static: -------------------------------------------------------------------------------- 1 | /* 2 | Errno::ENOENT: No such file or directory - static 3 | 4 | Backtrace: 5 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/lib/sass/plugin/compiler.rb:484:in `read' 6 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/lib/sass/plugin/compiler.rb:484:in `update_stylesheet' 7 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/lib/sass/plugin/compiler.rb:215:in `block in update_stylesheets' 8 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/lib/sass/plugin/compiler.rb:209:in `each' 9 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/lib/sass/plugin/compiler.rb:209:in `update_stylesheets' 10 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/lib/sass/plugin/compiler.rb:294:in `watch' 11 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/lib/sass/plugin.rb:109:in `method_missing' 12 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/lib/sass/exec/sass_scss.rb:360:in `watch_or_update' 13 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/lib/sass/exec/sass_scss.rb:51:in `process_result' 14 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/lib/sass/exec/base.rb:52:in `parse' 15 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/lib/sass/exec/base.rb:19:in `parse!' 16 | /Library/Ruby/Gems/2.0.0/gems/sass-3.4.24/bin/scss:13:in `' 17 | /usr/local/bin/scss:23:in `load' 18 | /usr/local/bin/scss:23:in `
' 19 | */ 20 | body:before { 21 | white-space: pre; 22 | font-family: monospace; 23 | content: "Errno::ENOENT: No such file or directory - static"; } 24 | -------------------------------------------------------------------------------- /test_upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rmtrash dist 3 | rmtrash build 4 | python3 setup.py sdist 5 | python3 setup.py bdist_wheel 6 | twine upload --repository-url https://test.pypi.org/legacy/ dist/* 7 | -------------------------------------------------------------------------------- /tests/dashboard_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from diva.dashboard import row_layout, get_grid_size 3 | 4 | def test_grid_size(): 5 | size = get_grid_size([[0, 0, 1, 1]]) 6 | assert size == (1, 1) 7 | size = get_grid_size([[0, 0, 1, 1], [1, 0, 1, 1]]) 8 | assert size == (2, 1) 9 | size = get_grid_size(row_layout(1, 2)) 10 | assert size == (2, 2) 11 | 12 | def test_row_layout(): 13 | layout = row_layout(1) 14 | assert layout == [[0, 0, 1, 1]] 15 | layout = row_layout(2) 16 | assert layout == [[0, 0, 1, 1], [1, 0, 1, 1]] 17 | layout = row_layout(1, 2) 18 | assert layout == [[0, 0, 2, 1], [0, 1, 1, 1], [1, 1, 1, 1]] 19 | layout = row_layout(2, 2) 20 | assert layout == [[0, 0, 2, 1], [2, 0, 2, 1], [0, 1, 2, 1], [2, 1, 2, 1]] 21 | -------------------------------------------------------------------------------- /tests/main_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from diva import Diva 3 | from diva.widgets import * 4 | from diva.converters import convert_to_html 5 | from jsonschema.exceptions import ValidationError 6 | from diva.exceptions import ValidationError as DivaValidationError, WidgetsError, WidgetInputError 7 | 8 | import matplotlib 9 | # use the Agg backend, which is non-interactivate (just for PNGs) 10 | # this way, a separate script isn't started by matplotlib 11 | matplotlib.use('Agg') 12 | import matplotlib.pyplot as plt 13 | import numpy as np 14 | import pandas as pd 15 | from bokeh.plotting import figure 16 | 17 | @pytest.fixture() 18 | def minapp(): 19 | app = Diva() 20 | @app.view('foo') 21 | def foo(): 22 | return 0 23 | @app.view('bar', [Int('my int')]) 24 | def bar(i): 25 | return i 26 | return app 27 | 28 | class TestWidgetArgs(): 29 | def test_correct_num_args(self): 30 | app = Diva() 31 | @app.view('hi', [Float('a'), Int('b')]) 32 | def sample(a, b): 33 | return a, b 34 | 35 | form_data = ["1.0", "2"] 36 | app.update_report(app.reports[0], form_data) 37 | 38 | @pytest.mark.skip(reason='no longer checks this') 39 | def test_too_many_widgets(self): 40 | app = Diva() 41 | with pytest.raises(WidgetsError): 42 | @app.view('hi', [Float('a')]) 43 | def sample(): 44 | return 0 45 | 46 | def test_too_few_widgets(self): 47 | app = Diva() 48 | @app.view('hi') 49 | def sample(a): 50 | return a 51 | form_data = [] 52 | with pytest.raises(WidgetInputError): 53 | app.update_report(app.reports[0], form_data) 54 | 55 | def test_defaults(self): 56 | app = Diva() 57 | @app.view('hi', [Int('a')]) 58 | def sample(a, b=2): 59 | return b 60 | form_data = ["1"] 61 | app.update_report(app.reports[0], form_data) 62 | 63 | def test_varargs(self): 64 | app = Diva() 65 | @app.view('hi', [Int('a')]) 66 | def sample(*varargs): 67 | return varargs 68 | app.update_report(app.reports[0], ["2"]) 69 | 70 | @pytest.mark.skip(reason='widgets will always result in kwargs={}') 71 | def test_kwargs(self): 72 | app = Diva() 73 | with pytest.raises(WidgetsError): 74 | @app.view('hi', [Int('b')]) 75 | def sample(**kwargs): 76 | return kwargs 77 | 78 | class TestReporter(): 79 | def test_index_bounds(self, minapp): 80 | minapp.validate_request({'reportIndex': 0, 'widgetValues': []}) 81 | with pytest.raises(DivaValidationError): 82 | minapp.validate_request({'reportIndex': 2, 'widgetValues': []}) 83 | with pytest.raises(DivaValidationError): 84 | minapp.validate_request({'reportIndex': -1, 'widgetValues': []}) 85 | 86 | def test_missing_data(self, minapp): 87 | with pytest.raises(DivaValidationError): 88 | minapp.validate_request({'widgetValues': [0]}) 89 | 90 | def missing_widget_value(self): 91 | with pytest.raises(DivaValidationError): 92 | minapp.validate_widget_values(minapp.reports[0], []) 93 | 94 | class TestWidgets(): 95 | def test_string(self): 96 | w = String('a') 97 | w.validate_input('hi') 98 | with pytest.raises(ValidationError): 99 | w.validate_input(1) 100 | assert w.parseForm('hi') == 'hi' 101 | 102 | def test_float(self): 103 | w = Float('f') 104 | w.validate_input('1.5') 105 | with pytest.raises(ValueError): 106 | w.validate_input('a') 107 | assert w.parseForm('2.0') == 2 108 | 109 | def test_float_bounds(self): 110 | w = Float('f', 0, 0, 1) 111 | with pytest.raises(ValueError): 112 | w.validate_input("-1.1") 113 | with pytest.raises(ValueError): 114 | w.validate_input("1.1") 115 | 116 | def test_bool(self): 117 | w = Bool('b') 118 | w.validate_input(True) 119 | with pytest.raises(ValidationError): 120 | w.validate_input('0') 121 | assert w.parseForm(True) == True 122 | 123 | def test_selectone(self): 124 | w = SelectOne('s', ['a', 'b']) 125 | w.validate_input('a') 126 | with pytest.raises(ValidationError): 127 | w.validate_input('c') 128 | assert w.parseForm('a') == 'a' 129 | 130 | def test_selectsubset(self): 131 | w = SelectSubset('s', ['a', 'b']) 132 | w.validate_input([]) 133 | w.validate_input(['a']) 134 | w.validate_input(['a', 'b']) 135 | with pytest.raises(DivaValidationError): 136 | w.validate_input(['a', 'b', 'c']) 137 | with pytest.raises(DivaValidationError): 138 | w.validate_input(['a', 'a']) 139 | assert w.parseForm(['a', 'b']) == ['a', 'b'] 140 | 141 | def test_date(self): 142 | w = Date('d') 143 | w.validate_input('2017-08-16') 144 | with pytest.raises(ValueError): 145 | w.validate_input('2017/08/16') 146 | 147 | def test_daterange(self): 148 | w = DateRange('d') 149 | data = ['2017-08-16', '2017-08-17'] 150 | w.validate_input(data) 151 | with pytest.raises(ValueError): 152 | w.validate_input(['foo', 'bar']) 153 | with pytest.raises(ValidationError): 154 | w.validate_input(['2017-08-16']) 155 | 156 | def test_time(self): 157 | w = Time('d') 158 | w.validate_input('23:01') 159 | with pytest.raises(ValueError): 160 | w.validate_input('01:10AM') 161 | 162 | class TestConverters(): 163 | def test_matplot(self): 164 | plt.figure() 165 | plt.plot([3,1,4,1,20], 'ks-', mec='w', mew=5, ms=20) 166 | convert_to_html(plt.gcf()) 167 | 168 | def test_pandas(self): 169 | convert_to_html(pd.DataFrame(np.random.randn(20, 20))) 170 | 171 | def test_bokeh(self): 172 | x = [1, 2, 3, 4, 5] 173 | y = [6, 7, 2, 4, 5] 174 | plot = figure(title="bokeh example", x_axis_label='x', y_axis_label='y') 175 | plot.line(x, y, legend="Temp", line_width=2) 176 | convert_to_html(plot) 177 | 178 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py34, py35, py36 8 | 9 | [testenv] 10 | commands = 11 | pytest 12 | deps = 13 | pytest 14 | -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rmtrash dist 3 | rmtrash build 4 | python3 setup.py sdist 5 | python3 setup.py bdist_wheel 6 | twine upload dist/* 7 | --------------------------------------------------------------------------------