Skip to content

Commit

Permalink
#529: resample timeseries
Browse files Browse the repository at this point in the history
  • Loading branch information
aschonfeld committed Jul 10, 2021
1 parent df6ac1f commit f727571
Show file tree
Hide file tree
Showing 16 changed files with 649 additions and 220 deletions.
42 changes: 42 additions & 0 deletions dtale/data_reshapers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ def __init__(self, data_id, shape_type, cfg):
self.builder = AggregateBuilder(cfg)
elif shape_type == "transpose":
self.builder = TransposeBuilder(cfg)
elif shape_type == "resample":
self.builder = ResampleBuilder(cfg)
else:
raise NotImplementedError(
"{} data re-shaper not implemented yet!".format(shape_type)
Expand Down Expand Up @@ -171,3 +173,43 @@ def build_code(self):
)
code.append("df = df.rename_axis(None, axis=1)")
return "\n".join(code)


class ResampleBuilder(object):
def __init__(self, cfg):
self.cfg = cfg

def reshape(self, data):
index, columns, freq, agg = (
self.cfg.get(p) for p in ["index", "columns", "freq", "agg"]
)
t_data = data.set_index(index)
if columns is not None:
t_data = t_data[columns]
t_data = getattr(t_data.resample(freq), agg)()
if not columns or len(columns) > 1:
t_data.columns = flatten_columns(t_data)
t_data.index.name = "{}_{}".format(index, freq)
t_data = t_data.reset_index()
return t_data

def build_code(self):
index, columns, freq, agg = (
self.cfg.get(p) for p in ["index", "columns", "freq", "agg"]
)
code = []
if columns is not None:
code.append(
"df = df.set_index('{}')['{}'].resample('{}').{}()".format(
index, "', '".join(columns), freq, agg
)
)
else:
code.append(
"df = df.set_index('{}').resample('{}').{}()".format(index, freq, agg)
)
if not columns or len(columns) > 1:
code.append(
"df.columns = [' '.join([str(c) for c in col]).strip() for col in df.columns.values]"
)
return "\n".join(code)
106 changes: 106 additions & 0 deletions static/__tests__/dtale/reshape/DataViewer-reshape-resample-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { mount } from "enzyme";
import React from "react";
import { Provider } from "react-redux";
import Select from "react-select";

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

import DimensionsHelper from "../../DimensionsHelper";
import mockPopsicle from "../../MockPopsicle";
import reduxUtils from "../../redux-test-utils";

import { buildInnerHTML, clickMainMenuButton, mockChartJS, tick, tickUpdate, withGlobalJquery } from "../../test-utils";

