Skip to content

Commit

Permalink
#595: probability histograms
Browse files Browse the repository at this point in the history
  • Loading branch information
aschonfeld committed Nov 16, 2021
1 parent ab589e0 commit fdb6eeb
Show file tree
Hide file tree
Showing 15 changed files with 509 additions and 18 deletions.
13 changes: 10 additions & 3 deletions dtale/column_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
find_dtype,
find_dtype_formatter,
find_selected_column,
get_bool_arg,
get_int_arg,
get_str_arg,
grid_columns,
Expand Down Expand Up @@ -147,9 +148,11 @@ class HistogramAnalysis(object):
def __init__(self, req):
self.bins = get_int_arg(req, "bins", 20)
self.target = get_str_arg(req, "target")
self.density = get_bool_arg(req, "density")

def build_histogram_data(self, series):
hist_data, hist_labels = np.histogram(series, bins=self.bins)
hist_kwargs = {"density": True} if self.density else {"bins": self.bins}
hist_data, hist_labels = np.histogram(series, **hist_kwargs)
hist_data = [json_float(h) for h in hist_data]
return (
dict(
Expand Down Expand Up @@ -225,9 +228,13 @@ def _build_code(self, parent, kde_code, desc_code):
).format(col=parent.selected_col)
)
if self.target is None:
hist_kwargs = (
"density=True" if self.density else "bins={}".format(self.bins)
)
code.append(
"chart, labels = np.histogram(s['{col}'], bins={bins})".format(
col=parent.selected_col, bins=self.bins
"chart, labels = np.histogram(s['{col}'], {hist_kwargs})".format(
col=parent.selected_col,
hist_kwargs=hist_kwargs,
)
)
code += kde_code + desc_code
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"<rootDir>/static/**/*.{js, jsx}"
],
"coveragePathIgnorePatterns": [
"<rootDir>/static/__tests__/"
"<rootDir>/static/__tests__/",
"<rootDir>/state/dash/lib/custom.js"
],
"coverageDirectory": "./JS_coverage",
"coverageReporters": [
Expand Down
2 changes: 1 addition & 1 deletion static/ButtonToggle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ ButtonToggle.displayName = "ButtonToggle";
ButtonToggle.propTypes = {
options: PropTypes.array,
update: PropTypes.func,
defaultValue: PropTypes.string,
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]),
allowDeselect: PropTypes.bool,
disabled: PropTypes.bool,
className: PropTypes.string,
Expand Down
21 changes: 19 additions & 2 deletions static/__tests__/popups/analysis/columnAnalysisUtils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import $ from "jquery";

import { withGlobalJquery } from "../../test-utils";
import chartUtils from "../../../chartUtils";
import * as fetcher from "../../../fetcher";

