From eaef6cdb5cf068949fa6ea44e2e936d323bea2cc Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Thu, 12 Oct 2023 12:53:41 +0200 Subject: [PATCH] feat(surveys): Single Choice question results (#17923) --- frontend/src/scenes/surveys/SurveyView.tsx | 16 +- frontend/src/scenes/surveys/surveyLogic.tsx | 74 ++++++++- frontend/src/scenes/surveys/surveyViewViz.tsx | 147 ++++++++++++++++-- 3 files changed, 216 insertions(+), 21 deletions(-) diff --git a/frontend/src/scenes/surveys/SurveyView.tsx b/frontend/src/scenes/surveys/SurveyView.tsx index 63ede94353f99..a3a2a9c5b7e86 100644 --- a/frontend/src/scenes/surveys/SurveyView.tsx +++ b/frontend/src/scenes/surveys/SurveyView.tsx @@ -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) @@ -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) @@ -265,14 +267,22 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool <> {survey.questions.map((question, i) => { - if (question.type === 'rating') { + if (question.type === SurveyQuestionType.Rating) { return ( + ) + } else if (question.type === SurveyQuestionType.SingleChoice) { + return ( + ) } diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index fc03107471aa6..4d816f2e19607 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -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' @@ -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 } @@ -175,12 +183,11 @@ export const surveyLogic = kea([ surveyRatingResults: { loadSurveyRatingResults: async ({ questionIndex, - question, }: { questionIndex: number - question: RatingSurveyQuestion - }): Promise<{ [key: string]: number[] }> => { + }): Promise => { 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') @@ -209,7 +216,44 @@ export const surveyLogic = kea([ resultArr[value - 1] = count }) - return { ...values.surveyRatingResults, [`question_${questionIndex}`]: resultArr } + return { ...values.surveyRatingResults, [questionIndex]: resultArr } + }, + }, + surveySingleChoiceResults: { + loadSurveySingleChoiceResults: async ({ + questionIndex, + }: { + questionIndex: number + }): Promise => { + 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 } } }, }, })), @@ -305,7 +349,21 @@ export const surveyLogic = kea([ {}, { 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 } }, }, ], diff --git a/frontend/src/scenes/surveys/surveyViewViz.tsx b/frontend/src/scenes/surveys/surveyViewViz.tsx index 460536bb35b6d..278a65b7a37b0 100644 --- a/frontend/src/scenes/surveys/surveyViewViz.tsx +++ b/frontend/src/scenes/surveys/surveyViewViz.tsx @@ -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 = { @@ -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 (
- {!surveyRatingResultsReady[`question_${questionIndex}`] ? ( + {!surveyRatingResultsReady[questionIndex] ? ( ) : (
@@ -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', }, @@ -212,3 +223,119 @@ export function RatingQuestionBarChart({
) } + +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 ( +
+ {!surveySingleChoiceResultsReady[questionIndex] ? ( + + ) : ( +
+
Single choice
+
{question.question}
+
+
+ + colors[i % colors.length] + ), + }, + ]} + labels={surveySingleChoiceResults[questionIndex].labels} + /> + +
+
{ + 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 ( +
+
+ {`${labels[i]}`} + {` ${percentage}% `} + {`(${count})`} +
+ ) + })} +
+
+
+ )} +
+ ) +}