Skip to content

Commit

Permalink
feat: add new visualization for NPS score [part 2]
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasheriques committed Feb 23, 2025
1 parent afcacb9 commit 4ec9698
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 86 deletions.
26 changes: 21 additions & 5 deletions frontend/src/scenes/surveys/SurveyView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import './SurveyView.scss'

import { IconGraph } from '@posthog/icons'
import { LemonButton, LemonDialog, LemonDivider, Link, Spinner } from '@posthog/lemon-ui'
import { IconGraph, IconInfo } from '@posthog/icons'
import { LemonButton, LemonDialog, LemonDivider, Link, Spinner, Tooltip } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog'
import { CompareFilter } from 'lib/components/CompareFilter/CompareFilter'
Expand Down Expand Up @@ -42,6 +42,7 @@ import { surveyLogic } from './surveyLogic'
import { surveysLogic } from './surveysLogic'
import {
MultipleChoiceQuestionBarChart,
NPSStackedBar,
NPSSurveyResultsBarChart,
OpenTextViz,
RatingQuestionBarChart,
Expand Down Expand Up @@ -623,13 +624,28 @@ function SurveyNPSResults({
surveyNPSScore?: string
questionIndex: number
}): JSX.Element {
const { dateRange, interval, compareFilter, defaultInterval } = useValues(surveyLogic)
const { dateRange, interval, compareFilter, defaultInterval, npsBreakdown } = useValues(surveyLogic)
const { setDateRange, setInterval, setCompareFilter } = useActions(surveyLogic)

return (
<div>
<div className="text-4xl font-bold">{surveyNPSScore}</div>
<div className="mb-2 font-semibold text-secondary">Latest NPS Score</div>
<div className="flex items-center gap-2">
<div className="text-4xl font-bold">{surveyNPSScore}</div>
</div>
<div className="mb-2 font-semibold text-secondary">
<Tooltip
placement="bottom"
title="NPS Score is calculated by subtracting the percentage of detractors (0-6) from the percentage of promoters (9-10). Passives (7-8) are not included in the calculation. It can range from -100 to 100."
>
<IconInfo className="text-muted" />
</Tooltip>{' '}
Latest NPS Score
</div>
{npsBreakdown && (
<div className="space-y-2 mt-2 mb-4">
<NPSStackedBar npsBreakdown={npsBreakdown} />
</div>
)}
<div className="space-y-2 bg-surface-primary p-2 rounded">
<div className="flex items-center justify-between gap-2">
<h4 className="text-lg font-semibold">NPS Trend</h4>
Expand Down
178 changes: 97 additions & 81 deletions frontend/src/scenes/surveys/surveyViewViz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
IconThumbsUpFilled,
} from '@posthog/icons'
import { LemonButton, LemonTable } from '@posthog/lemon-ui'
import clsx from 'clsx'
import { BindLogic, useActions, useValues } from 'kea'
import { FlaggedFeature } from 'lib/components/FlaggedFeature'
import { FEATURE_FLAGS } from 'lib/constants'
Expand All @@ -23,7 +24,7 @@ import { PieChart } from 'scenes/insights/views/LineGraph/PieChart'
import { maxGlobalLogic } from 'scenes/max/maxGlobalLogic'
import { PersonDisplay } from 'scenes/persons/PersonDisplay'
import { AIConsentPopoverWrapper } from 'scenes/settings/organization/AIConsentPopoverWrapper'
import { getSurveyResponseKey } from 'scenes/surveys/utils'
import { getSurveyResponseKey, NPSBreakdown } from 'scenes/surveys/utils'

import { GraphType, InsightLogicProps, SurveyQuestionType } from '~/types'

Expand Down Expand Up @@ -53,99 +54,70 @@ const formatCount = (count: number, total: number): string => {
return `${humanFriendlyNumber(count)}`
}

export function UsersCount({ surveyUserStats }: { surveyUserStats: SurveyUserStats }): JSX.Element {
const { seen, dismissed, sent } = surveyUserStats
const total = seen + dismissed + sent
const labelTotal = total === 1 ? 'Unique user shown' : 'Unique users shown'
const labelSent = sent === 1 ? 'Response sent' : 'Responses sent'

return (
<div className="inline-flex mb-4">
<div>
<div className="text-4xl font-bold">{humanFriendlyNumber(total)}</div>
<div className="font-semibold text-secondary">{labelTotal}</div>
</div>
{sent > 0 && (
<div className="ml-10">
<div className="text-4xl font-bold">{humanFriendlyNumber(sent)}</div>
<div className="font-semibold text-secondary">{labelSent}</div>
</div>
)}
</div>
)
type StackedBarSegment = {
count: number
label: string
color: string
}

export function UsersStackedBar({ surveyUserStats }: { surveyUserStats: SurveyUserStats }): JSX.Element {
const { seen, dismissed, sent } = surveyUserStats

const total = seen + dismissed + sent
const seenPercentage = (seen / total) * 100
const dismissedPercentage = (dismissed / total) * 100
const sentPercentage = (sent / total) * 100
function StackedBar({ segments }: { segments: StackedBarSegment[] }): JSX.Element {
const total = segments.reduce((sum, segment) => sum + segment.count, 0)
let accumulatedPercentage = 0

return (
<>
{total > 0 && (
<div>
<div className="relative w-full mx-auto h-10 mb-4">
{[
{
count: seen,
label: 'Unanswered',
classes: `rounded-l ${dismissed === 0 && sent === 0 ? 'rounded-r' : ''}`,
style: { backgroundColor: '#1D4AFF', width: `${seenPercentage}%` },
},
{
count: dismissed,
label: 'Dismissed',
classes: `${seen === 0 ? 'rounded-l' : ''} ${sent === 0 ? 'rounded-r' : ''}`,
style: {
backgroundColor: '#E3A506',
width: `${dismissedPercentage}%`,
left: `${seenPercentage}%`,
},
},
{
count: sent,
label: 'Sent',
classes: `rounded-r ${seen === 0 && dismissed === 0 ? 'rounded-l' : ''}`,
style: {
backgroundColor: '#529B08',
width: `${sentPercentage}%`,
left: `${seenPercentage + dismissedPercentage}%`,
},
},
].map(({ count, label, classes, style }) => (
<Tooltip
key={`survey-summary-chart-${label}`}
title={`${label} surveys: ${count}`}
delayMs={0}
placement="top"
>
<div
className={`h-10 text-white text-center absolute cursor-pointer ${classes}`}
// eslint-disable-next-line react/forbid-dom-props
style={style}
{segments.map(({ count, label, color }, index) => {
const percentage = (count / total) * 100
const left = accumulatedPercentage
accumulatedPercentage += percentage

const isFirst = index === 0
const isLast = index === segments.length - 1
const isOnly = segments.length === 1

return (
<Tooltip
key={`stacked-bar-${label}`}
title={`${label}: ${count} (${percentage.toFixed(1)}%)`}
delayMs={0}
placement="top"
>
<span className="inline-flex font-semibold max-w-full px-1 truncate leading-10">
{formatCount(count, total)}
</span>
</div>
</Tooltip>
))}
<div
className={clsx(
'h-10 text-white text-center absolute cursor-pointer',
isFirst || isOnly ? 'rounded-l' : '',
isLast || isOnly ? 'rounded-r' : ''
)}
// eslint-disable-next-line react/forbid-dom-props
style={{
backgroundColor: color,
width: `${percentage}%`,
left: `${left}%`,
}}
>
<span className="inline-flex font-semibold max-w-full px-1 truncate leading-10">
{formatCount(count, total)}
</span>
</div>
</Tooltip>
)
})}
</div>
<div className="w-full flex justify-center">
<div className="flex items-center">
{[
{ count: seen, label: 'Unanswered', style: { backgroundColor: '#1D4AFF' } },
{ count: dismissed, label: 'Dismissed', style: { backgroundColor: '#E3A506' } },
{ count: sent, label: 'Submitted', style: { backgroundColor: '#529B08' } },
].map(
({ count, label, style }) =>
{segments.map(
({ count, label, color }) =>
count > 0 && (
<div key={`survey-summary-legend-${label}`} className="flex items-center mr-6">
{/* eslint-disable-next-line react/forbid-dom-props */}
<div className="w-3 h-3 rounded-full mr-2" style={style} />
<div key={`stacked-bar-legend-${label}`} className="flex items-center mr-6">
{}
<div
className="w-3 h-3 rounded-full mr-2"
// eslint-disable-next-line react/forbid-dom-props
style={{ backgroundColor: color }}
/>
<span className="font-semibold text-secondary">{`${label} (${(
(count / total) *
100
Expand All @@ -161,6 +133,50 @@ export function UsersStackedBar({ surveyUserStats }: { surveyUserStats: SurveyUs
)
}

export function UsersCount({ surveyUserStats }: { surveyUserStats: SurveyUserStats }): JSX.Element {
const { seen, dismissed, sent } = surveyUserStats
const total = seen + dismissed + sent
const labelTotal = total === 1 ? 'Unique user shown' : 'Unique users shown'
const labelSent = sent === 1 ? 'Response sent' : 'Responses sent'

return (
<div className="inline-flex mb-4">
<div>
<div className="text-4xl font-bold">{humanFriendlyNumber(total)}</div>
<div className="font-semibold text-secondary">{labelTotal}</div>
</div>
{sent > 0 && (
<div className="ml-10">
<div className="text-4xl font-bold">{humanFriendlyNumber(sent)}</div>
<div className="font-semibold text-secondary">{labelSent}</div>
</div>
)}
</div>
)
}

export function UsersStackedBar({ surveyUserStats }: { surveyUserStats: SurveyUserStats }): JSX.Element {
const { seen, dismissed, sent } = surveyUserStats

const segments: StackedBarSegment[] = [
{ count: seen, label: 'Unanswered', color: '#1D4AFF' },
{ count: dismissed, label: 'Dismissed', color: '#E3A506' },
{ count: sent, label: 'Submitted', color: '#529B08' },
]

return <StackedBar segments={segments} />
}

export function NPSStackedBar({ npsBreakdown }: { npsBreakdown: NPSBreakdown }): JSX.Element {
const segments: StackedBarSegment[] = [
{ count: npsBreakdown.detractors, label: 'Detractors', color: 'var(--danger)' },
{ count: npsBreakdown.passives, label: 'Passives', color: 'var(--warning)' },
{ count: npsBreakdown.promoters, label: 'Promoters', color: 'var(--success)' },
]

return <StackedBar segments={segments} />
}

export function Summary({
surveyUserStats,
surveyUserStatsLoading,
Expand Down

0 comments on commit 4ec9698

Please sign in to comment.