var font_14pt = { family: 'Humbug', size: 14, color: '#000000', }; var button_selected = '#D8D8D8'; var button_unselected = '#F0F0F0'; // TODO: should take a dict of arrays and do it for all keys function partial_sums(array) { var count = 0; var cumulative = []; for (var i = 0; i < array.length; i += 1) { count += array[i]; cumulative[i] = count; } return cumulative; } // Assumes date is a round number of hours function floor_to_local_day(date) { var date_copy = new Date(date.getTime()); date_copy.setHours(0); return date_copy; } // Assumes date is a round number of hours function floor_to_local_week(date) { var date_copy = floor_to_local_day(date); date_copy.setHours(-24 * date.getDay()); return date_copy; } function format_date(date, include_hour) { var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; var month_str = months[date.getMonth()]; var year = date.getFullYear(); var day = date.getDate(); if (include_hour) { var hour = date.getHours(); var hour_str; if (hour === 0) { hour_str = '12 AM'; } else if (hour === 12) { hour_str = '12 PM'; } else if (hour < 12) { hour_str = hour + ' AM'; } else { hour_str = (hour-12) + ' PM'; } return month_str + ' ' + day + ', ' + hour_str; } return month_str + ' ' + day + ', ' + year; } function populate_messages_sent_over_time(data) { if (data.end_times.length === 0) { // TODO: do something nicer here return; } // Helper functions function make_traces(dates, values, type, date_formatter) { var text = dates.map(function (date) { return date_formatter(date); }); var common = { x: dates, type: type, hoverinfo: 'none', text: text }; return { human: $.extend({ // 5062a0 name: "Humans", y: values.human, marker: {color: '#5f6ea0'}}, common), bot: $.extend({ // a09b5f bbb56e name: "Bots", y: values.bot, marker: {color: '#b7b867'}}, common), }; } var layout = { barmode: 'group', width: 750, height: 400, margin: { l: 40, r: 0, b: 40, t: 0 }, xaxis: { fixedrange: true, rangeslider: { bordercolor: '#D8D8D8', borderwidth: 1 }, type: 'date', }, yaxis: { fixedrange: true, rangemode: 'tozero' }, legend: { x: 0.75, y: 1.12, orientation: 'h', font: font_14pt, }, font: font_14pt, }; function make_rangeselector(x, y, button1, button2) { return { x: x, y: y, buttons: [$.extend({stepmode: 'backward'}, button1), $.extend({stepmode: 'backward'}, button2), {step: 'all', label: 'All time'}] }; } var hourly_rangeselector = make_rangeselector( 0.66, -0.62, {count: 24, label: 'Last 24 Hours', step: 'hour'}, {count: 72, label: 'Last 72 Hours', step: 'hour'}); // This is also the cumulative rangeselector var daily_rangeselector = make_rangeselector( 0.68, -0.62, {count: 10, label: 'Last 10 Days', step: 'day'}, {count: 30, label: 'Last 30 Days', step: 'day'}); var weekly_rangeselector = make_rangeselector( 0.656, -0.62, {count: 2, label: 'Last 2 Months', step: 'month'}, {count: 6, label: 'Last 6 Months', step: 'month'}); function add_hover_handler() { document.getElementById('id_messages_sent_over_time').on('plotly_hover', function (data) { document.getElementById('hover_date').innerText = data.points[0].data.text[data.points[0].pointNumber]; var values = [null, null]; data.points.forEach(function (trace) { values[trace.curveNumber] = trace.y; }); if (values[0] !== null) { document.getElementById('hover_human').style.display = 'inline'; document.getElementById('hover_human_value').style.display = 'inline'; document.getElementById('hover_human_value').innerText = values[0]; } else { document.getElementById('hover_human').style.display = 'none'; document.getElementById('hover_human_value').style.display = 'none'; } if (values[1] !== null) { document.getElementById('hover_bot').style.display = 'inline'; document.getElementById('hover_bot_value').style.display = 'inline'; document.getElementById('hover_bot_value').innerText = values[1]; } else { document.getElementById('hover_bot').style.display = 'none'; document.getElementById('hover_bot_value').style.display = 'none'; } }); } var start_dates = data.end_times.map(function (timestamp) { // data.end_times are the ends of hour long intervals. return new Date(timestamp*1000 - 60*60*1000); }); function aggregate_data(aggregation) { var start; var is_boundary; if (aggregation === 'day') { start = floor_to_local_day(start_dates[0]); is_boundary = function (date) { return date.getHours() === 0; }; } else if (aggregation === 'week') { start = floor_to_local_week(start_dates[0]); is_boundary = function (date) { return date.getHours() === 0 && date.getDay() === 0; }; } var dates = [start]; var values = {human: [], bot: []}; var current = {human: 0, bot: 0}; var i_init = 0; if (is_boundary(start_dates[0])) { current = {human: data.realm.human[0], bot: data.realm.bot[0]}; i_init = 1; } for (var i = i_init; i < start_dates.length; i += 1) { if (is_boundary(start_dates[i])) { dates.push(start_dates[i]); values.human.push(current.human); values.bot.push(current.bot); current = {human: 0, bot: 0}; } current.human += data.realm.human[i]; current.bot += data.realm.bot[i]; } values.human.push(current.human); values.bot.push(current.bot); return { dates: dates, values: values, last_value_is_partial: !is_boundary(new Date( start_dates[start_dates.length-1].getTime() + 60*60*1000))}; } // Generate traces var date_formatter = function (date) { return format_date(date, true); }; var hourly_traces = make_traces(start_dates, data.realm, 'bar', date_formatter); var info = aggregate_data('day'); date_formatter = function (date) { return format_date(date, false); }; var last_day_is_partial = info.last_value_is_partial; var daily_traces = make_traces(info.dates, info.values, 'bar', date_formatter); info = aggregate_data('week'); date_formatter = function (date) { // return i18n.t("Week of __date__", {date: format_date(date, false)}); return "Week of " + format_date(date, false); }; var last_week_is_partial = info.last_value_is_partial; var weekly_traces = make_traces(info.dates, info.values, 'bar', date_formatter); var dates = data.end_times.map(function (timestamp) { return new Date(timestamp*1000); }); var values = {human: partial_sums(data.realm.human), bot: partial_sums(data.realm.bot)}; date_formatter = function (date) { return format_date(date, true); }; var cumulative_traces = make_traces(dates, values, 'scatter', date_formatter); // Functions to draw and interact with the plot // We need to redraw plot entirely if switching from (the cumulative) line // graph to any bar graph, since otherwise the rangeselector shows both (plotly bug) var clicked_cumulative = false; function draw_or_update_plot(rangeselector, traces, last_value_is_partial, initial_draw) { $('#daily_button').css('background', button_unselected); $('#weekly_button').css('background', button_unselected); $('#hourly_button').css('background', button_unselected); $('#cumulative_button').css('background', button_unselected); if (initial_draw) { traces.human.visible = true; traces.bot.visible = 'legendonly'; } else { var plotDiv = document.getElementById('id_messages_sent_over_time'); traces.human.visible = plotDiv.data[0].visible; traces.bot.visible = plotDiv.data[1].visible; } layout.xaxis.rangeselector = rangeselector; if (clicked_cumulative || initial_draw) { Plotly.newPlot('id_messages_sent_over_time', [traces.human, traces.bot], layout, {displayModeBar: false}); add_hover_handler(); } else { Plotly.deleteTraces('id_messages_sent_over_time', [0,1]); Plotly.addTraces('id_messages_sent_over_time', [traces.human, traces.bot]); Plotly.relayout('id_messages_sent_over_time', layout); } $('#id_messages_sent_over_time').attr('last_value_is_partial', last_value_is_partial); } // Click handlers for aggregation buttons $('#hourly_button').click(function () { draw_or_update_plot(hourly_rangeselector, hourly_traces, false, false); $(this).css('background', button_selected); clicked_cumulative = false; }); $('#daily_button').click(function () { draw_or_update_plot(daily_rangeselector, daily_traces, last_day_is_partial, false); $(this).css('background', button_selected); clicked_cumulative = false; }); $('#weekly_button').click(function () { draw_or_update_plot(weekly_rangeselector, weekly_traces, last_week_is_partial, false); $(this).css('background', button_selected); clicked_cumulative = false; }); $('#cumulative_button').click(function () { clicked_cumulative = false; draw_or_update_plot(daily_rangeselector, cumulative_traces, false, false); $(this).css('background', button_selected); clicked_cumulative = true; }); // Initial drawing of plot if (weekly_traces.human.x.length < 12) { draw_or_update_plot(daily_rangeselector, daily_traces, last_day_is_partial, true); $('#daily_button').css('background', button_selected); } else { draw_or_update_plot(weekly_rangeselector, weekly_traces, last_week_is_partial, true); $('#weekly_button').css('background', button_selected); } } $.get({ url: '/json/analytics/chart_data', data: {chart_name: 'messages_sent_over_time', min_length: '10'}, idempotent: true, success: function (data) { populate_messages_sent_over_time(data); }, error: function (xhr) { $('#id_stats_errors').show().text($.parseJSON(xhr.responseText).msg); }, }); function round_to_percentages(values, total) { return values.map(function (x) { if (x === total) { return '100%'; } if (x === 0) { return '0%'; } var unrounded = x/total*100; var precision = Math.min(6, Math.max(2, Math.floor( -Math.log(100-unrounded)/Math.log(10)) + 3)); return unrounded.toPrecision(precision) + '%'; }); } // Last label will turn into "Other" if time_series data has a label not in labels function compute_summary_chart_data(time_series_data, num_steps, labels_) { var data = {}; var key; for (key in time_series_data) { if (time_series_data[key].length < num_steps) { num_steps = time_series_data[key].length; } var sum = 0; for (var i=1; i<=num_steps; i+=1) { sum += time_series_data[key][time_series_data[key].length-i]; } data[key] = sum; } var labels = labels_.slice(); var values = []; labels.forEach(function (label) { if (data.hasOwnProperty(label)) { values.push(data[label]); delete data[label]; } else { values.push(0); } }); if (!$.isEmptyObject(data)) { labels[labels.length-1] = "Other"; for (key in data) { if (data.hasOwnProperty(key)) { values[labels.length-1] += data[key]; } } } var total = values.reduce(function (a, b) { return a + b; }, 0); return { values: values, labels: labels, percentages: round_to_percentages(values, total), total: total, }; } function populate_messages_sent_by_client(data) { var layout = { width: 750, height: null, // set in draw_plot() margin: { l: 3, r: 40, b: 40, t: 0 }, font: font_14pt, xaxis: { range: null }, // set in draw_plot() yaxis: { showticklabels: false }, showlegend: false, }; // sort labels so that values are descending in the default view var realm_cumulative = compute_summary_chart_data(data.realm, data.end_times.length, data.display_order.slice(0, 12)); var label_values = []; for (var i=0; i 0) { annotations.values.push(plot_data.values[i]); annotations.labels.push(plot_data.labels[i]); annotations.text.push(' ' + plot_data.labels[i] + ' (' + plot_data.percentages[i] + ')'); } } return { trace: { x: plot_data.values, y: plot_data.labels, type: 'bar', orientation: 'h', sort: false, textinfo: "text", hoverinfo: "none", marker: { color: '#537c5e' }, font: { family: 'Humbug', size: 18, color: '#000000' }, }, trace_annotations: { x: annotations.values, y: annotations.labels, mode: 'text', type: 'scatter', textposition: 'middle right', text: annotations.text, }, }; } var plot_data = { realm: { cumulative: make_plot_data(data.realm, data.end_times.length), thirty: make_plot_data(data.realm, 30), ten: make_plot_data(data.realm, 10), }, user: { cumulative: make_plot_data(data.user, data.end_times.length), thirty: make_plot_data(data.user, 30), ten: make_plot_data(data.user, 10), }, }; var user_button = 'realm'; var time_button = 'cumulative'; function draw_plot() { var data_ = plot_data[user_button][time_button]; layout.height = layout.margin.b + data_.trace.x.length * 30; layout.xaxis.range = [0, Math.max.apply(null, data_.trace.x) * 1.3]; Plotly.newPlot('id_messages_sent_by_client', [data_.trace, data_.trace_annotations], layout, {displayModeBar: false, staticPlot: true}); } draw_plot(); // Click handlers function set_user_button(button) { $('#messages_by_client_realm_button').css('background', button_unselected); $('#messages_by_client_user_button').css('background', button_unselected); button.css('background', button_selected); } function set_time_button(button) { $('#messages_by_client_cumulative_button').css('background', button_unselected); $('#messages_by_client_thirty_days_button').css('background', button_unselected); $('#messages_by_client_ten_days_button').css('background', button_unselected); button.css('background', button_selected); } $('#messages_by_client_realm_button').click(function () { set_user_button($(this)); user_button = 'realm'; draw_plot(); }); $('#messages_by_client_user_button').click(function () { set_user_button($(this)); user_button = 'user'; draw_plot(); }); $('#messages_by_client_cumulative_button').click(function () { set_time_button($(this)); time_button = 'cumulative'; draw_plot(); }); $('#messages_by_client_thirty_days_button').click(function () { set_time_button($(this)); time_button = 'thirty'; draw_plot(); }); $('#messages_by_client_ten_days_button').click(function () { set_time_button($(this)); time_button = 'ten'; draw_plot(); }); // handle links with @href started with '#' only $(document).on('click', 'a[href^="#"]', function (e) { // target element id var id = $(this).attr('href'); // target element var $id = $(id); if ($id.length === 0) { return; } // prevent standard hash navigation (avoid blinking in IE) e.preventDefault(); var pos = $id.offset().top+$('.page-content')[0].scrollTop-50; $('.page-content').animate({scrollTop: pos + "px"}, 500); }); } $.get({ url: '/json/analytics/chart_data', data: {chart_name: 'messages_sent_by_client', min_length: '10'}, idempotent: true, success: function (data) { populate_messages_sent_by_client(data); }, error: function (xhr) { $('#id_stats_errors').show().text($.parseJSON(xhr.responseText).msg); }, }); function populate_messages_sent_by_message_type(data) { var layout = { margin: { l: 90, r: 0, b: 0, t: 0 }, width: 550, height: 300, font: font_14pt, }; function make_plot_data(time_series_data, num_steps) { var plot_data = compute_summary_chart_data(time_series_data, num_steps, data.display_order); var labels = []; for (var i=0; i