UX: Convert sentiment analysis overview to horizontal bars (#1216)

The sentiment analysis report page initially showcases sentiments via category/tag by doughnut visualizations. However, this isn't an optimal view for quickly scanning and comparing each result. This PR updates the overview to include a table visualization with horizontal bars to represent sentiment analysis instead of doughnuts. Doughnut visualizations are still maintained however when accessing the sentiment data in the drill down for individual entries.

This approach is an intermediary step, as we will eventually add whole clustering and sizing visualization instead of a table. As such, no relevant tests are added in this PR.
This commit is contained in:
Keegan George 2025-03-25 13:36:52 -07:00 committed by GitHub
parent 76a48786d9
commit 9dfae3d472
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 157 additions and 23 deletions

View File

@ -23,6 +23,7 @@ import closeOnClickOutside from "discourse/modifiers/close-on-click-outside";
import { i18n } from "discourse-i18n";
import DTooltip from "float-kit/components/d-tooltip";
import DoughnutChart from "discourse/plugins/discourse-ai/discourse/components/doughnut-chart";
import AiSentimentHorizontalBar from "../components/ai-sentiment-horizontal-bar";
export default class AdminReportSentimentAnalysis extends Component {
@service router;
@ -82,6 +83,15 @@ export default class AdminReportSentimentAnalysis extends Component {
}
}
get groupingType() {
const dataSample = this.args.model.data[0];
const localePrefix =
"discourse_ai.sentiments.sentiment_analysis.group_types";
return dataSample.category_name
? i18n(`${localePrefix}.category`)
: i18n(`${localePrefix}.tag`);
}
get colors() {
return ["#2ecc71", "#95a5a6", "#e74c3c"];
}
@ -107,7 +117,17 @@ export default class AdminReportSentimentAnalysis extends Component {
this.calculateNeutralScore(data),
data.negative_count,
],
score_map: {
positive: data.positive_count,
neutral: this.calculateNeutralScore(data),
negative: data.negative_count,
},
total_score: data.total_count,
widths: {
positive: (data.positive_count / data.total_count) * 100,
neutral: (this.calculateNeutralScore(data) / data.total_count) * 100,
negative: (data.negative_count / data.total_count) * 100,
},
};
});
}
@ -302,29 +322,56 @@ export default class AdminReportSentimentAnalysis extends Component {
{{#unless this.showingSelectedChart}}
<div class="admin-report-sentiment-analysis">
{{#each this.transformedData as |data|}}
<div
class="admin-report-sentiment-analysis__chart-wrapper"
role="button"
{{on "click" (fn this.showDetails data)}}
{{closeOnClickOutside
(fn (mut this.selectedChart) null)
(hash
targetSelector=".admin-report-sentiment-analysis-details"
secondaryTargetSelector=".admin-report-sentiment-analysis"
)
}}
>
<DoughnutChart
@labels={{@model.labels}}
@colors={{this.colors}}
@data={{data.scores}}
@totalScore={{data.total_score}}
@doughnutTitle={{data.title}}
@displayLegend={{true}}
/>
</div>
{{/each}}
<table class="sentiment-analysis-table md-table">
<thead>
<th>{{this.groupingType}}</th>
<th>{{i18n
"discourse_ai.sentiments.sentiment_analysis.table.total_count"
}}</th>
<th>{{i18n
"discourse_ai.sentiments.sentiment_analysis.table.sentiment"
}}</th>
</thead>
<tbody>
{{#each this.transformedData as |data|}}
<tr
class="sentiment-analysis-table__row"
role="button"
{{on "click" (fn this.showDetails data)}}
{{closeOnClickOutside
(fn (mut this.selectedChart) null)
(hash
targetSelector=".admin-report-sentiment-analysis-details"
secondaryTargetSelector=".admin-report-sentiment-analysis"
)
}}
>
<td class="sentiment-analysis-table__title">{{data.title}}</td>
<td
class="sentiment-analysis-table__total-score"
>{{data.total_score}}</td>
<td class="sentiment-horizontal-bar">
<AiSentimentHorizontalBar
@type="positive"
@score={{data.score_map.positive}}
@width={{data.widths.positive}}
/>
<AiSentimentHorizontalBar
@type="negative"
@score={{data.score_map.negative}}
@width={{data.widths.negative}}
/>
<AiSentimentHorizontalBar
@type="neutral"
@score={{data.score_map.neutral}}
@width={{data.widths.neutral}}
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/unless}}

View File

@ -0,0 +1,30 @@
import { concat } from "@ember/helper";
import { htmlSafe } from "@ember/template";
import { gt } from "truth-helpers";
import { i18n } from "discourse-i18n";
import DTooltip from "float-kit/components/d-tooltip";
const AiSentimentHorizontalBar = <template>
{{#if (gt @score 0)}}
<DTooltip
class={{concat "sentiment-horizontal-bar__" @type}}
style={{htmlSafe (concat "width: " @width "%")}}
>
<:trigger>
<span class="sentiment-horizontal-bar__count">
{{@score}}
</span>
</:trigger>
<:content>
{{i18n
(concat
"discourse_ai.sentiments.sentiment_analysis.filter_types." @type
)
}}:
{{@score}}
</:content>
</DTooltip>
{{/if}}
</template>;
export default AiSentimentHorizontalBar;

View File

@ -248,3 +248,54 @@
display: none;
}
}
.sentiment-analysis-table {
margin: 1rem;
&__total-score {
font-weight: bold;
font-size: var(--font-up-1);
}
&__row {
cursor: pointer;
}
}
.sentiment-horizontal-bar {
display: flex;
&__count {
font-weight: bold;
font-size: var(--font-down-1);
color: var(--secondary);
}
&__positive,
&__neutral,
&__negative {
display: flex;
flex-flow: column nowrap;
justify-content: flex-end;
align-items: center;
padding: 0.75rem;
border-left: 2px solid var(--secondary);
border-right: 2px solid var(--secondary);
}
&__positive {
background: rgb(var(--d-sentiment-report-positive-rgb));
border-top-left-radius: var(--d-border-radius);
border-bottom-left-radius: var(--d-border-radius);
}
&__negative {
background: rgb(var(--d-sentiment-report-negative-rgb));
}
&__neutral {
background: rgb(var(--d-sentiment-report-neutral-rgb));
border-top-right-radius: var(--d-border-radius);
border-bottom-right-radius: var(--d-border-radius);
}
}

View File

@ -700,6 +700,12 @@ en:
positive: "Positive"
neutral: "Neutral"
negative: "Negative"
group_types:
category: "Category"
tag: "Tag"
table:
sentiment: "Sentiment"
total_count: "Total"
summarization:
chat: