Skip to content

Commit

Permalink
feat(surveys): add Rating question results viz (#17886)
Browse files Browse the repository at this point in the history
  • Loading branch information
jurajmajerik authored and daibhin committed Oct 23, 2023
1 parent 1da9e8b commit d327937
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 33 deletions.
4 changes: 3 additions & 1 deletion frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export interface LineGraphProps {
showValueOnSeries?: boolean | null
showPercentStackView?: boolean | null
supportsPercentStackView?: boolean
hideAnnotations?: boolean
}

export const LineGraph = (props: LineGraphProps): JSX.Element => {
Expand Down Expand Up @@ -245,6 +246,7 @@ export function LineGraph_({
showValueOnSeries,
showPercentStackView,
supportsPercentStackView,
hideAnnotations,
}: LineGraphProps): JSX.Element {
let datasets = _datasets

Expand Down Expand Up @@ -272,7 +274,7 @@ export function LineGraph_({
const isBar = [GraphType.Bar, GraphType.HorizontalBar, GraphType.Histogram].includes(type)
const isBackgroundBasedGraphType = [GraphType.Bar, GraphType.HorizontalBar].includes(type)
const isPercentStackView = !!supportsPercentStackView && !!showPercentStackView
const showAnnotations = isTrends && !isHorizontal
const showAnnotations = isTrends && !isHorizontal && !hideAnnotations
const shouldAutoResize = isHorizontal && !inCardView

// Remove tooltip element on unmount
Expand Down
53 changes: 36 additions & 17 deletions frontend/src/scenes/surveys/SurveyView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { dayjs } from 'lib/dayjs'
import { defaultSurveyAppearance, SURVEY_EVENT_NAME } from './constants'
import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { Summary } from './surveyViewViz'
import { RatingQuestionBarChart, Summary } from './surveyViewViz'

export function SurveyView({ id }: { id: string }): JSX.Element {
const { survey, surveyLoading, surveyPlugin, showSurveyAppWarning } = useValues(surveyLogic)
Expand Down Expand Up @@ -264,16 +264,33 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool
currentQuestionIndexAndType,
surveyUserStats,
surveyUserStatsLoading,
surveyRatingResults,
surveyRatingResultsReady,
} = useValues(surveyLogic)
const { setCurrentQuestionIndexAndType } = useActions(surveyLogic)
const { featureFlags } = useValues(featureFlagLogic)

return (
<>
{featureFlags[FEATURE_FLAGS.SURVEYS_RESULTS_VISUALIZATIONS] && (
<Summary surveyUserStatsLoading={surveyUserStatsLoading} surveyUserStats={surveyUserStats} />
<>
<Summary surveyUserStatsLoading={surveyUserStatsLoading} surveyUserStats={surveyUserStats} />
{survey.questions.map((question, i) => {
if (question.type === 'rating') {
return (
<RatingQuestionBarChart
key={`survey-q-${i}`}
surveyRatingResults={surveyRatingResults}
surveyRatingResultsReady={surveyRatingResultsReady}
questionIndex={i}
question={question}
/>
)
}
})}
</>
)}
{surveyMetricsQueries && (
{surveyMetricsQueries && !featureFlags[FEATURE_FLAGS.SURVEYS_RESULTS_VISUALIZATIONS] && (
<div className="flex flex-row gap-4 mb-4">
<div className="flex-1">
<Query query={surveyMetricsQueries.surveysShown} />
Expand All @@ -283,7 +300,7 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool
</div>
</div>
)}
{survey.questions.length > 1 && (
{survey.questions.length > 1 && !featureFlags[FEATURE_FLAGS.SURVEYS_RESULTS_VISUALIZATIONS] && (
<div className="mb-4 max-w-80">
<LemonSelect
dropdownMatchSelectWidth
Expand All @@ -301,19 +318,21 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool
/>
</div>
)}
{currentQuestionIndexAndType.type === SurveyQuestionType.Rating && (
<div className="mb-4">
<Query query={surveyRatingQuery} />
{featureFlags[FEATURE_FLAGS.SURVEY_NPS_RESULTS] &&
(survey.questions[currentQuestionIndexAndType.idx] as RatingSurveyQuestion).scale === 10 && (
<>
<LemonDivider className="my-4" />
<h2>NPS Score</h2>
<SurveyNPSResults survey={survey as Survey} />
</>
)}
</div>
)}
{currentQuestionIndexAndType.type === SurveyQuestionType.Rating &&
!featureFlags[FEATURE_FLAGS.SURVEYS_RESULTS_VISUALIZATIONS] && (
<div className="mb-4">
<Query query={surveyRatingQuery} />
{featureFlags[FEATURE_FLAGS.SURVEY_NPS_RESULTS] &&
(survey.questions[currentQuestionIndexAndType.idx] as RatingSurveyQuestion).scale ===
10 && (
<>
<LemonDivider className="my-4" />
<h2>NPS Score</h2>
<SurveyNPSResults survey={survey as Survey} />
</>
)}
</div>
)}
{(currentQuestionIndexAndType.type === SurveyQuestionType.SingleChoice ||
currentQuestionIndexAndType.type === SurveyQuestionType.MultipleChoice) && (
<div className="mb-4">
Expand Down
57 changes: 57 additions & 0 deletions frontend/src/scenes/surveys/surveyLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
SurveyQuestionType,
SurveyType,
SurveyUrlMatchType,
RatingSurveyQuestion,
} from '~/types'
import type { surveyLogicType } from './surveyLogicType'
import { DataTableNode, InsightVizNode, HogQLQuery, NodeKind } from '~/queries/schema'
Expand Down Expand Up @@ -51,6 +52,14 @@ export interface SurveyUserStats {
sent: number
}

export interface SurveyRatingResults {
[key: string]: number[]
}

export interface SurveyRatingResultsReady {
[key: string]: boolean
}

export const surveyLogic = kea<surveyLogicType>([
props({} as SurveyLogicProps),
key(({ id }) => id),
Expand Down Expand Up @@ -172,6 +181,46 @@ export const surveyLogic = kea<surveyLogicType>([
}
},
},
surveyRatingResults: {
loadSurveyRatingResults: async ({
questionIndex,
question,
}: {
questionIndex: number
question: RatingSurveyQuestion
}): Promise<{ [key: string]: number[] }> => {
const { survey } = values
const startDate = dayjs((survey as Survey).created_at).format('YYYY-MM-DD')
const endDate = survey.end_date
? dayjs(survey.end_date).add(1, 'day').format('YYYY-MM-DD')
: dayjs().add(1, 'day').format('YYYY-MM-DD')

const surveyResponseField =
questionIndex === 0 ? '$survey_response' : `$survey_response_${questionIndex}`

const query: HogQLQuery = {
kind: NodeKind.HogQLQuery,
query: `
SELECT properties.${surveyResponseField} AS survey_response, COUNT(survey_response)
FROM events
WHERE event = 'survey sent'
AND properties.$survey_id = '${props.id}'
AND timestamp >= '${startDate}'
AND timestamp <= '${endDate}'
GROUP BY survey_response
`,
}
const responseJSON = await api.query(query)
const { results } = responseJSON

const resultArr = new Array(question.scale).fill(0)
results?.forEach(([value, count]) => {
resultArr[value - 1] = count
})

return { ...values.surveyRatingResults, [`question_${questionIndex}`]: resultArr }
},
},
})),
listeners(({ actions }) => ({
createSurveySuccess: ({ survey }) => {
Expand Down Expand Up @@ -261,6 +310,14 @@ export const surveyLogic = kea<surveyLogicType>([
setCurrentQuestionIndexAndType: (_, { idx, type }) => ({ idx, type }),
},
],
surveyRatingResultsReady: [
{},
{
loadSurveyRatingResultsSuccess: (state, { payload }) => {
return { ...state, [`question_${payload?.questionIndex}`]: true }
},
},
],
writingHTMLDescription: [
false,
{
Expand Down
106 changes: 91 additions & 15 deletions frontend/src/scenes/surveys/surveyViewViz.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { LemonTable } from '@posthog/lemon-ui'
import { SurveyUserStats } from './surveyLogic'
import { surveyLogic, SurveyRatingResults, SurveyRatingResultsReady, SurveyUserStats } from './surveyLogic'
import { useActions, BindLogic } from 'kea'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { GraphType } from '~/types'
import { LineGraph } from 'scenes/insights/views/LineGraph/LineGraph'
import { insightLogic } from 'scenes/insights/insightLogic'
import { InsightLogicProps, RatingSurveyQuestion } from '~/types'
import { useEffect } from 'react'

const insightProps: InsightLogicProps = {
dashboardItemId: `new-survey`,
}

const formatCount = (count: number, total: number): string => {
if ((count / total) * 100 < 3) {
Expand Down Expand Up @@ -48,8 +58,8 @@ export function UsersStackedBar({ surveyUserStats }: { surveyUserStats: SurveyUs
{
count: seen,
label: 'Viewed',
classes: `bg-primary rounded-l ${dismissed === 0 && sent === 0 ? 'rounded-r' : ''}`,
style: { width: `${seenPercentage}%` },
classes: `rounded-l ${dismissed === 0 && sent === 0 ? 'rounded-r' : ''}`,
style: { backgroundColor: '#1D4AFF', width: `${seenPercentage}%` },
},
{
count: dismissed,
Expand Down Expand Up @@ -92,12 +102,12 @@ export function UsersStackedBar({ surveyUserStats }: { surveyUserStats: SurveyUs
<div className="w-full flex justify-center">
<div className="flex items-center">
{[
{ count: seen, label: 'Viewed', color: 'bg-primary' },
{ count: dismissed, label: 'Dismissed', color: 'bg-warning' },
{ count: sent, label: 'Submitted', color: 'bg-success' },
].map(({ count, label, color }) => (
{ count: seen, label: 'Viewed', style: { backgroundColor: '#1D4AFF' } },
{ count: dismissed, label: 'Dismissed', style: { backgroundColor: '#E3A506' } },
{ count: sent, label: 'Submitted', style: { backgroundColor: '#529B08' } },
].map(({ count, label, style }) => (
<div key={`survey-summary-legend-${label}`} className="flex items-center mr-6">
<div className={`w-3 h-3 rounded-full mr-2 ${color}`} />
<div className="w-3 h-3 rounded-full mr-2" style={style} />
<span className="font-semibold text-muted-alt">{`${label} (${(
(count / total) *
100
Expand All @@ -119,20 +129,86 @@ export function Summary({
surveyUserStats: SurveyUserStats
surveyUserStatsLoading: boolean
}): JSX.Element {
if (!surveyUserStats) {
return <></>
}

return (
<div className="mb-4">
<div className="mb-4 mt-2">
{surveyUserStatsLoading ? (
<LemonTable dataSource={[]} columns={[]} loading={true} />
) : (
<>
<UsersCount surveyUserStats={surveyUserStats} />
<UsersStackedBar surveyUserStats={surveyUserStats} />
{!surveyUserStats ? null : (
<>
<UsersCount surveyUserStats={surveyUserStats} />
<UsersStackedBar surveyUserStats={surveyUserStats} />
</>
)}
</>
)}
</div>
)
}

export function RatingQuestionBarChart({
questionIndex,
question,
surveyRatingResults,
surveyRatingResultsReady,
}: {
questionIndex: number
question: RatingSurveyQuestion
surveyRatingResults: SurveyRatingResults
surveyRatingResultsReady: SurveyRatingResultsReady
}): JSX.Element {
const { loadSurveyRatingResults } = useActions(surveyLogic)

useEffect(() => {
loadSurveyRatingResults({ questionIndex, question })
}, [question])

return (
<div className="mb-4">
{!surveyRatingResultsReady[`question_${questionIndex}`] ? (
<LemonTable dataSource={[]} columns={[]} loading={true} />
) : (
<div className="mb-8">
<div className="font-semibold text-muted-alt">{`1-${question.scale} rating`}</div>
<div className="text-xl font-bold mb-2">{question.question}</div>
<div className=" h-50 border rounded pt-6 pb-2 px-2">
<div className="relative h-full w-full">
<BindLogic logic={insightLogic} props={insightProps}>
<LineGraph
labelGroupType={1}
data-attr="survey-rating"
type={GraphType.Bar}
hideAnnotations={true}
formula="-"
tooltip={{
showHeader: false,
hideColorCol: true,
}}
datasets={[
{
id: 1,
label: 'Number of responses',
barPercentage: 0.7,
minBarLength: 2,
data: surveyRatingResults[`question_${questionIndex}`],
backgroundColor: '#1d4aff',
hoverBackgroundColor: '#1d4aff',
},
]}
labels={Array.from({ length: question.scale }, (_, i) => (i + 1).toString()).map(
(n) => n
)}
/>
</BindLogic>
</div>
</div>
<div className="flex flex-row justify-between mt-1">
<div className="text-muted-alt">{question.lowerBoundLabel}</div>
<div className="text-muted-alt">{question.upperBoundLabel}</div>
</div>
</div>
)}
</div>
)
}

0 comments on commit d327937

Please sign in to comment.