From 2ff3fe3a9f30ad80e076582efa46d3d0a7858ed4 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Fri, 2 Aug 2024 14:23:29 -0700 Subject: [PATCH] UX: Use stacked line chart for post sentiment (#737) --- .../modules/sentiment/common/dashboard.scss | 4 - .../modules/sentiment/desktop/dashboard.scss | 4 +- config/locales/server.en.yml | 16 ++-- lib/sentiment/entry_point.rb | 83 +++++++++---------- .../lib/modules/sentiment/entry_point_spec.rb | 39 +++------ 5 files changed, 62 insertions(+), 84 deletions(-) diff --git a/assets/stylesheets/modules/sentiment/common/dashboard.scss b/assets/stylesheets/modules/sentiment/common/dashboard.scss index 6e2058aa..d086f136 100644 --- a/assets/stylesheets/modules/sentiment/common/dashboard.scss +++ b/assets/stylesheets/modules/sentiment/common/dashboard.scss @@ -1,8 +1,4 @@ .dashboard.dashboard-sentiment { - .navigation-item.sentiment { - border-bottom: 0.4em solid var(--tertiary); - } - .charts { display: grid; grid-template-columns: repeat(12, 1fr); diff --git a/assets/stylesheets/modules/sentiment/desktop/dashboard.scss b/assets/stylesheets/modules/sentiment/desktop/dashboard.scss index 278d14d1..3dd8d416 100644 --- a/assets/stylesheets/modules/sentiment/desktop/dashboard.scss +++ b/assets/stylesheets/modules/sentiment/desktop/dashboard.scss @@ -1,8 +1,8 @@ .dashboard.dashboard-sentiment .charts { .overall-sentiment { - grid-column: span 8; + grid-column: span 6; } .post-emotion { - grid-column: span 4; + grid-column: span 6; } } diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 9ee9d8c8..06ddc53d 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -295,15 +295,13 @@ en: positive: "Positive" negative: "Negative" post_emotion: - tl_01: "Trust levels 0-1" - tl_234: "Trust levels 2+" - sadness: "Sadness" - surprise: "Surprise" - neutral: "Neutral" - fear: "Fear" - anger: "Anger" - joy: "Joy" - disgust: "Disgust" + sadness: "Sadness 😢" + surprise: "Surprise 😱" + neutral: "Neutral 😐" + fear: "Fear 😨" + anger: "Anger 😡" + joy: "Joy 😀" + disgust: "Disgust 🤢" llm: configuration: diff --git a/lib/sentiment/entry_point.rb b/lib/sentiment/entry_point.rb index 45bfe400..e25eea4e 100644 --- a/lib/sentiment/entry_point.rb +++ b/lib/sentiment/entry_point.rb @@ -67,39 +67,39 @@ module DiscourseAi end plugin.add_report("post_emotion") do |report| - report.modes = [:radar] + report.modes = [:stacked_line_chart] threshold = 30 emotion_count_clause = Proc.new { |emotion| <<~SQL } - COUNT( - CASE WHEN (cr.classification::jsonb->'#{emotion}')::integer > :threshold THEN 1 ELSE NULL END - ) AS #{emotion}_count - SQL + COUNT( + CASE WHEN (cr.classification::jsonb->'#{emotion}')::integer > :threshold THEN 1 ELSE NULL END + ) AS #{emotion}_count + SQL grouped_emotions = DB.query( <<~SQL, - SELECT - u.trust_level AS trust_level, - #{emotion_count_clause.call("sadness")}, - #{emotion_count_clause.call("surprise")}, - #{emotion_count_clause.call("fear")}, - #{emotion_count_clause.call("anger")}, - #{emotion_count_clause.call("joy")}, - #{emotion_count_clause.call("disgust")} - FROM - classification_results AS cr - INNER JOIN posts p ON p.id = cr.target_id AND cr.target_type = 'Post' - INNER JOIN users u ON p.user_id = u.id - INNER JOIN topics t ON t.id = p.topic_id - INNER JOIN categories c ON c.id = t.category_id - WHERE - t.archetype = 'regular' AND - p.user_id > 0 AND - cr.model_used = 'emotion' AND - (p.created_at > :report_start AND p.created_at < :report_end) - GROUP BY u.trust_level - SQL + SELECT + DATE_TRUNC('day', p.created_at)::DATE AS posted_at, + #{emotion_count_clause.call("sadness")}, + #{emotion_count_clause.call("surprise")}, + #{emotion_count_clause.call("fear")}, + #{emotion_count_clause.call("anger")}, + #{emotion_count_clause.call("joy")}, + #{emotion_count_clause.call("disgust")} + FROM + classification_results AS cr + INNER JOIN posts p ON p.id = cr.target_id AND cr.target_type = 'Post' + INNER JOIN users u ON p.user_id = u.id + INNER JOIN topics t ON t.id = p.topic_id + INNER JOIN categories c ON c.id = t.category_id + WHERE + t.archetype = 'regular' AND + p.user_id > 0 AND + cr.model_used = 'emotion' AND + (p.created_at > :report_start AND p.created_at < :report_end) + GROUP BY DATE_TRUNC('day', p.created_at) + SQL report_start: report.start_date, report_end: report.end_date, threshold: threshold, @@ -107,27 +107,24 @@ module DiscourseAi return report if grouped_emotions.empty? - emotions = %w[sadness disgust fear anger joy surprise] - level_groups = [[0, 1], [2, 3, 4]] + emotions = [ + { name: "sadness", color: report.colors[:turquoise] }, + { name: "disgust", color: report.colors[:lime] }, + { name: "fear", color: report.colors[:purple] }, + { name: "anger", color: report.colors[:magenta] }, + { name: "joy", color: report.colors[:yellow] }, + { name: "surprise", color: report.colors[:brown] }, + ] report.data = - level_groups.each_with_index.map do |lg, idx| - color = idx == 0 ? :turquoise : :lime - tl_emotion_avgs = grouped_emotions.select { |ge| lg.include?(ge.trust_level) } - + emotions.map do |emotion| { - req: "emotion_tl_#{lg.join}", - color: report.colors[color], - label: I18n.t("discourse_ai.sentiment.reports.post_emotion.tl_#{lg.join}"), + req: "emotion_#{emotion[:name]}", + color: emotion[:color], + label: I18n.t("discourse_ai.sentiment.reports.post_emotion.#{emotion[:name]}"), data: - emotions.map do |e| - { - x: I18n.t("discourse_ai.sentiment.reports.post_emotion.#{e}"), - y: - tl_emotion_avgs.sum do |tl_emotion_avg| - tl_emotion_avg.public_send("#{e}_count").to_i - end, - } + grouped_emotions.map do |ge| + { x: ge.posted_at, y: ge.public_send("#{emotion[:name]}_count") } end, } end diff --git a/spec/lib/modules/sentiment/entry_point_spec.rb b/spec/lib/modules/sentiment/entry_point_spec.rb index 2e1114a6..02cf1c18 100644 --- a/spec/lib/modules/sentiment/entry_point_spec.rb +++ b/spec/lib/modules/sentiment/entry_point_spec.rb @@ -100,10 +100,12 @@ RSpec.describe DiscourseAi::Sentiment::EntryPoint do ) end + def strip_emoji_and_downcase(str) + stripped_str = str.gsub(/[^\p{L}\p{N}]+/, "") # remove any non-alphanumeric characters + stripped_str.downcase + end + it "calculate averages using only public posts" do - post_1.user.update!(trust_level: TrustLevel[0]) - post_2.user.update!(trust_level: TrustLevel[3]) - pm.user.update!(trust_level: TrustLevel[0]) threshold = 30 emotion_classification(post_1, emotion_1) @@ -111,31 +113,16 @@ RSpec.describe DiscourseAi::Sentiment::EntryPoint do emotion_classification(pm, emotion_2) report = Report.find("post_emotion") - tl_01_point = report.data[0][:data] - tl_234_point = report.data[1][:data] - tl_01_point.each do |point| - expected = emotion_1[point[:x].downcase.to_sym] > threshold ? 1 : 0 - expect(point[:y]).to eq(expected) + data_point = report.data + + data_point.each do |point| + emotion = strip_emoji_and_downcase(point[:label]) + expected = + (emotion_1[emotion.to_sym] > threshold ? 1 : 0) + + (emotion_2[emotion.to_sym] > threshold ? 1 : 0) + expect(point[:data][0][:y]).to eq(expected) end - - tl_234_point.each do |point| - expected = emotion_2[point[:x].downcase.to_sym] > threshold ? 1 : 0 - expect(point[:y]).to eq(expected) - end - end - - it "doesn't try to divide by zero if there are no data in a TL group" do - post_1.user.update!(trust_level: TrustLevel[3]) - post_2.user.update!(trust_level: TrustLevel[3]) - - emotion_classification(post_1, emotion_1) - emotion_classification(post_2, emotion_2) - - report = Report.find("post_emotion") - tl_01_point = report.data[0][:data].first - - expect(tl_01_point[:y]).to be_zero end end end