From 9cf7f209eb34c84af399fa098366b7aceefddb1c Mon Sep 17 00:00:00 2001 From: Lucas Lira Gomes Date: Tue, 28 Jul 2020 13:29:09 +0200 Subject: [PATCH] Add split dimension feature to highchart widget. --- fireant/tests/widgets/test_highcharts.py | 707 +++++++++++++++-------- fireant/widgets/highcharts.py | 133 ++++- requirements-dev.txt | 3 +- 3 files changed, 578 insertions(+), 265 deletions(-) diff --git a/fireant/tests/widgets/test_highcharts.py b/fireant/tests/widgets/test_highcharts.py index b09b8819..a6528c7b 100644 --- a/fireant/tests/widgets/test_highcharts.py +++ b/fireant/tests/widgets/test_highcharts.py @@ -2,7 +2,10 @@ import pandas as pd -from fireant import CumSum, Rollup +from fireant import ( + CumSum, + Rollup, +) from fireant.tests.dataset.mocks import ( ElectionOverElection, day, @@ -14,6 +17,7 @@ dimx1_num_df, dimx1_str_df, dimx1_str_totals_df, + dimx2_category_index_str_df, dimx2_date_index_str_df, dimx2_date_num_df, dimx2_date_str_df, @@ -22,13 +26,14 @@ dimx2_date_str_totals_df, dimx2_date_str_totalsx2_df, dimx2_str_num_df, - dimx2_category_index_str_df, dimx2_str_str_df, - dimx3_date_str_str_df, mock_dataset, year, ) -from fireant.widgets.highcharts import DEFAULT_COLORS, HighCharts +from fireant.widgets.highcharts import ( + DEFAULT_COLORS, + HighCharts, +) class HighChartsLineChartTransformerTests(TestCase): @@ -93,9 +98,7 @@ def test_dimx1_year(self): result = ( HighCharts(title="Time Series, Single Metric") .axis(self.chart_class(mock_dataset.fields.votes)) - .transform( - dimx1_date_df, [year(mock_dataset.fields.timestamp)], [] - ) + .transform(dimx1_date_df, [year(mock_dataset.fields.timestamp)], []) ) self.assertEqual( @@ -248,11 +251,7 @@ def test_single_operation_line_chart(self): result = ( HighCharts(title="Time Series, Single Metric") .axis(self.chart_class(CumSum(mock_dataset.fields.votes))) - .transform( - dimx1_date_operation_df, - [mock_dataset.fields.timestamp], - [], - ) + .transform(dimx1_date_operation_df, [mock_dataset.fields.timestamp], [],) ) self.assertEqual( @@ -299,224 +298,437 @@ def test_single_operation_line_chart(self): result, ) - def test_single_metric_with_fetch_only_uni_dim_line_chart(self): - dimensions = [mock_dataset.fields.timestamp, mock_dataset.fields.political_party] - dimensions[1].fetch_only = True + def test_single_metric_with_a_split_dimension_dimx2_date_str_line_chart(self): + dimensions = [ + mock_dataset.fields.timestamp, + mock_dataset.fields.political_party, + ] result = ( - HighCharts(title="Time Series with Unique Dimension and Single Metric") + HighCharts( + title="Time Series with Datetime/Text Dimensions and Single Metric", + split_dimension=dimensions[1], + ) .axis(self.chart_class(mock_dataset.fields.votes)) .transform(dimx2_date_str_df, dimensions, []) ) - dimensions[1].fetch_only = False - self.assertEqual( - { - "title": { - "text": "Time Series with Unique Dimension and Single Metric" + with self.subTest("returns 3 charts"): + self.assertEqual(len(result), 3) + + with self.subTest("for Democrat split (1st chart)"): + self.assertEqual( + { + "chart": {"height": 240}, + "title": { + "text": "Time Series with Datetime/Text Dimensions and Single Metric (Democrat)" + }, + "xAxis": {"type": "datetime", "visible": True}, + "yAxis": [ + { + "id": "0", + "labels": {"style": {"color": None}}, + "title": {"text": None}, + "visible": True, + } + ], + "tooltip": {"shared": True, "useHTML": True, "enabled": True}, + "legend": {"useHTML": True}, + "series": [ + { + "color": "#DDDF0D", + "dashStyle": "Solid", + "data": [ + (820454400000, 7579518), + (946684800000, 8294949), + (1072915200000, 9578189), + (1199145600000, 11803106), + (1325376000000, 12424128), + (1451606400000, 4871678), + ], + "marker": {"fillColor": "#DDDF0D", "symbol": "circle"}, + "name": "Votes (Democrat)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", + }, + ], + "annotations": [], + "colors": DEFAULT_COLORS, }, - "xAxis": {"type": "datetime", "visible": True}, - "yAxis": [ - { - "id": "0", - "labels": {"style": {"color": None}}, - "title": {"text": None}, - "visible": True, - } - ], - "tooltip": {"shared": True, "useHTML": True, "enabled": True}, - "legend": {"useHTML": True}, - "series": [ - { - "color": "#DDDF0D", - "dashStyle": "Solid", - 'data': [ - (820454400000, 7579518), - (820454400000, 1076384), - (820454400000, 6564547), - (946684800000, 8294949), - (946684800000, 8367068), - (1072915200000, 9578189), - (1072915200000, 10036743), - (1199145600000, 11803106), - (1199145600000, 9491109), - (1325376000000, 12424128), - (1325376000000, 8148082), - (1451606400000, 4871678), - (1451606400000, 13438835), - ], - "marker": {"fillColor": "#DDDF0D", "symbol": "circle"}, - "name": "Votes", - "stacking": self.stacking, - "tooltip": { - "valueDecimals": None, - "valuePrefix": None, - "valueSuffix": None, + result[0], + ) + + with self.subTest("for Independent split (2nd chart)"): + self.assertEqual( + { + "chart": {"height": 240}, + "title": { + "text": "Time Series with Datetime/Text Dimensions and Single Metric (Independent)" + }, + "xAxis": {"type": "datetime", "visible": True}, + "yAxis": [ + { + "id": "0", + "labels": {"style": {"color": None}}, + "title": {"text": None}, + "visible": True, + } + ], + "tooltip": {"shared": True, "useHTML": True, "enabled": True}, + "legend": {"useHTML": True}, + "series": [ + { + "color": "#DDDF0D", + "dashStyle": "Solid", + "data": [(820454400000, 1076384),], + "marker": {"fillColor": "#DDDF0D", "symbol": "circle"}, + "name": "Votes (Independent)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", }, - "type": self.chart_type, - "yAxis": "0", - }, - ], - "annotations": [], - "colors": DEFAULT_COLORS, - }, - result, - ) + ], + "annotations": [], + "colors": DEFAULT_COLORS, + }, + result[1], + ) - def test_multi_metrics_single_axis_line_chart(self): - result = ( - HighCharts(title="Time Series with Unique Dimension and Multiple Metrics") - .axis( - self.chart_class(mock_dataset.fields.votes), - self.chart_class(mock_dataset.fields.wins), + with self.subTest("for Republican split (3rd chart)"): + self.assertEqual( + { + "chart": {"height": 240}, + "title": { + "text": "Time Series with Datetime/Text Dimensions and Single Metric (Republican)" + }, + "xAxis": {"type": "datetime", "visible": True}, + "yAxis": [ + { + "id": "0", + "labels": {"style": {"color": None}}, + "title": {"text": None}, + "visible": True, + } + ], + "tooltip": {"shared": True, "useHTML": True, "enabled": True}, + "legend": {"useHTML": True}, + "series": [ + { + "color": "#DDDF0D", + "dashStyle": "Solid", + "data": [ + (820454400000, 6564547), + (946684800000, 8367068), + (1072915200000, 10036743), + (1199145600000, 9491109), + (1325376000000, 8148082), + (1451606400000, 13438835), + ], + "marker": {"fillColor": "#DDDF0D", "symbol": "circle"}, + "name": "Votes (Republican)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", + }, + ], + "annotations": [], + "colors": DEFAULT_COLORS, + }, + result[2], ) - .transform( - dimx2_date_str_df, - [mock_dataset.fields.timestamp, mock_dataset.fields.political_party], - [], + + def test_single_metric_with_a_split_dimension_dimx2_str_str_line_chart(self): + dimensions = [ + mock_dataset.fields.political_party, + mock_dataset.fields["candidate-name"], + ] + result = ( + HighCharts( + title="Time Series with two Text Dimensions and Single Metric", + split_dimension=dimensions[0], ) + .axis(self.chart_class(mock_dataset.fields.votes)) + .transform(dimx2_str_str_df, dimensions, []) ) - self.assertEqual( - { - "title": { - "text": "Time Series with Unique Dimension and Multiple Metrics" - }, - "xAxis": {"type": "datetime", "visible": True}, - "yAxis": [ - { - "id": "0", - "labels": {"style": {"color": "#DDDF0D"}}, - "title": {"text": None}, + with self.subTest("returns 3 charts"): + self.assertEqual(len(result), 3) + + with self.subTest("for Democrat split (1st chart)"): + self.assertEqual( + { + "chart": {"height": 240}, + "title": { + "text": "Time Series with two Text Dimensions and Single Metric (Democrat)" + }, + "xAxis": { + "categories": ["Democrat", "Independent", "Republican"], + "type": "category", "visible": True, - } - ], - "tooltip": {"shared": True, "useHTML": True, "enabled": True}, - "legend": {"useHTML": True}, - "series": [ - { - "color": "#DDDF0D", - "dashStyle": "Solid", - "data": [ - (820454400000, 7579518), - (946684800000, 8294949), - (1072915200000, 9578189), - (1199145600000, 11803106), - (1325376000000, 12424128), - (1451606400000, 4871678), - ], - "marker": {"fillColor": "#DDDF0D", "symbol": "circle"}, - "name": "Votes (Democrat)", - "stacking": self.stacking, - "tooltip": { - "valueDecimals": None, - "valuePrefix": None, - "valueSuffix": None, - }, - "type": self.chart_type, - "yAxis": "0", }, - { - "color": "#55BF3B", - "dashStyle": "Solid", - "data": [(820454400000, 1076384)], - "marker": {"fillColor": "#DDDF0D", "symbol": "square"}, - "name": "Votes (Independent)", - "stacking": self.stacking, - "tooltip": { - "valueDecimals": None, - "valuePrefix": None, - "valueSuffix": None, + "yAxis": [ + { + "id": "0", + "labels": {"style": {"color": None}}, + "title": {"text": None}, + "visible": True, + } + ], + "tooltip": {"shared": True, "useHTML": True, "enabled": True}, + "legend": {"useHTML": True}, + "series": [ + { + "color": "#DDDF0D", + "dashStyle": "Solid", + "data": [{"x": 0, "y": 8294949},], + "marker": {"fillColor": "#DDDF0D", "symbol": "circle"}, + "name": "Votes (Al Gore)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", + }, + { + "color": "#55BF3B", + "dashStyle": "Solid", + "data": [{"x": 0, "y": 24227234}], + "marker": {"fillColor": "#DDDF0D", "symbol": "square"}, + "name": "Votes (Barrack Obama)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", + }, + { + "color": "#DF5353", + "dashStyle": "Solid", + "data": [{"x": 0, "y": 7579518}], + "marker": {"fillColor": "#DDDF0D", "symbol": "diamond"}, + "name": "Votes (Bill Clinton)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", + }, + { + "color": "#7798BF", + "dashStyle": "Solid", + "data": [{"x": 0, "y": 4871678}], + "marker": {"fillColor": "#DDDF0D", "symbol": "triangle"}, + "name": "Votes (Hillary Clinton)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", + }, + { + "color": "#AAEEEE", + "dashStyle": "Solid", + "data": [{"x": 0, "y": 9578189}], + "marker": { + "fillColor": "#DDDF0D", + "symbol": "triangle-down", + }, + "name": "Votes (John Kerry)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", }, - "type": self.chart_type, - "yAxis": "0", + ], + "annotations": [], + "colors": DEFAULT_COLORS, + }, + result[0], + ) + + with self.subTest("for Independent split (2nd chart)"): + self.assertEqual( + { + "chart": {"height": 240}, + "title": { + "text": "Time Series with two Text Dimensions and Single Metric (Independent)" }, - { - "color": "#DF5353", - "dashStyle": "Solid", - "data": [ - (820454400000, 6564547), - (946684800000, 8367068), - (1072915200000, 10036743), - (1199145600000, 9491109), - (1325376000000, 8148082), - (1451606400000, 13438835), - ], - "marker": {"fillColor": "#DDDF0D", "symbol": "diamond"}, - "name": "Votes (Republican)", - "stacking": self.stacking, - "tooltip": { - "valueDecimals": None, - "valuePrefix": None, - "valueSuffix": None, - }, - "type": self.chart_type, - "yAxis": "0", + "xAxis": { + "categories": ["Democrat", "Independent", "Republican"], + "type": "category", + "visible": True, }, - { - "color": "#7798BF", - "dashStyle": "Solid", - "data": [ - (820454400000, 2), - (946684800000, 0), - (1072915200000, 0), - (1199145600000, 2), - (1325376000000, 2), - (1451606400000, 0), - ], - "marker": {"fillColor": "#DDDF0D", "symbol": "circle"}, - "name": "Wins (Democrat)", - "stacking": self.stacking, - "tooltip": { - "valueDecimals": None, - "valuePrefix": None, - "valueSuffix": None, + "yAxis": [ + { + "id": "0", + "labels": {"style": {"color": None}}, + "title": {"text": None}, + "visible": True, + } + ], + "tooltip": {"shared": True, "useHTML": True, "enabled": True}, + "legend": {"useHTML": True}, + "series": [ + { + "color": "#DDDF0D", + "dashStyle": "Solid", + "data": [{"x": 1, "y": 1076384},], + "marker": {"fillColor": "#DDDF0D", "symbol": "circle"}, + "name": "Votes (Ross Perot)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", }, - "type": self.chart_type, - "yAxis": "0", + ], + "annotations": [], + "colors": DEFAULT_COLORS, + }, + result[1], + ) + + with self.subTest("for Republican split (3rd chart)"): + self.assertEqual( + { + "chart": {"height": 240}, + "title": { + "text": "Time Series with two Text Dimensions and Single Metric (Republican)" }, - { - "color": "#AAEEEE", - "dashStyle": "Solid", - "data": [(820454400000, 0)], - "marker": {"fillColor": "#DDDF0D", "symbol": "square"}, - "name": "Wins (Independent)", - "stacking": self.stacking, - "tooltip": { - "valueDecimals": None, - "valuePrefix": None, - "valueSuffix": None, - }, - "type": self.chart_type, - "yAxis": "0", + "xAxis": { + "categories": ["Democrat", "Independent", "Republican"], + "type": "category", + "visible": True, }, - { - "color": "#FF0066", - "dashStyle": "Solid", - "data": [ - (820454400000, 0), - (946684800000, 2), - (1072915200000, 2), - (1199145600000, 0), - (1325376000000, 0), - (1451606400000, 2), - ], - "marker": {"fillColor": "#DDDF0D", "symbol": "diamond"}, - "name": "Wins (Republican)", - "stacking": self.stacking, - "tooltip": { - "valueDecimals": None, - "valuePrefix": None, - "valueSuffix": None, + "yAxis": [ + { + "id": "0", + "labels": {"style": {"color": None}}, + "title": {"text": None}, + "visible": True, + } + ], + "tooltip": {"shared": True, "useHTML": True, "enabled": True}, + "legend": {"useHTML": True}, + "series": [ + { + "color": "#DDDF0D", + "dashStyle": "Solid", + "data": [{"x": 2, "y": 6564547},], + "marker": {"fillColor": "#DDDF0D", "symbol": "circle"}, + "name": "Votes (Bob Dole)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", + }, + { + "color": "#55BF3B", + "dashStyle": "Solid", + "data": [{"x": 2, "y": 13438835},], + "marker": {"fillColor": "#DDDF0D", "symbol": "square"}, + "name": "Votes (Donald Trump)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", + }, + { + "color": "#DF5353", + "dashStyle": "Solid", + "data": [{"x": 2, "y": 18403811},], + "marker": {"fillColor": "#DDDF0D", "symbol": "diamond"}, + "name": "Votes (George Bush)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", + }, + { + "color": "#7798BF", + "dashStyle": "Solid", + "data": [{"x": 2, "y": 9491109},], + "marker": {"fillColor": "#DDDF0D", "symbol": "triangle"}, + "name": "Votes (John McCain)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", + }, + { + "color": "#AAEEEE", + "dashStyle": "Solid", + "data": [{"x": 2, "y": 8148082},], + "marker": { + "fillColor": "#DDDF0D", + "symbol": "triangle-down", + }, + "name": "Votes (Mitt Romney)", + "stacking": self.stacking, + "tooltip": { + "valueDecimals": None, + "valuePrefix": None, + "valueSuffix": None, + }, + "type": self.chart_type, + "yAxis": "0", }, - "type": self.chart_type, - "yAxis": "0", - }, - ], - "annotations": [], - "colors": DEFAULT_COLORS, - }, - result, - ) + ], + "annotations": [], + "colors": DEFAULT_COLORS, + }, + result[2], + ) def test_multi_metrics_multi_axis_line_chart(self): result = ( @@ -704,7 +916,10 @@ def test_multi_dim_with_totals_line_chart_and_empty_data(self): .axis(self.chart_class(mock_dataset.fields.wins)) .transform( dataframe, - [mock_dataset.fields.timestamp, Rollup(mock_dataset.fields.political_party)], + [ + mock_dataset.fields.timestamp, + Rollup(mock_dataset.fields.political_party), + ], [], ) ) @@ -789,7 +1004,10 @@ def test_multi_dim_with_totals_line_chart(self): .axis(self.chart_class(mock_dataset.fields.wins)) .transform( dimx2_date_str_totals_df, - [mock_dataset.fields.timestamp, Rollup(mock_dataset.fields.political_party)], + [ + mock_dataset.fields.timestamp, + Rollup(mock_dataset.fields.political_party), + ], [], ) ) @@ -1196,7 +1414,10 @@ def test_multi_dim_with_totals_on_first_dim_line_chart(self): ) def test_uni_dim_with_ref_line_chart(self): - dimensions = [mock_dataset.fields.timestamp, mock_dataset.fields.political_party] + dimensions = [ + mock_dataset.fields.timestamp, + mock_dataset.fields.political_party, + ] references = [ElectionOverElection(mock_dataset.fields.timestamp)] result = ( HighCharts(title="Time Series with Unique Dimension and Reference") @@ -1313,14 +1534,15 @@ def test_uni_dim_with_ref_line_chart(self): ) def test_uni_dim_with_ref_delta_line_chart(self): - dimensions = [mock_dataset.fields.timestamp, mock_dataset.fields.political_party] + dimensions = [ + mock_dataset.fields.timestamp, + mock_dataset.fields.political_party, + ] references = [ElectionOverElection(mock_dataset.fields.timestamp, delta=True)] result = ( HighCharts(title="Time Series with Unique Dimension and Delta Reference") .axis(self.chart_class(mock_dataset.fields.votes)) - .transform( - dimx2_date_str_ref_delta_df, dimensions, references - ) + .transform(dimx2_date_str_ref_delta_df, dimensions, references) ) self.assertEqual( @@ -1492,14 +1714,15 @@ def test_invisible_y_axis(self): ) def test_ref_axes_set_to_same_visibility_as_parent_axis(self): - dimensions = [mock_dataset.fields.timestamp, mock_dataset.fields.political_party] + dimensions = [ + mock_dataset.fields.timestamp, + mock_dataset.fields.political_party, + ] references = [ElectionOverElection(mock_dataset.fields.timestamp, delta=True)] result = ( HighCharts(title="Time Series with Unique Dimension and Delta Reference") .axis(self.chart_class(mock_dataset.fields.votes), y_axis_visible=False) - .transform( - dimx2_date_str_ref_delta_df, dimensions, references - ) + .transform(dimx2_date_str_ref_delta_df, dimensions, references) ) self.assertEqual( @@ -1731,9 +1954,7 @@ def test_cat_dim_single_metric_bar_chart(self): result = ( HighCharts("Votes and Wins") .axis(self.chart_class(mock_dataset.fields.votes)) - .transform( - dimx1_str_df, [mock_dataset.fields.political_party], [] - ) + .transform(dimx1_str_df, [mock_dataset.fields.political_party], []) ) self.assertEqual( @@ -1786,9 +2007,7 @@ def test_cat_dim_multi_metric_bar_chart(self): self.chart_class(mock_dataset.fields.votes), self.chart_class(mock_dataset.fields.wins), ) - .transform( - dimx1_str_df, [mock_dataset.fields.political_party], [] - ) + .transform(dimx1_str_df, [mock_dataset.fields.political_party], []) ) self.assertEqual( @@ -1848,7 +2067,10 @@ def test_cat_dim_multi_metric_bar_chart(self): ) def test_cont_uni_dims_single_metric_bar_chart(self): - dimensions = [mock_dataset.fields.timestamp, mock_dataset.fields.political_party] + dimensions = [ + mock_dataset.fields.timestamp, + mock_dataset.fields.political_party, + ] result = ( HighCharts("Election Votes by State") .axis(self.chart_class(mock_dataset.fields.votes)) @@ -1931,7 +2153,10 @@ def test_cont_uni_dims_single_metric_bar_chart(self): ) def test_cont_uni_dims_multi_metric_single_axis_bar_chart(self): - dimensions = [mock_dataset.fields.timestamp, mock_dataset.fields.political_party] + dimensions = [ + mock_dataset.fields.timestamp, + mock_dataset.fields.political_party, + ] result = ( HighCharts(title="Election Votes by State") .axis( @@ -2070,7 +2295,10 @@ def test_cont_uni_dims_multi_metric_single_axis_bar_chart(self): ) def test_cont_uni_dims_multi_metric_multi_axis_bar_chart(self): - dimensions = [mock_dataset.fields.timestamp, mock_dataset.fields.political_party] + dimensions = [ + mock_dataset.fields.timestamp, + mock_dataset.fields.political_party, + ] result = ( HighCharts(title="Election Votes by State") .axis(self.chart_class(mock_dataset.fields.votes)) @@ -2217,9 +2445,7 @@ def test_cat_dim_with_totals_chart(self): HighCharts(title="Categorical Dimension with Totals") .axis(self.chart_class(mock_dataset.fields.votes)) .transform( - dimx1_str_totals_df, - [Rollup(mock_dataset.fields.political_party)], - [], + dimx1_str_totals_df, [Rollup(mock_dataset.fields.political_party)], [], ) ) @@ -2648,9 +2874,7 @@ def test_pie_chart_dimx1_date_year(self): result = ( HighCharts("Votes and Wins By Day") .axis(self.chart_class(mock_dataset.fields.votes)) - .transform( - dimx1_date_df, [year(mock_dataset.fields.timestamp)], [] - ) + .transform(dimx1_date_df, [year(mock_dataset.fields.timestamp)], []) ) self.assertEqual( @@ -2700,9 +2924,7 @@ def test_pie_chart_dimx1_str(self): result = ( HighCharts("Votes and Wins By Party") .axis(self.chart_class(mock_dataset.fields.votes)) - .transform( - dimx1_str_df, [mock_dataset.fields.political_party], [] - ) + .transform(dimx1_str_df, [mock_dataset.fields.political_party], []) ) self.assertEqual( @@ -2751,9 +2973,7 @@ def test_pie_chart_dimx1_num(self): result = ( HighCharts(title="Votes and Wins By Election") .axis(self.chart_class(mock_dataset.fields.votes)) - .transform( - dimx1_num_df, [mock_dataset.fields["candidate-id"]], [] - ) + .transform(dimx1_num_df, [mock_dataset.fields["candidate-id"]], []) ) self.assertEqual( @@ -2998,7 +3218,10 @@ def test_pie_chart_dimx2_yearly_date_num(self): ) def test_pie_chart_dimx2_date_str_reference(self): - dimensions = [mock_dataset.fields.timestamp, mock_dataset.fields.political_party] + dimensions = [ + mock_dataset.fields.timestamp, + mock_dataset.fields.political_party, + ] references = [ElectionOverElection(mock_dataset.fields.timestamp)] result = ( HighCharts(title="Election Votes by State") diff --git a/fireant/widgets/highcharts.py b/fireant/widgets/highcharts.py index 1005f96c..cf9bcf31 100644 --- a/fireant/widgets/highcharts.py +++ b/fireant/widgets/highcharts.py @@ -64,20 +64,24 @@ class HighCharts(ChartWidget, TransformableWidget): group_pagination = True def __init__( - self, title=None, colors=None, x_axis_visible=True, tooltip_visible=True + self, + title=None, + colors=None, + x_axis_visible=True, + tooltip_visible=True, + split_dimension=None, ): super(HighCharts, self).__init__() self.title = title self.colors = colors or DEFAULT_COLORS self.x_axis_visible = x_axis_visible self.tooltip_visible = tooltip_visible + self.split_dimension = split_dimension or None def __repr__(self): return ".".join(["HighCharts()"] + [repr(axis) for axis in self.items]) - def transform( - self, data_frame, dimensions, references, annotation_frame=None - ): + def transform(self, data_frame, dimensions, references, annotation_frame=None): """ - Main entry point - @@ -94,7 +98,7 @@ def transform( :param annotation_frame: A data frame containing annotation data. :return: - A dict meant to be dumped as JSON. + A dict or a list of dicts meant to be dumped as JSON. """ result_df = data_frame.copy() @@ -107,6 +111,60 @@ def transform( alias_selector(dimension.alias): dimension for dimension in dimensions } + render_group = [] + split_dimension = self.split_dimension + + if split_dimension: + split_dimension_alias = alias_selector(split_dimension.alias) + + categories = self._categories( + result_df, dimension_map, split_dimension_alias, + ) + + for category in categories: + render_group.append( + [ + result_df.xs( + category, level=split_dimension_alias, drop_level=False + ), + formats.display_value(category, split_dimension) or category, + ] + ) + + if not render_group: + render_group = [(result_df, None)] + + num_charts = len(render_group) + + charts = [ + self._render_individual_chart( + chart_df, + dimensions, + references, + annotation_frame=annotation_frame, + titleSuffix=titleSuffix, + num_charts=num_charts, + ) + for chart_df, titleSuffix in render_group + ] + + return charts[0] if num_charts == 1 else charts + + def _render_individual_chart( + self, + data_frame, + dimensions, + references, + annotation_frame=None, + titleSuffix="", + num_charts=1, + ): + result_df = data_frame + + dimension_map = { + alias_selector(dimension.alias): dimension for dimension in dimensions + } + colors = itertools.cycle(self.colors) is_timeseries = dimensions and dimensions[0].data_type == DataType.date @@ -149,12 +207,24 @@ def transform( is_timeseries, ) - x_axis = self._render_x_axis(result_df, dimensions, dimension_map) + categories = self._categories(result_df, dimension_map) + x_axis = self._render_x_axis(dimensions, categories) annotations = [] if has_only_line_series(axis) and annotation_frame is not None: annotations = self._render_annotation(annotation_frame, x_axis) + extra = {} + + if num_charts > 1: + extra["title"] = { + "text": f"{self.title} ({titleSuffix})" if self.title else titleSuffix + } + extra["chart"] = { + # Height of 240px is the smallest we can have, while still fully displaying the chart menu. + "height": 240 + } + return { "title": {"text": self.title}, "xAxis": x_axis, @@ -168,38 +238,57 @@ def transform( }, "legend": {"useHTML": True}, "annotations": annotations, + **extra, } - def _render_x_axis(self, data_frame, dimensions, dimension_map): + def _categories(self, data_frame, dimension_map, dimension_alias=None): + is_mi = isinstance(data_frame.index, pd.MultiIndex) + levels = data_frame.index.levels if is_mi else [data_frame.index] + + first_level = None + + if dimension_alias: + level_index = 0 + + for i, level in enumerate(levels): + if level.name == dimension_alias: + level_index = i + break + + first_level = levels[level_index] + else: + first_level = levels[0] + + if first_level is not None and first_level.name is not None: + dimension_alias = first_level.name + dimension = dimension_map[dimension_alias] + return [ + formats.display_value(category, dimension) or category + if not pd.isnull(category) + else None + for category in first_level + ] + + return [] + + def _render_x_axis(self, dimensions, categories): """ Renders the xAxis configuration. https://api.highcharts.com/highcharts/xAxis - :param data_frame: :param dimensions: - :param dimension_map: + :param categories: :return: """ - is_mi = isinstance(data_frame.index, pd.MultiIndex) - first_level = data_frame.index.levels[0] if is_mi else data_frame.index - is_timeseries = dimensions and dimensions[0].data_type == DataType.date + if is_timeseries: return {"type": "datetime", "visible": self.x_axis_visible} - categories = ["All"] - if first_level.name is not None: - dimension_alias = first_level.name - dimension = dimension_map[dimension_alias] - categories = [ - formats.display_value(category, dimension) or category - for category in first_level - ] - return { "type": "category", - "categories": categories, + "categories": categories if categories else ["All"], "visible": self.x_axis_visible, } diff --git a/requirements-dev.txt b/requirements-dev.txt index 79a1c2da..b708f580 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,8 @@ -r requirements-extras-postgresql.txt -r requirements-extras-ipython.txt mock -bumpversion==0.5.3 +bumpversion +black wheel==0.30.0 watchdog==0.8.3 flake8==3.5.0