-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/episode sparklines #1386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/episode sparklines #1386
Changes from 6 commits
fffd32a
1e807d4
c2237e2
d4fa1cf
caf92ad
65237da
d767717
2c4452c
7f056b4
5a68772
fc723ad
d6e5b62
84330c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,13 +3,32 @@ class PodcastMetricsController < ApplicationController | |
|
|
||
| before_action :set_podcast | ||
| before_action :check_clickhouse, except: %i[show] | ||
| before_action :set_date_range, except: %i[dropdays] | ||
| before_action :set_uniques, only: %i[show uniques] | ||
| before_action :set_dropday_range, only: %i[show dropdays] | ||
|
|
||
| def show | ||
| end | ||
|
|
||
| def episode_sparkline | ||
cavis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @episode = Episode.find_by(guid: metrics_params[:episode_id]) | ||
| @prev_episode = Episode.find_by(guid: metrics_params[:prev_episode_id]) | ||
|
|
||
| @alltime_downloads = | ||
cavis marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Rollups::HourlyDownload | ||
| .where(episode_id: @episode[:guid], hour: (@episode.first_rss_published_at..Date.utc_today)) | ||
| .final | ||
| .select(:episode_id, "DATE_TRUNC('DAY', hour) AS hour", "SUM(count) AS count") | ||
| .group(:episode_id, "DATE_TRUNC('DAY', hour) AS hour") | ||
| .order(Arel.sql("DATE_TRUNC('DAY', hour) ASC")) | ||
| .load_async | ||
cavis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @episode_trend = calculate_episode_trend(@episode, @prev_episode) | ||
|
|
||
| render partial: "metrics/episode_sparkline", locals: { | ||
| episode: @episode, | ||
| downloads: @alltime_downloads, | ||
| episode_trend: @episode_trend | ||
| } | ||
| end | ||
|
|
||
| def downloads | ||
| @downloads_within_date_range = | ||
| Rollups::HourlyDownload | ||
|
|
@@ -195,46 +214,31 @@ def set_podcast | |
| render_not_found(e) | ||
| end | ||
|
|
||
| def set_date_range | ||
| @date_start = metrics_params[:date_start] | ||
| @date_end = metrics_params[:date_end] | ||
| @interval = metrics_params[:interval] | ||
| @date_range = generate_date_range(@date_start, @date_end, @interval) | ||
| def metrics_params | ||
| params | ||
| .permit(:podcast_id, :episode_id, :prev_episode_id) | ||
| end | ||
|
|
||
| def set_uniques | ||
| @uniques_selection = uniques_params[:uniques_selection] | ||
| end | ||
| def calculate_episode_trend(episode, prev_episode) | ||
| return nil if (episode.first_rss_published_at + 1.day) > Date.utc_today | ||
| return nil unless episode.present? && prev_episode.present? | ||
|
||
|
|
||
| def set_dropday_range | ||
| @dropday_range = dropdays_params[:dropday_range] | ||
| end | ||
| dropday_downloads = episode_dropday_query(episode) | ||
| previous_ep_dropday = episode_dropday_query(prev_episode) | ||
|
|
||
| def metrics_params | ||
| params | ||
| .permit(:podcast_id, :date_start, :date_end, :interval) | ||
| .with_defaults( | ||
| date_start: 30.days.ago.utc_date, | ||
| date_end: Date.utc_today, | ||
| interval: "DAY" | ||
| ) | ||
| end | ||
| ep_sum = dropday_downloads.sum(&:count) | ||
| prev_ep_sum = previous_ep_dropday.sum(&:count) | ||
|
|
||
| def uniques_params | ||
| params | ||
| .permit(:uniques_selection) | ||
| .with_defaults( | ||
| uniques_selection: "last_7_rolling" | ||
| ) | ||
| .merge(metrics_params) | ||
| ((ep_sum.to_f / prev_ep_sum.to_f) - 1).round(2) | ||
|
||
| end | ||
|
|
||
| def dropdays_params | ||
| params | ||
| .permit(:dropday_range, :interval) | ||
| .with_defaults( | ||
| dropday_range: 7 | ||
| ) | ||
| .merge(metrics_params) | ||
| def episode_dropday_query(ep) | ||
| Rollups::HourlyDownload | ||
| .where(episode_id: ep[:guid], hour: (ep.first_rss_published_at..(ep.first_rss_published_at + 1.day))) | ||
cavis marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .final | ||
| .select(:episode_id, "DATE_TRUNC('HOUR', hour) AS hour", "SUM(count) AS count") | ||
| .group(:episode_id, "DATE_TRUNC('HOUR', hour) AS hour") | ||
cavis marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .order(Arel.sql("DATE_TRUNC('HOUR', hour) ASC")) | ||
| .load_async | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,9 @@ def index | |
| def show | ||
| authorize @podcast | ||
|
|
||
| @recently_published = @podcast.episodes.published.dropdate_desc.limit(3) | ||
| @recently_published_episodes = @podcast.episodes.published.dropdate_desc.limit(5) | ||
| @episode_trend_pairs = episode_trend_pairs(@recently_published_episodes) | ||
| @recently_published = @recently_published_episodes[0..2] | ||
| @next_scheduled = @podcast.episodes.draft_or_scheduled.dropdate_asc.limit(3) | ||
|
|
||
| @metrics_jwt = prx_jwt | ||
|
|
@@ -189,4 +191,16 @@ def compose_message(podcast) | |
| def sub_escapes(text) | ||
| text.gsub(/[&<>]/, "&" => "&", "<" => "<", ">" => ">") | ||
| end | ||
|
|
||
| def episode_trend_pairs(episodes) | ||
|
||
| pairs = [] | ||
| episodes.each_with_index do |ep, i| | ||
| pairs << { | ||
| episode: ep, | ||
| prev_episode: episodes[i + 1] | ||
| } | ||
| end | ||
|
|
||
| pairs.slice(0, pairs.length - 1) | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,34 @@ | ||
| import { Controller } from "@hotwired/stimulus" | ||
| import { buildDateTimeChart, buildDownloadsSeries, dynamicBarAndAreaType } from "util/apex" | ||
| import { buildSparklineChart, destroyChart } from "util/apex" | ||
|
|
||
| export default class extends Controller { | ||
| static values = { | ||
| id: String, | ||
| downloads: Object, | ||
| dateRange: Array, | ||
| interval: String, | ||
| downloads: Array, | ||
| } | ||
|
|
||
| static targets = ["chart"] | ||
|
|
||
| connect() { | ||
| const series = buildDownloadsSeries(this.downloadsValue, this.dateRangeValue) | ||
| const title = `Downloads by ${this.intervalValue.toLowerCase()}` | ||
| const chart = buildDateTimeChart( | ||
| this.idValue, | ||
| series, | ||
| this.chartTarget, | ||
| dynamicBarAndAreaType(this.dateRangeValue), | ||
| title | ||
| ) | ||
| const seriesData = this.downloadsValue.map((rollup) => { | ||
| return { | ||
| x: rollup.hour, | ||
| y: rollup.count, | ||
| } | ||
| }) | ||
| const series = [ | ||
| { | ||
| name: "Downloads", | ||
| data: seriesData, | ||
| }, | ||
| ] | ||
|
|
||
| const chart = buildSparklineChart(this.idValue, series, this.chartTarget) | ||
|
|
||
| chart.render() | ||
| } | ||
|
|
||
| disconnect() { | ||
| destroyChart(this.idValue) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <%= turbo_frame_tag "episode_sparkline" do %> | ||
| <% trend = parse_trend(episode_trend) %> | ||
|
|
||
| <p class="d-flex align-items-center <%= trend[:color] if trend.present? %>"> | ||
| <span class="z-1"><%= trend[:percent] if trend.present? %></span> | ||
| <span class="material-icons ms-1 z-1" aria-hidden="true"><%= trend[:direction] if trend.present? %></span> | ||
| </p> | ||
| <div class="position-absolute top-50 start-50 translate-middle w-100 h-100 z-0" | ||
| data-controller="apex-downloads" | ||
| data-apex-downloads-id-value="<%= episode.guid %>" | ||
| data-apex-downloads-downloads-value="<%= downloads.to_json %>"> | ||
| <div data-apex-downloads-target="chart"></div> | ||
|
||
| </div> | ||
| <% end %> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,50 +1,8 @@ | ||
| <% chart_id = SecureRandom.uuid %> | ||
|
|
||
| <%= turbo_frame_tag "episodes" do %> | ||
| <%= form_with(url: url, method: :get, id: form_id, data: {controller: "click", click_debounce_value: 200}) do |form| %> | ||
| <div class="row d-flex flex-wrap gx-4 h-100"> | ||
| <div class="col metrics-chart" | ||
| data-controller="apex-episodes" | ||
| data-apex-episodes-id-value="<%= chart_id %>" | ||
| data-apex-episodes-selected-episodes-value="<%= episode_rollups.to_json %>" | ||
| data-apex-episodes-date-range-value="<%= date_range.to_json %>" | ||
| data-apex-episodes-interval-value="<%= interval %>"> | ||
| <div data-apex-episodes-target="chart"></div> | ||
| </div> | ||
| <div class="col align-items-center metrics-table" data-controller="apex-toggles"> | ||
| <table class="table table-sm table-striped shadow rounded"> | ||
| <thead class="table-primary"> | ||
| <tr> | ||
| <td><%= t(".table_header.episodes") %></td> | ||
| <td><%= t(".table_header.published_at") %></td> | ||
| <td><%= t(".table_header.downloads") %></td> | ||
| <td><%= t(".table_header.all_time") %></td> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| <% episode_rollups.each_with_index do |er, i| %> | ||
| <% sum = sum_rollups(er[:rollups]) %> | ||
| <% total = sum_rollups(er[:totals]) %> | ||
| <% title = er[:episode][:title] %> | ||
| <tr class="align-items-center"> | ||
| <td class="d-flex align-items-center"> | ||
| <input id="episode<%= title %>" type="checkbox" class="form-check-input mt-0 me-2" checked="true" data-action="click->apex-toggles#toggleSeries" data-apex-toggles-series-param="<%= title %>" data-apex-toggles-id-param="<%= chart_id %>"> | ||
| <span class="material-icons me-2" aria-label="hidden" style="color: <%= er[:color] %>">circle</span> | ||
| <span class="text-nowrap overflow-hidden"><%= link_to title, episode_metrics_path(episode_id: er[:episode][:guid], date_start: date_start, date_end: date_end, interval: interval), data: {turbo_frame: "_top"} %></span> | ||
| </td> | ||
| <td><%= er[:episode][:published_at].strftime("%Y-%m-%d") %></td> | ||
| <td><% if sum %><%= sum %><% else %>—<% end %></td> | ||
| <td><% if total %><%= total %><% else %>—<% end %></td> | ||
| </tr> | ||
| <% end %> | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| </div> | ||
|
|
||
| <%= form.date_field :date_start, class: "d-none", data: {apex_nav_target: "startDate", action: "change->click#submit"}, value: date_start %> | ||
| <%= form.date_field :date_end, class: "d-none", data: {apex_nav_target: "endDate", action: "change->click#submit"}, value: date_end %> | ||
| <%= form.select :interval, interval_options, {selected: interval}, class: "d-none", data: {apex_nav_target: "interval", action: "change->click#submit", path: "episodes"} %> | ||
| <%= form.submit class: "d-none", data: {click_target: "submit"} %> | ||
| <div class="episode-card-info inside-card flex-fill px-3 py-2 position-relative"> | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. repurposing this partial for the published episode cards. |
||
| <p class="episode-card-date z-1"><%= local_date(episode.published_at, format: :short) %></p> | ||
| <div class="episode-card-inner inside-card"> | ||
| <h2 class="h5 episode-card-title m-0 z-1"><%= link_to episode.title, episode_path(episode) %></h2> | ||
| </div> | ||
| <%= turbo_frame_tag "episode_sparkline", src: episode_sparkline_podcast_metrics_path(podcast_id: podcast.id, episode_id: episode.guid, prev_episode_id: prev_episode.guid), loading: "lazy" do %> | ||
| <% end %> | ||
| <% end %> | ||
| </div> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| <%= turbo_frame_tag metrics_path do %> | ||
| <div class="h-100 d-flex justify-content-center align-items-center"> | ||
| Clickhouse not connected | ||
| </div> | ||
| <% if card_type == "error" %> | ||
| <%= turbo_frame_tag metrics_path do %> | ||
| <div class="h-100 d-flex justify-content-center align-items-center"> | ||
| Clickhouse not connected | ||
| </div> | ||
| <% end %> | ||
| <% elsif card_type == "blank" %> | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't render anything for the sparklines if clickhouse is not connected. |
||
| <% end %> | ||



Uh oh!
There was an error while loading. Please reload this page.