Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(experiments): detailed results for primary metrics #27046

Merged
merged 16 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { WebExperimentImplementationDetails } from 'scenes/experiments/WebExperi

import { ExperimentImplementationDetails } from '../ExperimentImplementationDetails'
import { experimentLogic } from '../experimentLogic'
import { PrimaryMetricModal } from '../Metrics/PrimaryMetricModal'
import { MetricsView } from '../MetricsView/MetricsView'
import {
ExperimentLoadingAnimation,
Expand Down Expand Up @@ -129,6 +130,7 @@ export function ExperimentView(): JSX.Element {
/>
</>
)}
<PrimaryMetricModal experimentId={experimentId} />
<DistributionModal experimentId={experimentId} />
<ReleaseConditionsModal experimentId={experimentId} />
</>
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/scenes/experiments/ExperimentView/Goal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { ExperimentFunnelsQuery, ExperimentTrendsQuery, FunnelsQuery, NodeKind,
import { ActionFilter, AnyPropertyFilter, ChartDisplayType, Experiment, FilterType, InsightType } from '~/types'

import { experimentLogic, getDefaultFilters, getDefaultFunnelsMetric } from '../experimentLogic'
import { PrimaryMetricModal } from '../Metrics/PrimaryMetricModal'
import { PrimaryTrendsExposureModal } from '../Metrics/PrimaryTrendsExposureModal'

export function MetricDisplayTrends({ query }: { query: TrendsQuery | undefined }): JSX.Element {
Expand Down Expand Up @@ -341,7 +340,6 @@ export function Goal(): JSX.Element {
)}
</div>
)}
<PrimaryMetricModal experimentId={experimentId} />
</div>
)
}
126 changes: 109 additions & 17 deletions frontend/src/scenes/experiments/MetricsView/DeltaChart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IconActivity, IconPencil } from '@posthog/icons'
import { LemonButton, LemonTag } from '@posthog/lemon-ui'
import { IconActivity, IconGraph, IconMinus, IconPencil, IconTrending } from '@posthog/icons'
import { LemonButton, LemonModal, LemonTag, LemonTagType, Tooltip } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { humanFriendlyNumber } from 'lib/utils'
import { useEffect, useRef, useState } from 'react'
Expand All @@ -8,7 +8,7 @@ import { themeLogic } from '~/layout/navigation-3000/themeLogic'
import { InsightType, TrendExperimentVariant } from '~/types'

import { experimentLogic } from '../experimentLogic'
import { VariantTag } from '../ExperimentView/components'
import { ResultsQuery, VariantTag } from '../ExperimentView/components'
import { NoResultEmptyState } from './NoResultEmptyState'