describe("columnAnalysisUtils", () => {
let createChart, createChartSpy;
let createChart, dataLoader, createChartSpy, fetchJsonSpy;

beforeEach(() => {
const mockJquery = withGlobalJquery(() => selector => {
Expand All @@ -17,9 +18,16 @@ describe("columnAnalysisUtils", () => {
jest.mock("jquery", () => mockJquery);
createChartSpy = jest.spyOn(chartUtils, "createChart");
createChartSpy.mockImplementation(() => undefined);
createChart = require("../../../popups/analysis/columnAnalysisUtils").createChart;
fetchJsonSpy = jest.spyOn(fetcher, "fetchJson");
fetchJsonSpy.mockImplementation(() => undefined);

const columnAnalysisUtils = require("../../../popups/analysis/columnAnalysisUtils");
createChart = columnAnalysisUtils.createChart;
dataLoader = columnAnalysisUtils.dataLoader;
});

afterEach(jest.restoreAllMocks);

it("correctly handles targeted histogram data", () => {
const fetchedData = {
targets: [
Expand All @@ -34,4 +42,13 @@ describe("columnAnalysisUtils", () => {
const finalChart = createChartSpy.mock.calls[0][1];
expect(finalChart.data.datasets.map(d => d.label)).toEqual(["foo", "bar"]);
});

it("correctly handles probability histogram load", () => {
const propagateState = jest.fn();
const props = { chartData: { selectedCol: "foo" }, height: 400, dataId: "1" };
dataLoader(props, {}, propagateState, { type: "histogram", density: true });
expect(fetchJsonSpy).toHaveBeenCalled();
const params = Object.fromEntries(new URLSearchParams(fetchJsonSpy.mock.calls[0][0].split("?")[1]));
expect(params).toMatchObject({ density: "true" });
});
});
35 changes: 32 additions & 3 deletions static/__tests__/popups/analysis/filters/DescribeFilters-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { expect, it } from "@jest/globals";
import ButtonToggle from "../../../../ButtonToggle";
import CategoryInputs from "../../../../popups/analysis/filters/CategoryInputs";
import DescribeFilters from "../../../../popups/analysis/filters/DescribeFilters";
import GeoFilters from "../../../../popups/analysis/filters/GeoFilters";
import OrdinalInputs from "../../../../popups/analysis/filters/OrdinalInputs";
import TextEnterFilter from "../../../../popups/analysis/filters/TextEnterFilter";

Expand Down Expand Up @@ -43,7 +44,7 @@ describe("DescribeFilters tests", () => {
expect(buildChart.mock.calls).toHaveLength(2);
});

describe(" rendering int column", () => {
describe("rendering int column", () => {
it("rendering boxplot", () => {
expect(result.find(OrdinalInputs)).toHaveLength(0);
expect(result.find(CategoryInputs)).toHaveLength(0);
Expand All @@ -61,6 +62,9 @@ describe("DescribeFilters tests", () => {
expect(result.find(OrdinalInputs)).toHaveLength(0);
expect(result.find(CategoryInputs)).toHaveLength(0);
expect(result.find(TextEnterFilter)).toHaveLength(1);
result.find(ButtonToggle).last().props().update(true);
expect(buildChart).toHaveBeenCalled();
expect(result.state()).toMatchObject({ density: true });
});

it("rendering value_counts", () => {
Expand All @@ -71,7 +75,7 @@ describe("DescribeFilters tests", () => {
});
});

describe(" rendering float column", () => {
describe("rendering float column", () => {
beforeEach(() => {
result.setProps({ dtype: "float64", details: { type: "float64" } });
result.update();
Expand Down Expand Up @@ -104,7 +108,7 @@ describe("DescribeFilters tests", () => {
});
});

describe(" rendering datetime column", () => {
describe("rendering datetime column", () => {
beforeEach(() => {
result.setProps({ dtype: "datetime[ns]", details: { type: "datetime" } });
result.update();
Expand All @@ -130,6 +134,31 @@ describe("DescribeFilters tests", () => {
});
});

describe("rendering geolocation column", () => {
beforeEach(() => {
result.setProps({
dtype: "float",
coord: "lat",
details: { type: "float" },
});
result.update();
});

it("loading options", () => {
expect(_.map(result.find(ButtonToggle).prop("options"), "value")).toEqual([
"boxplot",
"histogram",
"categories",
"qq",
]);
});

it("rendering geolocation", () => {
result.setState({ type: "geolocation" });
expect(result.find(GeoFilters)).toHaveLength(1);
});
});

describe("chart navigation", () => {
const move = prop => result.find(GlobalHotKeys).props().handlers[prop]();

Expand Down
60 changes: 60 additions & 0 deletions static/__tests__/popups/timeseries/BKFilter-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { shallow } from "enzyme";
import React from "react";

import { expect, it } from "@jest/globals";

import { BKFilter, chartConfig } from "../../../popups/timeseries/BKFilter";

describe("BKFilter", () => {
let wrapper, props;

beforeEach(() => {
props = {
baseCfg: {},
cfg: {},
updateState: jest.fn(),
};
wrapper = shallow(<BKFilter {...props} />);
});

it("renders successfully", () => {
expect(wrapper.find("div.col-md-4")).toHaveLength(3);
expect(props.updateState).toHaveBeenCalledTimes(1);
});

it("updates state", () => {
wrapper.find("input").forEach(input => {
input.simulate("change", { target: { value: 5 } });
input.simulate("keyDown", { key: "Enter" });
});
expect(props.updateState).toHaveBeenLastCalledWith({
cfg: { low: 5, high: 5, K: 5 },
});
});

it("updates state on baseCfg update", () => {
props.updateState.mockReset();
wrapper.setProps({ baseCfg: { col: "foo" } });
expect(props.updateState).toHaveBeenCalledTimes(1);
});

it("builds chart config correctly", () => {
const cfg = chartConfig(
{ col: "foo" },
{
data: { datasets: [{}, {}] },
options: {
scales: {
"y-cycle": {},
"y-foo": {},
x: { title: { display: false } },
},
plugins: {},
},
}
);
expect(cfg.options.scales["y-foo"].position).toBe("left");
expect(cfg.options.scales["y-cycle"].position).toBe("right");
expect(cfg.options.plugins.legend.display).toBe(true);
});
});
43 changes: 43 additions & 0 deletions static/__tests__/popups/timeseries/BaseInputs-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { shallow } from "enzyme";
import React from "react";
import Select from "react-select";

import { expect, it } from "@jest/globals";

import { BaseInputs } from "../../../popups/timeseries/BaseInputs";

describe("BaseInputs", () => {
let wrapper, props;

beforeEach(() => {
props = {
columns: [{ dtype: "int", name: "foo" }],
cfg: {},
updateState: jest.fn(),
};
wrapper = shallow(<BaseInputs {...props} />);
});

it("renders successfully", () => {
expect(wrapper.find("div.col-md-4")).toHaveLength(3);
});

it("updates state", () => {
wrapper
.find("ColumnSelect")
.first()
.props()
.updateState({ index: { value: "date" } });
wrapper
.find("ColumnSelect")
.last()
.props()
.updateState({ col: { value: "foo" } });
wrapper.find(Select).props().onChange({ value: "sum" });
expect(props.updateState).toHaveBeenLastCalledWith({
index: "date",
col: "foo",
agg: "sum",
});
});
});
64 changes: 64 additions & 0 deletions static/__tests__/popups/timeseries/CFFilter-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { shallow } from "enzyme";
import React from "react";

import { expect, it } from "@jest/globals";

import { CFFilter, chartConfig } from "../../../popups/timeseries/CFFilter";

describe("CFFilter", () => {
let wrapper, props;

beforeEach(() => {
props = {
baseCfg: {},
cfg: {},
updateState: jest.fn(),
};
wrapper = shallow(<CFFilter {...props} />);
});

it("renders successfully", () => {
expect(wrapper.find("div.col-md-4")).toHaveLength(3);
expect(props.updateState).toHaveBeenCalledTimes(1);
});

it("updates state", () => {
wrapper.find("input").forEach(input => {
input.simulate("change", { target: { value: 5 } });
input.simulate("keyDown", { key: "Enter" });
});
wrapper.find("i").simulate("click");
expect(props.updateState).toHaveBeenLastCalledWith({
cfg: { low: 5, high: 5, drift: true },
});
});

it("updates state on baseCfg update", () => {
props.updateState.mockReset();
wrapper.setProps({ baseCfg: { col: "foo" } });
expect(props.updateState).toHaveBeenCalledTimes(1);
});

it("builds chart config correctly", () => {
const cfg = chartConfig(
{ col: "foo" },
{
data: { datasets: [{}, {}, {}] },
options: {
scales: {
"y-cycle": {},
"y-trend": {},
"y-foo": { title: {} },
x: { title: { display: false } },
},
plugins: {},
},
}
);
expect(cfg.options.scales["y-foo"].position).toBe("left");
expect(cfg.options.scales["y-foo"].title.text).toBe("foo, trend");
expect(cfg.options.scales["y-cycle"].position).toBe("right");
expect(cfg.options.scales["y-trend"].display).toBe(false);
expect(cfg.options.plugins.legend.display).toBe(true);
});
});
Loading

0 comments on commit fdb6eeb

Please sign in to comment.