diff --git a/.circleci/config.yml b/.circleci/config.yml
index 0997114c4..1ca878cd7 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -43,6 +43,7 @@ defaults: &defaults
- run:
name: Run JS Tests & Coverage
command: |
+ export TZ=America/New_York
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
diff --git a/dtale/app.py b/dtale/app.py
index 03769261a..12022d44d 100644
--- a/dtale/app.py
+++ b/dtale/app.py
@@ -484,7 +484,7 @@ def _start():
if cli is not None:
cli.show_server_banner = lambda *x: None
- app.run(host='0.0.0.0', port=ACTIVE_PORT, debug=debug)
+ app.run(host='0.0.0.0', port=ACTIVE_PORT, debug=debug, threaded=True)
if subprocess:
if is_active:
diff --git a/dtale/static/css/main.css b/dtale/static/css/main.css
index 92b5a68a3..afbf7feb1 100644
--- a/dtale/static/css/main.css
+++ b/dtale/static/css/main.css
@@ -10224,7 +10224,7 @@ table tbody tr.highlight-row {
background-color: rgba(82, 187, 239, 0.2);
}
-.custom-select.tag-select {
+.custom-select.axis-select {
background: white url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 2'%3E%3Cpath fill='%23333' d='M2 2 L0 0 H4 Z'/%3E%3C/svg%3E") no-repeat right 0.75rem center;
background-clip: padding-box;
background-size: 8px 10px;
diff --git a/dtale/templates/dtale/base.html b/dtale/templates/dtale/base.html
index 8347a9de4..7d4adf202 100644
--- a/dtale/templates/dtale/base.html
+++ b/dtale/templates/dtale/base.html
@@ -23,9 +23,9 @@
display:block;
top:0;
right:0;
- width:200px;
+ width:125px;
overflow:hidden;
- height:200px;
+ height:125px;
z-index:9999;
}
#forkongithub a{
@@ -35,13 +35,13 @@
font-family:arial,sans-serif;
text-align:center;
font-weight:bold;
- font-size:0.75rem;
+ font-size:0.45rem;
line-height:2rem;
transition:0.5s;
- width:200px;
+ width:125px;
position:absolute;
- top:45px;
- right:-45px;
+ top:17px;
+ right:-31px;
transform:rotate(45deg);
-webkit-transform:rotate(45deg);
-ms-transform:rotate(45deg);
diff --git a/dtale/utils.py b/dtale/utils.py
index 7b17537e8..51fb5991c 100644
--- a/dtale/utils.py
+++ b/dtale/utils.py
@@ -323,7 +323,11 @@ def format_dicts(self, lsts):
return [self.format_dict(l) for l in lsts]
def format_lists(self, df):
- return {name: [f(v, nan_display=self.nan_display) for v in df[name].values] for _idx, name, f in self.fmts}
+ return {
+ name: [f(v, nan_display=self.nan_display) for v in df[name].values]
+ for _idx, name, f in self.fmts
+ if name in df.columns
+ }
def classify_type(type_name):
diff --git a/dtale/views.py b/dtale/views.py
index 6b7cd34eb..e83b26057 100644
--- a/dtale/views.py
+++ b/dtale/views.py
@@ -16,12 +16,12 @@
from dtale.cli.clickutils import retrieve_meta_info_and_version
from dtale.utils import (build_shutdown_url, classify_type, dict_merge,
filter_df_for_grid, find_dtype_formatter,
- find_selected_column, get_dtypes, get_int_arg,
- get_str_arg, grid_columns, grid_formatter, json_date,
- json_float, json_int, json_timestamp, jsonify,
- make_list, retrieve_grid_params,
- running_with_flask_debug, running_with_pytest,
- sort_df_for_grid, swag_from)
+ find_selected_column, get_bool_arg, get_dtypes,
+ get_int_arg, get_str_arg, grid_columns,
+ grid_formatter, json_date, json_float, json_int,
+ json_timestamp, jsonify, make_list,
+ retrieve_grid_params, running_with_flask_debug,
+ running_with_pytest, sort_df_for_grid, swag_from)
logger = getLogger(__name__)
@@ -849,7 +849,7 @@ def get_correlations(data_id):
return jsonify(dict(error=str(e), traceback=str(traceback.format_exc())))
-def build_chart(data, x, y, group_col=None, agg=None):
+def build_chart(data, x, y, group_col=None, agg=None, allow_duplicates=False, **kwargs):
"""
Helper function to return data for 'chart-data' & 'correlations-ts' endpoints. Will return a dictionary of
dictionaries (one for each series) which contain the data for the x & y axes of the chart as well as the minimum &
@@ -870,11 +870,21 @@ def build_chart(data, x, y, group_col=None, agg=None):
:type aggregation: str, optional
:return: dict
"""
- x_col, y_col = str('x'), str('y')
- if group_col is not None:
- data = data[group_col + [x, y]].sort_values(group_col + [x])
- data.columns = group_col + [x_col, y_col]
+ def build_formatters(df):
+ cols = grid_columns(df)
+ overrides = {'D': lambda f, i, c: f.add_timestamp(i, c)}
+ data_f = grid_formatter(cols, overrides=overrides, nan_display=None)
+ overrides['F'] = lambda f, i, c: f.add_float(i, c, precision=2)
+ range_f = grid_formatter(cols, overrides=overrides, nan_display=None)
+ return data_f, range_f
+
+ x_col = str('x')
+ y_cols = y.split(',')
+ if group_col is not None:
+ data = data[group_col + [x] + y_cols].sort_values(group_col + [x])
+ y_cols = [str(y_col) for y_col in y_cols]
+ data.columns = group_col + [x_col] + y_cols
if agg is not None:
data = data.groupby(group_col + [x_col])
data = getattr(data, agg)().reset_index()
@@ -885,35 +895,43 @@ def build_chart(data, x, y, group_col=None, agg=None):
' or else chart will be unreadable'
).format(', '.join(group_col), max_groups)
raise Exception(msg)
- f = grid_formatter(
- grid_columns(data[[x_col, y_col]]), overrides={'D': lambda f, i, c: f.add_timestamp(i, c)}, nan_display=None
+
+ data_f, range_f = build_formatters(data[[x_col] + y_cols])
+ ret_data = dict(
+ data={},
+ min={col: fmt(data[col].min(), None) for _, col, fmt in range_f.fmts if col in [x_col] + y_cols},
+ max={col: fmt(data[col].max(), None) for _, col, fmt in range_f.fmts if col in [x_col] + y_cols},
)
- y_fmt = next((fmt for _, name, fmt in f.fmts if name == y_col), None)
- ret_data = dict(data={}, min=y_fmt(data[y_col].min(), None), max=y_fmt(data[y_col].max(), None))
dtypes = get_dtypes(data)
group_fmts = {c: find_dtype_formatter(dtypes[c]) for c in group_col}
for group_val, grp in data.groupby(group_col):
group_val = '/'.join([
group_fmts[gc](gv, as_string=True) for gv, gc in zip(make_list(group_val), group_col)
])
- ret_data['data'][group_val] = f.format_lists(grp)
+ ret_data['data'][group_val] = data_f.format_lists(grp)
return ret_data
- data = data[[x, y]].sort_values(x)
- data.columns = [x_col, y_col]
+ data = data[[x] + y_cols].sort_values(x)
+ y_cols = [str(y_col) for y_col in y_cols]
+ data.columns = [x_col] + y_cols
if agg is not None:
- data = data.groupby(x_col)
- data = getattr(data, agg)().reset_index()
+ if agg == 'rolling':
+ window, comp = map(kwargs.get, ['rolling_win', 'rolling_comp'])
+ data = data.set_index(x_col).rolling(window=window)
+ data = pd.DataFrame({c: getattr(data[c], comp)() for c in y_cols})
+ data = data.reset_index()
+ else:
+ data = data.groupby(x_col)
+ data = getattr(data[y_cols], agg)().reset_index()
- if any(data[x_col].duplicated()):
+ if not allow_duplicates and any(data[x_col].duplicated()):
raise Exception('{} contains duplicates, please specify group or additional filtering'.format(x))
- f = grid_formatter(
- grid_columns(data), overrides={'D': lambda f, i, c: f.add_timestamp(i, c)}, nan_display=None
- )
- y_fmt = next((fmt for _, name, fmt in f.fmts if name == y_col), None)
+ if len(data) > 15000:
+ raise Exception('Dataset exceeds 15,000 records, cannot render. Please apply filter...')
+ data_f, range_f = build_formatters(data)
ret_data = dict(
- data={str('all'): f.format_lists(data)},
- min=y_fmt(data[y_col].min(), None),
- max=y_fmt(data[y_col].max(), None)
+ data={str('all'): data_f.format_lists(data)},
+ min={col: fmt(data[col].min(), None) for _, col, fmt in range_f.fmts if col in [x_col] + y_cols},
+ max={col: fmt(data[col].max(), None) for _, col, fmt in range_f.fmts if col in [x_col] + y_cols},
)
return ret_data
@@ -960,7 +978,12 @@ def get_chart_data(data_id):
if group_col is not None:
group_col = group_col.split(',')
agg = get_str_arg(request, 'agg')
- return jsonify(build_chart(data, x, y, group_col, agg))
+ allow_duplicates = get_bool_arg(request, 'allowDupes')
+ window = get_int_arg(request, 'rollingWin')
+ comp = get_str_arg(request, 'rollingComp')
+ data = build_chart(data, x, y, group_col, agg, allow_duplicates, rolling_win=window, rolling_comp=comp)
+ data['success'] = True
+ return jsonify(data)
except BaseException as e:
return jsonify(dict(error=str(e), traceback=str(traceback.format_exc())))
@@ -1061,8 +1084,10 @@ def get_scatter(data_id):
stats=stats,
error='Dataset exceeds 15,000 records, cannot render scatter. Please apply filter...'
)
- f = grid_formatter(grid_columns(data))
- data = f.format_dicts(data.itertuples())
- return jsonify(data=data, x=cols[0], y=cols[1], stats=stats)
+ data = build_chart(data, cols[0], str('{},index'.format(cols[1])), allow_duplicates=True)
+ data['x'] = cols[0]
+ data['y'] = cols[1]
+ data['stats'] = stats
+ return jsonify(data)
except BaseException as e:
return jsonify(dict(error=str(e), traceback=str(traceback.format_exc())))
diff --git a/static/__tests__/chartUtils-test.jsx b/static/__tests__/chartUtils-test.jsx
index 08c345533..c7d375b47 100644
--- a/static/__tests__/chartUtils-test.jsx
+++ b/static/__tests__/chartUtils-test.jsx
@@ -11,6 +11,12 @@ const GRADIENT_FUNCS = [
"pointHoverBorderColor",
];
+function updateDataCfg(prop, data, cfg) {
+ cfg.data.all[prop] = data;
+ cfg.min[prop] = data[0];
+ cfg.max[prop] = data[data.length - 1];
+}
+
describe("chartUtils tests", () => {
beforeAll(() => {
const mockChartUtils = withGlobalJquery(() => (ctx, cfg) => {
@@ -30,12 +36,12 @@ describe("chartUtils tests", () => {
const { colorScale } = require("../popups/correlations/correlationsUtils").default;
const { gradientLinePlugin } = require("../chartUtils").default;
- const plugin = gradientLinePlugin(colorScale, 1, -1);
+ const plugin = gradientLinePlugin(colorScale, "y-corr", 1, -1);
const dataset = { data: [{ x: 0, y: 0 }] };
const chartInstance = {
data: { datasets: [dataset] },
scales: {
- "y-axis-0": {
+ "y-corr": {
getPixelForValue: px => px,
},
},
@@ -83,4 +89,203 @@ describe("chartUtils tests", () => {
_.forEach(GRADIENT_FUNCS, f => t.ok(_.isFunction(dataset[f].addColorStop), "should set gradients"));
done();
});
+
+ test("chartUtils: testing timestampLabel", done => {
+ const { timestampLabel } = require("../chartUtils").default;
+
+ t.equal(timestampLabel(1514782800000), "2018-01-01", "should return date string");
+ const tsLabel = timestampLabel(1514786400000);
+ t.ok(tsLabel === "2018-01-01 1:00:00 am", "should return timestamp string");
+ done();
+ });
+
+ test("chartUtils: testing createLineCfg with date axes", done => {
+ const { createLineCfg } = require("../chartUtils").default;
+
+ const data = {
+ data: {
+ all: {
+ y: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6],
+ x: [1514782800000, 1514786400000, 1514790000000, 1514793600000, 1514797200000, 1514800800000],
+ },
+ },
+ min: { x: 1514782800000, y: 0.1 },
+ max: { x: 1514800800000, y: 0.6 },
+ x: "x",
+ y: "y",
+ };
+ const columns = [
+ { name: "x", dtype: "datetime64[ns]" },
+ { name: "y", dtype: "float64" },
+ ];
+ let cfg = createLineCfg(data, {
+ columns,
+ x: "x",
+ y: "y",
+ configHandler: cfg => cfg,
+ });
+ t.deepEqual(
+ cfg.options.scales.xAxes[0].time,
+ { unit: "hour", stepSize: 4, displayFormats: { hour: "YYYYMMDD hA" } },
+ "should build hourly config"
+ );
+ t.equal(cfg.options.tooltips.callbacks.label({ yLabel: 0.1, datasetIndex: 0 }, cfg.data), "0.1");
+ updateDataCfg(
+ "x",
+ [1578200400000, 1578805200000, 1579410000000, 1580014800000, 1580619600000, 1581224400000],
+ data
+ );
+ cfg = createLineCfg(data, {
+ columns,
+ x: "x",
+ y: "y",
+ configHandler: cfg => cfg,
+ });
+ t.deepEqual(
+ cfg.options.scales.xAxes[0].time,
+ { unit: "day", stepSize: 1, displayFormats: { day: "YYYYMMDD" } },
+ "should build daily config"
+ );
+ updateDataCfg(
+ "x",
+ [1578200400000, 1578805200000, 1579410000000, 1592107200000, 1592712000000, 1593316800000],
+ data
+ );
+ cfg = createLineCfg(data, {
+ columns,
+ x: "x",
+ y: "y",
+ configHandler: cfg => cfg,
+ });
+ t.deepEqual(
+ cfg.options.scales.xAxes[0].time,
+ { unit: "week", stepSize: 1, displayFormats: { week: "YYYYMMDD" } },
+ "should build weekly config"
+ );
+ updateDataCfg(
+ "x",
+ [1580446800000, 1582952400000, 1585627200000, 1635652800000, 1638248400000, 1640926800000],
+ data
+ );
+ cfg = createLineCfg(data, {
+ columns,
+ x: "x",
+ y: "y",
+ configHandler: cfg => cfg,
+ });
+ t.deepEqual(
+ cfg.options.scales.xAxes[0].time,
+ { unit: "month", stepSize: 1, displayFormats: { month: "YYYYMMDD" } },
+ "should build monthly config"
+ );
+ updateDataCfg(
+ "x",
+ [1585627200000, 1593489600000, 1601438400000, 1719720000000, 1727668800000, 1735621200000],
+ data
+ );
+ cfg = createLineCfg(data, {
+ columns,
+ x: "x",
+ y: "y",
+ configHandler: cfg => cfg,
+ });
+ t.deepEqual(
+ cfg.options.scales.xAxes[0].time,
+ { unit: "quarter", stepSize: 1, displayFormats: { quarter: "YYYYMMDD" } },
+ "should build quarterly config"
+ );
+ updateDataCfg(
+ "x",
+ [1609390800000, 1640926800000, 1672462800000, 1988082000000, 2019618000000, 2051154000000],
+ data
+ );
+ cfg = createLineCfg(data, {
+ columns,
+ x: "x",
+ y: "y",
+ configHandler: cfg => cfg,
+ });
+ t.deepEqual(
+ cfg.options.scales.xAxes[0].time,
+ { unit: "year", stepSize: 1, displayFormats: { year: "YYYYMMDD" } },
+ "should build yearly config"
+ );
+ done();
+ });
+
+ test("chartUtils: testing createLineCfg with date y-axis", done => {
+ const { createLineCfg, createScatterCfg } = require("../chartUtils").default;
+
+ const data = {
+ data: {
+ all: {
+ x: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6],
+ y: [1514782800000, 1514786400000, 1514790000000, 1514793600000, 1514797200000, 1514800800000],
+ },
+ },
+ min: { y: 1514782800000, x: 0.1 },
+ max: { y: 1514800800000, x: 0.6 },
+ x: "x",
+ y: "y",
+ };
+ const columns = [
+ { name: "y", dtype: "datetime64[ns]" },
+ { name: "x", dtype: "float64" },
+ ];
+ let cfg = createLineCfg(data, {
+ columns,
+ x: "x",
+ y: "y",
+ configHandler: cfg => cfg,
+ });
+ t.deepEqual(
+ cfg.options.scales.yAxes[0].time,
+ { unit: "hour", stepSize: 4, displayFormats: { hour: "YYYYMMDD hA" } },
+ "should build hourly config for y-axis"
+ );
+ cfg = createScatterCfg(data, {
+ columns,
+ x: "x",
+ y: "y",
+ configHandler: cfg => cfg,
+ });
+ t.ok(cfg.data.datasets[0].data[0].y == 1514782800000);
+ done();
+ });
+
+ test("chartUtils: testing buildTicks", done => {
+ const { buildTicks } = require("../chartUtils").default;
+ const range = { min: { y: 0.1 }, max: { y: 0.6 } };
+ t.deepEqual(buildTicks("y", range, true), { min: 0.095, max: 0.605 }, "should build padded ticks");
+ done();
+ });
+
+ test("chartUtils: testing buildSeries", done => {
+ const { createLineCfg } = require("../chartUtils").default;
+
+ const series = {
+ y: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6],
+ x: [1514782800000, 1514786400000, 1514790000000, 1514793600000, 1514797200000, 1514800800000],
+ };
+ const data = {
+ data: { series1: series, series2: series },
+ min: { x: 1514782800000, y: 0.1 },
+ max: { x: 1514800800000, y: 0.6 },
+ x: "x",
+ y: "y",
+ };
+ const columns = [
+ { name: "x", dtype: "datetime64[ns]" },
+ { name: "y", dtype: "float64" },
+ ];
+ const cfg = createLineCfg(data, {
+ columns,
+ x: "x",
+ y: "y",
+ configHandler: cfg => cfg,
+ });
+ t.equal(cfg.data.datasets[0].label, "series1", "should construct series label");
+ t.equal(cfg.options.tooltips.callbacks.label({ yLabel: 0.1, datasetIndex: 0 }, cfg.data), "series1: 0.1");
+ done();
+ });
});
diff --git a/static/__tests__/data/charts-grouped.json b/static/__tests__/data/charts-grouped.json
index 8e8719558..aa7d54700 100644
--- a/static/__tests__/data/charts-grouped.json
+++ b/static/__tests__/data/charts-grouped.json
@@ -11,1446 +11,9 @@
1545627600000,
1545714000000,
1545800400000,
- 1545886800000,
- 1545973200000,
- 1546059600000,
- 1546146000000,
- 1546232400000,
- 1546318800000,
- 1546405200000,
- 1546491600000,
- 1546578000000,
- 1546664400000,
- 1546750800000,
- 1546837200000,
- 1546923600000,
- 1547010000000,
- 1547096400000,
- 1547182800000,
- 1547269200000,
- 1547355600000,
- 1547442000000,
- 1547528400000,
- 1547614800000,
- 1547701200000,
- 1547787600000,
- 1547874000000,
- 1547960400000,
- 1548046800000,
- 1548133200000,
- 1548219600000,
- 1548306000000,
- 1548392400000,
- 1548478800000,
- 1548565200000,
- 1548651600000,
- 1548738000000,
- 1548824400000,
- 1548910800000,
- 1548997200000,
- 1549083600000,
- 1549170000000,
- 1549256400000,
- 1549342800000,
- 1549429200000,
- 1549515600000,
- 1549602000000,
- 1549688400000,
- 1549774800000,
- 1549861200000,
- 1549947600000,
- 1550034000000,
- 1550120400000,
- 1550206800000,
- 1550293200000,
- 1550379600000,
- 1550466000000,
- 1550552400000,
- 1550638800000,
- 1550725200000,
- 1550811600000,
- 1550898000000,
- 1550984400000,
- 1551070800000,
- 1551157200000,
- 1551243600000,
- 1551330000000,
- 1551416400000,
- 1551502800000,
- 1551589200000,
- 1551675600000,
- 1551762000000,
- 1551848400000,
- 1551934800000,
- 1552021200000,
- 1552107600000,
- 1552194000000,
- 1552276800000,
- 1552363200000,
- 1552449600000,
- 1552536000000,
- 1552622400000,
- 1552708800000,
- 1552795200000,
- 1552881600000,
- 1552968000000,
- 1553054400000,
- 1553140800000,
- 1553227200000,
- 1553313600000,
- 1553400000000,
- 1553486400000,
- 1553572800000,
- 1553659200000,
- 1553745600000,
- 1553832000000,
- 1553918400000,
- 1554004800000,
- 1554091200000,
- 1554177600000,
- 1554264000000,
- 1554350400000,
- 1554436800000,
- 1554523200000,
- 1554609600000,
- 1554696000000,
- 1554782400000,
- 1554868800000,
- 1554955200000,
- 1555041600000,
- 1555128000000,
- 1555214400000,
- 1555300800000,
- 1555387200000,
- 1555473600000,
- 1555560000000,
- 1555646400000,
- 1555732800000,
- 1555819200000,
- 1555905600000,
- 1555992000000,
- 1556078400000,
- 1556164800000,
- 1556251200000,
- 1556337600000,
- 1556424000000,
- 1556510400000,
- 1556596800000,
- 1556683200000,
- 1556769600000,
- 1556856000000,
- 1556942400000,
- 1557028800000,
- 1557115200000,
- 1557201600000,
- 1557288000000,
- 1557374400000,
- 1557460800000,
- 1557547200000,
- 1557633600000,
- 1557720000000,
- 1557806400000,
- 1557892800000,
- 1557979200000,
- 1558065600000,
- 1558152000000,
- 1558238400000,
- 1558324800000,
- 1558411200000,
- 1558497600000,
- 1558584000000,
- 1558670400000,
- 1558756800000,
- 1558843200000,
- 1558929600000,
- 1559016000000,
- 1559102400000,
- 1559188800000,
- 1559275200000,
- 1559361600000,
- 1559448000000,
- 1559534400000,
- 1559620800000,
- 1559707200000,
- 1559793600000,
- 1559880000000,
- 1559966400000,
- 1560052800000,
- 1560139200000,
- 1560225600000,
- 1560312000000,
- 1560398400000,
- 1560484800000,
- 1560571200000,
- 1560657600000,
- 1560744000000,
- 1560830400000,
- 1560916800000,
- 1561003200000,
- 1561089600000,
- 1561176000000,
- 1561262400000,
- 1561348800000,
- 1561435200000,
- 1561521600000,
- 1561608000000,
- 1561694400000,
- 1561780800000,
- 1561867200000,
- 1561953600000,
- 1562040000000,
- 1562126400000,
- 1562212800000,
- 1562299200000,
- 1562385600000,
- 1562472000000,
- 1562558400000,
- 1562644800000,
- 1562731200000,
- 1562817600000,
- 1562904000000,
- 1562990400000,
- 1563076800000,
- 1563163200000,
- 1563249600000,
- 1563336000000,
- 1563422400000,
- 1563508800000,
- 1563595200000,
- 1563681600000,
- 1563768000000,
- 1563854400000,
- 1563940800000,
- 1564027200000,
- 1564113600000,
- 1564200000000,
- 1564286400000,
- 1564372800000,
- 1564459200000,
- 1564545600000,
- 1564632000000,
- 1564718400000,
- 1564804800000,
- 1564891200000,
- 1564977600000,
- 1565064000000,
- 1565150400000,
- 1565236800000,
- 1565323200000,
- 1565409600000,
- 1565496000000,
- 1565582400000,
- 1565668800000,
- 1565755200000,
- 1565841600000,
- 1565928000000,
- 1566014400000,
- 1566100800000,
- 1566187200000,
- 1566273600000,
- 1566360000000,
- 1566446400000,
- 1566532800000,
- 1566619200000,
- 1566705600000,
- 1566792000000,
- 1566878400000,
- 1566964800000,
- 1567051200000,
- 1567137600000,
- 1567224000000,
- 1567310400000,
- 1567396800000,
- 1567483200000,
- 1567569600000,
- 1567656000000,
- 1567742400000,
- 1567828800000,
- 1567915200000,
- 1568001600000,
- 1568088000000,
- 1568174400000,
- 1568260800000,
- 1568347200000,
- 1568433600000,
- 1568520000000,
- 1568606400000,
- 1568692800000,
- 1568779200000,
- 1568865600000,
- 1568952000000,
- 1569038400000,
- 1569124800000,
- 1569211200000,
- 1569297600000,
- 1569384000000,
- 1569470400000,
- 1569556800000,
- 1569643200000,
- 1569729600000,
- 1569816000000,
- 1569902400000,
- 1569988800000,
- 1570075200000,
- 1570161600000,
- 1570248000000,
- 1570334400000,
- 1570420800000,
- 1570507200000,
- 1570593600000,
- 1570680000000,
- 1570766400000,
- 1570852800000,
- 1570939200000,
- 1571025600000,
- 1571112000000,
- 1571198400000,
- 1571284800000,
- 1571371200000,
- 1571457600000,
- 1571544000000,
- 1571630400000,
- 1571716800000,
- 1571803200000,
- 1571889600000,
- 1571976000000,
- 1572062400000,
- 1572148800000,
- 1572235200000,
- 1572321600000,
- 1572408000000,
- 1572494400000,
- 1572580800000,
- 1572667200000,
- 1572753600000,
- 1572843600000,
- 1572930000000,
- 1573016400000,
- 1573102800000,
- 1573189200000,
- 1573275600000,
- 1573362000000,
- 1573448400000,
- 1573534800000,
- 1573621200000,
- 1573707600000,
- 1573794000000,
- 1573880400000,
- 1573966800000,
- 1574053200000,
- 1574139600000,
- 1574226000000,
- 1574312400000,
- 1574398800000,
- 1574485200000,
- 1574571600000,
- 1574658000000,
- 1574744400000,
- 1574830800000,
- 1574917200000,
- 1575003600000,
- 1575090000000,
- 1575176400000,
- 1575262800000,
- 1575349200000,
- 1575435600000,
- 1575522000000,
- 1575608400000,
- 1575694800000,
- 1575781200000,
- 1575867600000,
- 1575954000000,
- 1576040400000,
- 1576126800000,
- 1576213200000,
- 1576299600000,
- 1576386000000,
- 1576472400000,
- 1576558800000
+ 1545886800000
],
- "y": [
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41
- ]
- },
- "val2": {
- "x": [
- 1545109200000,
- 1545195600000,
- 1545282000000,
- 1545368400000,
- 1545454800000,
- 1545541200000,
- 1545627600000,
- 1545714000000,
- 1545800400000,
- 1545886800000,
- 1545973200000,
- 1546059600000,
- 1546146000000,
- 1546232400000,
- 1546318800000,
- 1546405200000,
- 1546491600000,
- 1546578000000,
- 1546664400000,
- 1546750800000,
- 1546837200000,
- 1546923600000,
- 1547010000000,
- 1547096400000,
- 1547182800000,
- 1547269200000,
- 1547355600000,
- 1547442000000,
- 1547528400000,
- 1547614800000,
- 1547701200000,
- 1547787600000,
- 1547874000000,
- 1547960400000,
- 1548046800000,
- 1548133200000,
- 1548219600000,
- 1548306000000,
- 1548392400000,
- 1548478800000,
- 1548565200000,
- 1548651600000,
- 1548738000000,
- 1548824400000,
- 1548910800000,
- 1548997200000,
- 1549083600000,
- 1549170000000,
- 1549256400000,
- 1549342800000,
- 1549429200000,
- 1549515600000,
- 1549602000000,
- 1549688400000,
- 1549774800000,
- 1549861200000,
- 1549947600000,
- 1550034000000,
- 1550120400000,
- 1550206800000,
- 1550293200000,
- 1550379600000,
- 1550466000000,
- 1550552400000,
- 1550638800000,
- 1550725200000,
- 1550811600000,
- 1550898000000,
- 1550984400000,
- 1551070800000,
- 1551157200000,
- 1551243600000,
- 1551330000000,
- 1551416400000,
- 1551502800000,
- 1551589200000,
- 1551675600000,
- 1551762000000,
- 1551848400000,
- 1551934800000,
- 1552021200000,
- 1552107600000,
- 1552194000000,
- 1552276800000,
- 1552363200000,
- 1552449600000,
- 1552536000000,
- 1552622400000,
- 1552708800000,
- 1552795200000,
- 1552881600000,
- 1552968000000,
- 1553054400000,
- 1553140800000,
- 1553227200000,
- 1553313600000,
- 1553400000000,
- 1553486400000,
- 1553572800000,
- 1553659200000,
- 1553745600000,
- 1553832000000,
- 1553918400000,
- 1554004800000,
- 1554091200000,
- 1554177600000,
- 1554264000000,
- 1554350400000,
- 1554436800000,
- 1554523200000,
- 1554609600000,
- 1554696000000,
- 1554782400000,
- 1554868800000,
- 1554955200000,
- 1555041600000,
- 1555128000000,
- 1555214400000,
- 1555300800000,
- 1555387200000,
- 1555473600000,
- 1555560000000,
- 1555646400000,
- 1555732800000,
- 1555819200000,
- 1555905600000,
- 1555992000000,
- 1556078400000,
- 1556164800000,
- 1556251200000,
- 1556337600000,
- 1556424000000,
- 1556510400000,
- 1556596800000,
- 1556683200000,
- 1556769600000,
- 1556856000000,
- 1556942400000,
- 1557028800000,
- 1557115200000,
- 1557201600000,
- 1557288000000,
- 1557374400000,
- 1557460800000,
- 1557547200000,
- 1557633600000,
- 1557720000000,
- 1557806400000,
- 1557892800000,
- 1557979200000,
- 1558065600000,
- 1558152000000,
- 1558238400000,
- 1558324800000,
- 1558411200000,
- 1558497600000,
- 1558584000000,
- 1558670400000,
- 1558756800000,
- 1558843200000,
- 1558929600000,
- 1559016000000,
- 1559102400000,
- 1559188800000,
- 1559275200000,
- 1559361600000,
- 1559448000000,
- 1559534400000,
- 1559620800000,
- 1559707200000,
- 1559793600000,
- 1559880000000,
- 1559966400000,
- 1560052800000,
- 1560139200000,
- 1560225600000,
- 1560312000000,
- 1560398400000,
- 1560484800000,
- 1560571200000,
- 1560657600000,
- 1560744000000,
- 1560830400000,
- 1560916800000,
- 1561003200000,
- 1561089600000,
- 1561176000000,
- 1561262400000,
- 1561348800000,
- 1561435200000,
- 1561521600000,
- 1561608000000,
- 1561694400000,
- 1561780800000,
- 1561867200000,
- 1561953600000,
- 1562040000000,
- 1562126400000,
- 1562212800000,
- 1562299200000,
- 1562385600000,
- 1562472000000,
- 1562558400000,
- 1562644800000,
- 1562731200000,
- 1562817600000,
- 1562904000000,
- 1562990400000,
- 1563076800000,
- 1563163200000,
- 1563249600000,
- 1563336000000,
- 1563422400000,
- 1563508800000,
- 1563595200000,
- 1563681600000,
- 1563768000000,
- 1563854400000,
- 1563940800000,
- 1564027200000,
- 1564113600000,
- 1564200000000,
- 1564286400000,
- 1564372800000,
- 1564459200000,
- 1564545600000,
- 1564632000000,
- 1564718400000,
- 1564804800000,
- 1564891200000,
- 1564977600000,
- 1565064000000,
- 1565150400000,
- 1565236800000,
- 1565323200000,
- 1565409600000,
- 1565496000000,
- 1565582400000,
- 1565668800000,
- 1565755200000,
- 1565841600000,
- 1565928000000,
- 1566014400000,
- 1566100800000,
- 1566187200000,
- 1566273600000,
- 1566360000000,
- 1566446400000,
- 1566532800000,
- 1566619200000,
- 1566705600000,
- 1566792000000,
- 1566878400000,
- 1566964800000,
- 1567051200000,
- 1567137600000,
- 1567224000000,
- 1567310400000,
- 1567396800000,
- 1567483200000,
- 1567569600000,
- 1567656000000,
- 1567742400000,
- 1567828800000,
- 1567915200000,
- 1568001600000,
- 1568088000000,
- 1568174400000,
- 1568260800000,
- 1568347200000,
- 1568433600000,
- 1568520000000,
- 1568606400000,
- 1568692800000,
- 1568779200000,
- 1568865600000,
- 1568952000000,
- 1569038400000,
- 1569124800000,
- 1569211200000,
- 1569297600000,
- 1569384000000,
- 1569470400000,
- 1569556800000,
- 1569643200000,
- 1569729600000,
- 1569816000000,
- 1569902400000,
- 1569988800000,
- 1570075200000,
- 1570161600000,
- 1570248000000,
- 1570334400000,
- 1570420800000,
- 1570507200000,
- 1570593600000,
- 1570680000000,
- 1570766400000,
- 1570852800000,
- 1570939200000,
- 1571025600000,
- 1571112000000,
- 1571198400000,
- 1571284800000,
- 1571371200000,
- 1571457600000,
- 1571544000000,
- 1571630400000,
- 1571716800000,
- 1571803200000,
- 1571889600000,
- 1571976000000,
- 1572062400000,
- 1572148800000,
- 1572235200000,
- 1572321600000,
- 1572408000000,
- 1572494400000,
- 1572580800000,
- 1572667200000,
- 1572753600000,
- 1572843600000,
- 1572930000000,
- 1573016400000,
- 1573102800000,
- 1573189200000,
- 1573275600000,
- 1573362000000,
- 1573448400000,
- 1573534800000,
- 1573621200000,
- 1573707600000,
- 1573794000000,
- 1573880400000,
- 1573966800000,
- 1574053200000,
- 1574139600000,
- 1574226000000,
- 1574312400000,
- 1574398800000,
- 1574485200000,
- 1574571600000,
- 1574658000000,
- 1574744400000,
- 1574830800000,
- 1574917200000,
- 1575003600000,
- 1575090000000,
- 1575176400000,
- 1575262800000,
- 1575349200000,
- 1575435600000,
- 1575522000000,
- 1575608400000,
- 1575694800000,
- 1575781200000,
- 1575867600000,
- 1575954000000,
- 1576040400000,
- 1576126800000,
- 1576213200000,
- 1576299600000,
- 1576386000000,
- 1576472400000,
- 1576558800000
- ],
- "y": [
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
+ "col1": [
41,
41,
41,
@@ -1460,6 +23,23 @@
41,
41,
41,
+ 41
+ ]
+ },
+ "val2": {
+ "x": [
+ 1545109200000,
+ 1545195600000,
+ 1545282000000,
+ 1545368400000,
+ 1545454800000,
+ 1545541200000,
+ 1545627600000,
+ 1545714000000,
+ 1545800400000,
+ 1545886800000
+ ],
+ "col1": [
41,
41,
41,
@@ -1473,6 +53,8 @@
]
}
},
- "max": 41,
- "min": 41
+ "max": {"col1": 41, "col4": 1545109200000},
+ "min": {"col1": 41, "col4": 1545800400000},
+ "x": "col4",
+ "y": "col1"
}
\ No newline at end of file
diff --git a/static/__tests__/data/charts.json b/static/__tests__/data/charts.json
index 6d7a99d60..9e9411f91 100644
--- a/static/__tests__/data/charts.json
+++ b/static/__tests__/data/charts.json
@@ -10,720 +10,9 @@
1545541200000,
1545627600000,
1545714000000,
- 1545800400000,
- 1545886800000,
- 1545973200000,
- 1546059600000,
- 1546146000000,
- 1546232400000,
- 1546318800000,
- 1546405200000,
- 1546491600000,
- 1546578000000,
- 1546664400000,
- 1546750800000,
- 1546837200000,
- 1546923600000,
- 1547010000000,
- 1547096400000,
- 1547182800000,
- 1547269200000,
- 1547355600000,
- 1547442000000,
- 1547528400000,
- 1547614800000,
- 1547701200000,
- 1547787600000,
- 1547874000000,
- 1547960400000,
- 1548046800000,
- 1548133200000,
- 1548219600000,
- 1548306000000,
- 1548392400000,
- 1548478800000,
- 1548565200000,
- 1548651600000,
- 1548738000000,
- 1548824400000,
- 1548910800000,
- 1548997200000,
- 1549083600000,
- 1549170000000,
- 1549256400000,
- 1549342800000,
- 1549429200000,
- 1549515600000,
- 1549602000000,
- 1549688400000,
- 1549774800000,
- 1549861200000,
- 1549947600000,
- 1550034000000,
- 1550120400000,
- 1550206800000,
- 1550293200000,
- 1550379600000,
- 1550466000000,
- 1550552400000,
- 1550638800000,
- 1550725200000,
- 1550811600000,
- 1550898000000,
- 1550984400000,
- 1551070800000,
- 1551157200000,
- 1551243600000,
- 1551330000000,
- 1551416400000,
- 1551502800000,
- 1551589200000,
- 1551675600000,
- 1551762000000,
- 1551848400000,
- 1551934800000,
- 1552021200000,
- 1552107600000,
- 1552194000000,
- 1552276800000,
- 1552363200000,
- 1552449600000,
- 1552536000000,
- 1552622400000,
- 1552708800000,
- 1552795200000,
- 1552881600000,
- 1552968000000,
- 1553054400000,
- 1553140800000,
- 1553227200000,
- 1553313600000,
- 1553400000000,
- 1553486400000,
- 1553572800000,
- 1553659200000,
- 1553745600000,
- 1553832000000,
- 1553918400000,
- 1554004800000,
- 1554091200000,
- 1554177600000,
- 1554264000000,
- 1554350400000,
- 1554436800000,
- 1554523200000,
- 1554609600000,
- 1554696000000,
- 1554782400000,
- 1554868800000,
- 1554955200000,
- 1555041600000,
- 1555128000000,
- 1555214400000,
- 1555300800000,
- 1555387200000,
- 1555473600000,
- 1555560000000,
- 1555646400000,
- 1555732800000,
- 1555819200000,
- 1555905600000,
- 1555992000000,
- 1556078400000,
- 1556164800000,
- 1556251200000,
- 1556337600000,
- 1556424000000,
- 1556510400000,
- 1556596800000,
- 1556683200000,
- 1556769600000,
- 1556856000000,
- 1556942400000,
- 1557028800000,
- 1557115200000,
- 1557201600000,
- 1557288000000,
- 1557374400000,
- 1557460800000,
- 1557547200000,
- 1557633600000,
- 1557720000000,
- 1557806400000,
- 1557892800000,
- 1557979200000,
- 1558065600000,
- 1558152000000,
- 1558238400000,
- 1558324800000,
- 1558411200000,
- 1558497600000,
- 1558584000000,
- 1558670400000,
- 1558756800000,
- 1558843200000,
- 1558929600000,
- 1559016000000,
- 1559102400000,
- 1559188800000,
- 1559275200000,
- 1559361600000,
- 1559448000000,
- 1559534400000,
- 1559620800000,
- 1559707200000,
- 1559793600000,
- 1559880000000,
- 1559966400000,
- 1560052800000,
- 1560139200000,
- 1560225600000,
- 1560312000000,
- 1560398400000,
- 1560484800000,
- 1560571200000,
- 1560657600000,
- 1560744000000,
- 1560830400000,
- 1560916800000,
- 1561003200000,
- 1561089600000,
- 1561176000000,
- 1561262400000,
- 1561348800000,
- 1561435200000,
- 1561521600000,
- 1561608000000,
- 1561694400000,
- 1561780800000,
- 1561867200000,
- 1561953600000,
- 1562040000000,
- 1562126400000,
- 1562212800000,
- 1562299200000,
- 1562385600000,
- 1562472000000,
- 1562558400000,
- 1562644800000,
- 1562731200000,
- 1562817600000,
- 1562904000000,
- 1562990400000,
- 1563076800000,
- 1563163200000,
- 1563249600000,
- 1563336000000,
- 1563422400000,
- 1563508800000,
- 1563595200000,
- 1563681600000,
- 1563768000000,
- 1563854400000,
- 1563940800000,
- 1564027200000,
- 1564113600000,
- 1564200000000,
- 1564286400000,
- 1564372800000,
- 1564459200000,
- 1564545600000,
- 1564632000000,
- 1564718400000,
- 1564804800000,
- 1564891200000,
- 1564977600000,
- 1565064000000,
- 1565150400000,
- 1565236800000,
- 1565323200000,
- 1565409600000,
- 1565496000000,
- 1565582400000,
- 1565668800000,
- 1565755200000,
- 1565841600000,
- 1565928000000,
- 1566014400000,
- 1566100800000,
- 1566187200000,
- 1566273600000,
- 1566360000000,
- 1566446400000,
- 1566532800000,
- 1566619200000,
- 1566705600000,
- 1566792000000,
- 1566878400000,
- 1566964800000,
- 1567051200000,
- 1567137600000,
- 1567224000000,
- 1567310400000,
- 1567396800000,
- 1567483200000,
- 1567569600000,
- 1567656000000,
- 1567742400000,
- 1567828800000,
- 1567915200000,
- 1568001600000,
- 1568088000000,
- 1568174400000,
- 1568260800000,
- 1568347200000,
- 1568433600000,
- 1568520000000,
- 1568606400000,
- 1568692800000,
- 1568779200000,
- 1568865600000,
- 1568952000000,
- 1569038400000,
- 1569124800000,
- 1569211200000,
- 1569297600000,
- 1569384000000,
- 1569470400000,
- 1569556800000,
- 1569643200000,
- 1569729600000,
- 1569816000000,
- 1569902400000,
- 1569988800000,
- 1570075200000,
- 1570161600000,
- 1570248000000,
- 1570334400000,
- 1570420800000,
- 1570507200000,
- 1570593600000,
- 1570680000000,
- 1570766400000,
- 1570852800000,
- 1570939200000,
- 1571025600000,
- 1571112000000,
- 1571198400000,
- 1571284800000,
- 1571371200000,
- 1571457600000,
- 1571544000000,
- 1571630400000,
- 1571716800000,
- 1571803200000,
- 1571889600000,
- 1571976000000,
- 1572062400000,
- 1572148800000,
- 1572235200000,
- 1572321600000,
- 1572408000000,
- 1572494400000,
- 1572580800000,
- 1572667200000,
- 1572753600000,
- 1572843600000,
- 1572930000000,
- 1573016400000,
- 1573102800000,
- 1573189200000,
- 1573275600000,
- 1573362000000,
- 1573448400000,
- 1573534800000,
- 1573621200000,
- 1573707600000,
- 1573794000000,
- 1573880400000,
- 1573966800000,
- 1574053200000,
- 1574139600000,
- 1574226000000,
- 1574312400000,
- 1574398800000,
- 1574485200000,
- 1574571600000,
- 1574658000000,
- 1574744400000,
- 1574830800000,
- 1574917200000,
- 1575003600000,
- 1575090000000,
- 1575176400000,
- 1575262800000,
- 1575349200000,
- 1575435600000,
- 1575522000000,
- 1575608400000,
- 1575694800000,
- 1575781200000,
- 1575867600000,
- 1575954000000,
- 1576040400000,
- 1576126800000,
- 1576213200000,
- 1576299600000,
- 1576386000000,
- 1576472400000,
- 1576558800000
+ 1545800400000
],
- "y": [
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
- 41,
+ "col1": [
41,
41,
41,
@@ -737,6 +26,8 @@
]
}
},
- "max": 41,
- "min": 41
+ "max": {"col1": 41, "col4": 1545109200000},
+ "min": {"col1": 41, "col4": 1545800400000},
+ "x": "col4",
+ "y": "col1"
}
\ No newline at end of file
diff --git a/static/__tests__/data/correlations-ts.json b/static/__tests__/data/correlations-ts.json
index b6d5d89a8..f1dbf1d47 100644
--- a/static/__tests__/data/correlations-ts.json
+++ b/static/__tests__/data/correlations-ts.json
@@ -18,7 +18,7 @@
1546318800000,
1546405200000
],
- "y": [
+ "corr": [
0.194918,
-0.201896,
0.115315,
@@ -38,5 +38,7 @@
}
},
"max": 0.40548625523544046,
- "min": -0.5101782784596804
+ "min": -0.5101782784596804,
+ "x": "x",
+ "y": "corr"
}
\ No newline at end of file
diff --git a/static/__tests__/data/scatter.json b/static/__tests__/data/scatter.json
index b93e7f8dc..7ab1e0ccd 100644
--- a/static/__tests__/data/scatter.json
+++ b/static/__tests__/data/scatter.json
@@ -1,16 +1,11 @@
{
- "data": [
- {"col": 1.4, "col2": 1.5, "index": 0},
- {"col": 2.4, "col2": 1.5, "index": 1},
- {"col": 3.4, "col2": 2.1, "index": 2},
- {"col": 4.4, "col2": 1.3, "index": 3},
- {"col": 9.1, "col2": -3.7, "index": 4},
- {"col": 0.2, "col2": 6.6, "index": 5},
- {"col": -1.3, "col2": 1.9, "index": 6},
- {"col": 5.6, "col2": 8.8, "index": 7},
- {"col": 3.7, "col2": 7.2, "index": 8},
- {"col": 4.2, "col2": 3.0, "index": 9}
- ],
+ "data": {
+ "all": {
+ "x": [1.4, 2.4, 3.4, 4.4, 9.2, 0.2, -1.3, 5.6, 3.7, 4.2],
+ "col2": [1.5, 1.5, 2.1, 1.3, -3.7, 6.6, 1.9, 8.8, 7.2, 3.0],
+ "index": [0,1,2,3,4,5,6,7,8,9]
+ }
+ },
"stats": {
"correlated": 10,
"only_in_s0": 0,
@@ -19,5 +14,15 @@
"spearman": 0.01647169200912469
},
"x": "col1",
- "y": "col2"
+ "y": "col2",
+ "max": {
+ "index": 9,
+ "col2": 8.8,
+ "x": 9.2
+ },
+ "min": {
+ "index": 0,
+ "col2": -3.7,
+ "x": -1.3
+ }
}
\ No newline at end of file
diff --git a/static/__tests__/dtale/DataViewer-base-test.jsx b/static/__tests__/dtale/DataViewer-base-test.jsx
index f415fd0c6..ab89adbb5 100644
--- a/static/__tests__/dtale/DataViewer-base-test.jsx
+++ b/static/__tests__/dtale/DataViewer-base-test.jsx
@@ -204,7 +204,7 @@ describe("DataViewer tests", () => {
result.update();
t.deepEqual(
result.find(".main-grid div.headerCell").map(hc => hc.text()),
- ["▲col4", "col1", "col2", "col3"],
+ ["▼col4", "col1", "col2", "col3"],
"should move col4 to front of main grid"
);
@@ -235,7 +235,7 @@ describe("DataViewer tests", () => {
.first()
.find("div.headerCell")
.map(hc => hc.text()),
- ["▲col4", "col1", "col2", "col3"],
+ ["▼col4", "col1", "col2", "col3"],
"should move col4 back into main grid"
);
diff --git a/static/__tests__/dtale/DataViewer-correlations-test.jsx b/static/__tests__/dtale/DataViewer-correlations-test.jsx
index 97837cc12..8c1ebc0e1 100644
--- a/static/__tests__/dtale/DataViewer-correlations-test.jsx
+++ b/static/__tests__/dtale/DataViewer-correlations-test.jsx
@@ -90,7 +90,7 @@ describe("DataViewer tests", () => {
const layoutObj = {
chart: tsChart,
scales: {
- "y-axis-0": {
+ "y-corr": {
getPixelForValue: px => px,
},
},
diff --git a/static/__tests__/iframe/DataViewer-base-test.jsx b/static/__tests__/iframe/DataViewer-base-test.jsx
index 98382b7eb..b7649d050 100644
--- a/static/__tests__/iframe/DataViewer-base-test.jsx
+++ b/static/__tests__/iframe/DataViewer-base-test.jsx
@@ -201,7 +201,7 @@ describe("DataViewer iframe tests", () => {
clickColMenuButton(result, "Move To Front");
t.deepEqual(
result.find(".main-grid div.headerCell").map(hc => hc.text()),
- ["▼col4", "col1", "col2", "col3"],
+ ["▲col4", "col1", "col2", "col3"],
"should move col4 to front of main grid"
);
result
@@ -235,7 +235,7 @@ describe("DataViewer iframe tests", () => {
.first()
.find("div.headerCell")
.map(hc => hc.text()),
- ["▼col4", "col1", "col2", "col3"],
+ ["▲col4", "col1", "col2", "col3"],
"should move col4 back into main grid"
);
diff --git a/static/__tests__/popups/ChartLabel-test.jsx b/static/__tests__/popups/ChartLabel-test.jsx
new file mode 100644
index 000000000..d81567764
--- /dev/null
+++ b/static/__tests__/popups/ChartLabel-test.jsx
@@ -0,0 +1,19 @@
+import { mount } from "enzyme";
+import React from "react";
+
+import ChartLabel from "../../popups/charts/ChartLabel";
+import * as t from "../jest-assertions";
+
+describe("ChartLabel tests", () => {
+ test("ChartLabel rendering", done => {
+ const props = {
+ x: { value: "foo" },
+ y: [{ value: "bar" }],
+ aggregation: "count",
+ };
+ const result = mount();
+ result.render();
+ t.equal(result.text(), "Count of bar by foo", "should render label");
+ done();
+ });
+});
diff --git a/static/__tests__/popups/ChartsBody-test.jsx b/static/__tests__/popups/ChartsBody-test.jsx
index 56330f089..dd74381b5 100644
--- a/static/__tests__/popups/ChartsBody-test.jsx
+++ b/static/__tests__/popups/ChartsBody-test.jsx
@@ -58,6 +58,8 @@ describe("ChartsBody tests", () => {
setTimeout(() => {
result.update();
t.ok(_.includes(result.html(), "Error test."), "should render error");
+ result.setProps({ visible: false });
+ t.equal(result.html(), null);
done();
}, 200);
});
diff --git a/static/__tests__/popups/Correlations-test.jsx b/static/__tests__/popups/Correlations-test.jsx
index 231a01707..b458fc2bb 100644
--- a/static/__tests__/popups/Correlations-test.jsx
+++ b/static/__tests__/popups/Correlations-test.jsx
@@ -204,7 +204,7 @@ describe("Correlations tests", () => {
{ datasetIndex: 0, index: 0 },
scatterChart.data
);
- t.deepEqual(label, ["col1: NaN", "col2: 1.5"], "should render label");
+ t.deepEqual(label, ["col1: 1.4", "col2: 1.5"], "should render label");
scatterChart.cfg.options.onClick({});
const corr = result.instance();
diff --git a/static/__tests__/popups/Histogram-test.jsx b/static/__tests__/popups/Histogram-test.jsx
index ebd8714bc..27c202179 100644
--- a/static/__tests__/popups/Histogram-test.jsx
+++ b/static/__tests__/popups/Histogram-test.jsx
@@ -94,11 +94,7 @@ describe("Histogram tests", () => {
const result = mount(, {
attachTo: document.getElementById("content"),
});
- t.deepEqual(
- result.find("option").map(o => o.text()),
- ["5", "10", "20", "50"],
- "Should render bins options"
- );
+ t.deepEqual(result.find("input").prop("value"), "20", "Should render default bins");
let chart = null;
setTimeout(() => {
@@ -111,7 +107,7 @@ describe("Histogram tests", () => {
t.equal(xlabel, "Bin", "should display correct x-axis label");
const event = { target: { value: 50 } };
- result.find("select").simulate("change", event);
+ result.find("input").simulate("change", event);
setTimeout(() => {
result.update();
diff --git a/static/__tests__/popups/WordcloudBody-test.jsx b/static/__tests__/popups/WordcloudBody-test.jsx
index 8f3ce8d43..921b9d364 100644
--- a/static/__tests__/popups/WordcloudBody-test.jsx
+++ b/static/__tests__/popups/WordcloudBody-test.jsx
@@ -60,22 +60,36 @@ describe("WordcloudBody tests", () => {
test("WordcloudBody missing data", done => {
const WordcloudBody = require("../../popups/charts/WordcloudBody").default;
- const result = mount(, {
+ const result = mount(, {
attachTo: document.getElementById("content"),
});
result.update();
- t.notOk(result.html(), "shouldn't render anything");
+ t.equal(result.html(), '
', "shouldn't render anything");
done();
});
test("WordcloudBody invalid chartType type", done => {
const WordcloudBody = require("../../popups/charts/WordcloudBody").default;
- const result = mount(, {
+ const result = mount(, {
attachTo: document.getElementById("content"),
});
result.update();
t.notOk(result.html(), "shouldn't render anything");
done();
});
+
+ test("WordcloudBody missing yProp data", done => {
+ const WordcloudBody = require("../../popups/charts/WordcloudBody").default;
+
+ const result = mount(
+ ,
+ {
+ attachTo: document.getElementById("content"),
+ }
+ );
+ result.update();
+ t.equal(result.html(), '', "shouldn't render anything");
+ done();
+ });
});
diff --git a/static/__tests__/popups/window/Charts-bar-test.jsx b/static/__tests__/popups/window/Charts-bar-test.jsx
new file mode 100644
index 000000000..59f62b982
--- /dev/null
+++ b/static/__tests__/popups/window/Charts-bar-test.jsx
@@ -0,0 +1,165 @@
+import qs from "querystring";
+
+import { mount } from "enzyme";
+import _ from "lodash";
+import React from "react";
+import Select from "react-select";
+
+import AxisEditor from "../../../popups/charts/AxisEditor";
+import mockPopsicle from "../../MockPopsicle";
+import * as t from "../../jest-assertions";
+import { buildInnerHTML, withGlobalJquery } from "../../test-utils";
+
+const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
+const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth");
+
+function updateChartType(result, cmp, chartType) {
+ result
+ .find(cmp)
+ .find(Select)
+ .first()
+ .instance()
+ .onChange({ value: chartType });
+ result.update();
+}
+
+describe("Charts bar tests", () => {
+ beforeAll(() => {
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
+ configurable: true,
+ value: 500,
+ });
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
+ configurable: true,
+ value: 500,
+ });
+
+ const mockBuildLibs = withGlobalJquery(() =>
+ mockPopsicle.mock(url => {
+ const urlParams = qs.parse(url.split("?")[1]);
+ if (urlParams.x === "error" && urlParams.y === "error2") {
+ return { data: {} };
+ }
+ const { urlFetcher } = require("../../redux-test-utils").default;
+ return urlFetcher(url);
+ })
+ );
+
+ const mockChartUtils = withGlobalJquery(() => (ctx, cfg) => {
+ const chartCfg = { ctx, cfg, data: cfg.data, destroyed: false };
+ chartCfg.destroy = () => (chartCfg.destroyed = true);
+ chartCfg.getElementAtEvent = _evt => [{ _index: 0 }];
+ chartCfg.update = _.noop;
+ chartCfg.options = { scales: { xAxes: [{}] } };
+ return chartCfg;
+ });
+
+ const mockD3Cloud = withGlobalJquery(() => () => {
+ const cloudCfg = {};
+ const propUpdate = prop => val => {
+ cloudCfg[prop] = val;
+ return cloudCfg;
+ };
+ cloudCfg.size = propUpdate("size");
+ cloudCfg.padding = propUpdate("padding");
+ cloudCfg.words = propUpdate("words");
+ cloudCfg.rotate = propUpdate("rotate");
+ cloudCfg.spiral = propUpdate("spiral");
+ cloudCfg.random = propUpdate("random");
+ cloudCfg.text = propUpdate("text");
+ cloudCfg.font = propUpdate("font");
+ cloudCfg.fontStyle = propUpdate("fontStyle");
+ cloudCfg.fontWeight = propUpdate("fontWeight");
+ cloudCfg.fontSize = () => ({
+ on: () => ({ start: _.noop }),
+ });
+ return cloudCfg;
+ });
+
+ jest.mock("popsicle", () => mockBuildLibs);
+ jest.mock("d3-cloud", () => mockD3Cloud);
+ jest.mock("chart.js", () => mockChartUtils);
+ jest.mock("chartjs-plugin-zoom", () => ({}));
+ jest.mock("chartjs-chart-box-and-violin-plot/build/Chart.BoxPlot.js", () => ({}));
+ });
+
+ afterAll(() => {
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", originalOffsetHeight);
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", originalOffsetWidth);
+ });
+
+ test("Charts: rendering", done => {
+ const Charts = require("../../../popups/charts/Charts").ReactCharts;
+ const ChartsBody = require("../../../popups/charts/ChartsBody").default;
+ buildInnerHTML({ settings: "" });
+ const result = mount(, {
+ attachTo: document.getElementById("content"),
+ });
+
+ setTimeout(() => {
+ result.update();
+ const filters = result.find(Charts).find(Select);
+ filters
+ .first()
+ .instance()
+ .onChange({ value: "col4" });
+ filters
+ .at(1)
+ .instance()
+ .onChange([{ value: "col1" }]);
+ updateChartType(result, ChartsBody, "bar");
+ result
+ .find(Charts)
+ .find("button")
+ .first()
+ .simulate("click");
+ setTimeout(() => {
+ result.update();
+ t.ok(result.find(ChartsBody).instance().state.charts.length == 1, "should render charts");
+ const sortBtn = result
+ .find(ChartsBody)
+ .find("button")
+ .findWhere(b => b.text() === "Sort Bars")
+ .first();
+ sortBtn.simulate("click");
+ let axisEditor = result.find(AxisEditor).first();
+ axisEditor.find("span.axis-select").simulate("click");
+ axisEditor
+ .find("input")
+ .first()
+ .simulate("change", { target: { value: "40" } });
+ axisEditor
+ .find("input")
+ .last()
+ .simulate("change", { target: { value: "42" } });
+ axisEditor.instance().closeMenu();
+ const chartObj = result.find(ChartsBody).instance().state.charts[0];
+ t.deepEqual(chartObj.cfg.options.scales.yAxes[0].ticks, {
+ min: 40,
+ max: 42,
+ });
+ axisEditor = result.find(AxisEditor).first();
+ axisEditor.find("span.axis-select").simulate("click");
+ axisEditor
+ .find("input")
+ .first()
+ .simulate("change", { target: { value: "40" } });
+ axisEditor
+ .find("input")
+ .last()
+ .simulate("change", { target: { value: "a" } });
+ axisEditor.instance().closeMenu();
+ axisEditor = result.find(AxisEditor).first();
+ t.equal(axisEditor.instance().state.errors[0], "col1 has invalid max!");
+ axisEditor
+ .find("input")
+ .last()
+ .simulate("change", { target: { value: "39" } });
+ axisEditor.instance().closeMenu();
+ axisEditor = result.find(AxisEditor).first();
+ t.equal(axisEditor.instance().state.errors[0], "col1 must have a min < max!");
+ done();
+ }, 400);
+ }, 600);
+ });
+});
diff --git a/static/__tests__/popups/window/Charts-multi-column-test.jsx b/static/__tests__/popups/window/Charts-multi-column-test.jsx
new file mode 100644
index 000000000..bd2c770c3
--- /dev/null
+++ b/static/__tests__/popups/window/Charts-multi-column-test.jsx
@@ -0,0 +1,244 @@
+/* eslint max-statements: "off" */
+import qs from "querystring";
+
+import { mount } from "enzyme";
+import _ from "lodash";
+import React from "react";
+import Select from "react-select";
+
+import { Aggregations } from "../../../popups/charts/Aggregations";
+import mockPopsicle from "../../MockPopsicle";
+import * as t from "../../jest-assertions";
+import { buildInnerHTML, withGlobalJquery } from "../../test-utils";
+
+const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
+const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth");
+
+function updateChartType(result, cmp, chartType) {
+ result
+ .find(cmp)
+ .find(Select)
+ .first()
+ .instance()
+ .onChange({ value: chartType });
+ result.update();
+}
+
+describe("Charts tests", () => {
+ beforeAll(() => {
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
+ configurable: true,
+ value: 500,
+ });
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
+ configurable: true,
+ value: 500,
+ });
+
+ const mockBuildLibs = withGlobalJquery(() =>
+ mockPopsicle.mock(url => {
+ const urlParams = qs.parse(url.split("?")[1]);
+ if (urlParams.x === "error" && urlParams.y === "error2") {
+ return { data: {} };
+ }
+ const { urlFetcher } = require("../../redux-test-utils").default;
+ return urlFetcher(url);
+ })
+ );
+
+ const mockChartUtils = withGlobalJquery(() => (ctx, cfg) => {
+ const chartCfg = { ctx, cfg, data: cfg.data, destroyed: false };
+ chartCfg.destroy = () => (chartCfg.destroyed = true);
+ chartCfg.getElementAtEvent = _evt => [{ _index: 0 }];
+ chartCfg.update = _.noop;
+ chartCfg.options = { scales: { xAxes: [{}] } };
+ return chartCfg;
+ });
+
+ const mockD3Cloud = withGlobalJquery(() => () => {
+ const cloudCfg = {};
+ const propUpdate = prop => val => {
+ cloudCfg[prop] = val;
+ return cloudCfg;
+ };
+ cloudCfg.size = propUpdate("size");
+ cloudCfg.padding = propUpdate("padding");
+ cloudCfg.words = propUpdate("words");
+ cloudCfg.rotate = propUpdate("rotate");
+ cloudCfg.spiral = propUpdate("spiral");
+ cloudCfg.random = propUpdate("random");
+ cloudCfg.text = propUpdate("text");
+ cloudCfg.font = propUpdate("font");
+ cloudCfg.fontStyle = propUpdate("fontStyle");
+ cloudCfg.fontWeight = propUpdate("fontWeight");
+ cloudCfg.fontSize = () => ({
+ on: () => ({ start: _.noop }),
+ });
+ return cloudCfg;
+ });
+
+ jest.mock("popsicle", () => mockBuildLibs);
+ jest.mock("d3-cloud", () => mockD3Cloud);
+ jest.mock("chart.js", () => mockChartUtils);
+ jest.mock("chartjs-plugin-zoom", () => ({}));
+ jest.mock("chartjs-chart-box-and-violin-plot/build/Chart.BoxPlot.js", () => ({}));
+ });
+
+ afterAll(() => {
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", originalOffsetHeight);
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", originalOffsetWidth);
+ });
+
+ test("Charts: rendering", done => {
+ const Charts = require("../../../popups/charts/Charts").ReactCharts;
+ const ChartsBody = require("../../../popups/charts/ChartsBody").default;
+ const ReactWordcloud = require("react-wordcloud").default;
+ buildInnerHTML({ settings: "" });
+ const result = mount(, {
+ attachTo: document.getElementById("content"),
+ });
+
+ setTimeout(() => {
+ result.update();
+ let filters = result.find(Charts).find(Select);
+ filters
+ .first()
+ .instance()
+ .onChange({ value: "col4" });
+ filters
+ .at(1)
+ .instance()
+ .onChange([{ value: "col1" }, { value: "col2" }]);
+ filters
+ .at(3)
+ .instance()
+ .onChange({ value: "rolling", label: "Rolling" });
+ result.update();
+ result
+ .find(Aggregations)
+ .find("input")
+ .at(1)
+ .simulate("change", { target: { value: "10" } });
+ result
+ .find(Aggregations)
+ .find(Select)
+ .last()
+ .instance()
+ .onChange({ value: "corr", label: "Correlation" });
+ result
+ .find(Charts)
+ .find("input.form-control")
+ .first()
+ .simulate("change", { target: { value: "col4 == '20181201'" } });
+ result.update();
+ result
+ .find(Charts)
+ .find("button")
+ .first()
+ .simulate("click");
+ setTimeout(() => {
+ result.update();
+ t.ok(result.find(ChartsBody).instance().state.charts.length == 1, "should render charts");
+ t.ok(
+ _.endsWith(
+ result.find(Charts).instance().state.url,
+ "x=col4&y=col1%2Ccol2&query=col4%20%3D%3D%20'20181201'&agg=rolling&rollingWin=10&rollingComp=corr"
+ ),
+ "should update chart URL"
+ );
+ result
+ .find(ChartsBody)
+ .instance()
+ .state.charts[0].cfg.options.onClick();
+ result.update();
+ t.deepEqual(
+ result.find(ChartsBody).instance().state.charts[0].options.scales.xAxes[0],
+ { ticks: { max: 1545800400000, min: 1545109200000 } },
+ "should limit x-axis"
+ );
+ const chartObj = result.find(ChartsBody).instance().state.charts[0];
+ const tsTitle = chartObj.cfg.options.tooltips.callbacks.title([{ index: 0 }], chartObj.data);
+ t.ok(tsTitle === "2018-12-18", "should return timestamp in tooltip");
+ result
+ .find(ChartsBody)
+ .instance()
+ .resetZoom();
+ result.update();
+ t.notOk(
+ result.find(ChartsBody).instance().state.charts[0].options.scales.xAxes[0].length == 0,
+ "should clear limited x-axis"
+ );
+ updateChartType(result, ChartsBody, "bar");
+ t.ok(result.find(ChartsBody).instance().state.charts[0].cfg.type === "bar");
+ updateChartType(result, ChartsBody, "wordcloud");
+ updateChartType(result, ChartsBody, "stacked");
+ t.ok(result.find(ChartsBody).instance().state.charts[0].cfg.options.scales.xAxes[0].stacked);
+ updateChartType(result, ChartsBody, "scatter");
+ t.ok(result.find(ChartsBody).instance().state.charts[0].cfg.type === "scatter");
+ updateChartType(result, ChartsBody, "pie");
+ t.ok(result.find(ChartsBody).instance().state.charts[0].cfg.type === "pie");
+ filters = result.find(Charts).find(Select);
+ filters
+ .at(3)
+ .instance()
+ .onChange(null);
+ filters
+ .at(2)
+ .instance()
+ .onChange([{ value: "col1" }, { value: "col3" }]);
+ result
+ .find(Charts)
+ .find("button")
+ .first()
+ .simulate("click");
+ setTimeout(() => {
+ result.update();
+ updateChartType(result, ChartsBody, "line");
+ let chartObj = result.find(ChartsBody).instance().state.charts[0];
+ t.equal(
+ chartObj.cfg.options.tooltips.callbacks.label(
+ { xLabel: 1545973200000, yLabel: 1.123456, datasetIndex: 0 },
+ chartObj.data
+ ),
+ "val1 - col1: 1.1235",
+ "should render tooltip label"
+ );
+ updateChartType(result, ChartsBody, "wordcloud");
+ updateChartType(result, ChartsBody, "line");
+ result
+ .find(Charts)
+ .find("input")
+ .findWhere(i => i.prop("type") === "checkbox")
+ .first()
+ .simulate("change", { target: { checked: true } });
+ result.update();
+ t.ok(result.find(ChartsBody).instance().state.charts.length == 2, "should render multiple charts");
+ chartObj = result.find(ChartsBody).instance().state.charts[0];
+ t.equal(
+ chartObj.cfg.options.tooltips.callbacks.label(
+ { xLabel: 1545973200000, yLabel: 1.123456, datasetIndex: 0 },
+ chartObj.data
+ ),
+ "col1: 1.1235",
+ "should render tooltip label"
+ );
+ updateChartType(result, ChartsBody, "wordcloud");
+ const wc = result.find(ReactWordcloud).first();
+ t.ok(wc.props().callbacks.getWordTooltip({ fullText: "test", value: 5 }), "test (5)", "should show tooltip");
+ const cb = result.find(ChartsBody).instance();
+ t.notOk(cb.shouldComponentUpdate(cb.props, cb.state), "shouldn't update chart body");
+ t.ok(
+ cb.shouldComponentUpdate(cb.props, _.assignIn({}, cb.state, { error: "test" })),
+ "should update chart body"
+ );
+ t.ok(cb.shouldComponentUpdate(cb.props, _.assignIn({}, cb.state, { data: {} })), "should update chart body");
+ t.notOk(
+ cb.shouldComponentUpdate(cb.props, _.assignIn({}, cb.state, { chart: true })),
+ "shouldn't update chart body"
+ );
+ done();
+ }, 400);
+ }, 400);
+ }, 600);
+ });
+});
diff --git a/static/__tests__/popups/window/Charts-rolling-test.jsx b/static/__tests__/popups/window/Charts-rolling-test.jsx
new file mode 100644
index 000000000..30a2409b5
--- /dev/null
+++ b/static/__tests__/popups/window/Charts-rolling-test.jsx
@@ -0,0 +1,147 @@
+import qs from "querystring";
+
+import { mount } from "enzyme";
+import _ from "lodash";
+import React from "react";
+import Select from "react-select";
+
+import { Aggregations } from "../../../popups/charts/Aggregations";
+import mockPopsicle from "../../MockPopsicle";
+import * as t from "../../jest-assertions";
+import { buildInnerHTML, withGlobalJquery } from "../../test-utils";
+
+const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
+const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth");
+
+describe("Charts rolling tests", () => {
+ beforeAll(() => {
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
+ configurable: true,
+ value: 500,
+ });
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
+ configurable: true,
+ value: 500,
+ });
+
+ const mockBuildLibs = withGlobalJquery(() =>
+ mockPopsicle.mock(url => {
+ const urlParams = qs.parse(url.split("?")[1]);
+ if (urlParams.x === "error" && urlParams.y === "error2") {
+ return { data: {} };
+ }
+ const { urlFetcher } = require("../../redux-test-utils").default;
+ return urlFetcher(url);
+ })
+ );
+
+ const mockChartUtils = withGlobalJquery(() => (ctx, cfg) => {
+ const chartCfg = { ctx, cfg, data: cfg.data, destroyed: false };
+ chartCfg.destroy = () => (chartCfg.destroyed = true);
+ chartCfg.getElementAtEvent = _evt => [{ _index: 0 }];
+ chartCfg.update = _.noop;
+ chartCfg.options = { scales: { xAxes: [{}] } };
+ return chartCfg;
+ });
+
+ const mockD3Cloud = withGlobalJquery(() => () => {
+ const cloudCfg = {};
+ const propUpdate = prop => val => {
+ cloudCfg[prop] = val;
+ return cloudCfg;
+ };
+ cloudCfg.size = propUpdate("size");
+ cloudCfg.padding = propUpdate("padding");
+ cloudCfg.words = propUpdate("words");
+ cloudCfg.rotate = propUpdate("rotate");
+ cloudCfg.spiral = propUpdate("spiral");
+ cloudCfg.random = propUpdate("random");
+ cloudCfg.text = propUpdate("text");
+ cloudCfg.font = propUpdate("font");
+ cloudCfg.fontStyle = propUpdate("fontStyle");
+ cloudCfg.fontWeight = propUpdate("fontWeight");
+ cloudCfg.fontSize = () => ({
+ on: () => ({ start: _.noop }),
+ });
+ return cloudCfg;
+ });
+
+ jest.mock("popsicle", () => mockBuildLibs);
+ jest.mock("d3-cloud", () => mockD3Cloud);
+ jest.mock("chart.js", () => mockChartUtils);
+ jest.mock("chartjs-plugin-zoom", () => ({}));
+ jest.mock("chartjs-chart-box-and-violin-plot/build/Chart.BoxPlot.js", () => ({}));
+ });
+
+ afterAll(() => {
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", originalOffsetHeight);
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", originalOffsetWidth);
+ });
+
+ test("Charts: rendering", done => {
+ const Charts = require("../../../popups/charts/Charts").ReactCharts;
+ buildInnerHTML({ settings: "" });
+ const result = mount(, {
+ attachTo: document.getElementById("content"),
+ });
+
+ setTimeout(() => {
+ result.update();
+ const filters = result.find(Charts).find(Select);
+ filters
+ .first()
+ .instance()
+ .onChange({ value: "col4" });
+ filters
+ .at(1)
+ .instance()
+ .onChange([{ value: "col1" }]);
+ filters
+ .at(3)
+ .instance()
+ .onChange({ value: "rolling", label: "Rolling" });
+ result.update();
+ result
+ .find(Aggregations)
+ .find("input")
+ .at(1)
+ .simulate("change", { target: { value: "" } });
+ result
+ .find(Aggregations)
+ .find(Select)
+ .last()
+ .instance()
+ .onChange({ value: "corr", label: "Correlation" });
+ result
+ .find(Charts)
+ .find("button")
+ .first()
+ .simulate("click");
+ setTimeout(() => {
+ result.update();
+ t.equal(result.find(Charts).instance().state.error, "Aggregation (rolling) requires a window");
+ result
+ .find(Aggregations)
+ .find("input")
+ .at(1)
+ .simulate("change", { target: { value: "10" } });
+ result
+ .find(Aggregations)
+ .find(Select)
+ .last()
+ .instance()
+ .onChange(null);
+ result
+ .find(Charts)
+ .find("button")
+ .first()
+ .simulate("click");
+ setTimeout(() => {
+ result.update();
+ t.equal(result.find(Charts).instance().state.error, "Aggregation (rolling) requires a computation");
+ done();
+ }, 400);
+ }, 400);
+ }, 600);
+ });
+});
diff --git a/static/__tests__/popups/window/Charts-scatter-test.jsx b/static/__tests__/popups/window/Charts-scatter-test.jsx
new file mode 100644
index 000000000..8797b4fad
--- /dev/null
+++ b/static/__tests__/popups/window/Charts-scatter-test.jsx
@@ -0,0 +1,129 @@
+import qs from "querystring";
+
+import { mount } from "enzyme";
+import _ from "lodash";
+import React from "react";
+import Select from "react-select";
+
+import mockPopsicle from "../../MockPopsicle";
+import { buildInnerHTML, withGlobalJquery } from "../../test-utils";
+
+const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
+const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth");
+
+function updateChartType(result, cmp, chartType) {
+ result
+ .find(cmp)
+ .find(Select)
+ .first()
+ .instance()
+ .onChange({ value: chartType });
+ result.update();
+}
+
+describe("Charts scatter tests", () => {
+ beforeAll(() => {
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
+ configurable: true,
+ value: 500,
+ });
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
+ configurable: true,
+ value: 500,
+ });
+
+ const mockBuildLibs = withGlobalJquery(() =>
+ mockPopsicle.mock(url => {
+ const urlParams = qs.parse(url.split("?")[1]);
+ if (urlParams.x === "error" && urlParams.y === "error2") {
+ return { data: {} };
+ }
+ const { urlFetcher } = require("../../redux-test-utils").default;
+ return urlFetcher(url);
+ })
+ );
+
+ const mockChartUtils = withGlobalJquery(() => (ctx, cfg) => {
+ const chartCfg = { ctx, cfg, data: cfg.data, destroyed: false };
+ chartCfg.destroy = () => (chartCfg.destroyed = true);
+ chartCfg.getElementAtEvent = _evt => [{ _index: 0 }];
+ chartCfg.update = _.noop;
+ chartCfg.options = { scales: { xAxes: [{}] } };
+ return chartCfg;
+ });
+
+ const mockD3Cloud = withGlobalJquery(() => () => {
+ const cloudCfg = {};
+ const propUpdate = prop => val => {
+ cloudCfg[prop] = val;
+ return cloudCfg;
+ };
+ cloudCfg.size = propUpdate("size");
+ cloudCfg.padding = propUpdate("padding");
+ cloudCfg.words = propUpdate("words");
+ cloudCfg.rotate = propUpdate("rotate");
+ cloudCfg.spiral = propUpdate("spiral");
+ cloudCfg.random = propUpdate("random");
+ cloudCfg.text = propUpdate("text");
+ cloudCfg.font = propUpdate("font");
+ cloudCfg.fontStyle = propUpdate("fontStyle");
+ cloudCfg.fontWeight = propUpdate("fontWeight");
+ cloudCfg.fontSize = () => ({
+ on: () => ({ start: _.noop }),
+ });
+ return cloudCfg;
+ });
+
+ jest.mock("popsicle", () => mockBuildLibs);
+ jest.mock("d3-cloud", () => mockD3Cloud);
+ jest.mock("chart.js", () => mockChartUtils);
+ jest.mock("chartjs-plugin-zoom", () => ({}));
+ jest.mock("chartjs-chart-box-and-violin-plot/build/Chart.BoxPlot.js", () => ({}));
+ });
+
+ afterAll(() => {
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", originalOffsetHeight);
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", originalOffsetWidth);
+ });
+
+ test("Charts: rendering", done => {
+ const Charts = require("../../../popups/charts/Charts").ReactCharts;
+ const ChartsBody = require("../../../popups/charts/ChartsBody").default;
+ buildInnerHTML({ settings: "" });
+ const result = mount(, {
+ attachTo: document.getElementById("content"),
+ });
+
+ setTimeout(() => {
+ result.update();
+ const filters = result.find(Charts).find(Select);
+ filters
+ .first()
+ .instance()
+ .onChange({ value: "col4" });
+ filters
+ .at(1)
+ .instance()
+ .onChange([{ value: "col1" }]);
+ updateChartType(result, ChartsBody, "scatter");
+ result
+ .find(Charts)
+ .find("button")
+ .first()
+ .simulate("click");
+ setTimeout(() => {
+ result.update();
+ updateChartType(result, ChartsBody, "bar");
+ result
+ .find(Charts)
+ .find("button")
+ .first()
+ .simulate("click");
+ setTimeout(() => {
+ result.update();
+ done();
+ }, 400);
+ }, 400);
+ }, 600);
+ });
+});
diff --git a/static/__tests__/popups/window/Charts-test.jsx b/static/__tests__/popups/window/Charts-test.jsx
index a7948f0ba..ad94e5546 100644
--- a/static/__tests__/popups/window/Charts-test.jsx
+++ b/static/__tests__/popups/window/Charts-test.jsx
@@ -1,3 +1,4 @@
+/* eslint max-statements: "off" */
import qs from "querystring";
import { mount } from "enzyme";
@@ -5,6 +6,8 @@ import _ from "lodash";
import React from "react";
import Select from "react-select";
+import { RemovableError } from "../../../RemovableError";
+import { Aggregations } from "../../../popups/charts/Aggregations";
import mockPopsicle from "../../MockPopsicle";
import * as t from "../../jest-assertions";
import { buildInnerHTML, withGlobalJquery } from "../../test-utils";
@@ -12,6 +15,16 @@ import { buildInnerHTML, withGlobalJquery } from "../../test-utils";
const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth");
+function updateChartType(result, cmp, chartType) {
+ result
+ .find(cmp)
+ .find(Select)
+ .first()
+ .instance()
+ .onChange({ value: chartType });
+ result.update();
+}
+
describe("Charts tests", () => {
beforeAll(() => {
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
@@ -29,6 +42,9 @@ describe("Charts tests", () => {
if (urlParams.x === "error" && urlParams.y === "error2") {
return { data: {} };
}
+ if (_.startsWith(url, "/dtale/dtypes/9")) {
+ return { error: "error test" };
+ }
const { urlFetcher } = require("../../redux-test-utils").default;
return urlFetcher(url);
})
@@ -96,11 +112,23 @@ describe("Charts tests", () => {
filters
.at(1)
.instance()
- .onChange({ value: "col1" });
+ .onChange([{ value: "col1" }]);
filters
.at(3)
.instance()
- .onChange({ value: "count", label: "Count" });
+ .onChange({ value: "rolling", label: "Rolling" });
+ result.update();
+ result
+ .find(Aggregations)
+ .find("input")
+ .at(1)
+ .simulate("change", { target: { value: "10" } });
+ result
+ .find(Aggregations)
+ .find(Select)
+ .last()
+ .instance()
+ .onChange({ value: "corr", label: "Correlation" });
result
.find(Charts)
.find("input.form-control")
@@ -116,8 +144,10 @@ describe("Charts tests", () => {
result.update();
t.ok(result.find(ChartsBody).instance().state.charts.length == 1, "should render charts");
t.ok(
- result.find(Charts).instance().state.url,
- "/dtale/chart-data/1?x=date&y=security_id&query=date%20%3D%3D%20\\'20181201\\'&agg=count",
+ _.endsWith(
+ result.find(Charts).instance().state.url,
+ "x=col4&y=col1&query=col4%20%3D%3D%20'20181201'&agg=rolling&rollingWin=10&rollingComp=corr"
+ ),
"should update chart URL"
);
result
@@ -127,19 +157,14 @@ describe("Charts tests", () => {
result.update();
t.deepEqual(
result.find(ChartsBody).instance().state.charts[0].options.scales.xAxes[0],
- { ticks: { max: 1545973200000, min: 1545109200000 } },
+ { ticks: { max: 1545800400000, min: 1545109200000 } },
"should limit x-axis"
);
- t.equal(
- result
- .find(ChartsBody)
- .instance()
- .state.charts[0].cfg.options.tooltips.callbacks.title([{ xLabel: 1545973200000 }]),
- "2018-12-28",
- "should correctly render dates in tooltip"
- );
+ const chartObj = result.find(ChartsBody).instance().state.charts[0];
+ const tsTitle = chartObj.cfg.options.tooltips.callbacks.title([{ index: 0 }], chartObj.data);
+ t.ok(tsTitle === "2018-12-18", "should return timestamp in tooltip");
result
- .find(Charts)
+ .find(ChartsBody)
.instance()
.resetZoom();
result.update();
@@ -147,27 +172,15 @@ describe("Charts tests", () => {
result.find(ChartsBody).instance().state.charts[0].options.scales.xAxes[0].length == 0,
"should clear limited x-axis"
);
- filters = result.find(Charts).find(Select);
- filters
- .last()
- .instance()
- .onChange({ value: "bar" });
- result.update();
- filters
- .last()
- .instance()
- .onChange({ value: "wordcloud" });
- result.update();
- filters
- .last()
- .instance()
- .onChange({ value: "stacked" });
- result.update();
- filters
- .last()
- .instance()
- .onChange({ value: "pie" });
- result.update();
+ updateChartType(result, ChartsBody, "bar");
+ t.ok(result.find(ChartsBody).instance().state.charts[0].cfg.type === "bar");
+ updateChartType(result, ChartsBody, "wordcloud");
+ updateChartType(result, ChartsBody, "stacked");
+ t.ok(result.find(ChartsBody).instance().state.charts[0].cfg.options.scales.xAxes[0].stacked);
+ updateChartType(result, ChartsBody, "scatter");
+ t.ok(result.find(ChartsBody).instance().state.charts[0].cfg.type === "scatter");
+ updateChartType(result, ChartsBody, "pie");
+ t.ok(result.find(ChartsBody).instance().state.charts[0].cfg.type === "pie");
filters = result.find(Charts).find(Select);
filters
.at(3)
@@ -184,30 +197,18 @@ describe("Charts tests", () => {
.simulate("click");
setTimeout(() => {
result.update();
- filters
- .last()
- .instance()
- .onChange({ value: "line" });
- result.update();
+ updateChartType(result, ChartsBody, "line");
let chartObj = result.find(ChartsBody).instance().state.charts[0];
t.equal(
chartObj.cfg.options.tooltips.callbacks.label(
{ xLabel: 1545973200000, yLabel: 1.123456, datasetIndex: 0 },
chartObj.data
),
- "val1: 1.1235",
+ "val1 - col1: 1.1235",
"should render tooltip label"
);
- filters
- .last()
- .instance()
- .onChange({ value: "wordcloud" });
- result.update();
- filters
- .last()
- .instance()
- .onChange({ value: "line" });
- result.update();
+ updateChartType(result, ChartsBody, "wordcloud");
+ updateChartType(result, ChartsBody, "line");
result
.find(Charts)
.find("input")
@@ -225,11 +226,7 @@ describe("Charts tests", () => {
"1.1235",
"should render tooltip label"
);
- filters
- .last()
- .instance()
- .onChange({ value: "wordcloud" });
- result.update();
+ updateChartType(result, ChartsBody, "wordcloud");
const wc = result.find(ReactWordcloud).first();
t.ok(wc.props().callbacks.getWordTooltip({ fullText: "test", value: 5 }), "test (5)", "should show tooltip");
const cb = result.find(ChartsBody).instance();
@@ -259,7 +256,7 @@ describe("Charts tests", () => {
setTimeout(() => {
result.update();
- let filters = result.find(Charts).find(Select);
+ const filters = result.find(Charts).find(Select);
filters
.first()
.instance()
@@ -267,7 +264,7 @@ describe("Charts tests", () => {
filters
.at(1)
.instance()
- .onChange({ value: "error2" });
+ .onChange([{ value: "error2" }]);
result
.find(Charts)
.find("button")
@@ -275,15 +272,24 @@ describe("Charts tests", () => {
.simulate("click");
setTimeout(() => {
result.update();
- filters = result.find(Charts).find(Select);
- filters
- .last()
- .instance()
- .onChange({ value: "bar" });
- result.update();
+ updateChartType(result, ChartsBody, "bar");
t.ok(result.find(ChartsBody).instance().state.charts === null, "should not render chart");
done();
}, 400);
}, 400);
});
+
+ test("Charts: rendering empty data", done => {
+ const Charts = require("../../../popups/charts/Charts").ReactCharts;
+ buildInnerHTML({ settings: "" });
+ const result = mount(, {
+ attachTo: document.getElementById("content"),
+ });
+
+ setTimeout(() => {
+ result.update();
+ t.equal(result.find(RemovableError).text(), "error test");
+ done();
+ }, 400);
+ });
});
diff --git a/static/__tests__/redux-test-utils.jsx b/static/__tests__/redux-test-utils.jsx
index d97d23a1f..b8f7ff1bb 100644
--- a/static/__tests__/redux-test-utils.jsx
+++ b/static/__tests__/redux-test-utils.jsx
@@ -121,7 +121,7 @@ const PROCESSES = [
columns: 3,
},
];
-
+// eslint-disable-next-line max-statements
function urlFetcher(url) {
const urlParams = qs.parse(url.split("?")[1]);
const query = urlParams.query;
@@ -140,8 +140,18 @@ function urlFetcher(url) {
return scatterData;
} else if (url.startsWith("/dtale/chart-data")) {
if (urlParams.group) {
+ if (_.includes(urlParams.y, ",")) {
+ return _.assignIn({}, groupedChartsData, {
+ data: _.mapValues(groupedChartsData.data, d => _.assignIn(d, { col2: d.col1 })),
+ });
+ }
return groupedChartsData;
}
+ if (_.includes(urlParams.y, ",")) {
+ return _.assignIn({}, chartsData, {
+ data: _.mapValues(chartsData.data, d => _.assignIn(d, { col2: d.col1 })),
+ });
+ }
return chartsData;
} else if (url.startsWith("/dtale/update-settings")) {
return { success: true };
diff --git a/static/__tests__/popups/scatterChartUtils-test.jsx b/static/__tests__/scatterChartUtils-test.jsx
similarity index 68%
rename from static/__tests__/popups/scatterChartUtils-test.jsx
rename to static/__tests__/scatterChartUtils-test.jsx
index 0ab7c6174..3d67969bc 100644
--- a/static/__tests__/popups/scatterChartUtils-test.jsx
+++ b/static/__tests__/scatterChartUtils-test.jsx
@@ -1,8 +1,7 @@
import _ from "lodash";
-import corrUtils from "../../popups/correlations/correlationsUtils";
-import { formatScatterPoints } from "../../popups/scatterChartUtils";
-import * as t from "../jest-assertions";
+import { basePointFormatter, formatScatterPoints, getScatterMax, getScatterMin } from "../scatterChartUtils";
+import * as t from "./jest-assertions";
describe("scatterChartUtils tests", () => {
test("scatterChartUtils: testing filtering/highlighting logic", done => {
@@ -20,11 +19,16 @@ describe("scatterChartUtils tests", () => {
];
const scatterData = formatScatterPoints(
data,
- corrUtils.pointFormatter("col", "col2"),
+ basePointFormatter("col", "col2"),
_.matches({ index: 0 }),
_.matches({ index: 9 })
);
t.deepEqual(scatterData.pointRadius, [3, 3, 3, 3, 3, 3, 3, 3, 0, 5], "should set correct radii");
+
+ t.equal(getScatterMin(data, "col"), -2.5);
+ t.equal(getScatterMax(data, "col"), 10.5);
+ t.equal(getScatterMax([3, 4, 2, 1]), 5.5);
+ t.equal(getScatterMin([3, 4, 2, 1]), -0.5);
done();
});
});
diff --git a/static/__tests__/toggleUtils-test.jsx b/static/__tests__/toggleUtils-test.jsx
new file mode 100644
index 000000000..7e3c16655
--- /dev/null
+++ b/static/__tests__/toggleUtils-test.jsx
@@ -0,0 +1,15 @@
+import { buildButton } from "../toggleUtils";
+import * as t from "./jest-assertions";
+
+describe("toggleUtils tests", () => {
+ test("toggleUtils: testing buildButton", done => {
+ let props = buildButton(true, () => "active");
+ t.equal(props.className, "btn btn-primary active");
+ t.equal(props.onClick(), undefined);
+ props = buildButton(false, () => "active", true);
+ t.equal(props.className, "btn btn-primary ");
+ t.equal(props.onClick(), "active");
+ t.ok(props.disabled);
+ done();
+ });
+});
diff --git a/static/chartUtils.jsx b/static/chartUtils.jsx
index 2f5475fd0..b12a57f11 100644
--- a/static/chartUtils.jsx
+++ b/static/chartUtils.jsx
@@ -1,3 +1,4 @@
+/* eslint max-lines: "off" */
import Chart from "chart.js";
import "chartjs-chart-box-and-violin-plot/build/Chart.BoxPlot.js";
import "chartjs-plugin-zoom";
@@ -6,6 +7,7 @@ import _ from "lodash";
import moment from "moment";
import { isDateCol } from "./dtale/gridUtils";
+import { formatScatterPoints, getScatterMax, getScatterMin } from "./scatterChartUtils";
// needed to add these parameters because Chart.Zoom.js causes Chart.js to look for them
const DEFAULT_OPTIONS = { pan: { enabled: false }, zoom: { enabled: false } };
@@ -66,7 +68,7 @@ function buildRGBA(colorScale) {
")";
}
-const gradientLinePlugin = (colorScale, minY = null, maxY = null) => ({
+const gradientLinePlugin = (colorScale, yAxisID, minY = null, maxY = null) => ({
afterLayout: chartInstance => {
const rgbaBuilder = buildRGBA(colorScale);
// The context, needed for the creation of the linear gradient.
@@ -82,7 +84,7 @@ const gradientLinePlugin = (colorScale, minY = null, maxY = null) => ({
finalMaxY = _.isNull(finalMaxY) ? _.last(data).y : finalMaxY;
}
// Calculate Y pixels for min and max values.
- const yAxis = chartInstance.scales["y-axis-0"];
+ const yAxis = chartInstance.scales[yAxisID || "y-axis-0"];
const minValueYPixel = yAxis.getPixelForValue(minY);
const maxValueYPixel = yAxis.getPixelForValue(maxY);
// Create the gradient.
@@ -105,28 +107,98 @@ const gradientLinePlugin = (colorScale, minY = null, maxY = null) => ({
const COLOR_SCALE = chroma.scale(["orange", "yellow", "green", "lightblue", "darkblue"]);
-function updateCfgForDates(cfg, { columns, x }) {
- if (isDateCol(_.find(columns, { name: x }).dtype)) {
- const units = _.size(cfg.data.labels) > 150 ? "month" : "day";
+function timestampLabel(ts) {
+ const startOfDay = moment(new Date(ts))
+ .startOf("day")
+ .valueOf();
+ if (startOfDay == ts) {
+ return moment(new Date(ts)).format("YYYY-MM-DD");
+ }
+ return moment(new Date(ts)).format("YYYY-MM-DD h:mm:ss a");
+}
+
+function getTimeCfg(data, minTS, maxTS) {
+ const diffInDays = (maxTS - minTS) / (1000 * 3600 * 24);
+ let units = "year";
+ let stepSize = 1;
+ let unitsFmt = "YYYYMMDD";
+ if (diffInDays < 10 && diffInDays < _.size(data) - 2) {
+ units = "hour";
+ stepSize = 4;
+ unitsFmt += " hA";
+ } else if (diffInDays < 50) {
+ units = "day";
+ } else if (diffInDays < 365 / 2) {
+ units = "week";
+ } else if (diffInDays < 365 * 4) {
+ units = "month";
+ } else if (diffInDays < 365 * 10) {
+ units = "quarter";
+ }
+ return { units, stepSize, unitsFmt };
+}
+
+function updateCfgForDates(cfg, { columns, x, y }, { min, max }) {
+ if (isDateCol(_.get(_.find(columns || {}, { name: x }), "dtype", ""))) {
+ const minTS = min[x];
+ const maxTS = max[x];
+ const { units, stepSize, unitsFmt } = getTimeCfg(cfg.data.labels || _.map(cfg.data.datasets[0], "x"), minTS, maxTS);
cfg.options.scales.xAxes = [
{
type: "time",
distribution: "series",
time: {
unit: units,
+ stepSize,
displayFormats: {
- [units]: "YYYYMMDD",
+ [units]: unitsFmt,
},
},
ticks: {
- min: _.get(cfg.data.labels, "0"),
- max: _.get(cfg.data.labels, cfg.data.labels.length - 1),
+ min: minTS,
+ max: maxTS,
},
},
];
- cfg.options.tooltips.callbacks.title = (tooltipItems, _data) =>
- moment(new Date(tooltipItems[0].xLabel)).format("YYYY-MM-DD");
+ cfg.options.tooltips.callbacks.title = (tooltipItems, data) =>
+ timestampLabel(_.get(data, ["labels", tooltipItems[0].index]));
}
+ _.forEach(y, (yProp, idx) => {
+ if (isDateCol(_.get(_.find(columns || [], { name: yProp }), "dtype", ""))) {
+ let series = _.get(cfg, ["data", "datasets", idx, "data"], []);
+ if (_.isObject(_.head(series))) {
+ series = _.map(series, "y");
+ }
+ const minTS = min[yProp];
+ const maxTS = max[yProp];
+ const { units, stepSize, unitsFmt } = getTimeCfg(series, minTS, maxTS);
+ cfg.options.scales.yAxes[idx] = _.assignIn(cfg.options.scales.yAxes[idx], {
+ type: "time",
+ distribution: "series",
+ time: {
+ unit: units,
+ stepSize,
+ displayFormats: {
+ [units]: unitsFmt,
+ },
+ },
+ ticks: {
+ min: minTS,
+ max: maxTS,
+ },
+ });
+ /*cfg.options.tooltips.callbacks.label = (tooltipItem, data) => {
+ const value = timestampLabel(tooltipItem.yLabel);
+ if (_.size(data.datasets) * _.size(y) > 1) {
+ const label = data.datasets[tooltipItem.datasetIndex].label || "";
+ if (label) {
+ return `${label}: ${value}`;
+ }
+ }
+ return value;
+ }*/
+ }
+ });
}
function updateLegend(cfg) {
@@ -135,26 +207,45 @@ function updateLegend(cfg) {
}
}
-function buildSeries(label, { y }, _idx) {
+function buildSeries(label, data, _idx, yProp) {
const ptCfg = {
fill: false,
lineTension: 0.1,
pointRadius: 0,
pointHoverRadius: 5,
pointHitRadius: 5,
- data: y,
+ data: data[yProp],
+ yAxisID: `y-${yProp}`,
};
+ const labels = [];
if (label !== "all") {
- ptCfg.label = label;
+ labels.push(label);
+ }
+ if (yProp !== "y") {
+ labels.push(yProp);
+ }
+ if (_.size(labels) > 0) {
+ ptCfg.label = _.join(labels, " - ");
}
return ptCfg;
}
-function createBaseCfg({ data }, { x, y, additionalOptions }, seriesFormatter = buildSeries) {
+// eslint-disable-next-line no-unused-vars
+function buildTicks(prop, { min, max }, pad = false) {
+ const range = { min: min[prop], max: max[prop] };
+ if (pad) {
+ const padFactor = (range.max - range.min) * 0.01;
+ return { min: range.min - padFactor, max: range.max + padFactor };
+ }
+ return range;
+}
+
+function createBaseCfg({ data, min, max }, { x, y, additionalOptions }, seriesFormatter = buildSeries) {
+ let seriesIdx = 0;
const cfg = {
data: {
labels: _.get(_.values(data), "0.x"),
- datasets: _.map(_.toPairs(data), ([k, v], i) => seriesFormatter(k, v, i)),
+ datasets: _.flatMap(_.toPairs(data), ([k, v]) => _.map(y, yProp => seriesFormatter(k, v, seriesIdx++, yProp))),
},
options: _.assignIn(
{
@@ -167,7 +258,7 @@ function createBaseCfg({ data }, { x, y, additionalOptions }, seriesFormatter =
callbacks: {
label: (tooltipItem, chartData) => {
const value = _.round(tooltipItem.yLabel, 4);
- if (_.size(data) > 1) {
+ if (_.size(data) * _.size(y) > 1) {
const label = chartData.datasets[tooltipItem.datasetIndex].label || "";
if (label) {
return `${label}: ${value}`;
@@ -190,14 +281,15 @@ function createBaseCfg({ data }, { x, y, additionalOptions }, seriesFormatter =
},
},
],
- yAxes: [
- {
- scaleLabel: {
- display: true,
- labelString: y,
- },
+ yAxes: _.map(y, (yProp, idx) => ({
+ scaleLabel: {
+ display: true,
+ labelString: yProp,
},
- ],
+ ticks: buildTicks(yProp, { min, max }),
+ id: `y-${yProp}`,
+ position: idx % 2 == 0 ? "left" : "right",
+ })),
},
},
additionalOptions
@@ -207,43 +299,43 @@ function createBaseCfg({ data }, { x, y, additionalOptions }, seriesFormatter =
return cfg;
}
-function createLineCfg({ data }, { columns, x, y, additionalOptions, configHandler }) {
- const seriesCt = _.size(data);
+function createLineCfg({ data, min, max }, { columns, x, y, additionalOptions, configHandler }) {
+ const seriesCt = _.size(data) * _.size(y);
const colors = COLOR_SCALE.domain([0, seriesCt]);
- const seriesFormatter = (k, v, i) => {
- const ptCfg = buildSeries(k, v, i);
+ const seriesFormatter = (k, v, i, yProp) => {
+ const ptCfg = buildSeries(k, v, i, yProp);
_.forEach(COLOR_PROPS, cp => (ptCfg[cp] = seriesCt == 1 ? "rgb(42, 145, 209)" : colors(i).hex()));
return ptCfg;
};
- const cfg = createBaseCfg({ data }, { columns, x, y, additionalOptions }, seriesFormatter);
+ const cfg = createBaseCfg({ data, min, max }, { columns, x, y, additionalOptions }, seriesFormatter);
cfg.type = "line";
- updateCfgForDates(cfg, { columns, x });
+ updateCfgForDates(cfg, { columns, x, y }, { min, max });
return configHandler(cfg);
}
-function createBarCfg({ data }, { columns, x, y, additionalOptions, configHandler }) {
- const cfg = createLineCfg({ data }, { columns, x, y, additionalOptions, configHandler });
+function createBarCfg({ data, min, max }, { columns, x, y, additionalOptions, configHandler }) {
+ const cfg = createLineCfg({ data, min, max }, { columns, x, y, additionalOptions, configHandler });
cfg.type = "bar";
return cfg;
}
-function createStackedCfg({ data }, { columns, x, y, additionalOptions, configHandler }) {
- const cfg = createLineCfg({ data }, { columns, x, y, additionalOptions, configHandler });
+function createStackedCfg({ data, min, max }, { columns, x, y, additionalOptions, configHandler }) {
+ const cfg = createLineCfg({ data, min, max }, { columns, x, y, additionalOptions, configHandler });
cfg.type = "bar";
cfg.options.scales.xAxes[0].stacked = true;
- cfg.options.scales.yAxes[0].stacked = true;
+ _.forEach(cfg.options.scales.yAxes, axisCfg => (axisCfg.stacked = true));
return cfg;
}
-function createPieCfg({ data }, { columns, x, y, additionalOptions, configHandler }) {
+function createPieCfg({ data, min, max }, { columns, x, y, additionalOptions, configHandler }) {
const seriesCt = _.get(_.values(data), "0.x.length");
const colors = COLOR_SCALE.domain([0, seriesCt]);
- const seriesFormatter = (k, v, i) => {
- const ptCfg = buildSeries(k, v, i);
+ const seriesFormatter = (k, v, i, yProp) => {
+ const ptCfg = buildSeries(k, v, i, yProp);
ptCfg.backgroundColor = _.map(v.y, (_p, i) => (seriesCt == 1 ? "rgb(42, 145, 209)" : colors(i).hex()));
return ptCfg;
};
- const cfg = createBaseCfg({ data }, { columns, x, y, additionalOptions, configHandler }, seriesFormatter);
+ const cfg = createBaseCfg({ data, min, max }, { columns, x, y, additionalOptions, configHandler }, seriesFormatter);
cfg.type = "pie";
delete cfg.options.scales;
delete cfg.options.tooltips;
@@ -253,6 +345,66 @@ function createPieCfg({ data }, { columns, x, y, additionalOptions, configHandle
return configHandler(cfg);
}
+const SCATTER_BUILDER = (data, prop) =>
+ _.map(_.zip(data.all.x, data.all[prop]), ([xVal, yVal]) => ({
+ x: xVal,
+ y: yVal,
+ }));
+
+function createScatterCfg(
+ { data, min, max },
+ { columns, x, y, additionalOptions, configHandler },
+ builder = SCATTER_BUILDER
+) {
+ const yProp = _.head(y);
+ const chartData = builder(data, yProp);
+ const scatterData = formatScatterPoints(chartData);
+ const cfg = {
+ type: "scatter",
+ data: {
+ datasets: [_.assign({ xLabels: [x], yLabels: [yProp] }, scatterData)],
+ },
+ options: _.assignIn(
+ {
+ tooltips: {
+ callbacks: {
+ label: (tooltipItem, chartData) => {
+ const dataset = chartData.datasets[tooltipItem.datasetIndex];
+ const pointData = dataset.data[tooltipItem.index];
+ return [
+ `${dataset.xLabels[0]}: ${_.round(pointData.x, 4)}`,
+ `${dataset.yLabels[0]}: ${_.round(pointData.y, 4)}`,
+ ];
+ },
+ },
+ },
+ scales: {
+ xAxes: [
+ {
+ scaleLabel: { display: true, labelString: x },
+ },
+ ],
+ yAxes: [
+ {
+ ticks: buildTicks(yProp, { min, max }),
+ scaleLabel: { display: true, labelString: yProp },
+ },
+ ],
+ },
+ legend: { display: false },
+ pan: { enabled: true, mode: "x" },
+ zoom: { enabled: true, mode: "x" },
+ maintainAspectRatio: false,
+ responsive: false,
+ showLines: false,
+ },
+ additionalOptions
+ ),
+ };
+ updateCfgForDates(cfg, { columns, x, y }, { min, max });
+ return _.isUndefined(configHandler) ? cfg : configHandler(cfg);
+}
+
export default {
createChart,
chartWrapper,
@@ -262,5 +414,11 @@ export default {
createLineCfg,
createBarCfg,
createStackedCfg,
+ createScatterCfg,
createPieCfg,
+ timestampLabel,
+ formatScatterPoints,
+ getScatterMax,
+ getScatterMin,
+ buildTicks,
};
diff --git a/static/dtale/Header.jsx b/static/dtale/Header.jsx
index 54a8f6ca6..13cd803b6 100644
--- a/static/dtale/Header.jsx
+++ b/static/dtale/Header.jsx
@@ -8,8 +8,8 @@ import menuUtils from "../menuUtils";
import * as gu from "./gridUtils";
const SORT_CHARS = {
- DESC: String.fromCharCode("9650"),
- ASC: String.fromCharCode("9660"),
+ ASC: String.fromCharCode("9650"),
+ DESC: String.fromCharCode("9660"),
};
class ReactHeader extends React.Component {
diff --git a/static/dtale/iframe/ColumnMenu.jsx b/static/dtale/iframe/ColumnMenu.jsx
index ec8fab5da..ea8c8e529 100644
--- a/static/dtale/iframe/ColumnMenu.jsx
+++ b/static/dtale/iframe/ColumnMenu.jsx
@@ -32,7 +32,7 @@ class ReactColumnMenu extends React.Component {
col: selectedCol,
});
const openDescribe = () => {
- window.open(describeUrl, "_blank", "titlebar=1,location=1,status=1,width=500,height=450");
+ window.open(describeUrl, "_blank", "titlebar=1,location=1,status=1,width=1100,height=450");
};
const histogramUrl = buildURLString(`/dtale/popup/histogram/${dataId}`, {
col: selectedCol,
diff --git a/static/popups/Correlations.jsx b/static/popups/Correlations.jsx
index f8815bdd8..2a02326f9 100644
--- a/static/popups/Correlations.jsx
+++ b/static/popups/Correlations.jsx
@@ -11,6 +11,7 @@ import { closeChart } from "../actions/charts";
import { buildURL } from "../actions/url-utils";
import chartUtils from "../chartUtils";
import { fetchJson } from "../fetcher";
+import { toggleBouncer } from "../toggleUtils";
import ChartsBody from "./charts/ChartsBody";
import CorrelationScatterStats from "./correlations/CorrelationScatterStats";
import CorrelationsGrid from "./correlations/CorrelationsGrid";
@@ -134,9 +135,9 @@ class ReactCorrelations extends React.Component {
if (this.state.scatterUrl === scatterUrl) {
return;
}
- corrUtils.toggleBouncer();
+ toggleBouncer(["scatter-bouncer", "rawScatterChart"]);
fetchJson(scatterUrl, fetchedChartData => {
- corrUtils.toggleBouncer();
+ toggleBouncer(["scatter-bouncer", "rawScatterChart"]);
const newState = {
selectedCols,
stats: fetchedChartData.stats,
@@ -148,11 +149,11 @@ class ReactCorrelations extends React.Component {
newState.scatterError = ;
}
const builder = ctx => {
- if (!_.get(fetchedChartData, "data", []).length) {
+ if (!_.get(fetchedChartData, "data.all.x", []).length) {
return null;
}
- const { data, x, y } = fetchedChartData;
- return corrUtils.createScatter(ctx, data, x, y, this.props.chartData.title, this.viewScatterRow);
+ const { x, y } = fetchedChartData;
+ return corrUtils.createScatter(ctx, fetchedChartData, x, y, this.viewScatterRow);
};
newState.chart = chartUtils.chartWrapper("rawScatterChart", this.state.chart, builder);
this.setState(newState);
@@ -196,11 +197,11 @@ class ReactCorrelations extends React.Component {
visible={true}
url={tsUrl}
columns={[
- { name: "date", dtype: "datetime[ns]" },
+ { name: "x", dtype: "datetime[ns]" },
{ name: "corr", dtype: "float64" },
]}
- x="date"
- y="corr"
+ x={{ value: "x" }}
+ y={[{ value: "corr" }]}
configHandler={config => {
config.options.scales.yAxes = [
{
@@ -209,16 +210,18 @@ class ReactCorrelations extends React.Component {
data.ticks[0] = null;
data.ticks[data.ticks.length - 1] = null;
},
+ id: "y-corr",
},
];
if (!this.state.rolling) {
config.options.onClick = this.viewScatter;
}
config.options.legend = { display: false };
- config.plugins = [chartUtils.gradientLinePlugin(corrUtils.colorScale, -1, 1)];
+ config.plugins = [chartUtils.gradientLinePlugin(corrUtils.colorScale, "y-corr", -1, 1)];
return config;
}}
height={300}
+ showControls={false}
/>
diff --git a/static/popups/Histogram.jsx b/static/popups/Histogram.jsx
index 51adbd68e..56732fc1f 100644
--- a/static/popups/Histogram.jsx
+++ b/static/popups/Histogram.jsx
@@ -51,7 +51,7 @@ function createHistogram(ctx, fetchedData, col) {
class ReactHistogram extends React.Component {
constructor(props) {
super(props);
- this.state = { chart: null, bins: 20 };
+ this.state = { chart: null, bins: "20" };
this.buildHistogramFilters = this.buildHistogramFilters.bind(this);
this.buildHistogram = this.buildHistogram.bind(this);
}
@@ -76,7 +76,9 @@ class ReactHistogram extends React.Component {
componentDidUpdate(_prevProps, prevState) {
if (this.state.bins !== prevState.bins) {
- this.buildHistogram();
+ if (this.state.bins && parseInt(this.state.bins)) {
+ this.buildHistogram();
+ }
}
}
@@ -85,17 +87,16 @@ class ReactHistogram extends React.Component {
}
buildHistogramFilters() {
- const binChange = e => this.setState({ bins: _.parseInt(e.target.value) });
return (
-
-
+
+ this.setState({ bins: e.target.value })}
+ />
);
diff --git a/static/popups/charts/Aggregations.jsx b/static/popups/charts/Aggregations.jsx
new file mode 100644
index 000000000..c4a3c7d2c
--- /dev/null
+++ b/static/popups/charts/Aggregations.jsx
@@ -0,0 +1,143 @@
+import _ from "lodash";
+import PropTypes from "prop-types";
+import React from "react";
+import Select, { createFilter } from "react-select";
+
+import { isDateCol } from "../../dtale/gridUtils";
+
+const AGGREGATION_OPTS = [
+ { value: "count", label: "Count" },
+ { value: "nunique", label: "Unique Count" },
+ { value: "sum", label: "Sum" },
+ { value: "mean", label: "Mean" },
+ { value: "rolling", label: "Rolling" },
+ { value: "first", label: "First" },
+ { value: "last", label: "Last" },
+ { value: "median", label: "Median" },
+ { value: "min", label: "Minimum" },
+ { value: "max", label: "Maximum" },
+ { value: "std", label: "Standard Deviation" },
+ { value: "var", label: "Variance" },
+ { value: "mad", label: "Mean Absolute Deviation" },
+ { value: "prod", label: "Product of All Items" },
+];
+
+function getAggregations({ columns, x, group }) {
+ if (isDateCol(_.get(_.find(columns, { name: _.get(x, "value") }), "dtype", "")) && _.isEmpty(group)) {
+ return AGGREGATION_OPTS;
+ }
+ return _.reject(AGGREGATION_OPTS, { value: "rolling" });
+}
+
+const ROLLING_COMPS = [
+ { value: "corr", label: "Correlation" },
+ { value: "count", label: "Count" },
+ { value: "cov", label: "Covariance" },
+ { value: "kurt", label: "Kurtosis" },
+ { value: "max", label: "Maximum" },
+ { value: "mean", label: "Mean" },
+ { value: "median", label: "median" },
+ { value: "min", label: "Minimum" },
+ { value: "skew", label: "Skew" },
+ { value: "std", label: "Standard Deviation" },
+ { value: "sum", label: "Sum" },
+ { value: "var", label: "Variance" },
+];
+
+class Aggregations extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ aggregation: props.aggregation ? _.find(AGGREGATION_OPTS, { value: props.aggregation }) : null,
+ rollingComputation: null,
+ rollingWindow: "4",
+ };
+ this.update = this.update.bind(this);
+ this.renderRolling = this.renderRolling.bind(this);
+ }
+
+ update(val, upperVal) {
+ this.setState(val, () => this.props.propagateState(upperVal));
+ }
+
+ renderRolling() {
+ if (_.get(this.state, "aggregation.value") === "rolling") {
+ const { rollingWindow, rollingComputation } = this.state;
+ return [
+
+ Window:
+ ,
+
+
+ this.update(
+ { rollingWindow: _.get(e, "target.value", "") },
+ { rollingWindow: _.get(e, "target.value", "") }
+ )
+ }
+ />
+
,
+
+ Computation:
+ ,
+
+
+
+
,
+
,
+ ];
+ }
+ return
;
+ }
+
+ render() {
+ return [
+
+ Aggregation:
+ ,
+
+
+
+
,
+ this.renderRolling(),
+ ];
+ }
+}
+Aggregations.displayName = "Aggregations";
+Aggregations.propTypes = {
+ propagateState: PropTypes.func,
+ columns: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/no-unused-prop-types
+ x: PropTypes.object, // eslint-disable-line react/no-unused-prop-types
+ group: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/no-unused-prop-types
+ aggregation: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
+};
+
+export { Aggregations, AGGREGATION_OPTS, ROLLING_COMPS };
diff --git a/static/popups/charts/AxisEditor.css b/static/popups/charts/AxisEditor.css
new file mode 100644
index 000000000..47334d87e
--- /dev/null
+++ b/static/popups/charts/AxisEditor.css
@@ -0,0 +1,52 @@
+.axis-toggle__dropdown {
+ position: absolute;
+ top: 100%;
+ min-width: 15em;
+ width: 100%;
+ color: #404040;
+ font-weight: 400;
+ z-index: 10;
+ margin-top: -1px;
+ background-color: white;
+ border-width: 1px 1px 0 1px;
+ border-style: solid;
+ border-color: #a7b3b7;
+ border-top-color: #d3d9db;
+ border-radius: 0 0 0.25rem 0.25rem;
+}
+
+.axis-toggle__dropdown ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.axis-toggle__dropdown li {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ width: 100%;
+}
+
+.axis-toggle__dropdown li:last-child {
+ border-bottom: solid 1px #a7b3b7;
+}
+
+.axis-toggle__dropdown li span {
+ padding: 0.3em;
+}
+
+.axis-toggle__dropdown li span.toggler-action {
+ border: none;
+ margin-top: -0.1em;
+}
+
+.axis-toggle__dropdown li span.toggler-action button i {
+ vertical-align: text-bottom;
+}
+
+.axis-input {
+ text-align: center;
+ width: 3em !important;
+ padding: 0.3rem 0.3rem !important;
+}
diff --git a/static/popups/charts/AxisEditor.jsx b/static/popups/charts/AxisEditor.jsx
new file mode 100644
index 000000000..5ce25feae
--- /dev/null
+++ b/static/popups/charts/AxisEditor.jsx
@@ -0,0 +1,126 @@
+import _ from "lodash";
+import PropTypes from "prop-types";
+import React from "react";
+
+import menuUtils from "../../menuUtils";
+
+function buildState({ y, data }) {
+ const state = {};
+ _.forEach(y, ({ value }) => {
+ state[`${value}-min`] = _.get(data, ["min", value], "");
+ state[`${value}-max`] = _.get(data, ["max", value], "");
+ });
+ return state;
+}
+
+require("./AxisEditor.css");
+
+class AxisEditor extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = _.assignIn({ open: false }, buildState(props));
+ this.closeMenu = this.closeMenu.bind(this);
+ }
+
+ shouldComponentUpdate(newProps, newState) {
+ return !_.isEqual(this.props.data, newProps.data) || !_.isEqual(this.state, newState);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (!_.isEqual(this.props.data, prevProps.data)) {
+ this.setState(buildState(this.props));
+ }
+ }
+
+ closeMenu() {
+ const settings = {
+ min: _.assign({}, this.props.data.min),
+ max: _.assign({}, this.props.data.max),
+ };
+ const errors = [];
+ _.forEach(this.props.y, ({ value }) => {
+ _.forEach(["min", "max"], prop => {
+ const curr = this.state[`${value}-${prop}`];
+ if (curr !== "" && !_.isNaN(parseFloat(curr))) {
+ settings[prop][value] = parseFloat(curr);
+ } else {
+ errors.push(`${value} has invalid ${prop}!`);
+ }
+ });
+ });
+ _.forEach(this.props.y, ({ value }) => {
+ if (settings.min[value] > settings.max[value]) {
+ errors.push(`${value} must have a min < max!`);
+ }
+ });
+ if (_.size(errors)) {
+ this.setState({ errors });
+ } else {
+ this.setState({ open: false }, () => this.props.updateAxis(settings));
+ }
+ }
+
+ render() {
+ if (_.isEmpty(this.props.data)) {
+ return null;
+ }
+ const { min, max } = this.props.data;
+ const { y } = this.props;
+ const axisMarkup = _.map(y, ({ value }, idx) => {
+ const minProp = `${value}-min`;
+ const maxProp = `${value}-max`;
+ return (
+
+ {value}
+ Min:
+
+ this.setState({ [minProp]: e.target.value })}
+ />
+
+ Max:
+
+ this.setState({ [maxProp]: e.target.value })}
+ />
+
+
+ );
+ });
+ const menuHandler = menuUtils.openMenu("axisEditor", () => this.setState({ open: true }), this.closeMenu);
+ return (
+
+
+
Axis Ranges
+
+
+ {_.truncate(
+ _.join(
+ _.map(y, ({ value }) => `${value} (${min[value]},${max[value]})`),
+ ", "
+ )
+ )}
+
+
+
+
+
+ );
+ }
+}
+AxisEditor.displayName = "AxisEditor";
+AxisEditor.propTypes = {
+ data: PropTypes.object,
+ y: PropTypes.arrayOf(PropTypes.object),
+ updateAxis: PropTypes.func,
+};
+
+export default AxisEditor;
diff --git a/static/popups/charts/ChartLabel.jsx b/static/popups/charts/ChartLabel.jsx
index 5ffc8b1e2..6af4aab53 100644
--- a/static/popups/charts/ChartLabel.jsx
+++ b/static/popups/charts/ChartLabel.jsx
@@ -2,27 +2,54 @@ import _ from "lodash";
import PropTypes from "prop-types";
import React from "react";
+import { AGGREGATION_OPTS, ROLLING_COMPS } from "./Aggregations";
+
+function buildLabel({ x, y, group, aggregation, rollingWindow, rollingComputation }) {
+ const yLabel = _.join(_.map(y, "value"), ", ");
+ let labelStr = yLabel;
+ if (aggregation) {
+ const aggLabel = _.find(AGGREGATION_OPTS, { value: aggregation }).label;
+ if (aggregation === "rolling") {
+ const compLabel = _.find(ROLLING_COMPS, { value: rollingComputation }).label;
+ labelStr = `${aggLabel} ${compLabel} (window: ${rollingWindow}) of ${yLabel}`;
+ } else {
+ labelStr = `${aggLabel} of ${yLabel}`;
+ }
+ }
+ labelStr = `${labelStr} by ${_.get(x, "value")}`;
+ if (!_.isEmpty(group)) {
+ labelStr = `${labelStr} grouped by ${_.join(_.map(group, "value"), ", ")}`;
+ }
+ return labelStr;
+}
+
class ChartLabel extends React.Component {
- render() {
- const { x, y, group, aggregation } = this.props;
- let labelStr = aggregation ? `${aggregation.label} of ${y.value}` : y.value;
- labelStr = `${labelStr} by ${x.value}`;
- if (!_.isEmpty(group)) {
- labelStr = `${labelStr} grouped by ${_.join(_.map(group, "value"), ", ")}`;
+ constructor(props) {
+ super(props);
+ this.state = { label: buildLabel(this.props) };
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.url !== prevProps.url) {
+ this.setState({ label: buildLabel(this.props) });
}
+ }
+
+ render() {
return (
- {labelStr}
+ {this.state.label}
);
}
}
ChartLabel.displayName = "ChartLabel";
ChartLabel.propTypes = {
- x: PropTypes.object,
- y: PropTypes.object,
- group: PropTypes.arrayOf(PropTypes.object),
- aggregation: PropTypes.object,
+ url: PropTypes.string,
+ x: PropTypes.object, // eslint-disable-line react/no-unused-prop-types
+ y: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/no-unused-prop-types
+ group: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/no-unused-prop-types
+ aggregation: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
};
export default ChartLabel;
diff --git a/static/popups/charts/Charts.css b/static/popups/charts/Charts.css
index 30bbffa33..750524bde 100644
--- a/static/popups/charts/Charts.css
+++ b/static/popups/charts/Charts.css
@@ -4,8 +4,6 @@ div.charts-body input[type='checkbox'] {
div.charts-body {
padding: 1em;
}
-/*.charts-filters .Select__control,
-.charts-filters .Select__value-container,
-.charts-filters .Select__placeholder {
- height: calc(1.51562rem + 2px);
-}*/
+div.charts-filters > span:first-child {
+ width: 5.5em;
+}
diff --git a/static/popups/charts/Charts.jsx b/static/popups/charts/Charts.jsx
index f432fb017..b37518263 100644
--- a/static/popups/charts/Charts.jsx
+++ b/static/popups/charts/Charts.jsx
@@ -1,59 +1,55 @@
import _ from "lodash";
-import moment from "moment";
import PropTypes from "prop-types";
import React from "react";
import { connect } from "react-redux";
import Select, { createFilter } from "react-select";
-import ConditionalRender from "../../ConditionalRender";
-import { JSAnchor } from "../../JSAnchor";
import { RemovableError } from "../../RemovableError";
import { buildURLString } from "../../actions/url-utils";
-import { isDateCol } from "../../dtale/gridUtils";
import { fetchJson } from "../../fetcher";
-import ChartLabel from "./ChartLabel";
+import { Aggregations } from "./Aggregations";
import ChartsBody from "./ChartsBody";
-const AGGREGATIONS = [
- { value: "count", label: "Count" },
- { value: "first", label: "First" },
- { value: "last", label: "Last" },
- { value: "mean", label: "Mean" },
- { value: "median", label: "Median" },
- { value: "min", label: "Minimum" },
- { value: "max", label: "Maximum" },
- { value: "std", label: "Standard Deviation" },
- { value: "var", label: "Variance" },
- { value: "mad", label: "Mean Absolute Deviation" },
- { value: "prod", label: "Product of All Items" },
- { value: "sum", label: "Sum" },
-];
-const CHART_TYPES = ["line", "bar", "stacked", "pie", "wordcloud"];
-
-function generateChartState({ group, x, y, query, aggregation }, { dataId }) {
+function generateChartState(state, { dataId }) {
+ const { group, x, y, query, aggregation, rollingWindow, rollingComputation } = state;
if (_.isNull(x) || _.isNull(y)) {
return { url: null };
}
- const params = { x: x.value, y: y.value, query };
+ const params = { x: x.value, y: _.join(_.map(y, "value"), ","), query };
if (!_.isNull(group)) {
params.group = _.join(_.map(group, "value"), ",");
}
if (!_.isNull(aggregation)) {
- params.agg = aggregation.value;
+ params.agg = aggregation;
+ if (aggregation === "rolling") {
+ if (rollingWindow && parseInt(rollingWindow)) {
+ params.rollingWin = parseInt(rollingWindow);
+ } else {
+ return { url: null, error: "Aggregation (rolling) requires a window" };
+ }
+ if (rollingComputation) {
+ params.rollingComp = rollingComputation;
+ } else {
+ return {
+ url: null,
+ error: "Aggregation (rolling) requires a computation",
+ };
+ }
+ }
}
return { url: buildURLString(`/dtale/chart-data/${dataId}`, params) };
}
-const baseState = ({ query, x, y, group, aggregation, chartType, chartPerGroup }) => ({
+const baseState = ({ query, x, y, group, aggregation }) => ({
x: x ? { value: x } : null,
- y: y ? { value: y } : null,
+ y: y ? _.map(y, y2 => ({ value: y2 })) : null,
group: group ? _.map(group, g => ({ value: g })) : null,
- aggregation: aggregation ? _.find(AGGREGATIONS, { value: aggregation }) : null,
- chartType: { value: chartType || "line" },
+ aggregation,
+ rollingComputation: null,
+ rollingWindow: "4",
url: null,
- zoomed: null,
- chartPerGroup: chartPerGroup === "true",
query,
+ error: null,
});
require("./Charts.css");
@@ -62,9 +58,6 @@ class ReactCharts extends React.Component {
constructor(props) {
super(props);
this.state = baseState(_.get(props, "chartData") || {});
- this.viewTimeDetails = this.viewTimeDetails.bind(this);
- this.resetZoom = this.resetZoom.bind(this);
- this.renderLabel = this.renderLabel.bind(this);
this.renderSelect = this.renderSelect.bind(this);
}
@@ -111,101 +104,34 @@ class ReactCharts extends React.Component {
);
}
- resetZoom() {
- const charts = _.get(this, "_chart.state.charts");
- if (charts) {
- _.forEach(charts, c => {
- delete c.options.scales.xAxes[0].ticks;
- c.update();
- });
- this.setState({ zoomed: false });
- }
- }
-
- viewTimeDetails(evt) {
- const charts = _.get(this, "_chart.state.charts");
- if (charts) {
- const selectedChart = _.find(charts, c => !_.isEmpty(c.getElementAtEvent(evt)));
- const selectedPoint = _.head(selectedChart.getElementAtEvent(evt));
- if (selectedPoint) {
- const ticks = {
- min: selectedChart.data.labels[_.max([0, selectedPoint._index - 10])],
- max: selectedChart.data.labels[_.min([selectedChart.data.labels.length - 1, selectedPoint._index + 10])],
- };
- _.forEach(charts, c => {
- c.options.scales.xAxes[0].ticks = ticks;
- c.update();
- });
- let zoomed = `${ticks.min} - ${ticks.max}`;
- if (isDateCol(_.find(this.state.columns, { name: this.state.x.value }).dtype)) {
- const buildLabel = x => moment(new Date(x)).format("YYYY-MM-DD");
- zoomed = `${buildLabel(ticks.min)} - ${buildLabel(ticks.max)}`;
- }
- this.setState({ zoomed });
- }
- }
- }
-
- renderLabel() {
- const { url, error, zoomed } = this.state;
- return (
-
-
-
-
- {`Zoomed: ${zoomed}`}
- {"X"}
-
-
-
- );
- }
-
render() {
- const { columns } = this.state;
+ const { columns, query, error } = this.state;
if (_.isEmpty(columns)) {
- return null;
- }
- const additionalOptions = {};
- if (this.state.chartType.value === "line") {
- additionalOptions.onClick = this.viewTimeDetails;
+ return error;
}
return (
-
X:
-
{this.renderSelect("x", ["y", "group"])}
-
Y:
-
{this.renderSelect("y", ["x", "group"])}
-
Group:
-
{this.renderSelect("group", ["x", "y"], true)}
-
Aggregation:
-
-
-
-
-
-
-
Query
+
Query:
this.setState({ query: e.target.value })}
/>
+
+
+
X:
+
{this.renderSelect("x", ["y", "group"])}
+
Y:
+
{this.renderSelect("y", ["x", "group"], true)}
+
Group:
+
{this.renderSelect("group", ["x", "y"], true)}
+
+
+
this.setState(state)} {...this.state} />
-
-
Chart:
-
-
-
-
-
0}>
- Chart per Group:
-
- this.setState({ chartPerGroup: e.target.checked })}
- />
-
-
-
-
(this._chart = r)}
+ chartType={_.get(this.props, "chartData.chartType")}
+ chartPerGroup={_.get(this.props, "chartData.chartPerGroup")}
visible={_.get(this.props, "chartData.visible", false)}
- url={this.state.url}
- columns={this.state.columns}
- chartType={_.get(this.state, "chartType.value")}
- chartPerGroup={this.state.chartPerGroup}
- x={_.get(this.state, "x.value")}
- y={_.get(this.state, "y.value")}
- group={_.join(_.map(_.get(this.state, "group") || [], "value"), ",")}
- additionalOptions={additionalOptions}
+ {...this.state}
height={450}
/>
diff --git a/static/popups/charts/ChartsBody.jsx b/static/popups/charts/ChartsBody.jsx
index 917121f3b..4fa815f59 100644
--- a/static/popups/charts/ChartsBody.jsx
+++ b/static/popups/charts/ChartsBody.jsx
@@ -1,66 +1,121 @@
-import $ from "jquery";
+/* eslint max-lines: "off" */
import _ from "lodash";
+import moment from "moment";
import PropTypes from "prop-types";
import React from "react";
+import Select, { createFilter } from "react-select";
import { Bouncer } from "../../Bouncer";
+import ConditionalRender from "../../ConditionalRender";
+import { JSAnchor } from "../../JSAnchor";
import { RemovableError } from "../../RemovableError";
import chartUtils from "../../chartUtils";
+import { isDateCol } from "../../dtale/gridUtils";
import { fetchJson } from "../../fetcher";
+import { toggleBouncer } from "../../toggleUtils";
+import AxisEditor from "./AxisEditor";
+import ChartLabel from "./ChartLabel";
import WordcloudBody from "./WordcloudBody";
-function toggleBouncer() {
- $("#chart-bouncer").toggle();
- $("#coveragePopup").toggle();
+function chartType(state) {
+ return _.get(state, "chartType.value");
}
-function createChartCfg(ctx, { data }, { columns, x, y, additionalOptions, chartType, configHandler }) {
+function chartTypes({ y }) {
+ const types = ["line", "bar", "stacked"];
+ const yList = _.concat([], y || []);
+ if (_.size(yList) < 2) {
+ types.push("scatter");
+ types.push("pie");
+ }
+ types.push("wordcloud");
+ return types;
+}
+
+function createChartCfg(ctx, data, { columns, x, y, additionalOptions, chartType, configHandler }, funcs = {}) {
let cfg = null;
+ const mainProps = {
+ columns,
+ x: _.get(x, "value"),
+ y: _.map(y || [], "value"),
+ additionalOptions,
+ configHandler,
+ };
switch (chartType) {
case "bar":
- cfg = chartUtils.createBarCfg({ data }, { columns, x, y, additionalOptions, configHandler });
+ cfg = chartUtils.createBarCfg(data, mainProps);
break;
case "stacked":
- cfg = chartUtils.createStackedCfg({ data }, { columns, x, y, additionalOptions, configHandler });
+ cfg = chartUtils.createStackedCfg(data, mainProps);
+ break;
+ case "scatter":
+ cfg = chartUtils.createScatterCfg(data, mainProps);
break;
case "pie":
- cfg = chartUtils.createPieCfg({ data }, { columns, x, y, additionalOptions, configHandler });
+ cfg = chartUtils.createPieCfg(data, mainProps);
break;
case "line":
- default:
- cfg = chartUtils.createLineCfg({ data }, { columns, x, y, additionalOptions, configHandler });
+ default: {
+ if (_.has(funcs, "viewTimeDetails")) {
+ mainProps.additionalOptions = _.assignIn(additionalOptions || {}, {
+ onClick: funcs.viewTimeDetails,
+ });
+ }
+ cfg = chartUtils.createLineCfg(data, mainProps);
break;
+ }
}
return chartUtils.createChart(ctx, cfg);
}
-function createCharts(data, props) {
+function createCharts(data, props, state, funcs = {}) {
if (_.isEmpty(_.get(data, "data", {}))) {
return null;
}
- if (props.chartType === "wordcloud") {
+ const chartTypeVal = chartType(state);
+ if (chartTypeVal === "wordcloud") {
return null;
}
- if (props.chartPerGroup) {
+ if (state.chartPerGroup) {
return _.map(_.get(data, "data", {}), (series, seriesKey) => {
- const mainProps = _.pick(props, ["columns", "x", "y", "additionalOptions", "chartType", "configHandler"]);
+ const mainProps = _.pick(props, ["columns", "x", "y", "additionalOptions", "configHandler"]);
+ mainProps.chartType = chartTypeVal;
mainProps.additionalOptions = _.assignIn(mainProps.additionalOptions, {
title: { display: true, text: seriesKey },
});
- const builder = ctx => createChartCfg(ctx, { data: { all: series } }, mainProps);
+ const subData = { data: { all: series }, min: data.min, max: data.max };
+ const builder = ctx => createChartCfg(ctx, subData, mainProps, funcs);
return chartUtils.chartWrapper(`chartCanvas-${seriesKey}`, null, builder);
});
}
- const builder = ctx => createChartCfg(ctx, data, props);
+ const builder = ctx => createChartCfg(ctx, data, _.assignIn({}, props, { chartType: chartTypeVal }), funcs);
return [chartUtils.chartWrapper("chartCanvas", null, builder)];
}
+function sortBars(data, props, state) {
+ _.forEach(_.get(data, "data", {}), series => {
+ const [sortedX, sortedY] = _.unzip(_.sortBy(_.zip(series.x, series[props.y]), "1"));
+ series.x = sortedX;
+ series[props.y] = sortedY;
+ });
+ return createCharts(data, props, state);
+}
+
class ChartsBody extends React.Component {
constructor(props) {
super(props);
this.mounted = false;
- this.state = { charts: null, error: null };
- this.buildChart = this.buildChart.bind(this);
+ this.state = {
+ chartType: { value: props.chartType || "line" },
+ chartPerGroup: props.chartPerGroup === "true",
+ charts: null,
+ error: null,
+ zoomed: null,
+ };
+ _.forEach(
+ ["buildChart", "sortBars", "updateAxis", "viewTimeDetails", "resetZoom", "renderLabel", "renderControls"],
+ f => (this[f] = this[f].bind(this))
+ );
}
shouldComponentUpdate(newProps, newState) {
@@ -72,11 +127,12 @@ class ChartsBody extends React.Component {
return true;
}
- if (this.props.chartType == "wordcloud" && !_.isEqual(this.state.data, newState.data)) {
+ const selectedState = ["chartType", "chartPerGroup", "data", "zoomed"];
+ if (!_.isEqual(_.pick(this.state, selectedState), _.pick(newState, selectedState))) {
return true;
}
- if (this.state.chart != newState.chart) {
+ if (this.state.charts != newState.charts) {
// Don't re-render if we've only changed the chart.
return false;
}
@@ -84,7 +140,7 @@ class ChartsBody extends React.Component {
return false; // Otherwise, use the default react behaviour.
}
- componentDidUpdate(prevProps) {
+ componentDidUpdate(prevProps, prevState) {
if (!this.props.visible) {
return;
}
@@ -92,13 +148,18 @@ class ChartsBody extends React.Component {
this.buildChart();
return;
}
- const selectedProps = ["chartType", "chartPerGroup"];
- if (!_.isEqual(_.pick(this.props, selectedProps), _.pick(prevProps, selectedProps))) {
- _.forEach(this.state.charts || [], c => c.destroy());
- this.setState({
- error: null,
- charts: createCharts(_.get(this.state, "data", {}), this.props),
- });
+ const selectedState = ["chartType", "chartPerGroup"];
+ if (!_.isEqual(_.pick(this.state, selectedState), _.pick(prevState, selectedState))) {
+ if (chartType(this.state) !== chartType({ state: prevState }) && chartType({ state: prevState }) === "scatter") {
+ this.buildChart(); //need to reload chart data because scatter charts allow duplicates
+ } else {
+ _.forEach(this.state.charts || [], c => c.destroy());
+ const funcs = _.pick(this, ["viewTimeDetails"]);
+ this.setState({
+ error: null,
+ charts: createCharts(_.get(this.state, "data", {}), this.props, this.state, funcs),
+ });
+ }
}
}
@@ -115,10 +176,10 @@ class ChartsBody extends React.Component {
if (_.isNil(this.props.url)) {
return;
}
- toggleBouncer();
+ toggleBouncer(["chart-bouncer", "coveragePopup"]);
_.forEach(this.state.charts || [], c => c.destroy());
- fetchJson(this.props.url, fetchedChartData => {
- toggleBouncer();
+ fetchJson(`${this.props.url}${chartType(this.state) === "scatter" ? "&allowDupes=true" : ""}`, fetchedChartData => {
+ toggleBouncer(["chart-bouncer", "coveragePopup"]);
if (this.mounted) {
if (fetchedChartData.error) {
this.setState({
@@ -138,41 +199,161 @@ class ChartsBody extends React.Component {
this.setState({
error: null,
data: fetchedChartData,
- charts: createCharts(fetchedChartData, this.props),
+ charts: createCharts(fetchedChartData, this.props, this.state, _.pick(this, ["viewTimeDetails"])),
});
}
});
}
+ resetZoom() {
+ const { charts } = this.state;
+ if (charts) {
+ _.forEach(charts, c => {
+ delete c.options.scales.xAxes[0].ticks;
+ c.update();
+ });
+ this.setState({ zoomed: null });
+ }
+ }
+
+ viewTimeDetails(evt) {
+ const { charts } = this.state;
+ if (charts) {
+ const selectedChart = _.find(charts, c => !_.isEmpty(c.getElementAtEvent(evt)));
+ if (selectedChart) {
+ const selectedPoint = _.head(selectedChart.getElementAtEvent(evt));
+ if (selectedPoint) {
+ const ticks = {
+ min: selectedChart.data.labels[_.max([0, selectedPoint._index - 10])],
+ max: selectedChart.data.labels[_.min([selectedChart.data.labels.length - 1, selectedPoint._index + 10])],
+ };
+ _.forEach(charts, c => {
+ c.options.scales.xAxes[0].ticks = ticks;
+ c.update();
+ });
+ let zoomed = `${ticks.min} - ${ticks.max}`;
+ if (isDateCol(_.find(this.props.columns, { name: this.props.x.value }).dtype)) {
+ const buildLabel = x => moment(new Date(x)).format("YYYY-MM-DD");
+ zoomed = `${buildLabel(ticks.min)} - ${buildLabel(ticks.max)}`;
+ }
+ this.setState({ zoomed });
+ }
+ }
+ }
+ }
+
+ renderLabel() {
+ const { data, error, zoomed } = this.state;
+ return (
+
+
+
+
+ {`Zoomed: ${zoomed}`}
+ {"X"}
+
+
+
+ );
+ }
+
+ sortBars() {
+ _.forEach(this.state.charts || [], c => c.destroy());
+ this.setState({
+ charts: sortBars(_.get(this.state, "data", {}), this.props, this.state),
+ });
+ }
+
+ updateAxis(settings) {
+ if (_.isEqual(settings, _.pick(this.state.data, ["min", "max"]))) {
+ return;
+ }
+ _.forEach(this.state.charts || [], c => c.destroy());
+ const updatedData = _.assignIn({}, _.get(this.state, "data", {}), settings);
+ this.setState({
+ data: updatedData,
+ charts: createCharts(updatedData, this.props, this.state, _.pick(this, ["viewTimeDetails"])),
+ });
+ }
+
+ renderControls() {
+ if (this.props.showControls) {
+ const showBarSort = chartType(this.state) === "bar" && _.isNull(this.props.group) && !_.isEmpty(this.state.data);
+ return [
+
+
Chart:
+
+
+
+
+
0}>
+ Chart per Group:
+
+ this.setState({ chartPerGroup: e.target.checked })}
+ />
+
+
+
+
+
+
+
,
+
,
+ ];
+ }
+ return null;
+ }
+
render() {
+ if (!this.props.visible) {
+ return null;
+ }
let charts = null;
- if (this.props.chartPerGroup || (this.props.chartType === "wordcloud" && this.props.group)) {
+ if (chartType(this.state) === "wordcloud") {
+ charts =
;
+ } else if (this.state.chartPerGroup) {
charts = (
- {_.map(_.keys(_.get(this.state, "data.data", {})), k => (
+ {_.map(_.get(this.state, "data.data", {}), (_v, k) => (
- {this.props.chartType !== "wordcloud" && }
- {this.props.chartType === "wordcloud" && (
-
- )}
+
))}
);
- } else if (this.props.chartType === "wordcloud") {
- charts =
;
} else {
charts =
;
}
- return (
-
+ return [
+ this.renderControls(),
+
{this.state.error}
{charts}
-
- );
+
,
+ ];
}
}
@@ -180,21 +361,26 @@ ChartsBody.displayName = "ChartsBody";
ChartsBody.propTypes = {
url: PropTypes.string,
columns: PropTypes.arrayOf(PropTypes.object),
- x: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
- y: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
- group: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
+ x: PropTypes.object, // eslint-disable-line react/no-unused-prop-types
+ y: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/no-unused-prop-types
+ group: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/no-unused-prop-types
+ aggregation: PropTypes.string,
+ rollingWindow: PropTypes.string,
+ rollingComputation: PropTypes.string,
chartType: PropTypes.string,
chartPerGroup: PropTypes.bool,
visible: PropTypes.bool.isRequired,
height: PropTypes.number,
additionalOptions: PropTypes.object, // eslint-disable-line react/no-unused-prop-types
configHandler: PropTypes.func,
+ showControls: PropTypes.bool,
};
ChartsBody.defaultProps = {
height: 400,
chartPerGroup: false,
chartType: "line",
configHandler: config => config,
+ showControls: true,
};
export default ChartsBody;
diff --git a/static/popups/charts/WordcloudBody.jsx b/static/popups/charts/WordcloudBody.jsx
index f8d4659a0..c8759337a 100644
--- a/static/popups/charts/WordcloudBody.jsx
+++ b/static/popups/charts/WordcloudBody.jsx
@@ -3,49 +3,77 @@ import PropTypes from "prop-types";
import React from "react";
import ReactWordcloud from "react-wordcloud";
+import chartUtils from "../../chartUtils";
+import { isDateCol } from "../../dtale/gridUtils";
+
class WordcloudBody extends React.Component {
constructor(props) {
super(props);
}
render() {
- const { chartType, seriesKey } = this.props;
- if (chartType !== "wordcloud") {
- return null;
- }
- const series = _.get(this.props, ["data", "data", seriesKey], {});
- if (_.isEmpty(series)) {
+ const { chartType, x, y, columns } = this.props;
+ if (chartType.value !== "wordcloud") {
return null;
}
+ const yProps = _.map(y || [], "value");
+ const colWidth = _.size(_.get(this.props, "data.data", {})) * _.size(yProps) == 1 ? "12" : "6";
return (
-
- {seriesKey !== "all" &&
{seriesKey}}
-
- ({
- text: _.truncate(l + "", { length: 24 }),
- fullText: l + "",
- value: series.y[i],
- })),
- "value"
- )}
- callbacks={{
- getWordTooltip: ({ fullText, value }) => `${fullText} (${value})`,
- }}
- />
-
+
+ {_.flatMap(_.get(this.props, "data.data", {}), (series, seriesKey) =>
+ _.map(yProps, yProp => {
+ if (_.isEmpty(series[yProp] || [])) {
+ return null;
+ }
+ const labels = [];
+ if (seriesKey !== "all") {
+ labels.push(seriesKey);
+ }
+ if (_.size(yProps) > 1) {
+ labels.push(yProp);
+ }
+ const hasLabel = _.size(labels) > 0;
+ return (
+
+
+ {hasLabel &&
{_.join(labels, " - ")}}
+
+ {
+ let labelText = l + "";
+ if (isDateCol(_.find(columns, { name: x.value }).dtype)) {
+ labelText = chartUtils.timestampLabel(l);
+ }
+ return {
+ text: _.truncate(labelText, { length: 24 }),
+ fullText: labelText,
+ value: series[yProp][i],
+ };
+ }),
+ "value"
+ )}
+ callbacks={{
+ getWordTooltip: ({ fullText, value }) => `${fullText} (${value})`,
+ }}
+ />
+
+
+
+ );
+ })
+ )}
);
}
@@ -54,9 +82,11 @@ class WordcloudBody extends React.Component {
WordcloudBody.displayName = "WordcloudBody";
WordcloudBody.propTypes = {
data: PropTypes.object, // eslint-disable-line react/no-unused-prop-types
- seriesKey: PropTypes.string,
- group: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
- chartType: PropTypes.string,
+ columns: PropTypes.arrayOf(PropTypes.object),
+ x: PropTypes.object,
+ y: PropTypes.arrayOf(PropTypes.object),
+ group: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/no-unused-prop-types
+ chartType: PropTypes.object,
height: PropTypes.number,
};
export default WordcloudBody;
diff --git a/static/popups/correlations/CorrelationScatterStats.jsx b/static/popups/correlations/CorrelationScatterStats.jsx
index cebd7ec21..eca701573 100644
--- a/static/popups/correlations/CorrelationScatterStats.jsx
+++ b/static/popups/correlations/CorrelationScatterStats.jsx
@@ -46,8 +46,8 @@ CorrelationScatterStats.propTypes = {
selectedCols: PropTypes.arrayOf(PropTypes.string),
date: PropTypes.string,
stats: PropTypes.shape({
- pearson: PropTypes.number,
- spearman: PropTypes.number,
+ pearson: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ spearman: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
correlated: PropTypes.number,
only_in_s0: PropTypes.number,
only_in_s1: PropTypes.number,
diff --git a/static/popups/correlations/correlationsUtils.jsx b/static/popups/correlations/correlationsUtils.jsx
index 7f628d927..81f8af962 100644
--- a/static/popups/correlations/correlationsUtils.jsx
+++ b/static/popups/correlations/correlationsUtils.jsx
@@ -1,84 +1,28 @@
import chroma from "chroma-js";
-import $ from "jquery";
import _ from "lodash";
import chartUtils from "../../chartUtils";
-import { formatScatterPoints, getMax, getMin } from "../scatterChartUtils";
-function toggleBouncer() {
- $("#scatter-bouncer").toggle();
- $("#rawScatterChart").toggle();
-}
-
-const pointFormatter = (xProp, yProp) => point => ({
- x: point[xProp],
- y: point[yProp],
- index: point.index,
-});
const colorScale = chroma.scale(["red", "yellow", "green"]).domain([-1, 0, 1]);
const percent = num => (num === "N/A" ? num : `${_.round(num * 100, 2)}%`);
-function createScatter(ctx, chartData, xProp, yProp, label, onClick) {
- const formatter = pointFormatter(xProp, yProp);
- const scatterData = formatScatterPoints(chartData, formatter);
+function createScatter(ctx, data, xProp, yProp, onClick) {
+ const builder = data =>
+ _.map(_.zip(data.all.x, data.all[yProp], data.all.index), ([xVal, yVal, index]) => ({ x: xVal, y: yVal, index }));
+ const scatterCfg = chartUtils.createScatterCfg(data, { x: xProp, y: [yProp] }, builder);
+ scatterCfg.options.tooltips.callbacks.title = (tooltipItems, data) => [
+ [`Index: ${data.datasets[tooltipItems[0].datasetIndex].data[tooltipItems[0].index].index}`],
+ ];
+ delete scatterCfg.options.scales.xAxes[0].ticks;
+ delete scatterCfg.options.scales.yAxes[0].ticks;
+ scatterCfg.options.onClick = onClick;
// eslint-disable-next-line new-cap
- const chart = chartUtils.createChart(ctx, {
- type: "scatter",
- data: {
- datasets: [_.assign({ label, xLabels: [xProp], yLabels: [yProp] }, scatterData)],
- },
- options: {
- tooltips: {
- callbacks: {
- title: (tooltipItems, data) => [
- [`Index: ${data.datasets[tooltipItems[0].datasetIndex].data[tooltipItems[0].index].index}`],
- ],
- label: (tooltipItem, data) => {
- const chartData = data.datasets[tooltipItem.datasetIndex];
- const pointData = chartData.data[tooltipItem.index];
- return [
- `${chartData.xLabels[0]}: ${_.round(pointData.x, 4)}`,
- `${chartData.yLabels[0]}: ${_.round(pointData.y, 4)}`,
- ];
- },
- },
- },
- scales: {
- xAxes: [
- {
- ticks: {
- min: getMin(scatterData.data, "x"),
- max: getMax(scatterData.data, "x"),
- },
- scaleLabel: { display: true, labelString: xProp },
- },
- ],
- yAxes: [
- {
- ticks: {
- min: getMin(scatterData.data, "y"),
- max: getMax(scatterData.data, "y"),
- },
- scaleLabel: { display: true, labelString: yProp },
- },
- ],
- },
- legend: { display: false },
- pan: { enabled: true, mode: "x" },
- zoom: { enabled: true, mode: "x" },
- maintainAspectRatio: false,
- responsive: false,
- showLines: false,
- onClick,
- },
- });
+ const chart = chartUtils.createChart(ctx, scatterCfg);
return chart;
}
export default {
- toggleBouncer,
colorScale,
createScatter,
percent,
- pointFormatter,
};
diff --git a/static/popups/scatterChartUtils.js b/static/scatterChartUtils.js
similarity index 78%
rename from static/popups/scatterChartUtils.js
rename to static/scatterChartUtils.js
index f413a7a7c..e066eeb68 100644
--- a/static/popups/scatterChartUtils.js
+++ b/static/scatterChartUtils.js
@@ -1,5 +1,7 @@
import _ from "lodash";
+const basePointFormatter = (xProp, yProp) => point => _.assignIn(point, { x: point[xProp], y: point[yProp] });
+
function formatScatterPoints(chartData, formatter = p => p, highlight = () => false, filter = () => false) {
const data = [],
pointBackgroundColor = [],
@@ -57,14 +59,14 @@ function formatScatterPoints(chartData, formatter = p => p, highlight = () => fa
};
}
-function getMin(data, prop) {
- const min = _.min(_.map(data, prop));
+function getScatterMin(data, prop = null) {
+ const min = _.min(_.isNull(prop) ? data : _.map(data, prop));
return _.floor(min + (min % 1 ? 0 : -1)) - 0.5;
}
-function getMax(data, prop) {
- const max = _.max(_.map(data, prop));
+function getScatterMax(data, prop = null) {
+ const max = _.max(_.isNull(prop) ? data : _.map(data, prop));
return _.ceil(max + (max % 1 ? 0 : 1)) + 0.5;
}
-export { formatScatterPoints, getMin, getMax };
+export { basePointFormatter, formatScatterPoints, getScatterMin, getScatterMax };
diff --git a/static/toggleButtonUtils.js b/static/toggleUtils.js
similarity index 58%
rename from static/toggleButtonUtils.js
rename to static/toggleUtils.js
index 3f11b469e..300009d0c 100644
--- a/static/toggleButtonUtils.js
+++ b/static/toggleUtils.js
@@ -1,7 +1,12 @@
import _ from "lodash";
+import $ from "jquery";
function buildButton(active, activate, disabled = false) {
return { className: `btn btn-primary ${active ? "active" : ""}`, onClick: active ? _.noop : activate, disabled };
}
-export { buildButton };
+function toggleBouncer(ids) {
+ _.forEach(ids, id => $("#" + id).toggle());
+}
+
+export { buildButton, toggleBouncer };
diff --git a/tests/dtale/test_views.py b/tests/dtale/test_views.py
index dda43362b..b6e9fdf26 100644
--- a/tests/dtale/test_views.py
+++ b/tests/dtale/test_views.py
@@ -479,10 +479,10 @@ def test_get_correlations_ts(unittest, rolling_data):
expected = {
'data': {'all': {
'x': [946702800000, 946789200000, 946875600000, 946962000000, 947048400000],
- 'y': [1.0, 1.0, 1.0, 1.0, 1.0]
+ 'corr': [1.0, 1.0, 1.0, 1.0, 1.0]
}},
- 'max': 1.0,
- 'min': 1.0,
+ 'max': {'corr': 1.0, 'x': 947048400000},
+ 'min': {'corr': 1.0, 'x': 946702800000},
'success': True,
}
unittest.assertEqual(response_data, expected, 'should return timeseries correlation')
@@ -530,13 +530,15 @@ def test_get_scatter(unittest):
'only_in_s1': 0,
'spearman': 0.9999999999999999
},
- data=[
- {u'index': 0, u'foo': 0, u'bar': 0},
- {u'index': 1, u'foo': 1, u'bar': 1},
- {u'index': 2, u'foo': 2, u'bar': 2},
- {u'index': 3, u'foo': 3, u'bar': 3},
- {u'index': 4, u'foo': 4, u'bar': 4}
- ],
+ data={
+ 'all': {
+ 'bar': [0, 1, 2, 3, 4],
+ 'index': [0, 1, 2, 3, 4],
+ 'x': [0, 1, 2, 3, 4]
+ }
+ },
+ max={'bar': 4, 'index': 4, 'x': 4},
+ min={'bar': 0, 'index': 0, 'x': 0},
x='foo'
)
unittest.assertEqual(response_data, expected, 'should return scatter')
@@ -579,7 +581,9 @@ def test_get_scatter(unittest):
@pytest.mark.unit
-def test_get_chart_data(unittest, test_data):
+def test_get_chart_data(unittest, test_data, rolling_data):
+ import dtale.views as views
+
test_data = pd.DataFrame(build_ts_data(size=50), columns=['date', 'security_id', 'foo', 'bar'])
with app.test_client() as c:
with mock.patch('dtale.views.DATA', {c.port: test_data}):
@@ -589,10 +593,11 @@ def test_get_chart_data(unittest, test_data):
expected = {
u'data': {u'all': {
u'x': [946702800000, 946789200000, 946875600000, 946962000000, 947048400000],
- u'y': [50, 50, 50, 50, 50]
+ u'security_id': [50, 50, 50, 50, 50]
}},
- u'min': 50,
- u'max': 50
+ u'max': {'security_id': 50, 'x': 947048400000},
+ u'min': {'security_id': 50, 'x': 946702800000},
+ u'success': True,
}
unittest.assertEqual(response_data, expected, 'should return chart data')
@@ -603,11 +608,21 @@ def test_get_chart_data(unittest, test_data):
params = dict(x='date', y='security_id', group='baz', agg='mean')
response = c.get('/dtale/chart-data/{}'.format(c.port), query_string=params)
response_data = json.loads(response.data)
- assert response_data['min'] == 24.5
- assert response_data['max'] == 24.5
+ assert response_data['min']['security_id'] == 24.5
+ assert response_data['max']['security_id'] == 24.5
assert response_data['data']['baz']['x'][-1] == 947048400000
- assert len(response_data['data']['baz']['y']) == 5
- assert sum(response_data['data']['baz']['y']) == 122.5
+ assert len(response_data['data']['baz']['security_id']) == 5
+ assert sum(response_data['data']['baz']['security_id']) == 122.5
+
+ df, _ = views.format_data(rolling_data)
+ with app.test_client() as c:
+ with ExitStack() as stack:
+ stack.enter_context(mock.patch('dtale.views.DATA', {c.port: df}))
+ stack.enter_context(mock.patch('dtale.views.DTYPES', {c.port: views.build_dtypes_state(df)}))
+ params = dict(x='date', y='0', agg='rolling', rollingWin=10, rollingComp='count')
+ response = c.get('/dtale/chart-data/{}'.format(c.port), query_string=params)
+ response_data = json.loads(response.data)
+ assert response_data['success']
with app.test_client() as c:
with mock.patch('dtale.views.DATA', {c.port: test_data}):
@@ -642,6 +657,19 @@ def test_get_chart_data(unittest, test_data):
response_data['error'], 'query "security_id == 51" found no data, please alter'
)
+ df = pd.DataFrame([dict(a=i, b=i) for i in range(15500)])
+ df, _ = views.format_data(df)
+ with app.test_client() as c:
+ with ExitStack() as stack:
+ stack.enter_context(mock.patch('dtale.views.DATA', {c.port: df}))
+ stack.enter_context(mock.patch('dtale.views.DTYPES', {c.port: views.build_dtypes_state(df)}))
+ params = dict(x='a', y='b', allowDupes=True)
+ response = c.get('/dtale/chart-data/{}'.format(c.port), query_string=params)
+ response_data = json.loads(response.data)
+ unittest.assertEqual(
+ response_data['error'], 'Dataset exceeds 15,000 records, cannot render. Please apply filter...'
+ )
+
@pytest.mark.unit
def test_version_info():