diff --git a/src/components/Results/ResultsSummary.vue b/src/components/Results/ResultsSummary.vue index 4156ba3ef..5e7303a25 100644 --- a/src/components/Results/ResultsSummary.vue +++ b/src/components/Results/ResultsSummary.vue @@ -52,6 +52,44 @@ + +
+
+
{{ t('forms', 'Average') }}
+
{{ numericStats.mean }}
+
+
+
{{ t('forms', 'Median') }}
+
{{ numericStats.median }}
+
+
+
{{ t('forms', 'Lowest') }}
+
{{ numericStats.min }}
+
+
+
{{ t('forms', 'Highest') }}
+
{{ numericStats.max }}
+
+
+
{{ t('forms', 'Net Promoter Score') }}
+
+ {{ npsScore }} + + {{ + t( + 'forms', + '{promoters}% promoters, {detractors}% detractors', + { + promoters: npsBreakdown.promoters, + detractors: npsBreakdown.detractors, + }, + ) + }} + +
+
+
+
    (() => { + if (props.question.type !== 'linearscale') { + return [] + } + const values: number[] = [] + for (const submission of props.submissions) { + for (const answer of submission.answers ?? []) { + if (answer.questionId !== props.question.id) { + continue + } + const value = parseFloat(answer.text) + if (!isNaN(value)) { + values.push(value) + } + } + } + return values + }) + + /** + * Summary statistics for a scale question. + * + * The median is reported next to the average because a single extreme answer moves + * an average a long way at the response counts most forms see. + */ + const numericStats = computed(() => { + const values = numericValues.value + if (values.length === 0) { + return null + } + const sorted = [...values].sort((a, b) => a - b) + const middle = Math.floor(sorted.length / 2) + const median = + sorted.length % 2 + ? sorted[middle] + : (sorted[middle - 1] + sorted[middle]) / 2 + const round = (value: number) => Math.round(value * 100) / 100 + + return { + count: values.length, + mean: round(values.reduce((a, b) => a + b, 0) / values.length), + median: round(median), + min: round(sorted[0]), + max: round(sorted[sorted.length - 1]), + } + }) + + /** + * Promoter and detractor shares for a question shaped like a Net Promoter Score + * question, i.e. a 0-10 scale, using the usual 9-10 / 7-8 / 0-6 split. + */ + const npsBreakdown = computed(() => { + if ( + props.question.extraSettings?.optionsLowest !== 0 + || props.question.extraSettings?.optionsHighest !== 10 + ) { + return null + } + const values = numericValues.value + if (values.length === 0) { + return null + } + const share = (count: number) => + Math.round((count / values.length) * 100) + return { + promoters: share(values.filter((value) => value>= 9).length), + passives: share( + values.filter((value) => value>= 7 && value <= 8).length, + ), + detractors: share(values.filter((value) => value <= 6).length), + } + }) + + /** + * The Net Promoter Score itself: promoters minus detractors. + */ + const npsScore = computed(() => + npsBreakdown.value + ? npsBreakdown.value.promoters - npsBreakdown.value.detractors + : null, + ) + return { IconFile, answerTypes, + numericStats, + npsBreakdown, + npsScore, questionTypeLabel, questionOptions, rankingStats, @@ -768,4 +894,29 @@ export default defineComponent({ } } } + +.question-summary__figures { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin: 0 0 8px; + + dt { + color: var(--color-text-maxcontrast); + font-size: 0.9em; + } + + dd { + font-size: 1.2em; + font-weight: bold; + margin: 0; + } + + &-detail { + color: var(--color-text-maxcontrast); + display: block; + font-size: 0.75em; + font-weight: normal; + } +}

    AltStyle によって変換されたページ (->オリジナル) /