function formatTickValue(value: number): string {
Expand Down Expand Up @@ -68,9 +68,23 @@ export function DeltaChart({
const [tooltipData, setTooltipData] = useState<{ x: number; y: number; variant: string } | null>(null)
const [emptyStateTooltipVisible, setEmptyStateTooltipVisible] = useState(true)
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 })
const [isModalOpen, setIsModalOpen] = useState(false)

const BAR_HEIGHT = 8
const BAR_PADDING = 10
const getScaleAddition = (variantCount: number): number => {
if (variantCount < 3) {
return 6
}
if (variantCount < 4) {
return 3
}
if (variantCount < 5) {
return 1
}
return 0
}

const BAR_HEIGHT = 8 + getScaleAddition(variants.length)
const BAR_PADDING = 10 + getScaleAddition(variants.length)
const TICK_PANEL_HEIGHT = 20
const VIEW_BOX_WIDTH = 800
const HORIZONTAL_PADDING = 20
Expand Down Expand Up @@ -101,6 +115,7 @@ export function DeltaChart({

const metricTitlePanelWidth = '20%'
const variantsPanelWidth = '10%'
const detailedResultsPanelWidth = '125px'

const ticksSvgRef = useRef<SVGSVGElement>(null)
const chartSvgRef = useRef<SVGSVGElement>(null)
Expand Down Expand Up @@ -184,7 +199,7 @@ export function DeltaChart({
{isFirstMetric && (
<svg
// eslint-disable-next-line react/forbid-dom-props
style={{ height: `${ticksSvgHeight}px` }}
style={{ height: `${ticksSvgHeight}px`, width: '100%' }}
/>
)}
{isFirstMetric && <div className="w-full border-t border-border" />}
Expand All @@ -206,13 +221,12 @@ export function DeltaChart({
))}
</div>
</div>

{/* SVGs container */}
<div
// eslint-disable-next-line react/forbid-dom-props
style={{
display: 'inline-block',
width: `calc(100% - ${metricTitlePanelWidth} - ${variantsPanelWidth})`,
width: `calc(100% - ${metricTitlePanelWidth} - ${variantsPanelWidth} - ${detailedResultsPanelWidth})`,
verticalAlign: 'top',
}}
>
Expand Down Expand Up @@ -407,15 +421,6 @@ export function DeltaChart({
y={chartHeight / 2 - 10} // Roughly center vertically
width="200"
height="20"
onMouseEnter={(e) => {
const rect = e.currentTarget.getBoundingClientRect()
setTooltipPosition({
x: rect.left + rect.width / 2,
y: rect.top,
})
setEmptyStateTooltipVisible(true)
}}
onMouseLeave={() => setEmptyStateTooltipVisible(false)}
>
<div
className="flex items-center justify-center text-muted cursor-default"
Expand Down Expand Up @@ -662,6 +667,93 @@ export function DeltaChart({
</div>
)}
</div>
{/* Detailed results panel */}
<div
// eslint-disable-next-line react/forbid-dom-props
style={{
display: 'inline-block',
width: detailedResultsPanelWidth,
verticalAlign: 'top',
}}
>
{isFirstMetric && (
<svg
// eslint-disable-next-line react/forbid-dom-props
style={{ height: `${ticksSvgHeight}px`, width: '100%' }}
/>
)}
{isFirstMetric && <div className="w-full border-t border-border" />}
{result && (
<div
// eslint-disable-next-line react/forbid-dom-props
style={{
height: `${chartSvgHeight}px`,
borderLeft: result ? `1px solid ${COLORS.BOUNDARY_LINES}` : 'none',
display: 'flex',
flexDirection: 'column',
}}
>
<SignificanceHighlight metricIndex={metricIndex} />
<div className="flex-1 flex items-center justify-center">
<LemonButton
type="secondary"
size="xsmall"
icon={<IconGraph />}
onClick={() => setIsModalOpen(true)}
>
Detailed results
</LemonButton>
</div>
</div>
)}
</div>

<LemonModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
width={1200}
title={`Results for ${metric.name || 'Untitled metric'}`}
footer={
<LemonButton
form="secondary-metric-modal-form"
type="secondary"
onClick={() => setIsModalOpen(false)}
>
Close
</LemonButton>
}
>
<ResultsQuery targetResults={result} showTable={true} />
</LemonModal>
</div>
)
}

function SignificanceHighlight({ metricIndex = 0 }: { metricIndex?: number }): JSX.Element {
const { areResultsSignificant, significanceDetails } = useValues(experimentLogic)
const result: { color: LemonTagType; label: string } = areResultsSignificant(metricIndex)
? { color: 'success', label: 'Significant' }
: { color: 'primary', label: 'Not significant' }

const inner = areResultsSignificant(metricIndex) ? (
<div className="bg-success-highlight text-success p-1 flex items-center gap-1">
<IconTrending fontSize={20} fontWeight={600} />
<span className="text-xs font-semibold">{result.label}</span>
</div>
) : (
<div className="bg-warning-highlight text-warning p-1 flex items-center gap-1">
<IconMinus fontSize={20} fontWeight={600} />
<span className="text-xs font-semibold">{result.label}</span>
</div>
)

const details = significanceDetails(metricIndex)

return details ? (
<Tooltip title={details}>
<div className="cursor-pointer">{inner}</div>
</Tooltip>
) : (
<div>{inner}</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function MetricsView(): JSX.Element {
</div>
{metrics.length > 0 ? (
<div className="w-full overflow-x-auto">
<div className="min-w-[800px]">
<div className="min-w-[1000px]">
{metrics.map((metric, metricIndex) => {
const result = metricResults?.[metricIndex]
const isFirstMetric = metricIndex === 0
Expand Down
Loading