diff --git a/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx b/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx
index 15a079aee48fb..666e7894fdf3b 100644
--- a/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx
+++ b/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx
@@ -207,6 +207,7 @@ export interface LineGraphProps {
showPersonsModal?: boolean
tooltip?: TooltipConfig
inCardView?: boolean
+ inSurveyView?: boolean
isArea?: boolean
incompletenessOffsetFromEnd?: number // Number of data points at end of dataset to replace with a dotted line. Only used in line graphs.
labelGroupType: number | 'people' | 'none'
@@ -217,6 +218,8 @@ export interface LineGraphProps {
showPercentStackView?: boolean | null
supportsPercentStackView?: boolean
hideAnnotations?: boolean
+ hideXAxis?: boolean
+ hideYAxis?: boolean
}
export const LineGraph = (props: LineGraphProps): JSX.Element => {
@@ -238,6 +241,7 @@ export function LineGraph_({
showPersonsModal = true,
compare = false,
inCardView,
+ inSurveyView,
isArea = false,
incompletenessOffsetFromEnd = -1,
tooltip: tooltipConfig,
@@ -248,6 +252,8 @@ export function LineGraph_({
showPercentStackView,
supportsPercentStackView,
hideAnnotations,
+ hideXAxis,
+ hideYAxis,
}: LineGraphProps): JSX.Element {
let datasets = _datasets
@@ -566,6 +572,7 @@ export function LineGraph_({
if (type === GraphType.Bar) {
options.scales = {
x: {
+ display: !hideXAxis,
beginAtZero: true,
stacked: true,
ticks: {
@@ -575,9 +582,11 @@ export function LineGraph_({
grid: gridOptions,
},
y: {
+ display: !hideYAxis,
beginAtZero: true,
stacked: true,
ticks: {
+ display: !hideYAxis,
...tickOptions,
precision,
callback: (value) => {
@@ -590,8 +599,8 @@ export function LineGraph_({
} else if (type === GraphType.Line) {
options.scales = {
x: {
+ display: !hideXAxis,
beginAtZero: true,
- display: true,
ticks: tickOptions,
grid: {
...gridOptions,
@@ -600,10 +609,11 @@ export function LineGraph_({
},
},
y: {
+ display: !hideYAxis,
beginAtZero: true,
- display: true,
stacked: showPercentStackView || isArea,
ticks: {
+ display: !hideYAxis,
...tickOptions,
precision,
callback: (value) => {
@@ -616,9 +626,10 @@ export function LineGraph_({
} else if (isHorizontal) {
options.scales = {
x: {
+ display: !hideXAxis,
beginAtZero: true,
- display: true,
ticks: {
+ display: !hideXAxis,
...tickOptions,
precision,
callback: (value) => {
@@ -628,8 +639,15 @@ export function LineGraph_({
grid: gridOptions,
},
y: {
+ display: true,
beforeFit: (scale) => {
- if (shouldAutoResize) {
+ if (inSurveyView) {
+ const ROW_HEIGHT = 60
+ const dynamicHeight = scale.ticks.length * ROW_HEIGHT
+ const height = dynamicHeight
+ const parentNode: any = scale.chart?.canvas?.parentNode
+ parentNode.style.height = `${height}px`
+ } else if (shouldAutoResize) {
// automatically resize the chart container to fit the number of rows
const MIN_HEIGHT = 575
const ROW_HEIGHT = 16
@@ -656,7 +674,10 @@ export function LineGraph_({
return labelDescriptors.join(' - ')
},
},
- grid: gridOptions,
+ grid: {
+ ...gridOptions,
+ display: !inSurveyView,
+ },
},
}
options.indexAxis = 'y'
diff --git a/frontend/src/scenes/surveys/SurveyView.tsx b/frontend/src/scenes/surveys/SurveyView.tsx
index 9805fb361babc..0acde15f1df26 100644
--- a/frontend/src/scenes/surveys/SurveyView.tsx
+++ b/frontend/src/scenes/surveys/SurveyView.tsx
@@ -28,7 +28,12 @@ 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, RatingQuestionBarChart, SingleChoiceQuestionPieChart } from './surveyViewViz'
+import {
+ Summary,
+ RatingQuestionBarChart,
+ SingleChoiceQuestionPieChart,
+ MultipleChoiceQuestionBarChart,
+} from './surveyViewViz'
export function SurveyView({ id }: { id: string }): JSX.Element {
const { survey, surveyLoading } = useValues(surveyLogic)
@@ -263,6 +268,8 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool
surveyRatingResultsReady,
surveySingleChoiceResults,
surveySingleChoiceResultsReady,
+ surveyMultipleChoiceResults,
+ surveyMultipleChoiceResultsReady,
} = useValues(surveyLogic)
const { setCurrentQuestionIndexAndType } = useActions(surveyLogic)
const { featureFlags } = useValues(featureFlagLogic)
@@ -291,6 +298,15 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool
questionIndex={i}
/>
)
+ } else if (question.type === SurveyQuestionType.MultipleChoice) {
+ return (
+
+ )
}
})}
>
@@ -337,11 +353,12 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool
)}
{(currentQuestionIndexAndType.type === SurveyQuestionType.SingleChoice ||
- currentQuestionIndexAndType.type === SurveyQuestionType.MultipleChoice) && (
-
-
-
- )}
+ currentQuestionIndexAndType.type === SurveyQuestionType.MultipleChoice) &&
+ !featureFlags[FEATURE_FLAGS.SURVEYS_RESULTS_VISUALIZATIONS] && (
+
+
+
+ )}
{!disableEventsTable && (surveyLoading ? : )}
>
)
diff --git a/frontend/src/scenes/surveys/surveyLogic.test.ts b/frontend/src/scenes/surveys/surveyLogic.test.ts
new file mode 100644
index 0000000000000..0eb577d4d94a7
--- /dev/null
+++ b/frontend/src/scenes/surveys/surveyLogic.test.ts
@@ -0,0 +1,99 @@
+import { initKeaTests } from '~/test/init'
+import { surveyLogic } from 'scenes/surveys/surveyLogic'
+import { expectLogic } from 'kea-test-utils'
+import { useMocks } from '~/mocks/jest'
+import { Survey, SurveyQuestionType, SurveyType } from '~/types'
+
+const SURVEY: Survey = {
+ id: '018b22a3-09b1-0000-2f5b-1bd8352ceec9',
+ name: 'Multiple Choice survey',
+ description: '',
+ type: SurveyType.Popover,
+ linked_flag: null,
+ linked_flag_id: null,
+ targeting_flag: null,
+ questions: [
+ {
+ type: SurveyQuestionType.MultipleChoice,
+ choices: ['Tutorials', 'Customer case studies', 'Product announcements'],
+ question: 'Which types of content would you like to see more of?',
+ description: '',
+ },
+ ],
+ conditions: null,
+ appearance: {
+ position: 'right',
+ whiteLabel: false,
+ borderColor: '#c9c6c6',
+ placeholder: '',
+ backgroundColor: '#eeeded',
+ submitButtonText: 'Submit',
+ ratingButtonColor: 'white',
+ submitButtonColor: 'black',
+ thankYouMessageHeader: 'Thank you for your feedback!',
+ displayThankYouMessage: true,
+ ratingButtonActiveColor: 'black',
+ },
+ created_at: '2023-10-12T06:46:32.113745Z',
+ created_by: {
+ id: 1,
+ uuid: '018aa8a6-10e8-0000-dba2-0e956f7bae38',
+ distinct_id: 'TGqg9Cn4jLkj9X87oXni9ZPBD6VbOxMtGV1GfJeB5LO',
+ first_name: 'test',
+ email: 'test@posthog.com',
+ is_email_verified: false,
+ },
+ start_date: '2023-10-12T06:46:34.482000Z',
+ end_date: null,
+ archived: false,
+ targeting_flag_filters: undefined,
+}
+
+describe('survey logic', () => {
+ let logic: ReturnType
+
+ beforeEach(() => {
+ initKeaTests()
+ logic = surveyLogic({ id: 'new' })
+ logic.mount()
+
+ useMocks({
+ get: {
+ '/api/projects/:team/surveys/': () => [200, { results: [] }],
+ '/api/projects/:team/surveys/responses_count': () => [200, {}],
+ },
+ post: {
+ '/api/projects/:team/query/': () => [
+ 200,
+ {
+ results: [
+ [336, '"Tutorials"'],
+ [312, '"Customer case studies"'],
+ ],
+ },
+ ],
+ },
+ })
+ })
+
+ describe('main', () => {
+ it('post processes return values', async () => {
+ await expectLogic(logic, () => {
+ logic.actions.loadSurveySuccess(SURVEY)
+ }).toDispatchActions(['loadSurveySuccess'])
+
+ await expectLogic(logic, () => {
+ logic.actions.loadSurveyMultipleChoiceResults({ questionIndex: 0 })
+ })
+ .toFinishAllListeners()
+ .toMatchValues({
+ surveyMultipleChoiceResults: {
+ 0: {
+ labels: ['Tutorials', 'Customer case studies', 'Product announcements'],
+ data: [336, 312, 0],
+ },
+ },
+ })
+ })
+ })
+})
diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx
index 4d816f2e19607..896f2b11a9fa3 100644
--- a/frontend/src/scenes/surveys/surveyLogic.tsx
+++ b/frontend/src/scenes/surveys/surveyLogic.tsx
@@ -10,7 +10,6 @@ import {
ChartDisplayType,
PropertyFilterType,
PropertyOperator,
- RatingSurveyQuestion,
Survey,
SurveyQuestionBase,
SurveyQuestionType,
@@ -49,7 +48,10 @@ export interface SurveyUserStats {
}
export interface SurveyRatingResults {
- [key: number]: number[]
+ [key: number]: {
+ data: number[]
+ total: number
+ }
}
export interface SurveySingleChoiceResults {
@@ -60,6 +62,13 @@ export interface SurveySingleChoiceResults {
}
}
+export interface SurveyMultipleChoiceResults {
+ [key: number]: {
+ labels: string[]
+ data: number[]
+ }
+}
+
export interface QuestionResultsReady {
[key: string]: boolean
}
@@ -187,7 +196,12 @@ export const surveyLogic = kea([
questionIndex: number
}): Promise => {
const { survey } = values
- const question = values.survey.questions[questionIndex] as RatingSurveyQuestion
+
+ const question = values.survey.questions[questionIndex]
+ if (question.type !== SurveyQuestionType.Rating) {
+ throw new Error(`Survey question type must be ${SurveyQuestionType.Rating}`)
+ }
+
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')
@@ -211,12 +225,14 @@ export const surveyLogic = kea([
const responseJSON = await api.query(query)
const { results } = responseJSON
- const resultArr = new Array(question.scale).fill(0)
+ let total = 0
+ const data = new Array(question.scale).fill(0)
results?.forEach(([value, count]) => {
- resultArr[value - 1] = count
+ total += count
+ data[value - 1] = count
})
- return { ...values.surveyRatingResults, [questionIndex]: resultArr }
+ return { ...values.surveyRatingResults, [questionIndex]: { total, data } }
},
},
surveySingleChoiceResults: {
@@ -256,6 +272,58 @@ export const surveyLogic = kea([
return { ...values.surveySingleChoiceResults, [questionIndex]: { labels, data, total } }
},
},
+ surveyMultipleChoiceResults: {
+ loadSurveyMultipleChoiceResults: async ({
+ questionIndex,
+ }: {
+ questionIndex: number
+ }): Promise => {
+ const { survey } = values
+
+ const question = values.survey.questions[questionIndex]
+ if (question.type !== SurveyQuestionType.MultipleChoice) {
+ throw new Error(`Survey question type must be ${SurveyQuestionType.MultipleChoice}`)
+ }
+
+ 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 query: HogQLQuery = {
+ kind: NodeKind.HogQLQuery,
+ query: `
+ SELECT count(), arrayJoin(JSONExtractArrayRaw(properties, '$survey_response')) AS choice
+ FROM events
+ WHERE event == 'survey sent'
+ AND properties.$survey_id == '${survey.id}'
+ AND timestamp >= '${startDate}'
+ AND timestamp <= '${endDate}'
+ GROUP BY choice
+ ORDER BY count() DESC
+ `,
+ }
+ const responseJSON = await api.query(query)
+ let { results } = responseJSON
+
+ // Remove outside quotes
+ results = results?.map((r) => {
+ return [r[0], r[1].slice(1, r[1].length - 1)]
+ })
+
+ // Zero-fill
+ question.choices.forEach((choice) => {
+ if (results?.length && !results.some((r) => r[1] === choice)) {
+ results.push([0, choice])
+ }
+ })
+
+ const data = results?.map((r) => r[0])
+ const labels = results?.map((r) => r[1])
+
+ return { ...values.surveyRatingResults, [questionIndex]: { labels, data } }
+ },
+ },
})),
listeners(({ actions }) => ({
createSurveySuccess: ({ survey }) => {
@@ -367,6 +435,17 @@ export const surveyLogic = kea([
},
},
],
+ surveyMultipleChoiceResultsReady: [
+ {},
+ {
+ loadSurveyMultipleChoiceResultsSuccess: (state, { payload }) => {
+ if (!payload || !payload.hasOwnProperty('questionIndex')) {
+ return { ...state }
+ }
+ return { ...state, [payload.questionIndex]: true }
+ },
+ },
+ ],
writingHTMLDescription: [
false,
{
diff --git a/frontend/src/scenes/surveys/surveyViewViz.tsx b/frontend/src/scenes/surveys/surveyViewViz.tsx
index 278a65b7a37b0..df818be92fcbc 100644
--- a/frontend/src/scenes/surveys/surveyViewViz.tsx
+++ b/frontend/src/scenes/surveys/surveyViewViz.tsx
@@ -4,6 +4,7 @@ import {
SurveyRatingResults,
QuestionResultsReady,
SurveySingleChoiceResults,
+ SurveyMultipleChoiceResults,
SurveyUserStats,
} from './surveyLogic'
import { useActions, useValues, BindLogic } from 'kea'
@@ -165,6 +166,7 @@ export function RatingQuestionBarChart({
}): JSX.Element {
const { loadSurveyRatingResults } = useActions(surveyLogic)
const { survey } = useValues(surveyLogic)
+ const barColor = '#1d4aff'
const question = survey.questions[questionIndex]
if (question.type !== SurveyQuestionType.Rating) {
@@ -179,6 +181,8 @@ export function RatingQuestionBarChart({
{!surveyRatingResultsReady[questionIndex] ? (
+ ) : !surveyRatingResults[questionIndex].total ? (
+ <>>
) : (
{`1-${question.scale} rating`}
@@ -187,6 +191,10 @@ export function RatingQuestionBarChart({
(i + 1).toString()).map(
@@ -215,8 +224,8 @@ export function RatingQuestionBarChart({
-
{question.lowerBoundLabel}
-
{question.upperBoundLabel}
+
{question.lowerBoundLabel}
+
{question.upperBoundLabel}
)}
@@ -268,6 +277,8 @@ export function SingleChoiceQuestionPieChart({
{!surveySingleChoiceResultsReady[questionIndex] ? (
+ ) : !surveySingleChoiceResults[questionIndex].data.length ? (
+ <>>
) : (
Single choice
@@ -339,3 +350,74 @@ export function SingleChoiceQuestionPieChart({
)
}
+
+export function MultipleChoiceQuestionBarChart({
+ questionIndex,
+ surveyMultipleChoiceResults,
+ surveyMultipleChoiceResultsReady,
+}: {
+ questionIndex: number
+ surveyMultipleChoiceResults: SurveyMultipleChoiceResults
+ surveyMultipleChoiceResultsReady: QuestionResultsReady
+}): JSX.Element {
+ const { loadSurveyMultipleChoiceResults } = useActions(surveyLogic)
+ const { survey } = useValues(surveyLogic)
+ const barColor = '#1d4aff'
+
+ const question = survey.questions[questionIndex]
+ if (question.type !== SurveyQuestionType.MultipleChoice) {
+ throw new Error(`Question type must be ${SurveyQuestionType.MultipleChoice}`)
+ }
+
+ useEffect(() => {
+ loadSurveyMultipleChoiceResults({ questionIndex })
+ }, [questionIndex])
+
+ return (
+
+ {!surveyMultipleChoiceResultsReady[questionIndex] ? (
+
+ ) : !surveyMultipleChoiceResults[questionIndex].data.length ? (
+ <>>
+ ) : (
+
+
Multiple choice
+
{question.question}
+
+
+
+
+
+
+ )}
+
+ )
+}