Skip to content

Commit

Permalink
1.8.11
Browse files Browse the repository at this point in the history
* #191: improving outlier filter suggestions
* #190: hide "Animate" inputs when "Percentage Sum" or "Percentage Count" aggregations are used
* #189: hide "Barsort" when grouping is being applied
* #187: missing & outlier tooltip descriptions on column headers
* #186: close "Describe" tab after clicking "Update Grid"
* #122: editable cells
* npm package upgrades
* circleci build script refactoring
  • Loading branch information
aschonfeld committed May 2, 2020
1 parent a586f5c commit 1bd04fb
Show file tree
Hide file tree
Showing 84 changed files with 2,464 additions and 2,820 deletions.
381 changes: 215 additions & 166 deletions .circleci/config.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"trailingComma": "es5",
"printWidth": 120,
"jsxBracketSameLine": true,
"arrowParens": "avoid",
"overrides": [
{
"files": "*.css",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

[![CircleCI](https://circleci.com/gh/man-group/dtale.svg?style=shield&circle-token=4b67588a87157cc03b484fb96be438f70b5cd151)](https://circleci.com/gh/man-group/dtale)
[![PyPI](https://img.shields.io/pypi/pyversions/dtale.svg)](https://pypi.python.org/pypi/dtale/)
![PyPI](https://img.shields.io/pypi/v/dtale)
[![ReadTheDocs](https://readthedocs.org/projects/dtale/badge)](https://dtale.readthedocs.io)
[![codecov](https://codecov.io/gh/man-group/dtale/branch/master/graph/badge.svg)](https://codecov.io/gh/man-group/dtale)
[![Downloads](https://pepy.tech/badge/dtale)](https://pepy.tech/project/dtale)
Expand Down
2 changes: 1 addition & 1 deletion dtale/dash_application/charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,7 @@ def heatmap_builder(data_id, export=False, **inputs):
code += agg_code
if not len(data):
raise Exception('No data returned for this computation!')
check_exceptions(data[dupe_cols], agg not in ['corr', 'raw'], unlimited_data=True)
check_exceptions(data[dupe_cols], agg in ['corr', 'raw'], unlimited_data=True)
dtypes = {c: classify_type(dtype) for c, dtype in get_dtypes(data).items()}
data_f, _ = chart_formatters(data)
data = data_f.format_df(data)
Expand Down
13 changes: 8 additions & 5 deletions dtale/dash_application/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,17 +407,20 @@ def bar_input_style(**inputs):
"""
Sets display CSS property for bar chart inputs
"""
return dict(display='block' if inputs.get('chart_type') == 'bar' else 'none')
chart_type, group_col = (inputs.get(p) for p in ['chart_type', 'group'])
show_bar = chart_type == 'bar'
show_barsort = show_bar and group_col is None
return dict(display='block' if show_bar else 'none'), dict(display='block' if show_barsort else 'none')


def colorscale_input_style(**inputs):
return dict(display='block' if inputs.get('chart_type') in ['heatmap', 'maps'] else 'none')


def animate_styles(df, **inputs):
chart_type, cpg = (inputs.get(p) for p in ['chart_type', 'cpg'])
chart_type, agg, cpg = (inputs.get(p) for p in ['chart_type', 'agg', 'cpg'])
opts = []
if cpg:
if cpg or agg in ['pctsum', 'pctct']:
return dict(display='none'), dict(display='none'), opts
if chart_type in ANIMATION_CHARTS:
return dict(display='block'), dict(display='none'), opts
Expand Down Expand Up @@ -512,7 +515,7 @@ def charts_layout(df, settings, **inputs):
show_input = show_input_handler(chart_type)
show_cpg = show_chart_per_group(**inputs)
show_yaxis = show_yaxis_ranges(**inputs)
bar_style = bar_input_style(**inputs)
bar_style, barsort_input_style = bar_input_style(**inputs)
animate_style, animate_by_style, animate_opts = animate_styles(df, **inputs)

options = build_input_options(df, **inputs)
Expand Down Expand Up @@ -803,7 +806,7 @@ def show_map_style(show):
), className='col-auto addon-min-width', style=bar_style, id='barmode-input'),
build_input('Barsort', dcc.Dropdown(
id='barsort-dropdown', options=barsort_options, value=inputs.get('barsort')
), className='col-auto addon-min-width', style=bar_style, id='barsort-input'),
), className='col-auto addon-min-width', style=barsort_input_style, id='barsort-input'),
html.Div(
html.Div(
[
Expand Down
4 changes: 2 additions & 2 deletions dtale/dash_application/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,15 @@ def input_toggles(_ts, inputs, pathname):
group_style = {'display': 'block' if show_input('group') else 'none'}
rolling_style = {'display': 'inherit' if agg == 'rolling' else 'none'}
cpg_style = {'display': 'block' if show_chart_per_group(**inputs) else 'none'}
bar_style = bar_input_style(**inputs)
bar_style, barsort_style = bar_input_style(**inputs)
yaxis_style = {'display': 'block' if show_yaxis_ranges(**inputs) else 'none'}

data_id = get_data_id(pathname)
df = global_state.get_data(data_id)
animate_style, animate_by_style, animate_opts = animate_styles(df, **inputs)

return (
y_multi_style, y_single_style, z_style, group_style, rolling_style, cpg_style, bar_style, bar_style,
y_multi_style, y_single_style, z_style, group_style, rolling_style, cpg_style, bar_style, barsort_style,
yaxis_style, animate_style, animate_by_style, animate_opts
)

Expand Down
38 changes: 33 additions & 5 deletions dtale/static/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -1702,7 +1702,7 @@ select.form-control-lg:not([size]):not([multiple]), .input-group-lg > select.for
margin-left: 0.75rem;
}

.invalianeld-feedback {
.invalid-feedback {
display: none;
margin-top: .25rem;
font-size: .875rem;
Expand Down Expand Up @@ -10124,6 +10124,9 @@ select.form-control:focus,
position: relative;
border-bottom: dashed 1px #004c93;
}
.cell.hoverable {
border-bottom: solid 1px rgba(170, 170, 170, 0.25);
}
.hoverable-click {
display: inline;
position: relative;
Expand All @@ -10139,6 +10142,9 @@ select.form-control:focus,
li.hoverable:hover {
border-bottom: solid 1px #a7b3b7;
}
.cell.hoverable:hover {
border-bottom: solid 1px rgba(170, 170, 170, 0.25);
}

.hoverable:focus {
outline: 0;
Expand Down Expand Up @@ -10169,7 +10175,8 @@ li.hoverable:hover {
text-align: left;
color: #404040;
}
.hoverable__content.menu-description {
.hoverable__content.menu-description,
.hoverable__content.edit-cell {
top: -0.8em;
left: 13em;
}
Expand All @@ -10196,6 +10203,12 @@ li.hoverable:hover {
top: unset;
bottom: 110%;
}
.hoverable__content.col-menu-desc {
padding: .5em .5em;
text-align: center;
top: unset;
bottom: 110%;
}

div.hoverable.label {
border-bottom: none;
Expand Down Expand Up @@ -10224,13 +10237,21 @@ div.hoverable.label > div.hoverable__content {
right: inherit;
transform: rotate(270deg);
}
.hoverable__content.edit-cell::before {
left: -0.7em;
top: 2.75em;
right: inherit;
transform: rotate(270deg);
}
.hoverable__content.build-code::before,
.hoverable__content.map-types::before {
right: unset;
left: 2em;
}
.hoverable__content.copy-tt-top::before {
.hoverable__content.copy-tt-top::before,
.hoverable__content.col-menu-desc::before {
bottom: unset;
border-bottom: none;
top: 95%;
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
Expand Down Expand Up @@ -10258,12 +10279,19 @@ div.hoverable.label > div.hoverable__content {
right: inherit;
transform: rotate(270deg);
}
.hoverable__content.edit-cell::after {
left: -0.6em;
top: 2.75em;
right: inherit;
transform: rotate(270deg);
}
.hoverable__content.build-code::after,
.hoverable__content.map-types::after {
right: unset;
left: 2em;
}
.hoverable__content.copy-tt-top::after {
.hoverable__content.copy-tt-top::after,
.hoverable__content.col-menu-desc::after {
bottom: unset;
top: calc(92% + .1em);
-moz-transform: rotate(180deg);
Expand Down Expand Up @@ -10567,7 +10595,7 @@ div.container-fluid.filter > div#popup-content > div.modal-body {
}

div.container-fluid.code-popup > div#popup-content > div.modal-footer,
div.container-fluid.code-export > div#popup-content > div.modal-footer, {
div.container-fluid.code-export > div#popup-content > div.modal-footer {
position: absolute;
bottom: 0;
width: 100%;
Expand Down
84 changes: 78 additions & 6 deletions dtale/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ def _formatter(col_index, col):
visible = prev_dtypes[col].get('visible', True)
s = data[col]
dtype_data = dict(name=col, dtype=dtype, index=col_index, visible=visible,
hasMissing=bool(s.isnull().any()), hasOutliers=False)
hasMissing=int(s.isnull().sum()), hasOutliers=0)
classification = classify_type(dtype)
if classification in ['F', 'I'] and not data[col].isnull().all() and col in data_ranges: # floats/ints
col_ranges = data_ranges[col]
Expand All @@ -419,11 +419,11 @@ def _formatter(col_index, col):
# load outlier information
o_s, o_e = calc_outlier_range(s)
if not any((np.isnan(v) or np.isinf(v) for v in [o_s, o_e])):
dtype_data['hasOutliers'] = bool(((s < o_s) | (s > o_e)).any())
dtype_data['hasOutliers'] += int(((s < o_s) | (s > o_e)).sum())
dtype_data['outlierRange'] = dict(lower=o_s, upper=o_e)

if classification == 'S' and not dtype_data['hasMissing']:
dtype_data['hasMissing'] = bool((s.str.strip() == '').any())
dtype_data['hasMissing'] += int((s.str.strip() == '').sum())
return dtype_data
return _formatter

Expand Down Expand Up @@ -1069,10 +1069,17 @@ def outliers(data_id, column):
iqr_lower, iqr_upper = calc_outlier_range(s)
formatter = find_dtype_formatter(find_dtype(df[column]))
outliers = s[(s < iqr_lower) | (s > iqr_upper)].unique()
if not len(outliers):
return jsonify(outliers=[])
top = len(outliers) > 100
outliers = [formatter(v) for v in outliers[:100]]
query = '(({column} < {lower}) or ({column} > {upper}))'.format(column=column, lower=json_float(iqr_lower),
upper=json_float(iqr_upper))
queries = []
if iqr_lower > s.min():
queries.append('{column} < {lower}'.format(column=column, lower=json_float(iqr_lower)))
if iqr_upper < s.max():
queries.append('{column} > {upper}'.format(column=column, upper=json_float(iqr_upper)))
query = '(({}))'.format(') or ('.join(queries)) if len(queries) > 1 else queries[0]

code = (
"s = df['{column}']\n"
"q1 = s.quantile(0.25)\n"
Expand Down Expand Up @@ -1114,7 +1121,7 @@ def rename_col(data_id, column):
rename = get_str_arg(request, 'rename')
data = global_state.get_data(data_id)
if column != rename and rename in data.columns:
return jsonify(dict(error='Column name "{}" already exists!'))
return jsonify(error='Column name "{}" already exists!')

data = data.rename(columns={column: rename})
curr_history = global_state.get_history(data_id) or []
Expand All @@ -1132,6 +1139,71 @@ def rename_col(data_id, column):
return jsonify_error(e)


@dtale.route('/edit-cell/<data_id>/<column>')
def edit_cell(data_id, column):
try:
row_index = get_int_arg(request, "rowIndex")
updated = get_str_arg(request, "updated")
updated_str = updated
curr_settings = global_state.get_settings(data_id)
data = run_query(
global_state.get_data(data_id),
build_query(data_id, curr_settings.get('query')),
global_state.get_context_variables(data_id),
ignore_empty=True,
)
dtype = find_dtype(data[column])

code = []
if updated in ['nan', 'inf']:
updated_str = 'np.{}'.format(updated)
updated = getattr(np, updated)
data.loc[row_index, column] = updated
code.append("df.loc[{row_index}, '{column}'] = {updated}".format(row_index=row_index, column=column,
updated=updated_str))
else:
classification = classify_type(dtype)
if classification == 'B':
updated = updated.lower() == 'true'
updated_str = str(updated)
elif classification == 'I':
updated = int(updated)
elif classification == 'F':
updated = float(updated)
elif classification == 'D':
updated_str = 'pd.Timestamp({})'.format(updated)
updated = pd.Timestamp(updated)
elif classification == 'TD':
updated_str = 'pd.Timedelta({})'.format(updated)
updated = pd.Timedelta(updated)
else:
if dtype.startswith('category') and updated not in data[column].unique():
data[column].cat.add_categories(updated, inplace=True)
code.append("data['{column}'].cat.add_categories('{updated}', inplace=True)".format(
column=column, updated=updated))
updated_str = "'{}'".format(updated)
data.at[row_index, column] = updated
code.append("df.at[{row_index}, '{column}'] = {updated}".format(row_index=row_index, column=column,
updated=updated_str))
curr_history = global_state.get_history(data_id) or []
curr_history += code
global_state.set_history(data_id, curr_history)

data = global_state.get_data(data_id)
dtypes = global_state.get_dtypes(data_id)
ranges = {}
try:
ranges[column] = data[[column]].agg(['min', 'max']).to_dict()[column]
except ValueError:
pass
dtype_f = dtype_formatter(data, {column: dtype}, ranges)
dtypes = [dtype_f(dt['index'], column) if dt['name'] == column else dt for dt in dtypes]
global_state.set_dtypes(data_id, dtypes)
return jsonify(success=True)
except BaseException as e:
return jsonify_error(e)


@dtale.route('/column-filter-data/<data_id>/<column>')
def get_column_filter_data(data_id, column):
try:
Expand Down
Loading

0 comments on commit 1bd04fb

Please sign in to comment.