describe("DataViewer tests", () => {
const { location, open, opener } = window;
const dimensions = new DimensionsHelper({
offsetWidth: 800,
offsetHeight: 500,
innerWidth: 1205,
innerHeight: 775,
});
let result, Reshape, Resample, validateResampleCfg;

beforeAll(() => {
dimensions.beforeAll();

delete window.location;
delete window.open;
delete window.opener;
window.location = {
reload: jest.fn(),
pathname: "/dtale/iframe/1",
assign: jest.fn(),
};
window.open = jest.fn();
window.opener = { code_popup: { code: "test code", title: "Test" } };
const mockBuildLibs = withGlobalJquery(() =>
mockPopsicle.mock(url => {
const { urlFetcher } = require("../../redux-test-utils").default;
return urlFetcher(url);
})
);

mockChartJS();

jest.mock("popsicle", () => mockBuildLibs);

Reshape = require("../../../popups/reshape/Reshape").ReactReshape;
Resample = require("../../../popups/reshape/Resample").ReactResample;
validateResampleCfg = require("../../../popups/reshape/Resample").validateResampleCfg;
});

beforeEach(async () => {
const { DataViewer } = require("../../../dtale/DataViewer");
const store = reduxUtils.createDtaleStore();
buildInnerHTML({ settings: "" }, store);
result = mount(
<Provider store={store}>
<DataViewer />
</Provider>,
{ attachTo: document.getElementById("content") }
);
await tick();
clickMainMenuButton(result, "Timeseries");
await tickUpdate(result);
});

afterAll(() => {
dimensions.afterAll();
window.location = location;
window.open = open;
window.opener = opener;
});

it("DataViewer: reshape resample", async () => {
result.find(Reshape).find("div.modal-body").find("button").at(2).simulate("click");
expect(result.find(Resample).length).toBe(1);
const resampleComp = result.find(Resample).first();
const resampleInputs = resampleComp.find(Select);
resampleInputs.first().instance().onChange({ value: "col1" });
resampleComp
.find("div.form-group.row")
.at(2)
.find("input")
.first()
.simulate("change", { target: { value: "17min" } });
resampleInputs.last().instance().onChange({ value: "mean" });
result.find("div.modal-body").find("div.row").last().find("button").last().simulate("click");
result.find("div.modal-footer").first().find("button").first().simulate("click");
await tickUpdate(result);
expect(result.find(Reshape).length).toBe(1);
result.find("div.modal-body").find("div.row").last().find("button").first().simulate("click");
result.find("div.modal-footer").first().find("button").first().simulate("click");
await tickUpdate(result);
expect(result.find(Reshape).length).toBe(0);

const cfg = { index: null };
expect(validateResampleCfg(cfg)).toBe("Missing an index selection!");
cfg.index = "x";
expect(validateResampleCfg(cfg)).toBe("Missing offset!");
cfg.freq = "x";
expect(validateResampleCfg(cfg)).toBe("Missing aggregation!");
cfg.agg = "x";
expect(validateResampleCfg(cfg)).toBeNull();
});
});
2 changes: 2 additions & 0 deletions static/dtale/menu/DataViewerMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import ShowHideColumnsOption from "./ShowHideColumnsOption";
import { ShutdownOption } from "./ShutdownOption";
import SummarizeOption from "./SummarizeOption";
import { ThemeOption } from "./ThemeOption";
import TimeseriesOption from "./TimeseriesOption";
import UploadOption from "./UploadOption";
import { XArrayOption } from "./XArrayOption";
import menuFuncs from "./dataViewerMenuUtils";
Expand Down Expand Up @@ -96,6 +97,7 @@ class ReactDataViewerMenu extends React.Component {
<CleanColumn open={buttonHandlers.CLEAN} />
<MergeOption open={() => window.open(menuFuncs.fullPath("/dtale/popup/merge"), "_blank")} />
<SummarizeOption open={openPopup("reshape", 400, 770)} />
<TimeseriesOption open={openPopup("timeseries", 400, 770)} />
<DuplicatesOption open={buttonHandlers.DUPLICATES} />
<MissingOption open={() => this.props.showSidePanel("missingno")} />
<CorrelationAnalysisOption open={() => this.props.showSidePanel("corr_analysis")} />
Expand Down
31 changes: 31 additions & 0 deletions static/dtale/menu/TimeseriesOption.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import PropTypes from "prop-types";
import React from "react";
import { withTranslation } from "react-i18next";

import { MenuItem } from "./MenuItem";

class TimeseriesOption extends React.Component {
constructor(props) {
super(props);
}

render() {
return (
<MenuItem description={this.props.t("menu_description:timeseries")} onClick={this.props.open}>
<span className="toggler-action">
<button className="btn btn-plain">
<i className="ico-schedule ml-2" />
<span className="font-weight-bold">{this.props.t("menu:Timeseries")}</span>
</button>
</span>
</MenuItem>
);
}
}
TimeseriesOption.displayName = "TimeseriesOption";
TimeseriesOption.propTypes = {
open: PropTypes.func,
t: PropTypes.func,
};

export default withTranslation(["menu", "menu_description"])(TimeseriesOption);
2 changes: 2 additions & 0 deletions static/dtale/ribbon/RibbonDropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { ShowNonNumericHeatmapColumns } from "../menu/ShowNonNumericHeatmapColum
import { ShutdownOption } from "../menu/ShutdownOption";
import SummarizeOption from "../menu/SummarizeOption";
import { ThemeOption } from "../menu/ThemeOption";
import TimeseriesOption from "../menu/TimeseriesOption";
import UploadOption from "../menu/UploadOption";
import { XArrayOption } from "../menu/XArrayOption";
import menuFuncs from "../menu/dataViewerMenuUtils";
Expand Down Expand Up @@ -177,6 +178,7 @@ class ReactRibbonDropdown extends React.Component {
<CleanColumn open={hideWrapper(buttonHandlers.CLEAN)} />
<MergeOption open={hideWrapper(() => window.open(menuFuncs.fullPath("/dtale/popup/merge"), "_blank"))} />
<SummarizeOption open={hideWrapper(openPopup("reshape", 400, 770))} />
<TimeseriesOption open={openPopup("timeseries", 400, 770)} />
<CorrelationAnalysisOption open={hideWrapper(() => this.props.showSidePanel("corr_analysis"))} />
</ul>
)}
Expand Down
8 changes: 3 additions & 5 deletions static/popups/analysis/filters/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,9 @@ export const rollingComps = t => [
{ value: "var", label: t("constants:Variance") },
];

