diff --git a/dtale/dash_application/charts.py b/dtale/dash_application/charts.py index 5aa780a9b..49e09ac3d 100644 --- a/dtale/dash_application/charts.py +++ b/dtale/dash_application/charts.py @@ -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) diff --git a/dtale/dash_application/layout.py b/dtale/dash_application/layout.py index ec1a837bd..c460cbe54 100644 --- a/dtale/dash_application/layout.py +++ b/dtale/dash_application/layout.py @@ -407,7 +407,10 @@ 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): @@ -415,9 +418,9 @@ def colorscale_input_style(**inputs): 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 @@ -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) @@ -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( [ diff --git a/dtale/dash_application/views.py b/dtale/dash_application/views.py index 54e3751af..f420ce22e 100644 --- a/dtale/dash_application/views.py +++ b/dtale/dash_application/views.py @@ -262,7 +262,7 @@ 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) @@ -270,7 +270,7 @@ def input_toggles(_ts, inputs, pathname): 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 ) diff --git a/dtale/static/css/main.css b/dtale/static/css/main.css index ee29a65f1..378a414d0 100644 --- a/dtale/static/css/main.css +++ b/dtale/static/css/main.css @@ -10196,6 +10196,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; @@ -10229,8 +10235,10 @@ div.hoverable.label > div.hoverable__content { 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); @@ -10263,7 +10271,8 @@ div.hoverable.label > div.hoverable__content { 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); diff --git a/dtale/views.py b/dtale/views.py index 31453c274..66c0211a1 100644 --- a/dtale/views.py +++ b/dtale/views.py @@ -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] @@ -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 @@ -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" diff --git a/static/__tests__/dtale/DataViewer-describe-test.jsx b/static/__tests__/dtale/DataViewer-describe-test.jsx index 88933159f..8351888bf 100644 --- a/static/__tests__/dtale/DataViewer-describe-test.jsx +++ b/static/__tests__/dtale/DataViewer-describe-test.jsx @@ -13,7 +13,7 @@ const originalInnerHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototyp describe("DataViewer tests", () => { const { post } = $; - const { opener } = window; + const { close, opener } = window; beforeAll(() => { Object.defineProperty(HTMLElement.prototype, "offsetHeight", { @@ -34,7 +34,9 @@ describe("DataViewer tests", () => { }); delete window.opener; + delete window.close; window.opener = { location: { reload: jest.fn() } }; + window.close = jest.fn(); const mockBuildLibs = withGlobalJquery(() => mockPopsicle.mock(url => { @@ -66,6 +68,7 @@ describe("DataViewer tests", () => { Object.defineProperty(window, "innerHeight", originalInnerHeight); $.post = post; window.opener = opener; + window.close = close; }); test("DataViewer: describe", done => { diff --git a/static/__tests__/redux-test-utils.jsx b/static/__tests__/redux-test-utils.jsx index 66729db6a..ded437286 100644 --- a/static/__tests__/redux-test-utils.jsx +++ b/static/__tests__/redux-test-utils.jsx @@ -22,8 +22,8 @@ const DTYPES = { min: 2, max: 5, visible: true, - hasMissing: true, - hasOutliers: false, + hasMissing: 1, + hasOutliers: 0, }, { name: "col2", @@ -32,8 +32,8 @@ const DTYPES = { min: 2.5, max: 5.5, visible: true, - hasMissing: false, - hasOutliers: false, + hasMissing: 0, + hasOutliers: 0, outlierRange: { lower: 3.5, upper: 4.5 }, }, { name: "col3", index: 2, dtype: "object", visible: true }, diff --git a/static/dtale/DataViewerInfo.jsx b/static/dtale/DataViewerInfo.jsx index df0467f8a..accadf8d3 100644 --- a/static/dtale/DataViewerInfo.jsx +++ b/static/dtale/DataViewerInfo.jsx @@ -202,8 +202,11 @@ class ReactDataViewerInfo extends React.Component { const hidden = _.map(_.filter(columns, { visible: false }), "name"); const clearHidden = () => { const visibility = _.reduce(columns, (ret, { name }) => _.assignIn(ret, { [name]: true }), {}); - const updatedColumns = _.map(columns, c => _.assignIn({}, c, { visible: true })); - serverState.updateVisibility(dataId, visibility, () => propagateState({ columns: updatedColumns })); + const updatedState = { + columns: _.map(columns, c => _.assignIn({}, c, { visible: true })), + triggerResize: true, + }; + serverState.updateVisibility(dataId, visibility, () => propagateState(updatedState)); }; const clearAll = ( diff --git a/static/dtale/Header.jsx b/static/dtale/Header.jsx index 6f56614bb..6791b116c 100644 --- a/static/dtale/Header.jsx +++ b/static/dtale/Header.jsx @@ -70,10 +70,10 @@ class ReactHeader extends React.Component { colNameMarkup =