Skip to content
Open
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
41 changes: 41 additions & 0 deletions frontend/common/hooks/useFitText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useLayoutEffect, useRef, useState } from 'react'
Comment thread
kyle-ssg marked this conversation as resolved.

const MIN_FONT_SIZE = 14

// Shrinks the font from maxFontSize (down to MIN_FONT_SIZE) until the
// element's content fits its width, refitting when the element is resized.
// Pass undefined to leave the element alone.
export default function useFitText<T extends HTMLElement>(
maxFontSize: number | undefined,
text: string,
) {
const ref = useRef<T>(null)
const [fontSize, setFontSize] = useState(maxFontSize)

useLayoutEffect(() => {
const el = ref.current
if (!el || !maxFontSize) return
const fit = () => {
let size = maxFontSize
el.style.fontSize = `${size}px`
while (size > MIN_FONT_SIZE && el.scrollWidth > el.clientWidth) {
size -= 1
el.style.fontSize = `${size}px`
}
setFontSize(size)
}
fit()
// Fitting changes the element's height, so react to width changes only —
// refitting on our own mutations would loop the observer.
let lastWidth = el.clientWidth
const observer = new ResizeObserver(() => {
if (el.clientWidth === lastWidth) return
lastWidth = el.clientWidth
fit()
})
observer.observe(el)
return () => observer.disconnect()
}, [maxFontSize, text])

return { fontSize, ref }
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ const ExperimentDetailHeader: FC<ExperimentDetailHeaderProps> = ({
{renderActions()}
</div>
<div className='d-flex align-items-center gap-2 fs-caption mt-2'>
{metricName && <strong>{metricName}</strong>}
{metricName && <strong style={{ fontSize: 15 }}>{metricName}</strong>}
{[startedFact, endedFact].filter(Boolean).length > 0 && (
<span className='text-muted'>
{metricName ? '· ' : ''}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ const ExperimentRecommendation: FC<ExperimentRecommendationProps> = ({
<VariantName name={summary.winnerName} colour={summary.winnerColour} />{' '}
is outperforming{' '}
<VariantName name='Control' colour={summary.controlColour} /> by{' '}
{summary.liftVsControl} with {summary.chanceToBest} probability of being
the best variant.
{summary.liftValue} with {summary.chanceToBest} probability of being the
best variant.
</div>
<div className='mt-2'>
Consider rolling out{' '}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import {
valueToPercent,
} from './derive'

// A row label is right-aligned at the start of its interval, so the space it
// has to work with is whatever sits to the left of it. Intervals starting near
// the axis edge leave almost nothing, so floor the width — the name then
// ellipsises rather than collapsing to a sliver.
const LABEL_MIN_WIDTH = 64

const TickLines: FC<{ ticks: number[]; range: AxisRange }> = ({
range,
ticks,
Expand Down Expand Up @@ -98,10 +104,16 @@ const ExperimentResultsAxisChart: FC<ExperimentResultsAxisChartProps> = ({
<div className='experiment-results__axis-track'>
<span
className='experiment-results__axis-row-label'
style={{ left: `${ciLeft}%` }}
style={{
left: `${ciLeft}%`,
maxWidth: `max(${LABEL_MIN_WIDTH}px, calc(${ciLeft}% - 8px))`,
}}
title={v.name}
>
<ColorSwatch color={v.colour} shape='circle' size='sm' />
{v.name}
<span className='experiment-results__axis-row-label-text'>
{v.name}
</span>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</span>
<div
className='experiment-results__axis-bar'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
} from 'common/types/responses'
import {
VariantIdentity,
formatChancePct,
formatLiftPct,
getControlChanceToWin,
getLiftColour,
liftToPercent,
} from './derive'
Expand Down Expand Up @@ -102,12 +104,11 @@ const renderCI = (
}

const renderWinProbability = (
identity: VariantIdentity,
inference: Inference | null,
chanceToWin: number | null,
isHighest: boolean,
): ReactNode => {
if (identity.isControl || !inference) return '—'
const pct = Math.round(inference.chance_to_win * 100)
if (chanceToWin === null) return '—'
const pct = Math.round(chanceToWin * 100)
const colour = isHighest ? colorTextSuccess : colorTextSecondary
return (
<div className='experiment-results__win-prob'>
Expand All @@ -117,7 +118,7 @@ const renderWinProbability = (
style={{ background: colour, width: `${pct}%` }}
/>
</div>
<span style={{ color: colour }}>{pct}%</span>
<span style={{ color: colour }}>{formatChancePct(chanceToWin)}</span>
</div>
)
}
Expand All @@ -133,95 +134,106 @@ type ExperimentResultsScorecardTableProps = {

const ExperimentResultsScorecardTable: FC<
ExperimentResultsScorecardTableProps
> = ({ identities, liftRange, metric, metricResult, srmBroken, winnerKey }) => (
<div className='mb-4'>
<div className='experiment-results__scorecard'>
<table className='experiment-results__scorecard-table'>
<thead>
<tr>
<th style={{ width: '10%' }}>Variant</th>
<th style={{ width: '8%' }}>Exposures</th>
<th style={{ width: '12%' }}>
{AGGREGATION_HEADER[metric.aggregation]}
</th>
<th style={{ width: '24%' }}>
<Tooltip
title={
<span className='d-inline-flex align-items-center gap-1 flex-nowrap'>
Delta
<Icon
className='flex-shrink-0'
name='info-outlined'
width={16}
fill={colorIconSecondary}
/>
</span>
}
>
How much better or worse a variant performed compared to
control, as a percentage of the control's value.
</Tooltip>
</th>
<th style={{ width: '16%' }}>
<Tooltip
title={
<span className='d-inline-flex align-items-center gap-1 flex-nowrap'>
Credible Interval (95%)
<Icon
className='flex-shrink-0'
name='info-outlined'
width={16}
fill={colorIconSecondary}
/>
</span>
}
>
The range we are 95% confident the true lift falls within. If it
doesn't cross zero, the result is statistically significant.
</Tooltip>
</th>
<th style={{ width: '16%' }}>Win Probability</th>
</tr>
</thead>
<tbody>
{identities.map((v) => {
const stats = metricResult?.variants[v.key]
const inference = metricResult?.inference[v.key] ?? null
return (
<tr key={v.key}>
<td>
<span className='d-flex align-items-center gap-2'>
<ColorSwatch color={v.colour} shape='circle' size='sm' />
{v.name}
</span>
</td>
<td>{stats ? stats.n.toLocaleString() : '—'}</td>
<td>{renderMetricValue(stats, metric.aggregation)}</td>
<td>
{renderLift(
v,
inference,
metric.direction ?? 'up',
liftRange,
)}
</td>
<td>{renderCI(v, inference)}</td>
<td>
{renderWinProbability(v, inference, v.key === winnerKey)}
</td>
</tr>
)
})}
</tbody>
</table>
> = ({ identities, liftRange, metric, metricResult, srmBroken, winnerKey }) => {
const controlChance = metricResult
? getControlChanceToWin(metricResult, identities)
: null
return (
<div className='mb-4'>
<div className='experiment-results__scorecard'>
<table className='experiment-results__scorecard-table'>
<thead>
<tr>
<th style={{ width: '18%' }}>Variant</th>
<th style={{ width: '8%' }}>Exposures</th>
<th style={{ width: '12%' }}>
{AGGREGATION_HEADER[metric.aggregation]}
</th>
<th style={{ width: '16%' }}>
<Tooltip
title={
<span className='d-inline-flex align-items-center gap-1 flex-nowrap'>
Delta
<Icon
className='flex-shrink-0'
name='info-outlined'
width={16}
fill={colorIconSecondary}
/>
</span>
}
>
How much better or worse a variant performed compared to
control, as a percentage of the control's value.
</Tooltip>
</th>
<th style={{ width: '16%' }}>
<Tooltip
title={
<span className='d-inline-flex align-items-center gap-1 flex-nowrap'>
Credible Interval (95%)
<Icon
className='flex-shrink-0'
name='info-outlined'
width={16}
fill={colorIconSecondary}
/>
</span>
}
>
The range we are 95% confident the true lift falls within. If
it doesn't cross zero, the result is statistically
significant.
</Tooltip>
</th>
<th style={{ width: '16%' }}>Win Probability</th>
</tr>
</thead>
<tbody>
{identities.map((v) => {
const stats = metricResult?.variants[v.key]
const inference = metricResult?.inference[v.key] ?? null
return (
<tr key={v.key}>
<td>
<span className='d-flex align-items-center gap-2'>
<ColorSwatch color={v.colour} shape='circle' size='sm' />
{v.name}
</span>
</td>
<td>{stats ? stats.n.toLocaleString() : '—'}</td>
<td>{renderMetricValue(stats, metric.aggregation)}</td>
<td>
{renderLift(
v,
inference,
metric.direction ?? 'up',
liftRange,
)}
</td>
<td>{renderCI(v, inference)}</td>
<td>
{renderWinProbability(
v.isControl
? controlChance
: inference && inference.chance_to_win,
v.key === winnerKey,
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
{srmBroken && (
<p className='text-danger fst-italic fs-caption mt-1 mb-0'>
Sample ratio mismatch detected — the variation split looks broken;
interpret results with caution.
</p>
)}
</div>
{srmBroken && (
<p className='text-danger fst-italic fs-caption mt-1 mb-0'>
Sample ratio mismatch detected — the variation split looks broken;
interpret results with caution.
</p>
)}
</div>
)
)
}

export default ExperimentResultsScorecardTable
Loading
Loading