export const analysisAggs = t =>
_.concat(
_.reject(aggregationOpts(t), ({ value }) => value === "rolling"),
[{ value: "pctsum", label: t("constants:Percentage Sum") }]
);
export const analysisAggs = t => _.concat(pivotAggs(t), [{ value: "pctsum", label: t("constants:Percentage Sum") }]);

export const resampleAggs = t => _.concat(pivotAggs(t), [{ value: "ohlc", label: t("constants:OHLC") }]);

export const titles = t => ({
histogram: t("constants:Histogram"),
Expand Down
4 changes: 2 additions & 2 deletions static/popups/create/CreateExpanding.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from "react";
import { withTranslation } from "react-i18next";
import Select, { createFilter } from "react-select";

import { aggregationOpts } from "../analysis/filters/Constants";
import { pivotAggs } from "../analysis/filters/Constants";
import ColumnSelect from "./ColumnSelect";

export function validateExpandingCfg(t, { col, agg }) {
Expand Down Expand Up @@ -77,7 +77,7 @@ class CreateExpanding extends React.Component {
<Select
className="Select is-clearable is-searchable Select--single"
classNamePrefix="Select"
options={_.reject(aggregationOpts(t), { value: "rolling" })}
options={pivotAggs(t)}
getOptionLabel={_.property("label")}
getOptionValue={_.property("value")}
value={this.state.agg}
Expand Down
4 changes: 2 additions & 2 deletions static/popups/create/CreateTransform.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from "react";
import { withTranslation } from "react-i18next";
import Select, { createFilter } from "react-select";

import { aggregationOpts } from "../analysis/filters/Constants";
import { pivotAggs } from "../analysis/filters/Constants";
import ColumnSelect from "./ColumnSelect";

export function validateTransformCfg(t, { group, agg, col }) {
Expand Down Expand Up @@ -80,7 +80,7 @@ class CreateTransform extends React.Component {
<Select
className="Select is-clearable is-searchable Select--single"
classNamePrefix="Select"
options={_.reject(aggregationOpts(t), { value: "rolling" })}
options={pivotAggs(t)}
getOptionLabel={_.property("label")}
getOptionValue={_.property("value")}
value={this.state.agg}
Expand Down
12 changes: 12 additions & 0 deletions static/popups/popupUtils.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ function buildReshape(props) {
return { title, body };
}

function buildTimeseries(props) {
const title = (
<React.Fragment>
<i className="ico-schedule" />
<strong>{props.t("menu:Timeseries")}</strong>
</React.Fragment>
);
const body = <Reshape operation="timeseries" />;
return { title, body };
}

function buildAbout(props) {
const title = (
<React.Fragment>
Expand Down Expand Up @@ -298,6 +309,7 @@ const POPUP_MAP = {
"type-conversion": buildTypeConversion,
cleaners: buildCleaners,
reshape: buildReshape,
timeseries: buildTimeseries,
about: buildAbout,
confirm: buildConfirm,
"copy-range": buildCopyRange,
Expand Down
6 changes: 2 additions & 4 deletions static/popups/replacement/Value.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Select, { createFilter } from "react-select";

import { RemovableError } from "../../RemovableError";
import * as gu from "../../dtale/gridUtils";
import { aggregationOpts } from "../analysis/filters/Constants";
import { pivotAggs } from "../analysis/filters/Constants";

function validateValueCfg(cfgs) {
if (!_.size(cfgs)) {
Expand Down Expand Up @@ -209,9 +209,7 @@ class Value extends React.Component {
<Select
className="Select is-clearable is-searchable Select--single"
classNamePrefix="Select"
options={_.reject(aggregationOpts(this.props.t), {
value: "rolling",
})}
options={pivotAggs(this.props.t)}
getOptionLabel={_.property("value")}
getOptionValue={_.property("value")}
value={this.state.agg}
Expand Down
Loading

0 comments on commit f727571

Please sign in to comment.