diff --git a/.circleci/config.yml b/.circleci/config.yml
index 951f94e2..0997114c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -43,8 +43,8 @@ defaults: &defaults
- run:
name: Run JS Tests & Coverage
command: |
- yarn run test-with-coverage
- bash <(curl -s https://codecov.io/bash) -c -F javascript
+ yarn run test-with-coverage --maxWorkers=50%
+ bash <(curl -s https://codecov.io/bash) -c -F javascript -f ./JS_coverage/lcov.info
yarn run report-duplicate-code
cp -r ./JS_coverage /tmp/circleci-test-results
- run:
diff --git a/CHANGES.md b/CHANGES.md
index 6e8defc1..808a0824 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,79 +1,85 @@
-Changelog
-
-### 1.0.0 (2019-09-06)
+## Changelog
+
+### 1.5.0 (2019-12-02)
+ * ipython integration
+ * ipython output cell adjustment
+ * column-wise menu support
+ * browser window popups for: Correlations, Coverage, Describe, Histogram & Instances
+
+### 1.4.1 (2019-11-20)
- * Initial public release
+ * [#32](https://github.com/man-group/dtale/issues/32): unpin jsonschema by moving flasgger to `extras_require`
-### 1.1.0 (2019-10-08)
+### 1.4.0 (2019-11-19)
- * IE support
- * **Describe** & **About** popups
- * Custom CLI support
+ * Correlations Pearson Matrix filters
+ * "name" display in title tab
+ * "Heat Map" toggle
+ * dropped unused "Flask-Caching" requirement
-### 1.1.1 (2019-10-23)
+### 1.3.7 (2019-11-12)
- * [#13](https://github.com/man-group/dtale/issues/13): fix for auto-detection of column widths for strings and floats
+ * Bug fixes for:
+ * [#28](https://github.com/man-group/dtale/issues/28): "Instances" menu option will now be displayed by default
+ * [#29](https://github.com/man-group/dtale/issues/29): add hints to how users can navigate the correlations popup
+ * add "unicode" as a string classification for column width calculation
-### 1.2.0 (2019-10-24)
+### 1.3.6 (2019-11-08)
- * [#20](https://github.com/man-group/dtale/issues/13): fix for data being overriden with each new instance
- * [#21](https://github.com/man-group/dtale/issues/13): fix for displaying timestamps if they exist
- * calling `show()` now returns an object which can alter the state of a process
- * accessing/altering state through the `data` property
- * shutting down a process using the `kill()` function
+ * Bug fixes for:
+ * choose between `pandas.corr` & `numpy.corrcoef` depending on presence of NaNs
+ * hide timeseries correlations when date columns only contain one day
-### 1.3.0 (2019-10-29)
-
- * `webbrowser` integration (the ability to automatically open a webbrowser upon calling `dtale.show()`)
- * flag for hiding the "Shutdown" button for long-running demos
- * "Instances" navigator popup for viewing all activate D-Tale instances for the current python process
+### 1.3.5 (2019-11-07)
-### 1.3.1 (2019-10-29)
-
- * fix for incompatible str types when directly altering state of data in running D-Tale instance
+ * Bug fixes for:
+ * duplicate loading of histogram data
+ * string serialization failing when mixing `future.str` & `str` in scatter function
-### 1.3.2 (2019-11-05)
+### 1.3.4 (2019-11-07)
- * Bug fixes for:
- * display of histogram column information
- * reload of hidden "processes" input when loading instances data
- * correlations json failures on string conversion
+ * updated correlation calculation to use `numpy.corrcoef` for performance purposes
+ * github rebranding from manahl -> man-group
### 1.3.3 (2019-11-05)
* hotfix for failing test under certain versions of `future` package
-### 1.3.4 (2019-11-07)
+### 1.3.2 (2019-11-05)
- * updated correlation calculation to use `numpy.corrcoef` for performance purposes
- * github rebranding from manahl -> man-group
+ * Bug fixes for:
+ * display of histogram column information
+ * reload of hidden "processes" input when loading instances data
+ * correlations json failures on string conversion
-### 1.3.5 (2019-11-07)
+### 1.3.1 (2019-10-29)
+
+ * fix for incompatible str types when directly altering state of data in running D-Tale instance
- * Bug fixes for:
- * duplicate loading of histogram data
- * string serialization failing when mixing `future.str` & `str` in scatter function
+### 1.3.0 (2019-10-29)
+
+ * `webbrowser` integration (the ability to automatically open a webbrowser upon calling `dtale.show()`)
+ * flag for hiding the "Shutdown" button for long-running demos
+ * "Instances" navigator popup for viewing all activate D-Tale instances for the current python process
-### 1.3.6 (2019-11-08)
+### 1.2.0 (2019-10-24)
- * Bug fixes for:
- * choose between `pandas.corr` & `numpy.corrcoef` depending on presence of NaNs
- * hide timeseries correlations when date columns only contain one day
+ * [#20](https://github.com/man-group/dtale/issues/13): fix for data being overriden with each new instance
+ * [#21](https://github.com/man-group/dtale/issues/13): fix for displaying timestamps if they exist
+ * calling `show()` now returns an object which can alter the state of a process
+ * accessing/altering state through the `data` property
+ * shutting down a process using the `kill()` function
-### 1.3.7 (2019-11-12)
+### 1.1.1 (2019-10-23)
- * Bug fixes for:
- * [#28](https://github.com/man-group/dtale/issues/28): "Instances" menu option will now be displayed by default
- * [#29](https://github.com/man-group/dtale/issues/29): add hints to how users can navigate the correlations popup
- * add "unicode" as a string classification for column width calculation
+ * [#13](https://github.com/man-group/dtale/issues/13): fix for auto-detection of column widths for strings and floats
-### 1.4.0 (2019-11-19)
+### 1.1.0 (2019-10-08)
- * Correlations Pearson Matrix filters
- * "name" display in title tab
- * "Heat Map" toggle
- * dropped unused "Flask-Caching" requirement
+ * IE support
+ * **Describe** & **About** popups
+ * Custom CLI support
-### 1.4.1 (2019-11-20)
+### 1.0.0 (2019-09-06)
- * [#32](https://github.com/man-group/dtale/issues/32): unpin jsonschema by moving flasgger to `extras_require`
\ No newline at end of file
+ * Initial public release
\ No newline at end of file
diff --git a/README.md b/README.md
index 20bb602c..9fde3b54 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,40 @@
[](https://codecov.io/gh/man-group/dtale)
[](https://pepy.tech/project/dtale)
+## What is it?
+
+D-Tale was born out a conversion from SAS to Python. What was originally a perl script wrapper on top of SAS's `insight` function is now a lightweight web client on top of Pandas dat structures. D-Tale is the combination of a Flask back-end and a React front-end to bring you an easy way to view & analyze Pandas data structures. Currently this tool supports such Pandas objects as DataFrame, Series, MultiIndex, DatetimeIndex & RangeIndex. It integrates seamlessly with ipython notebooks & python/ipython terminals.
+
+## Contents
+
+- [Getting Started](#getting-started)
+ - [Python Terminal](#python-terminal)
+ - [Jupyter Notebook](#jupyter-notebook)
+ - [Command-line](#command-line)
+- [UI](#ui)
+ - [Dimensions/Main Menu](#dimensionsmain-menu)
+ - [Selecting/Deselecting Columns](#selectingdeselecting-columns)
+ - [Menu functions w/ no columns selected](#menu-functions-w-no-columns-selected)
+ - [Describe](#describe), [Coverage](#coverage), [Correlations](#correlations), [Heat Map](#heat-map), [Instances](#instances), [About](#about), [Resize](#resize), [Iframe-Mode/Full-Mode](#iframe-modefull-mode), [Shutdown](#shutdown)
+ - [Menu functions w/ column(s) selected](#menu-functions-w-columns-selected)
+ - [Move To Front](#move-to-front), [Lock](#lock), [Unlock](#unlock), [Sorting](#sorting), [Formats](#formats), [Histogram](#histogram)
+ - [Menu functions within a Jupyter Notebook](#menu-functions-within-a-jupyter-notebook)
+- [For Developers](#for-developers)
+ - [Cloning](#cloning)
+ - [Running Tests](#running-tests)
+ - [Linting](#linting)
+ - [Formatting JS](#formatting-js)
+ - [Docker Development](#docker-development)
+- [Documentation](#documentation)
+- [Requirements](#requirements)
+- [Acknowledgements](#acknowledgements)
+- [License](#license)
+
## Getting Started
-
+|PyCharm|jupyter|
+|:------:|:------:|
+|||
Setup/Activate your environment and install the egg
@@ -36,6 +67,57 @@ $ pip install --upgrade dtale
```
Now you will have to ability to use D-Tale from the command-line or within a python-enabled terminal
+### Python Terminal
+This comes courtesy of PyCharm
+
+Feel free to invoke `python` or `ipython` directly and use the commands in the screenshot above and it should work
+
+#### Additional functions available programatically
+```python
+import dtale
+import pandas as pd
+
+df = pd.DataFrame([dict(a=1,b=2,c=3)])
+
+# Assigning a reference to a running D-Tale process
+d = dtale.show(df)
+
+# Accessing data associated with D-Tale process
+tmp = d.data.copy()
+tmp['d'] = 4
+
+# Altering data associated with D-Tale process
+# FYI: this will clear any front-end settings you have at the time for this process (filter, sorts, formatting)
+d.data = tmp
+
+# Shutting down D-Tale process
+d.kill()
+
+# using Python's `webbrowser` package it will try and open your server's default browser to this process
+d.open_browser()
+
+# There is also some helpful metadata about the process
+d._port # the process's port
+d._url # the url to access the process
+
+```
+
+### Jupyter Notebook
+Within any jupyter (ipython) notebook executing a cell like this will display a small instance of D-Tale in the output cell. Here are some examples:
+
+|`dtale.show`|assignment|instance|
+|:------:|:------:|:------:|
+||||
+
+If you are running ipython<=5.0 then you also have the ability to adjust the size of your output cell for the most recent instance displayed:
+
+
+
+One thing of note is that alot of the modal popups you see in the standard browser version will now open separate browser windows for spacial convienence:
+
+|Column Menus|Correlations|Describe|Histogram|Coverage|Instances|
+|:------:|:------:|:------:|:------:|:------:|:------:|
+|||||||
### Command-line
Base CLI options (run `dtale --help` to see all options available)
@@ -121,52 +203,18 @@ Here's how you would use this loader:
DTALE_CLI_LOADERS=./path_to_loaders bash -c 'dtale --testdata-rows 10 --testdata-columns 5'
```
-
-### Python Terminal
-This comes courtesy of PyCharm
-
-Feel free to invoke `python` or `ipython` directly and use the commands in the screenshot above and it should work
-#####Additional functions available programatically
-```python
-import dtale
-import pandas as pd
-
-df = pd.DataFrame([dict(a=1,b=2,c=3)])
-
-# Assigning a reference to a running D-Tale process
-d = dtale.show(df)
-
-# Accessing data associated with D-Tale process
-tmp = d.data.copy()
-tmp['d'] = 4
-
-# Altering data associated with D-Tale process
-# FYI: this will clear any front-end settings you have at the time for this process (filter, sorts, formatting)
-d.data = tmp
-
-# Shutting down D-Tale process
-d.kill()
-
-# using Python's `webbrowser` package it will try and open your server's default browser to this process
-d.open_browser()
-
-# There is also some helpful metadata about the process
-d._port # the process's port
-d._url # the url to access the process
-
-```
-
## UI
Once you have kicked off your D-Tale session please copy & paste the link on the last line of output in your browser

-The information in the upper right-hand corner is similar to saslook 
+### Dimensions/Main Menu
+The information in the upper right-hand corner gives grid dimensions 
- lower-left => row count
- upper-right => column count
- clicking the triangle displays the menu of standard functions (click outside menu to close it)

-Selecting/Deselecting Columns
+### Selecting/Deselecting Columns
- to select a column, simply click on the column header (to deselect, click the column header again)
- You'll notice that the columns you've selected will display in the top of your browser

@@ -175,7 +223,8 @@ Selecting/Deselecting Columns

-- **Describe**: view all the columns & their data types as well as individual details of each column
+#### Describe
+View all the columns & their data types as well as individual details of each column

@@ -186,13 +235,15 @@ Selecting/Deselecting Columns
|int||Anything with standard numeric classifications (min, max, 25%, 50%, 75%) will have a nice boxplot with the mean (if it exists) displayed as an outlier if you look closely.|
|float|||
-- **Filter**: apply a simple pandas `query` to your data (link to pandas documentation included in popup)
+#### Filter
+Apply a simple pandas `query` to your data (link to pandas documentation included in popup)
|Editing|Result|
|--------|:------:|
|||
-- **Coverage**: check for coverage gaps on column(s) by way of other column(s) as group(s)
+#### Coverage
+Check for coverage gaps on column(s) by way of other column(s) as group(s)
- Select column(s) in "Group(s)" & "Col(s)"
- date-type columns you can also specify a frequency of D, W, M, Q, Y
- Select multiple values in "Cols(s)" and/or "Groups(s)" by holdings the SHIFT key as you click
@@ -203,7 +254,8 @@ Selecting/Deselecting Columns
|-----|:-------------:|
|||
-- **Correlations**: shows a pearson correlation matrix of all numeric columns against all other numeric columns
+#### Correlations
+Shows a pearson correlation matrix of all numeric columns against all other numeric columns
- By deafult, it will show a grid of pearson correlations (filtering available by using drop-down see 2nd table of screenshots)
- If you have a date-type column, you can click an individual cell and see a timeseries of pearson correlations for that column combination
- Currently if you have multiple date-type columns you will have the ability to toggle between them by way of a drop-down
@@ -217,7 +269,8 @@ Selecting/Deselecting Columns
|------|----------|-------|
||||
-- **Heat Map**: this will hide any non-float columns (with the exception of the index on the right) and apply a color to the background of each cell
+#### Heat Map
+This will hide any non-float columns (with the exception of the index on the right) and apply a color to the background of each cell
- Each float is renormalized to be a value between 0 and 1.0
- Each renormalized value is passed to a color scale of red(0) - yellow(0.5) - green(1.0)

@@ -225,7 +278,8 @@ Selecting/Deselecting Columns
Turn off Heat Map by clicking menu option again

-- **Instances**: this will give you information about other D-Tale instances are running under your current Python process.
+#### Instances
+This will give you information about other D-Tale instances are running under your current Python process.
For example, if you ran the following script:
```python
@@ -258,27 +312,44 @@ Here is an example of clicking the "Preview" button:

-- **About**: This will give you information about what version of D-Tale you're running as well as if its out of date to whats on PyPi.
+#### About
+This will give you information about what version of D-Tale you're running as well as if its out of date to whats on PyPi.
|Up To Date|Out Of Date|
|--------|:------:|
|||
-- **Resize**: mostly a fail-safe in the event that your columns are no longer lining up. Click this and should fix that
-- **Shutdown**: pretty self-explanatory, kills your D-Tale session (there is also an auto-kill process that will kill your D-Tale after an hour of inactivity)
+#### Resize
+Mostly a fail-safe in the event that your columns are no longer lining up. Click this and should fix that
+
+#### Iframe-mode/Full-mode
+This is only available if you are not viewing D-Tale from an jupyter notebook output cell. This will toggle between the two types of functionality:
+- **Full-mode**: column selection, column-specific options in in the main menu & all tools are displayed in modal windows
+- **Iframe-mode**: no column selection, column-specific menus on head click & some tools will now open separate browser windows (Correlations, Coverage, Describe, Histogram & Instances)
-### Menu functions w/ one column is selected
+#### Shutdown
+Pretty self-explanatory, kills your D-Tale session (there is also an auto-kill process that will kill your D-Tale after an hour of inactivity)
+
+### Menu functions w/ column(s) selected

-- **Move To Front**: moves your column to the front of the "unlocked" columns
-- **Lock**: adds your column to "locked" columns
+#### Move To Front
+Moves your column to the front of the "unlocked" columns
+
+#### Lock
+Adds your column to "locked" columns
- "locked" means that if you scroll horizontally these columns will stay pinned to the right-hand side
- this is handy when you want to keep track of which date or security_id you're looking at
- by default, any index columns on the data passed to D-Tale will be locked
-- **Unlock**: removed column from "locked" columns
-- **Sorting** (Ascending/Descending/Clear): applies/removes sorting to the column selected
- - Important: as you add sorts they sort added will be added to the end of the multi-sort. For example:
+
+#### Unlock
+Removed column from "locked" columns
+
+#### Sorting
+Applies/removes sorting (Ascending/Descending/Clear) to the column selected
+
+*Important*: as you add sorts they sort added will be added to the end of the multi-sort. For example:
| Action | Sort |
| ------------- |:--------------:|
@@ -294,7 +365,8 @@ Here is an example of clicking the "Preview" button:
| clear sort | |
| sort asc | a (asc), b(asc) |
-- **Formats**: apply simple formats to numeric values in your grid
+#### Formats
+Apply simple formats to numeric values in your grid
|Editing|Result|
|--------|:------:|
@@ -311,13 +383,20 @@ Here's a grid of all the formats available with -123456.789 as input:
| BPS | -1234567890BPS |
| Red Negatives | -123457|
-- **Histogram**: display histograms in bins of 5, 10, 20 or 50 for any numeric column
+#### Histogram
+Display histograms in bins of 5, 10, 20 or 50 for any numeric column

+### Menu functions within a Jupyter Notebook
+These are the same functions as the menu listed earlier, but there is no more column selection (instead theres menus for each column). Also the following buttons will no longer open modals, but separate browser windows: Correlations, Describe, Coverage & Instances (see images from [Jupyter Notebook](#jupyter-notebook))
+
+There are also menus associated with each column header which can be trigger by clicking on a column header. The functions that are contained within each are: Sorting, Move To Front, Lock/Unlock, Histogram, Describe, Formats (see image from [Jupyter Notebook](#jupyter-notebook))
+ - Histogram & Describe open separate browser windows
+
## For Developers
-### Getting Started
+### Cloning
Clone the code (git clone ssh://git@github.com:manahl/dtale.git), then start the backend server:
@@ -378,7 +457,7 @@ You can auto-format code as follows:
$ npm run format
```
-### Docker development
+### Docker Development
You can build python 27-3 & run D-Tale as follows:
```bash
@@ -439,6 +518,9 @@ Contributors:
* [Dominik Christ](https://github.com/DominikMChrist)
* [Chris Boddy](https://github.com/cboddy)
* [Jason Holden](https://github.com/jasonkholden)
+ * [Tom Taylor](https://github.com/TomTaylorLondon)
+ * [Vincent Riemer](https://github.com/vincentriemer)
+ * Mike Kelly
* [Youssef Habchi](http://youssef-habchi.com/) - title font
* ... and many others ...
diff --git a/docker/2_7/Dockerfile b/docker/2_7/Dockerfile
index 0ddb4782..e9e63673 100644
--- a/docker/2_7/Dockerfile
+++ b/docker/2_7/Dockerfile
@@ -44,4 +44,4 @@ WORKDIR /app
RUN set -eux \
; . /root/.bashrc \
- ; easy_install dtale-1.4.1-py2.7.egg
+ ; easy_install dtale-1.5.0-py2.7.egg
diff --git a/docker/3_6/Dockerfile b/docker/3_6/Dockerfile
index 5b1adffa..8cd33ec9 100644
--- a/docker/3_6/Dockerfile
+++ b/docker/3_6/Dockerfile
@@ -44,4 +44,4 @@ WORKDIR /app
RUN set -eux \
; . /root/.bashrc \
- ; easy_install dtale-1.4.1-py3.7.egg
+ ; easy_install dtale-1.5.0-py3.7.egg
diff --git a/docs/images/Column_menu.png b/docs/images/Column_menu.png
new file mode 100644
index 00000000..295b1733
Binary files /dev/null and b/docs/images/Column_menu.png differ
diff --git a/docs/images/Heatmap_toggle.png b/docs/images/Heatmap_toggle.png
index 9a9aea07..ad9aa4b3 100644
Binary files a/docs/images/Heatmap_toggle.png and b/docs/images/Heatmap_toggle.png differ
diff --git a/docs/images/Info_menu.png b/docs/images/Info_menu.png
index bd83d92d..d3d051ff 100644
Binary files a/docs/images/Info_menu.png and b/docs/images/Info_menu.png differ
diff --git a/docs/images/Info_menu_small.png b/docs/images/Info_menu_small.png
index 2798e446..e38ce40b 100644
Binary files a/docs/images/Info_menu_small.png and b/docs/images/Info_menu_small.png differ
diff --git a/docs/images/Menu_one_col.png b/docs/images/Menu_one_col.png
index 59b51aa1..0dece2b6 100644
Binary files a/docs/images/Menu_one_col.png and b/docs/images/Menu_one_col.png differ
diff --git a/docs/images/blog/dtale_ipython.gif b/docs/images/blog/dtale_ipython.gif
new file mode 100644
index 00000000..81929b35
Binary files /dev/null and b/docs/images/blog/dtale_ipython.gif differ
diff --git a/docs/images/correlations_popup.png b/docs/images/correlations_popup.png
new file mode 100644
index 00000000..9f2408e2
Binary files /dev/null and b/docs/images/correlations_popup.png differ
diff --git a/docs/images/coverage_popup.png b/docs/images/coverage_popup.png
new file mode 100644
index 00000000..60d45253
Binary files /dev/null and b/docs/images/coverage_popup.png differ
diff --git a/docs/images/describe_popup.png b/docs/images/describe_popup.png
new file mode 100644
index 00000000..61b47eb0
Binary files /dev/null and b/docs/images/describe_popup.png differ
diff --git a/docs/images/histogram_popup.png b/docs/images/histogram_popup.png
new file mode 100644
index 00000000..a0e712c6
Binary files /dev/null and b/docs/images/histogram_popup.png differ
diff --git a/docs/images/instances_popup.png b/docs/images/instances_popup.png
new file mode 100644
index 00000000..b64a40a4
Binary files /dev/null and b/docs/images/instances_popup.png differ
diff --git a/docs/images/ipython1.png b/docs/images/ipython1.png
new file mode 100644
index 00000000..92d77868
Binary files /dev/null and b/docs/images/ipython1.png differ
diff --git a/docs/images/ipython2.png b/docs/images/ipython2.png
new file mode 100644
index 00000000..7e88cd3a
Binary files /dev/null and b/docs/images/ipython2.png differ
diff --git a/docs/images/ipython3.png b/docs/images/ipython3.png
new file mode 100644
index 00000000..6d48cd06
Binary files /dev/null and b/docs/images/ipython3.png differ
diff --git a/docs/images/ipython_adjust.png b/docs/images/ipython_adjust.png
new file mode 100644
index 00000000..0a4e2a11
Binary files /dev/null and b/docs/images/ipython_adjust.png differ
diff --git a/docs/source/conf.py b/docs/source/conf.py
index b600e6f8..f705a53f 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -31,7 +31,7 @@
extensions = [
'sphinx.ext.autodoc',
# 'sphinx.ext.doctest',
- # 'sphinx.ext.intersphinx',
+ 'sphinx.ext.intersphinx',
# 'sphinx.ext.todo',
'sphinx.ext.coverage',
# 'sphinx.ext.mathjax',
@@ -63,9 +63,9 @@
# built documents.
#
# The short X.Y version.
-version = u'1.4.1'
+version = u'1.5.0'
# The full version, including alpha/beta/rc tags.
-release = u'1.4.1'
+release = u'1.5.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -363,7 +363,13 @@
# Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'https://docs.python.org/': None}
+intersphinx_mapping = {
+ 'python': ('https://docs.python.org/', None),
+ 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None),
+ 'ipython': ('https://ipython.readthedocs.io/en/stable/', None),
+ 'numpy': ('http://docs.scipy.org/doc/numpy/', None),
+ 'flask': ('https://flask-doc.readthedocs.io/en/latest/', None),
+}
import matplotlib
matplotlib.use('Agg')
diff --git a/dtale/app.py b/dtale/app.py
index 6c4d1fb8..4c7ac726 100644
--- a/dtale/app.py
+++ b/dtale/app.py
@@ -44,8 +44,8 @@ class DtaleFlaskTesting(FlaskClient):
we won't have SETTING keys colliding with other tests since the default
for every test would be 80.
- :param args: Optional arguments to be passed to :class:`flask.FlaskClient`
- :param kwargs: Optional keyword arguments to be passed to :class:`flask.FlaskClient`
+ :param args: Optional arguments to be passed to :class:`flask:flask.FlaskClient`
+ :param kwargs: Optional keyword arguments to be passed to :class:`flask:flask.FlaskClient`
"""
def __init__(self, *args, **kwargs):
@@ -58,8 +58,8 @@ def __init__(self, *args, **kwargs):
def get(self, *args, **kwargs):
"""
- :param args: Optional arguments to be passed to :meth:`flask.FlaskClient.get`
- :param kwargs: Optional keyword arguments to be passed to :meth:`flask.FlaskClient.get`
+ :param args: Optional arguments to be passed to :meth:`flask:flask.FlaskClient.get`
+ :param kwargs: Optional keyword arguments to be passed to :meth:`flask:flask.FlaskClient.get`
"""
return super(DtaleFlaskTesting, self).get(
base_url='http://{host}:{port}'.format(host=self.host, port=self.port), *args, **kwargs
@@ -74,8 +74,8 @@ class DtaleFlask(Flask):
:param import_name: the name of the application package
:param reaper_on: whether to run auto-reaper subprocess
:type reaper_on: bool
- :param args: Optional arguments to be passed to :class:`flask.Flask`
- :param kwargs: Optional keyword arguments to be passed to :class:`flask.Flask`
+ :param args: Optional arguments to be passed to :class:`flask:flask.Flask`
+ :param kwargs: Optional keyword arguments to be passed to :class:`flask:flask.Flask`
"""
def __init__(self, import_name, reaper_on=True, *args, **kwargs):
@@ -92,8 +92,8 @@ def __init__(self, import_name, reaper_on=True, *args, **kwargs):
def run(self, *args, **kwargs):
"""
- :param args: Optional arguments to be passed to :meth:`flask.run`
- :param kwargs: Optional keyword arguments to be passed to :meth:`flask.run`
+ :param args: Optional arguments to be passed to :meth:`flask:flask.run`
+ :param kwargs: Optional keyword arguments to be passed to :meth:`flask:flask.run`
"""
self.port = str(kwargs.get('port'))
self.shutdown_url = build_shutdown_url(self.port)
@@ -111,8 +111,8 @@ def test_client(self, reaper_on=False, port=None, *args, **kwargs):
:type reaper_on: bool
:param port: port number of flask application
:type port: int
- :param args: Optional arguments to be passed to :meth:`flask.Flask.test_client`
- :param kwargs: Optional keyword arguments to be passed to :meth:`flask.Flask.test_client`
+ :param args: Optional arguments to be passed to :meth:`flask:flask.Flask.test_client`
+ :param kwargs: Optional keyword arguments to be passed to :meth:`flask:flask.Flask.test_client`
:return: Flask's test client
:rtype: :class:`dtale.app.DtaleFlaskTesting`
"""
@@ -273,7 +273,7 @@ def shutdown():
@app.before_request
def before_request():
"""
- Logic executed before each flask request
+ Logic executed before each :attr:`flask:flask.request`
:return: text/html with server shutdown message
"""
@@ -317,6 +317,16 @@ def version_info():
_, version = retrieve_meta_info_and_version('dtale')
return str(version)
+ @app.route('/health')
+ @swag_from('swagger/dtale/health.yml')
+ def health_check():
+ """
+ Flask route for checking if D-Tale is up and running
+
+ :return: text/html 'ok'
+ """
+ return 'ok'
+
return app
@@ -333,12 +343,13 @@ def find_free_port():
def show(data=None, host='0.0.0.0', port=None, name=None, debug=False, subprocess=True, data_loader=None,
- reaper_on=True, open_browser=False, **kwargs):
+ reaper_on=True, open_browser=False, notebook=False, **kwargs):
"""
Entry point for kicking off D-Tale Flask process from python process
:param data: data which D-Tale will display
- :type data: Union[:class:`pandas.DataFrame`, :class:`pandas.Series`], optional
+ :type data: :class:`pandas:pandas.DataFrame` or :class:`pandas:pandas.Series`
+ or :class:`pandas:pandas.DatetimeIndex` or :class:`pandas:pandas.MultiIndex`, optional
:param host: hostname of D-Tale, defaults to 0.0.0.0
:type host: str, optional
:param port: port number of D-Tale process, defaults to any open port on server
@@ -353,9 +364,11 @@ def show(data=None, host='0.0.0.0', port=None, name=None, debug=False, subproces
:type data_loader: func, optional
:param reaper_on: turn on subprocess which will terminate D-Tale after 1 hour of inactivity
:type reaper_on: bool, optional
- :param open_browser: if true, this will try using the webbrowser package to automatically open you default
- browser to your D-Tale process
+ :param open_browser: if true, this will try using the :mod:`python:webbrowser` package to automatically open
+ your default browser to your D-Tale process
:type open_browser: bool, optional
+ :param notebook: if true, this will try displaying an :class:`ipython:IPython.display.IFrame`
+ :type notebook: bool, optional
:Example:
@@ -372,7 +385,7 @@ def show(data=None, host='0.0.0.0', port=None, name=None, debug=False, subproces
setup_logging(logfile, log_level or 'info', verbose)
selected_port = int(port or find_free_port())
- data_hook = startup(data=data, data_loader=data_loader, port=selected_port, name=name)
+ instance = startup(data=data, data_loader=data_loader, port=selected_port, name=name)
def _show():
app = build_app(reaper_on=reaper_on)
@@ -381,14 +394,19 @@ def _show():
app.config['TEMPLATES_AUTO_RELOAD'] = True
else:
getLogger("werkzeug").setLevel(LOG_ERROR)
- logger.info('D-Tale started at: {}'.format(build_url(selected_port)))
+ url = build_url(selected_port)
+ logger.info('D-Tale started at: {}'.format(url))
if open_browser:
- webbrowser.get().open(build_url(selected_port))
+ webbrowser.get().open(url)
+
app.run(host=host, port=selected_port, debug=debug)
if subprocess:
_thread.start_new_thread(_show, ())
+
+ if notebook:
+ instance.notebook()
else:
_show()
- return data_hook
+ return instance
diff --git a/dtale/swagger/dtale/health.yml b/dtale/swagger/dtale/health.yml
new file mode 100644
index 00000000..2e079b39
--- /dev/null
+++ b/dtale/swagger/dtale/health.yml
@@ -0,0 +1,8 @@
+summary: helper route for checking if D-Tale is up and running
+tags:
+ - D-Tale API
+responses:
+ 200:
+ description: text 'ok'
+ content:
+ text/html
diff --git a/dtale/templates/dtale/base.html b/dtale/templates/dtale/base.html
index a7c88fbd..4852889e 100644
--- a/dtale/templates/dtale/base.html
+++ b/dtale/templates/dtale/base.html
@@ -16,6 +16,10 @@
diff --git a/dtale/templates/dtale/popup.html b/dtale/templates/dtale/popup.html
new file mode 100644
index 00000000..0f7d41e9
--- /dev/null
+++ b/dtale/templates/dtale/popup.html
@@ -0,0 +1,15 @@
+{% extends "dtale/base.html" %}
+
+{% block title %}{{ title }}{% endblock %}
+
+{% block full_content %}
+
+
+
+
+{% endblock %}
+
+{% set js_file = 'dist/' + js_prefix + '_popup_bundle.js' %}
+{% block js %}
+
+{% endblock %}
diff --git a/dtale/utils.py b/dtale/utils.py
index 3853279f..5ca9258d 100644
--- a/dtale/utils.py
+++ b/dtale/utils.py
@@ -56,9 +56,9 @@ def build_shutdown_url(port):
def get_str_arg(r, name, default=None):
"""
- Retrieve argument from flask request object and convert to string
+ Retrieve argument from :attr:`flask:flask.request` and convert to string
- :param r: :meth:`flask.request`
+ :param r: :attr:`flask:flask.request`
:param name: argument name
:type: str
:param default: default value if parameter is non-existent, defaults to None
@@ -76,9 +76,9 @@ def get_str_arg(r, name, default=None):
def get_int_arg(r, name, default=None):
"""
- Retrieve argument from flask request object and convert to integer
+ Retrieve argument from :attr:`flask:flask.request` and convert to integer
- :param r: :meth:`flask.request`
+ :param r: :attr:`flask:flask.request`
:param name: argument name
:type: str
:param default: default value if parameter is non-existent, defaults to None
@@ -96,9 +96,9 @@ def get_int_arg(r, name, default=None):
def get_float_arg(r, name, default=None):
"""
- Retrieve argument from flask request object and convert to float
+ Retrieve argument from :attr:`flask:flask.request` and convert to float
- :param r: :meth:`flask.request`
+ :param r: :attr:`flask:flask.request`
:param name: argument name
:type: str
:param default: default value if parameter is non-existent, defaults to None
@@ -116,9 +116,9 @@ def get_float_arg(r, name, default=None):
def get_bool_arg(r, name):
"""
- Retrieve argument from flask request object and convert to boolean
+ Retrieve argument from :attr:`flask:flask.request` and convert to boolean
- :param r: :meth:`flask.request`
+ :param r: :attr:`flask:flask.request`
:param name: argument name
:type: str
:return: `True` if lowercase value equals 'true', `False` otherwise
@@ -131,7 +131,7 @@ def json_string(x, nan_display=''):
convert value to string to be used within JSON output
:param x: value to be converted to string
- :param nan_display: if `x` is nan then return this value
+ :param nan_display: if `x` is :attr:`numpy:numpy.nan` then return this value
:return: string value
:rtype: str
"""
@@ -145,7 +145,7 @@ def json_int(x, nan_display='', as_string=False):
Convert value to integer to be used within JSON output
:param x: value to be converted to integer
- :param nan_display: if `x` is nan then return this value
+ :param nan_display: if `x` is :attr:`numpy:numpy.nan` then return this value
:param as_string: return integer as a formatted string (EX: 1,000,000)
:return: integer value
:rtype: int
@@ -170,7 +170,7 @@ def json_float(x, precision=2, nan_display='nan', as_string=False):
:param x: value to be converted to integer
:param precision: precision of float to be returned
- :param nan_display: if `x` is nan then return this value
+ :param nan_display: if `x` is :attr:`numpy:numpy.nan` then return this value
:param as_string: return float as a formatted string (EX: 1,234.5643)
:return: float value
:rtype: float
@@ -194,7 +194,7 @@ def json_date(x, fmt='%Y-%m-%d %H:%M:%S', nan_display=''):
:param x: value to be converted to date string
:param fmt: the data string formatting to be applied
- :param nan_display: if `x` is nan then return this value
+ :param nan_display: if `x` is :attr:`numpy:numpy.nan` then return this value
:return: date string value
:rtype: str (YYYY-MM-DD)
"""
@@ -214,7 +214,7 @@ def json_timestamp(x, nan_display=''):
Convert value to timestamp (milliseconds) to be used within JSON output
:param x: value to be converted to milliseconds
- :param nan_display: if `x` is nan then return this value
+ :param nan_display: if `x` is :attr:`numpy:numpy.nan` then return this value
:return: millisecond value
:rtype: bigint
"""
@@ -280,7 +280,7 @@ def format_dicts(self, lsts):
def classify_type(type_name):
"""
- :param type_name: string label for value from :meth:`pandas.DataFrame.dtypes`
+ :param type_name: string label for value from :meth:`pandas:pandas.DataFrame.dtypes`
:return: shortened string label for dtype
S = str
B = bool
@@ -308,9 +308,9 @@ def classify_type(type_name):
def retrieve_grid_params(req, props=None):
"""
- Pull out grid parameters from :meth:`flask.request` arguments and return as a `dict`
+ Pull out grid parameters from :attr:`flask:flask.request` arguments and return as a `dict`
- :param req: :meth:`flask.request`
+ :param req: :attr:`flask:flask.request`
:param props: argument names
:type props: list
:return: dictionary of argument/value pairs
@@ -354,11 +354,11 @@ def sort_df_for_grid(df, params):
}
:param df: dataframe
- :type df: :class:`pandas.DataFrame`
- :param params: arguments from flask request
+ :type df: :class:`pandas:pandas.DataFrame`
+ :param params: arguments from :attr:`flask:flask.request`
:type params: dict
:return: sorted dataframe
- :rtype: :class:`pandas.DataFrame`
+ :rtype: :class:`pandas:pandas.DataFrame`
"""
if 'sort' in params:
cols, dirs = [], []
@@ -421,11 +421,11 @@ def filter_df_for_grid(df, params):
}
:param df: dataframe
- :type df: :class:`pandas.DataFrame`
- :param params: arguments from flask request
+ :type df: :class:`pandas:pandas.DataFrame`
+ :param params: arguments from :attr:`flask:flask.request`
:type params: dict
:return: filtering dataframe
- :rtype: :class:`pandas.DataFrame`
+ :rtype: :class:`pandas:pandas.DataFrame`
"""
data_type_info = get_dtypes(df)
for col, filter_cfg in params.get('filters', {}).items():
@@ -465,7 +465,7 @@ def filter_df_for_grid(df, params):
def get_dtypes(df):
"""
- Build dictionary of column/dtype name pairs from :class:`pandas.DataFrame`
+ Build dictionary of column/dtype name pairs from :class:`pandas:pandas.DataFrame`
"""
def _load():
for col, dtype in df.dtypes.to_dict().items():
@@ -478,7 +478,7 @@ def _load():
def grid_columns(df):
"""
- Build list of {name, dtype} dictionaries for columns in :class:`pandas.DataFrame`
+ Build list of {name, dtype} dictionaries for columns in :class:`pandas:pandas.DataFrame`
"""
data_type_info = get_dtypes(df)
return [dict(name=c, dtype=data_type_info[c]) for c in df.columns]
@@ -505,7 +505,7 @@ def find_dtype_formatter(dtype):
def grid_formatter(col_types, nan_display='', overrides=None):
"""
- Build :class:`dtale.utils.JSONFormatter` from :class:`pandas.DataFrame`
+ Build :class:`dtale.utils.JSONFormatter` from :class:`pandas:pandas.DataFrame`
"""
f = JSONFormatter(nan_display)
mappings = dict_merge(DF_MAPPINGS, overrides or {})
@@ -518,7 +518,7 @@ def grid_formatter(col_types, nan_display='', overrides=None):
def format_grid(df):
"""
- Translate :class:`pandas.DataFrame` to well-formed JSON. Structure is as follows:
+ Translate :class:`pandas:pandas.DataFrame` to well-formed JSON. Structure is as follows:
{
results: [
{col1: val1_row1,...,colN: valN_row1},
@@ -533,7 +533,7 @@ def format_grid(df):
}
:param df: dataframe
- :type df: :class:`pandas.DataFrame`
+ :type df: :class:`pandas:pandas.DataFrame`
:return: JSON
"""
col_types = grid_columns(df)
@@ -555,9 +555,9 @@ def jsonify(return_data={}, **kwargs):
"""
Overriding Flask's jsonify method to account for extra error handling
- :param return_data: dictionary of data to be passed to :meth:`flask.jsonify`
+ :param return_data: dictionary of data to be passed to :meth:`flask:flask.jsonify`
:param kwargs: Optional keyword arguments merged into return_data
- :return: output of :meth:`flask.jsonify`
+ :return: output of :meth:`flask:flask.jsonify`
"""
if isinstance(return_data, dict) and return_data.get('error'):
handle_error(return_data)
@@ -574,7 +574,7 @@ def find_selected_column(data, col):
in which case we want the last column
:param data: dataframe
- :type data: :class:`pandas.DataFrame`
+ :type data: :class:`pandas:pandas.DataFrame`
:param col: column name
:type col: str
:return: column name if it exists within the dataframe's columns, the last column within the dataframe otherwise
diff --git a/dtale/views.py b/dtale/views.py
index 0855360d..646a2adc 100644
--- a/dtale/views.py
+++ b/dtale/views.py
@@ -1,5 +1,6 @@
from __future__ import absolute_import, division
+import time
import traceback
import webbrowser
from builtins import map, range, str, zip
@@ -29,6 +30,23 @@
DTYPES = {}
SETTINGS = {}
METADATA = {}
+IDX_COL = str('dtale_index')
+
+
+def in_ipython_frontend():
+ """
+ Helper function which is variation of :meth:`pandas:pandas.io.formats.console.in_ipython_frontend` which
+ checks to see if we are inside an IPython zmq frontend
+
+ :return: `True` if D-Tale is being invoked within ipython notebook, `False` otherwise
+ """
+ try:
+ from IPython import get_ipython
+ ip = get_ipython()
+ return 'zmq' in str(type(ip)).lower()
+ except BaseException:
+ pass
+ return False
class DtaleData(object):
@@ -54,28 +72,132 @@ class DtaleData(object):
def __init__(self, port):
self._port = port
self._url = build_url(port)
+ self._notebook_handle = None
@property
def data(self):
+ """
+ Property which is a reference to the globally stored data associated with this instance
+
+ """
return DATA[self._port]
@data.setter
def data(self, data):
+ """
+ Setter which will go through all standard formatting to make sure changes will be handled correctly by D-Tale
+
+ """
startup(data=data, port=self._port)
def kill(self):
+ """
+ This function fires a request to this instance's 'shutdown' route to kill it
+
+ """
requests.get(build_shutdown_url(self._port))
def open_browser(self):
+ """
+ This function uses the :mod:`python:webbrowser` library to try and automatically open server's default browser
+ to this D-Tale instance
+ """
webbrowser.get().open(self._url)
+ def is_up(self):
+ """
+ This function checks to see if instance's :mod:`flask:flask.Flask` process is up by hitting 'health' route
+
+ :return: `True` if :mod:`flask:flask.Flask` process is up and running, `False` otherwise
+ """
+ try:
+ return requests.get(self._url + '/health').ok
+ except BaseException:
+ return False
+
+ def __str__(self):
+ """
+ Will try to create an :class:`ipython:IPython.display.IFrame` if being invoked from within ipython notebook
+ otherwise simply returns the output of running :meth:`pandas:pandas.DataFrame.__str__` on the data associated
+ with this instance
+
+ """
+ if in_ipython_frontend():
+ self.notebook()
+ return ''
+ return self.data.__str__()
+
+ def __repr__(self):
+ """
+ Will try to create an :class:`ipython:IPython.display.IFrame` if being invoked from within ipython notebook
+ otherwise simply returns the output of running :meth:`pandas:pandas.DataFrame.__repr__` on the data for
+ this instance
+
+ """
+ if in_ipython_frontend():
+ self.notebook()
+ return ''
+ return self.data.__repr__()
+
+ def _build_iframe(self, width='100%', height=350):
+ """
+ Helper function to build an :class:`ipython:IPython.display.IFrame` if that module exists within
+ your environment
+
+ :param width: width of the ipython cell
+ :param height: height of the ipython cell
+ :return: :class:`ipython:IPython.display.IFrame`
+ """
+ try:
+ from IPython.display import IFrame
+ except ImportError:
+ logger.info('in order to use this function, please install IPython')
+ return None
+ return IFrame('{}/dtale/iframe'.format(self._url), width=width, height=height)
+
+ def notebook(self, width='100%', height=350):
+ """
+ Helper function which checks to see if :mod:`flask:flask.Flask` process is up and running and then tries to
+ build an :class:`ipython:IPython.display.IFrame` and run :meth:`ipython:IPython.display.display` on it so
+ it will be displayed in the ipython notebook which invoked it.
+
+ A reference to the :class:`ipython:IPython.display.DisplayHandle` is stored in _notebook_handle for
+ updating if you are running ipython>=5.0
+
+ :param width: width of the ipython cell
+ :param height: height of the ipython cell
+ """
+ try:
+ from IPython.display import display
+ except ImportError:
+ logger.info('in order to use this function, please install IPython')
+ return self.data.__repr__()
+
+ while not self.is_up():
+ time.sleep(0.01)
+
+ self._notebook_handle = display(self._build_iframe(width=width, height=height), display_id=True)
+
+ def adjust_cell_dimensions(self, width='100%', height=350):
+ """
+ If you are running ipython>=5.0 then this will update the most recent notebook cell you displayed D-Tale in
+ for this instance with the height/width properties you have passed in as input
+
+ :param width: width of the ipython cell
+ :param height: height of the ipython cell
+ """
+ if self._notebook_handle is not None:
+ self._notebook_handle.update(self._build_iframe(width=width, height=height))
+ else:
+ logger.debug('You must ipython>=5.0 installed to use this functionality')
+
def build_dtypes_state(data):
"""
Helper function to build globally managed state pertaining to a D-Tale instances columns & data types
:param data: dataframe to build data type information for
- :type data: pandas.DataFrame
+ :type data: :class:`pandas:pandas.DataFrame`
:return: a list of dictionaries containing column names, indexes and data types
"""
dtypes = get_dtypes(data)
@@ -94,6 +216,17 @@ def _format_dtype(col_index, col):
def format_data(data):
+ """
+ Helper function to build globally managed state pertaining to a D-Tale instances data. Some updates being made:
+ - convert all column names to strings
+ - drop any indexes back into the dataframe so what we are left is a natural index [0,1,2,...,n]
+ - convert inputs that are indexes into dataframes
+
+ :param data: dataframe to build data type information for
+ :type data: :class:`pandas:pandas.DataFrame`
+ :return: formatted :class:`pandas:pandas.DataFrame` and a list of strings constituting what columns were originally
+ in the index
+ """
if isinstance(data, (pd.DatetimeIndex, pd.MultiIndex)):
data = data.to_frame(index=False)
@@ -107,12 +240,12 @@ def format_data(data):
def startup(data=None, data_loader=None, port=None, name=None):
"""
Loads and stores data globally
- - If data has indexes then it will lock save those columns as locked on the front-end
- - If data has column named index it will be dropped so that it won't collide with row numbering (dtale_index)
- - Create location in memory for storing settings which can be manipulated from the front-end (sorts, filter, ...)
+ - If data has indexes then it will lock save those columns as locked on the front-end
+ - If data has column named index it will be dropped so that it won't collide with row numbering (dtale_index)
+ - Create location in memory for storing settings which can be manipulated from the front-end (sorts, filter, ...)
- :param data: pandas.DataFrame or pandas.Series
- :param data_loader: function which returns pandas.DataFrame
+ :param data: :class:`pandas:pandas.DataFrame` or :class:`pandas:pandas.Series`
+ :param data_loader: function which returns :class:`pandas:pandas.DataFrame`
:param port: integer port for running Flask process
:param name: string label to apply to your session
"""
@@ -167,9 +300,48 @@ def cleanup(port):
def get_port():
+ """
+ Helper function to grab port information (SERVER_PORT) from Flask.request.environ
+
+ """
return get_str_arg(request, 'port', request.environ.get('SERVER_PORT', 'curr'))
+def base_render_template(template, **kwargs):
+ """
+ Overriden version of Flask.render_template which will also include vital instance information
+ - settings
+ - version
+ - processes
+ """
+ port = get_port()
+ curr_settings = SETTINGS.get(port, {})
+ _, version = retrieve_meta_info_and_version('dtale')
+ return render_template(
+ template,
+ settings=json.dumps(curr_settings),
+ version=str(version),
+ processes=len(DATA),
+ **kwargs
+ )
+
+
+def _view_main(iframe=False):
+ """
+ Helper function rendering main HTML which will also build title and store whether we are viewing from an