diff --git a/analytics/management/commands/populate_analytics_db.py b/analytics/management/commands/populate_analytics_db.py index 428911d7b7..54f646d7bc 100644 --- a/analytics/management/commands/populate_analytics_db.py +++ b/analytics/management/commands/populate_analytics_db.py @@ -85,17 +85,33 @@ class Command(BaseCommand): None: self.generate_fixture_data(stat, .08, .02, 3, .3, 6, partial_sum=True), } # type: Mapping[Optional[str], List[int]] insert_fixture_data(stat, realm_data, RealmCount) + installation_data = { + None: self.generate_fixture_data(stat, .8, .2, 4, .3, 6, partial_sum=True), + } # type: Mapping[Optional[str], List[int]] + insert_fixture_data(stat, installation_data, InstallationCount) FillState.objects.create(property=stat.property, end_time=last_end_time, state=FillState.DONE) stat = COUNT_STATS['realm_active_humans::day'] realm_data = { None: self.generate_fixture_data(stat, .1, .03, 3, .5, 3, partial_sum=True), - } # type: Mapping[Optional[str], List[int]] + } insert_fixture_data(stat, realm_data, RealmCount) installation_data = { None: self.generate_fixture_data(stat, 1, .3, 4, .5, 3, partial_sum=True), - } # type: Mapping[Optional[str], List[int]] + } + insert_fixture_data(stat, installation_data, InstallationCount) + FillState.objects.create(property=stat.property, end_time=last_end_time, + state=FillState.DONE) + + stat = COUNT_STATS['active_users_audit:is_bot:day'] + realm_data = { + 'false': self.generate_fixture_data(stat, .1, .03, 3.5, .8, 2, partial_sum=True), + } + insert_fixture_data(stat, realm_data, RealmCount) + installation_data = { + 'false': self.generate_fixture_data(stat, 1, .3, 6, .8, 2, partial_sum=True), + } insert_fixture_data(stat, installation_data, InstallationCount) FillState.objects.create(property=stat.property, end_time=last_end_time, state=FillState.DONE) diff --git a/analytics/tests/test_views.py b/analytics/tests/test_views.py index 0d4b1515ca..318dbc5a39 100644 --- a/analytics/tests/test_views.py +++ b/analytics/tests/test_views.py @@ -92,6 +92,10 @@ class TestGetChartData(ZulipTestCase): def test_number_of_humans(self) -> None: stat = COUNT_STATS['realm_active_humans::day'] self.insert_data(stat, [None], []) + stat = COUNT_STATS['1day_actives::day'] + self.insert_data(stat, [None], []) + stat = COUNT_STATS['active_users_audit:is_bot:day'] + self.insert_data(stat, ['false'], []) result = self.client_get('/json/analytics/chart_data', {'chart_name': 'number_of_humans'}) self.assert_json_success(result) @@ -100,7 +104,7 @@ class TestGetChartData(ZulipTestCase): 'msg': '', 'end_times': [datetime_to_timestamp(dt) for dt in self.end_times_day], 'frequency': CountStat.DAY, - 'everyone': {'human': self.data(100)}, + 'everyone': {'_1day': self.data(100), '_15day': self.data(100), 'all_time': self.data(100)}, 'display_order': None, 'result': 'success', }) @@ -173,7 +177,7 @@ class TestGetChartData(ZulipTestCase): {'chart_name': 'number_of_humans'}) self.assert_json_success(result) data = result.json() - self.assertEqual(data['everyone'], {'human': [0]}) + self.assertEqual(data['everyone'], {"_1day": [0], "_15day": [0], "all_time": [0]}) self.assertFalse('user' in data) FillState.objects.create( @@ -213,6 +217,10 @@ class TestGetChartData(ZulipTestCase): def test_start_and_end(self) -> None: stat = COUNT_STATS['realm_active_humans::day'] self.insert_data(stat, [None], []) + stat = COUNT_STATS['1day_actives::day'] + self.insert_data(stat, [None], []) + stat = COUNT_STATS['active_users_audit:is_bot:day'] + self.insert_data(stat, ['false'], []) end_time_timestamps = [datetime_to_timestamp(dt) for dt in self.end_times_day] # valid start and end @@ -223,7 +231,7 @@ class TestGetChartData(ZulipTestCase): self.assert_json_success(result) data = result.json() self.assertEqual(data['end_times'], end_time_timestamps[1:3]) - self.assertEqual(data['everyone'], {'human': [0, 100]}) + self.assertEqual(data['everyone'], {'_1day': [0, 100], '_15day': [0, 100], 'all_time': [0, 100]}) # start later then end result = self.client_get('/json/analytics/chart_data', @@ -235,6 +243,10 @@ class TestGetChartData(ZulipTestCase): def test_min_length(self) -> None: stat = COUNT_STATS['realm_active_humans::day'] self.insert_data(stat, [None], []) + stat = COUNT_STATS['1day_actives::day'] + self.insert_data(stat, [None], []) + stat = COUNT_STATS['active_users_audit:is_bot:day'] + self.insert_data(stat, ['false'], []) # test min_length is too short to change anything result = self.client_get('/json/analytics/chart_data', {'chart_name': 'number_of_humans', @@ -242,7 +254,7 @@ class TestGetChartData(ZulipTestCase): self.assert_json_success(result) data = result.json() self.assertEqual(data['end_times'], [datetime_to_timestamp(dt) for dt in self.end_times_day]) - self.assertEqual(data['everyone'], {'human': self.data(100)}) + self.assertEqual(data['everyone'], {'_1day': self.data(100), '_15day': self.data(100), 'all_time': self.data(100)}) # test min_length larger than filled data result = self.client_get('/json/analytics/chart_data', {'chart_name': 'number_of_humans', @@ -251,7 +263,7 @@ class TestGetChartData(ZulipTestCase): data = result.json() end_times = [ceiling_to_day(self.realm.date_created) + timedelta(days=i) for i in range(-1, 4)] self.assertEqual(data['end_times'], [datetime_to_timestamp(dt) for dt in end_times]) - self.assertEqual(data['everyone'], {'human': [0]+self.data(100)}) + self.assertEqual(data['everyone'], {'_1day': [0]+self.data(100), '_15day': [0]+self.data(100), 'all_time': [0]+self.data(100)}) def test_non_existent_chart(self) -> None: result = self.client_get('/json/analytics/chart_data', diff --git a/analytics/views.py b/analytics/views.py index 5371c348c8..6bd9ee5231 100644 --- a/analytics/views.py +++ b/analytics/views.py @@ -94,9 +94,15 @@ def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name: aggregate_table = InstallationCount if chart_name == 'number_of_humans': - stats = [COUNT_STATS['realm_active_humans::day']] + stats = [ + COUNT_STATS['1day_actives::day'], + COUNT_STATS['realm_active_humans::day'], + COUNT_STATS['active_users_audit:is_bot:day']] tables = [aggregate_table] - subgroup_to_label = {stats[0]: {None: 'human'}} # type: Dict[CountStat, Dict[Optional[str], str]] + subgroup_to_label = { + stats[0]: {None: '_1day'}, + stats[1]: {None: '_15day'}, + stats[2]: {'false': 'all_time'}} # type: Dict[CountStat, Dict[Optional[str], str]] labels_sort_function = None include_empty_subgroups = True elif chart_name == 'messages_sent_over_time': diff --git a/static/js/stats/stats.js b/static/js/stats/stats.js index da0ee11ad5..a20ec7b167 100644 --- a/static/js/stats/stats.js +++ b/static/js/stats/stats.js @@ -649,19 +649,49 @@ function populate_number_of_users(data) { var text = end_dates.map(format_date); - var trace = { - x: end_dates, - y: data.everyone.human, - type: 'scatter', - name: "Active users", - hoverinfo: 'none', - text: text, - visible: true, - }; + function make_traces(values, type) { + return { + x: end_dates, + y: values, + type: type, + name: i18n.t("Active users"), + hoverinfo: 'none', + text: text, + visible: true, + }; + } + + var _1day_trace = make_traces(data.everyone._1day, 'bar'); + var _15day_trace = make_traces(data.everyone._15day, 'scatter'); + var all_time_trace = make_traces(data.everyone.all_time, 'scatter'); $('#id_number_of_users > div').removeClass("spinner"); - Plotly.newPlot('id_number_of_users', [trace], layout, {displayModeBar: false}); + // Redraw the plot every time for simplicity. If we have perf problems with this in the + // future, we can copy the update behavior from populate_messages_sent_over_time + function draw_or_update_plot(trace) { + $('#1day_actives_button, #15day_actives_button, #all_time_actives_button').removeClass("selected"); + Plotly.newPlot('id_number_of_users', [trace], layout, {displayModeBar: false}); + } + + $('#1day_actives_button').click(function () { + draw_or_update_plot(_1day_trace); + $(this).addClass("selected"); + }); + + $('#15day_actives_button').click(function () { + draw_or_update_plot(_15day_trace); + $(this).addClass("selected"); + }); + + $('#all_time_actives_button').click(function () { + draw_or_update_plot(all_time_trace); + $(this).addClass("selected"); + }); + + // Initial drawing of plot + draw_or_update_plot(_15day_trace, true); + $('#15day_actives_button').addClass("selected"); document.getElementById('id_number_of_users').on('plotly_hover', function (data) { $("#users_hover_info").show(); diff --git a/static/styles/stats.scss b/static/styles/stats.scss index ed5971a3c8..266b852c7f 100644 --- a/static/styles/stats.scss +++ b/static/styles/stats.scss @@ -104,6 +104,10 @@ hr { margin: 3px; } +.button-active-users { + float: right; +} + .chart-container .button-container > * { display: inline-block; vertical-align: top; diff --git a/templates/analytics/stats.html b/templates/analytics/stats.html index 957e03de30..2819219a4d 100644 --- a/templates/analytics/stats.html +++ b/templates/analytics/stats.html @@ -85,6 +85,13 @@