Repository: PostHog/posthog
Pull request: feat: Display stickiness data as percentages along with count
## Problem

Update the stickiness insight to display the data as a percentage instead of the absolute account.

<!-- Who are we building for, what are their needs, why is this important? -->
Closes #16342 

## Changes

### Before
<img width="1234" alt="Screenshot 2025-02-20 at 4 41 14 PM" src="https://github.com/user-attachments/assets/6dbf34db-8a1d-438d-9c29-2c385995384d" />

### After 
<img width="1188" alt="Screenshot 2025-02-20 at 4 38 38 PM" src="https://github.com/user-attachments/assets/f2629768-2363-4e34-ab6a-b9dfdc4dc3d6" />


<!-- If there are frontend changes, please include screenshots. -->
<!-- If a reference design was involved, include a link to the relevant Figma frame! -->

👉 _Stay up-to-date with [PostHog coding conventions](https://posthog.com/docs/contribute/coding-conventions) for a smoother review._

## Does this work well for both Cloud and self-hosted?

<!-- Yes / no / it doesn't have an impact. -->

## How did you test this code?
Visually tested locally.


Problem statement:
Show stickiness as percentage in addition to absolute count
## Is your feature request related to a problem?

It would be useful to view not just the absolute numbers in stickiness results, but also percentages (funnel style).

## Additional context

Requested in ZEN-3914

#### *Thank you* for your feature request – we love each and every one!

Hints from issue discussion:

Commit message: Display stickiness data as percentages along with count

Patch to review:
diff --git a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx
index d4420426ed5bb..d127b000ec057 100644
--- a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx
+++ b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx
@@ -255,6 +255,17 @@ export function InsightsTable({
 
         const capitalizeFirstLetter = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1)
 
+        const dataSorter = (a: IndexedTrendResult, b: IndexedTrendResult, index: number): number => {
+            const aValue = a.data[index] ?? NaN
+            const bValue = b.data[index] ?? NaN
+
+            if (isStickiness) {
+                return aValue / a.count - bValue / b.count
+            }
+
+            return aValue - bValue
+        }
+
         return results.map((_, index) => ({
             title: isStickiness ? (
                 `${interval ? capitalizeFirstLetter(interval) : 'Day'} ${index + 1}`
@@ -266,11 +277,11 @@ export function InsightsTable({
                     interval={interval}
                 />
             ),
-            render: (_, item: IndexedTrendResult) => (
-                <ValueColumnItem index={index} item={item} trendsFilter={trendsFilter} />
-            ),
+            render: (_, item: IndexedTrendResult) => {
+                return <ValueColumnItem index={index} item={item} trendsFilter={trendsFilter} />
+            },
             key: `data-${index}`,
-            sorter: (a: IndexedTrendResult, b: IndexedTrendResult) => (a.data[index] ?? NaN) - (b.data[index] ?? NaN),
+            sorter: (a: IndexedTrendResult, b: IndexedTrendResult) => dataSorter(a, b, index),
             align: 'right',
         }))
     }, [indexedResults, trendsFilter, isStickiness, compareFilter?.compare, interval])
diff --git a/frontend/src/scenes/insights/views/InsightsTable/columns/ValueColumn.tsx b/frontend/src/scenes/insights/views/InsightsTable/columns/ValueColumn.tsx
index 8b977396b8dc9..4641669ce71de 100644
--- a/frontend/src/scenes/insights/views/InsightsTable/columns/ValueColumn.tsx
+++ b/frontend/src/scenes/insights/views/InsightsTable/columns/ValueColumn.tsx
@@ -1,7 +1,9 @@
 import { useValues } from 'kea'
 import { DateDisplay } from 'lib/components/DateDisplay'
 import { formatAggregationAxisValue } from 'scenes/insights/aggregationAxisFormat'
+import { insightLogic } from 'scenes/insights/insightLogic'
 import { formatAggregationValue } from 'scenes/insights/utils'
+import { trendsDataLogic } from 'scenes/trends/trendsDataLogic'
 import { IndexedTrendResult } from 'scenes/trends/types'
 
 import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel'
@@ -36,13 +38,23 @@ type ValueColumnItemProps = {
 
 export function ValueColumnItem({ index, item, trendsFilter }: ValueColumnItemProps): JSX.Element {
     const { formatPropertyValueForDisplay } = useValues(propertyDefinitionsModel)
+    const { insightProps } = useValues(insightLogic)
+    const { isStickiness } = useValues(trendsDataLogic(insightProps))
+    const formattedValue = formatAggregationValue(
+        item.action?.math_property,
+        item.data[index],
+        (value) => formatAggregationAxisValue(trendsFilter as Partial<TrendsFilterType>, value),
+        formatPropertyValueForDisplay
+    )
     return (
         <span>
-            {formatAggregationValue(
-                item.action?.math_property,
-                item.data[index],
-                (value) => formatAggregationAxisValue(trendsFilter as Partial<TrendsFilterType>, value),
-                formatPropertyValueForDisplay
+            {isStickiness ? (
+                <div>
+                    <div>{((item.data[index] / item.count) * 100).toFixed(1)}%</div>
+                    <div>({formattedValue})</div>
+                </div>
+            ) : (
+                formattedValue
             )}
         </span>
     )
diff --git a/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx b/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx
index bb0e8c64c1ac7..3dad86a4692db 100644
--- a/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx
+++ b/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx
@@ -257,6 +257,7 @@ export interface LineGraphProps {
     showValuesOnSeries?: boolean | null
     showPercentStackView?: boolean | null
     supportsPercentStackView?: boolean
+    showPercentView?: boolean | null
     hideAnnotations?: boolean
     hideXAxis?: boolean
     hideYAxis?: boolean
@@ -298,6 +299,7 @@ export function LineGraph_({
     showValuesOnSeries,
     showPercentStackView,
     supportsPercentStackView,
+    showPercentView,
     hideAnnotations,
     hideXAxis,
     hideYAxis,
@@ -306,6 +308,7 @@ export function LineGraph_({
     legend = { display: false },
     goalLines: _goalLines,
 }: LineGraphProps): JSX.Element {
+    const originalDatasets = _datasets
     let datasets = _datasets
 
     const { aggregationLabel } = useValues(groupsModel)
@@ -401,6 +404,12 @@ export function LineGraph_({
             adjustedData = adjustedData.map((value) => (value === 0 ? LOG_ZERO : value))
         }
 
+        // Transform data to percentages if showPercentView is enabled
+        if (showPercentView && Array.isArray(adjustedData)) {
+            const count = dataset.count
+            adjustedData = adjustedData.map((value) => (typeof value === 'number' ? (value / count) * 100 : value))
+        }
+
         // `horizontalBar` colors are set in `ActionsHorizontalBar.tsx` and overridden in spread of `dataset` below
         return {
             borderColor: mainColor,
@@ -448,6 +457,13 @@ export function LineGraph_({
         }
     }
 
+    function formatYAxisTick(value: number | string): string {
+        if (showPercentView) {
+            return `${Number(value).toFixed(1)}%`
+        }
+        return formatPercentStackAxisValue(trendsFilter, value, isPercentStackView)
+    }
+
     function generateYaxesForLineGraph(
         dataSetCount: number,
         seriesNonZeroMin: number,
@@ -468,8 +484,7 @@ export function LineGraph_({
                 ...tickOptions,
                 display: !hideYAxis,
                 ...(yAxisScaleType !== 'log10' && { precision }), // Precision is not supported for the log scale
-                callback: (value: number | string) =>
-                    formatPercentStackAxisValue(trendsFilter, value, isPercentStackView),
+                callback: formatYAxisTick,
                 color: (context: any) => {
                     if (context.tick) {
                         for (const annotation of goalLinesWithColor) {
@@ -492,7 +507,7 @@ export function LineGraph_({
                 )
                 const annotationTicks = goalLines.map((value) => ({
                     value: value.value,
-                    label: `⬤ ${formatPercentStackAxisValue(trendsFilter, value.value, isPercentStackView)}`,
+                    label: `⬤ ${formatYAxisTick(value.value)}`,
                 }))
 
                 // Guarantee that all annotations exist as ticks
@@ -711,6 +726,23 @@ export function LineGraph_({
                                     renderCount={
                                         tooltipConfig?.renderCount ||
                                         ((value: number): string => {
+                                            if (showPercentView) {
+                                                const series = seriesData.find((s) => s.count === value)
+                                                const datasetIndex = series?.datasetIndex
+                                                const dataIndex = series?.dataIndex
+                                                if (datasetIndex !== undefined && dataIndex !== undefined) {
+                                                    const originalDataset = originalDatasets[datasetIndex]
+                                                    const originalValue = originalDataset.data?.[dataIndex]
+
+                                                    if (originalValue !== undefined && originalValue !== null) {
+                                                        return `${value.toFixed(1)}% (${formatAggregationAxisValue(
+                                                            trendsFilter,
+                                                            originalValue
+                                                        )})`
+                                                    }
+                                                }
+                                            }
+
                                             if (!isPercentStackView) {
                                                 return formatAggregationAxisValue(trendsFilter, value)
                                             }
diff --git a/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx b/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx
index 9076457df4bd5..195a57052a1c9 100644
--- a/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx
+++ b/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx
@@ -94,6 +94,7 @@ export function ActionsLineGraph({
             trendsFilter={trendsFilter}
             formula={formula}
             showValuesOnSeries={showValuesOnSeries}
+            showPercentView={isStickiness}
             showPercentStackView={showPercentStackView}
             supportsPercentStackView={supportsPercentStackView}
             yAxisScaleType={yAxisScaleType}


Code review comments:
diff_hunk: @@ -36,13 +38,23 @@ type ValueColumnItemProps = {
 
 export function ValueColumnItem({ index, item, trendsFilter }: ValueColumnItemProps): JSX.Element {
     const { formatPropertyValueForDisplay } = useValues(propertyDefinitionsModel)
+    const { insightProps } = useValues(insightLogic)
+    const { isStickiness } = useValues(trendsDataLogic(insightProps))
+    const formattedValue = formatAggregationValue(
+        item.action?.math_property,
+        item.data[index],
+        (value) => formatAggregationAxisValue(trendsFilter as Partial<TrendsFilterType>, value),
+        formatPropertyValueForDisplay
+    )
     return (
         <span>
-            {formatAggregationValue(
-                item.action?.math_property,
-                item.data[index],
-                (value) => formatAggregationAxisValue(trendsFilter as Partial<TrendsFilterType>, value),
-                formatPropertyValueForDisplay
+            {isStickiness ? (
+                <div>
+                    <div>{((item.data[index] / item.count) * 100).toFixed(1)}%</div>
path: frontend/src/scenes/insights/views/InsightsTable/columns/ValueColumn.tsx
text: @user1:
logic: Potential division by zero if item.count is 0. Add a check to handle this case.

```suggestion
                    <div>{item.count ? ((item.data[index] / item.count) * 100).toFixed(1) : '0'}%</div>
```