Skip to content

Commit

Permalink
feat(surveys): Single Choice question results (#17923)
Browse files Browse the repository at this point in the history
  • Loading branch information
jurajmajerik authored Oct 12, 2023
1 parent 3c598e1 commit eaef6cd
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 21 deletions.
16 changes: 13 additions & 3 deletions frontend/src/scenes/surveys/SurveyView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,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 { RatingQuestionBarChart, Summary } from './surveyViewViz'
import { Summary, RatingQuestionBarChart, SingleChoiceQuestionPieChart } from './surveyViewViz'

export function SurveyView({ id }: { id: string }): JSX.Element {
const { survey, surveyLoading } = useValues(surveyLogic)
Expand Down Expand Up @@ -255,6 +255,8 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool
surveyUserStatsLoading,
surveyRatingResults,
surveyRatingResultsReady,
surveySingleChoiceResults,
surveySingleChoiceResultsReady,
} = useValues(surveyLogic)
const { setCurrentQuestionIndexAndType } = useActions(surveyLogic)
const { featureFlags } = useValues(featureFlagLogic)
Expand All @@ -265,14 +267,22 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool
<>
<Summary surveyUserStatsLoading={surveyUserStatsLoading} surveyUserStats={surveyUserStats} />
{survey.questions.map((question, i) => {
if (question.type === 'rating') {
if (question.type === SurveyQuestionType.Rating) {
return (
<RatingQuestionBarChart
key={`survey-q-${i}`}
surveyRatingResults={surveyRatingResults}
surveyRatingResultsReady={surveyRatingResultsReady}
questionIndex={i}
question={question}
/>
)
} else if (question.type === SurveyQuestionType.SingleChoice) {
return (
<SingleChoiceQuestionPieChart
key={`survey-q-${i}`}
surveySingleChoiceResults={surveySingleChoiceResults}
surveySingleChoiceResultsReady={surveySingleChoiceResultsReady}
questionIndex={i}
/>
)
}
Expand Down
74 changes: 66 additions & 8 deletions frontend/src/scenes/surveys/surveyLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import {
ChartDisplayType,
PropertyFilterType,
PropertyOperator,
RatingSurveyQuestion,
Survey,
SurveyQuestionBase,
SurveyQuestionType,
SurveyUrlMatchType,
RatingSurveyQuestion,
} from '~/types'
import type { surveyLogicType } from './surveyLogicType'
import { DataTableNode, InsightVizNode, HogQLQuery, NodeKind } from '~/queries/schema'
Expand Down Expand Up @@ -49,10 +49,18 @@ export interface SurveyUserStats {
}

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

export interface SurveySingleChoiceResults {
[key: number]: {
labels: string[]
data: number[]
total: number
}
}

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

Expand Down Expand Up @@ -175,12 +183,11 @@ export const surveyLogic = kea<surveyLogicType>([
surveyRatingResults: {
loadSurveyRatingResults: async ({
questionIndex,
question,
}: {
questionIndex: number
question: RatingSurveyQuestion
}): Promise<{ [key: string]: number[] }> => {
}): Promise<SurveyRatingResults> => {
const { survey } = values
const question = values.survey.questions[questionIndex] as RatingSurveyQuestion
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')
Expand Down Expand Up @@ -209,7 +216,44 @@ export const surveyLogic = kea<surveyLogicType>([
resultArr[value - 1] = count
})

return { ...values.surveyRatingResults, [`question_${questionIndex}`]: resultArr }
return { ...values.surveyRatingResults, [questionIndex]: resultArr }
},
},
surveySingleChoiceResults: {
loadSurveySingleChoiceResults: async ({
questionIndex,
}: {
questionIndex: number
}): Promise<SurveySingleChoiceResults> => {
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 labels = results?.map((r) => r[0])
const data = results?.map((r) => r[1])
const total = data?.reduce((a, b) => a + b, 0)

return { ...values.surveySingleChoiceResults, [questionIndex]: { labels, data, total } }
},
},
})),
Expand Down Expand Up @@ -305,7 +349,21 @@ export const surveyLogic = kea<surveyLogicType>([
{},
{
loadSurveyRatingResultsSuccess: (state, { payload }) => {
return { ...state, [`question_${payload?.questionIndex}`]: true }
if (!payload || !payload.hasOwnProperty('questionIndex')) {
return { ...state }
}
return { ...state, [payload.questionIndex]: true }
},
},
],
surveySingleChoiceResultsReady: [
{},
{
loadSurveySingleChoiceResultsSuccess: (state, { payload }) => {
if (!payload || !payload.hasOwnProperty('questionIndex')) {
return { ...state }
}
return { ...state, [payload.questionIndex]: true }
},
},
],
Expand Down
147 changes: 137 additions & 10 deletions frontend/src/scenes/surveys/surveyViewViz.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { LemonTable } from '@posthog/lemon-ui'
import { surveyLogic, SurveyRatingResults, SurveyRatingResultsReady, SurveyUserStats } from './surveyLogic'
import { useActions, BindLogic } from 'kea'
import {
surveyLogic,
SurveyRatingResults,
QuestionResultsReady,
SurveySingleChoiceResults,
SurveyUserStats,
} from './surveyLogic'
import { useActions, useValues, BindLogic } from 'kea'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { GraphType } from '~/types'
import { LineGraph } from 'scenes/insights/views/LineGraph/LineGraph'
import { PieChart } from 'scenes/insights/views/LineGraph/PieChart'
import { insightLogic } from 'scenes/insights/insightLogic'
import { InsightLogicProps, RatingSurveyQuestion } from '~/types'
import { InsightLogicProps, SurveyQuestionType } from '~/types'
import { useEffect } from 'react'

const insightProps: InsightLogicProps = {
Expand Down Expand Up @@ -149,24 +156,28 @@ export function Summary({

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

const question = survey.questions[questionIndex]
if (question.type !== SurveyQuestionType.Rating) {
throw new Error(`Question type must be ${SurveyQuestionType.Rating}`)
}

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

return (
<div className="mb-4">
{!surveyRatingResultsReady[`question_${questionIndex}`] ? (
{!surveyRatingResultsReady[questionIndex] ? (
<LemonTable dataSource={[]} columns={[]} loading={true} />
) : (
<div className="mb-8">
Expand All @@ -191,7 +202,7 @@ export function RatingQuestionBarChart({
label: 'Number of responses',
barPercentage: 0.7,
minBarLength: 2,
data: surveyRatingResults[`question_${questionIndex}`],
data: surveyRatingResults[questionIndex],
backgroundColor: '#1d4aff',
hoverBackgroundColor: '#1d4aff',
},
Expand All @@ -212,3 +223,119 @@ export function RatingQuestionBarChart({
</div>
)
}

export function SingleChoiceQuestionPieChart({
questionIndex,
surveySingleChoiceResults,
surveySingleChoiceResultsReady,
}: {
questionIndex: number
surveySingleChoiceResults: SurveySingleChoiceResults
surveySingleChoiceResultsReady: QuestionResultsReady
}): JSX.Element {
const { loadSurveySingleChoiceResults } = useActions(surveyLogic)
const { survey } = useValues(surveyLogic)

const question = survey.questions[questionIndex]
if (question.type !== SurveyQuestionType.SingleChoice) {
throw new Error(`Question type must be ${SurveyQuestionType.SingleChoice}`)
}

// Insights colors
// TODO: make available in Tailwind
const colors = [
'#1D4BFF',
'#CD0F74',
'#43827E',
'#621DA6',
'#F04F58',
'#539B0A',
'#E3A605',
'#0476FB',
'#36416B',
'#41CBC3',
'#A46FFF',
'#FE729E',
'#CE1175',
'#B64B01',
]

useEffect(() => {
loadSurveySingleChoiceResults({ questionIndex })
}, [questionIndex])

return (
<div className="mb-4">
{!surveySingleChoiceResultsReady[questionIndex] ? (
<LemonTable dataSource={[]} columns={[]} loading={true} />
) : (
<div className="mb-8">
<div className="font-semibold text-muted-alt">Single choice</div>
<div className="text-xl font-bold mb-2">{question.question}</div>
<div className="h-80 border rounded pt-4 pb-2 flex">
<div className="relative h-full w-80">
<BindLogic logic={insightLogic} props={insightProps}>
<PieChart
labelGroupType={1}
data-attr="survey-rating"
type={GraphType.Pie}
hideAnnotations={true}
formula="-"
tooltip={{
showHeader: false,
hideColorCol: true,
}}
datasets={[
{
id: 1,
data: surveySingleChoiceResults[questionIndex].data,
labels: surveySingleChoiceResults[questionIndex].labels,
backgroundColor: surveySingleChoiceResults[questionIndex].labels.map(
(_: string, i: number) => colors[i % colors.length]
),
},
]}
labels={surveySingleChoiceResults[questionIndex].labels}
/>
</BindLogic>
</div>
<div
className={`grid h-full pl-4 py-${(() => {
const dataLength = surveySingleChoiceResults[questionIndex].data.length
if (dataLength < 5) {
return 20
} else if (dataLength < 7) {
return 15
} else if (dataLength < 10) {
return 10
} else {
return 5
}
})()} grid-cols-${Math.ceil(surveySingleChoiceResults[questionIndex].data.length / 10)}`}
>
{surveySingleChoiceResults[questionIndex].data.map((count: number, i: number) => {
const { total, labels } = surveySingleChoiceResults[questionIndex]
const percentage = ((count / total) * 100).toFixed(1)

return (
<div
key={`single-choice-legend-${questionIndex}-${i}`}
className="flex items-center mr-6"
>
<div
className="w-3 h-3 rounded-full mr-2"
style={{ backgroundColor: colors[i % colors.length] }}
/>
<span className="font-semibold text-muted-alt max-w-30 truncate">{`${labels[i]}`}</span>
<span className="font-bold ml-1 truncate">{` ${percentage}% `}</span>
<span className="font-semibold text-muted-alt ml-1 truncate">{`(${count})`}</span>
</div>
)
})}
</div>
</div>
</div>
)}
</div>
)
}

0 comments on commit eaef6cd

Please sign in to comment.