Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
66677e1
feat(charts): Add LineChart + PieChart shared components
talissoncosta Apr 23, 2026
08d0320
docs(charts): Add Storybook stories for LineChart and PieChart
talissoncosta Apr 23, 2026
49a7101
refactor(admin-dashboard): Migrate UsageTrendsChart to shared LineChart
talissoncosta Apr 23, 2026
73f8c48
refactor(admin-dashboard): Migrate ReleasePipelineStatsTable to share…
talissoncosta Apr 23, 2026
2df062e
fix(experiment-results): Replace hardcoded axis colours with tokens
talissoncosta Apr 23, 2026
ea052de
refactor(release-pipelines): Use chart tokens for pie palette
talissoncosta Apr 23, 2026
eb2a85c
refactor(release-pipelines): Use ColorSwatch + text-success utility
talissoncosta Apr 23, 2026
eb52655
refactor(release-pipelines): Replace inline fontSize with fs-* utilities
talissoncosta Apr 23, 2026
0e84ad3
fix(charts): PieChart legend overlapping donut with raw fixed dimensions
talissoncosta Apr 23, 2026
4a2d24b
refactor(charts): Unify tooltip — PieChart now uses ChartTooltip too
talissoncosta Apr 23, 2026
a6420f6
docs(charts): Group chart stories under a Charts umbrella
talissoncosta Apr 23, 2026
78eafc9
refactor: fix ChartDataPoint type + add LineChart.verticalGrid
talissoncosta Apr 24, 2026
a0fe93e
refactor: align ExperimentResultsTab palette with shared chart tokens
talissoncosta Apr 24, 2026
10f7d70
refactor(release-pipeline-stats): use colorBorderDefault token for di…
talissoncosta Apr 30, 2026
025a5fb
refactor(experiment-results): memoise variantColorMap
talissoncosta Apr 30, 2026
6d1bfa4
refactor(chart-stories): extract shared fake-data helper
talissoncosta Apr 30, 2026
4483b13
refactor(pie-chart): fall back to colorTextSecondary for unmapped slices
talissoncosta Apr 30, 2026
7d03501
refactor(charts): move ChartDataPoint into a shared types module
talissoncosta Apr 30, 2026
d0d448c
refactor(pie-chart-stories): drop redundant useMemo over module const…
talissoncosta Apr 30, 2026
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
66 changes: 18 additions & 48 deletions frontend/documentation/components/BarChart.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useMemo, useState } from 'react'
import type { Meta, StoryObj } from 'storybook'
import BarChart, { ChartDataPoint } from 'components/charts/BarChart'
import BarChart from 'components/charts/BarChart'
import { MultiSelect } from 'components/base/select/multi-select'
import { buildChartColorMap } from 'components/charts/buildChartColorMap'
import { generateChartFakeData } from './_chartFakeData'

// ============================================================================
// Fake data
Expand All @@ -16,54 +17,23 @@ const SDKS = [
'flagsmith-ruby-sdk',
]

// Deterministic stand-in for Math.random — same `(label, day)` pair always
// produces the same value, so Chromatic snapshots stay stable across runs.
const pseudoRandom = (label: string, day: number): number => {
let hash = 0
const seed = `${label}-${day}`
for (let i = 0; i < seed.length; i++) {
hash = (hash << 5) - hash + seed.charCodeAt(i)
hash |= 0
}
return Math.abs(hash) / 0x7fffffff
const BAR_BASE_MAP: Record<string, number> = {
Development: 1200,
Production: 5000,
Staging: 2400,
'flagsmith-js-sdk': 4500,
'flagsmith-python-sdk': 2200,
}

// Pinned reference date — using `new Date()` would shift the x-axis daily and
// drift every Chromatic snapshot.
const REFERENCE_DATE = new Date('2026-04-15T00:00:00Z')

function generateFakeData(days: number, labels: string[]): ChartDataPoint[] {
const data: ChartDataPoint[] = []

const baseMap: Record<string, number> = {
Development: 1200,
Production: 5000,
Staging: 2400,
'flagsmith-js-sdk': 4500,
'flagsmith-python-sdk': 2200,
}

for (let i = days - 1; i >= 0; i--) {
const date = new Date(REFERENCE_DATE)
date.setUTCDate(date.getUTCDate() - i)
const dayStr = date.toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short',
timeZone: 'UTC',
})

const point: ChartDataPoint = { day: dayStr }
labels.forEach((label) => {
const base = baseMap[label] || 800
const variance = Math.floor(pseudoRandom(label, i) * base * 0.4)
const weekday = date.getUTCDay()
const weekendDip = weekday === 0 || weekday === 6 ? 0.4 : 1
point[label] = Math.floor((base + variance) * weekendDip)
})
data.push(point)
}
return data
}
const generateFakeData = (days: number, labels: string[]) =>
generateChartFakeData({
baseMap: BAR_BASE_MAP,
days,
defaultBase: 800,
labels,
variance: 0.4,
weekendDip: 0.4,
})

// ============================================================================
// Stories
Expand All @@ -72,7 +42,7 @@ function generateFakeData(days: number, labels: string[]): ChartDataPoint[] {
const meta: Meta<typeof BarChart> = {
component: BarChart,
tags: ['autodocs'],
title: 'Components/BarChart',
title: 'Components/Charts/BarChart',
}
export default meta

Expand Down
129 changes: 129 additions & 0 deletions frontend/documentation/components/LineChart.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React, { useMemo } from 'react'
import type { Meta, StoryObj } from 'storybook'
import LineChart from 'components/charts/LineChart'
import { buildChartColorMap } from 'components/charts/buildChartColorMap'
import { generateChartFakeData } from './_chartFakeData'

// ============================================================================
// Fake data
// ============================================================================

const LINE_BASE_MAP: Record<string, number> = {
'API Calls': 12000,
Clicks: 2000,
Conversions: 800,
Errors: 80,
'Flag Evaluations': 8000,
'Identity Requests': 5000,
'Page Views': 15000,
}

const generateFakeData = (days: number, labels: string[]) =>
generateChartFakeData({
baseMap: LINE_BASE_MAP,
days,
labels,
})

// ============================================================================
// Stories
// ============================================================================

const meta: Meta<typeof LineChart> = {
component: LineChart,
tags: ['autodocs'],
title: 'Components/Charts/LineChart',
}
export default meta

type Story = StoryObj<typeof LineChart>

export const UsageTrends: Story = {
decorators: [
() => {
const labels = useMemo(
() => ['API Calls', 'Flag Evaluations', 'Identity Requests'],
[],
)
const data = useMemo(() => generateFakeData(30, labels), [labels])
const colorMap = useMemo(() => buildChartColorMap(labels), [labels])

return (
<div className='mx-auto' style={{ maxWidth: 900 }}>
<p className='text-secondary fs-small mb-3'>
Mirrors the API Usage Trends dashboard — three independent metrics
plotted over 30 days.
</p>
<LineChart
data={data}
series={labels}
colorMap={colorMap}
xAxisInterval={2}
showLegend
/>
</div>
)
},
],
}

export const SingleLine: Story = {
decorators: [
() => {
const labels = useMemo(() => ['API Calls'], [])
const data = useMemo(() => generateFakeData(30, labels), [labels])
const colorMap = useMemo(() => buildChartColorMap(labels), [labels])

return (
<div className='mx-auto' style={{ maxWidth: 900 }}>
<p className='text-secondary fs-small mb-3'>
One metric over time — legend hidden since the series is obvious
from the chart title.
</p>
<LineChart
data={data}
series={labels}
colorMap={colorMap}
xAxisInterval={2}
/>
</div>
)
},
],
}

export const ManyLines: Story = {
decorators: [
() => {
const labels = useMemo(
() => [
'API Calls',
'Flag Evaluations',
'Identity Requests',
'Page Views',
'Clicks',
'Conversions',
'Errors',
],
[],
)
const data = useMemo(() => generateFakeData(30, labels), [labels])
const colorMap = useMemo(() => buildChartColorMap(labels), [labels])

return (
<div className='mx-auto' style={{ maxWidth: 900 }}>
<p className='text-secondary fs-small mb-3'>
Seven lines — stress-test of the colour palette.
</p>
<LineChart
data={data}
series={labels}
colorMap={colorMap}
xAxisInterval={2}
showLegend
/>
</div>
)
},
],
}
156 changes: 156 additions & 0 deletions frontend/documentation/components/PieChart.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React from 'react'
import type { Meta, StoryObj } from 'storybook'
import PieChart, { PieSlice } from 'components/charts/PieChart'
import { buildChartColorMap } from 'components/charts/buildChartColorMap'

// ============================================================================
// Fake data
// ============================================================================

const STAGE_DATA: PieSlice[] = [
{ name: 'Development', value: 12 },
{ name: 'Staging', value: 8 },
{ name: 'Pre-production', value: 5 },
{ name: 'Production', value: 15 },
{ name: 'Released', value: 32 },
]

const SDK_DATA: PieSlice[] = [
{ name: 'flagsmith-js-sdk', value: 4500 },
{ name: 'flagsmith-python-sdk', value: 2200 },
{ name: 'flagsmith-java-sdk', value: 1100 },
{ name: 'flagsmith-go-sdk', value: 800 },
{ name: 'flagsmith-ruby-sdk', value: 400 },
{ name: 'flagsmith-dotnet-sdk', value: 300 },
{ name: 'flagsmith-rust-sdk', value: 150 },
]

const TWO_SLICE_DATA: PieSlice[] = [
{ name: 'Released', value: 32 },
{ name: 'In progress', value: 18 },
]

const STAGE_COLOR_MAP = buildChartColorMap(STAGE_DATA.map((s) => s.name))
const SDK_COLOR_MAP = buildChartColorMap(SDK_DATA.map((s) => s.name))
const TWO_SLICE_COLOR_MAP = buildChartColorMap(
TWO_SLICE_DATA.map((s) => s.name),
)

// ============================================================================
// Stories
// ============================================================================

const meta: Meta<typeof PieChart> = {
component: PieChart,
tags: ['autodocs'],
title: 'Components/Charts/PieChart',
}
export default meta

type Story = StoryObj<typeof PieChart>

export const Donut: Story = {
decorators: [
() => (
<div className='d-flex justify-content-center'>
<div style={{ width: 240 }}>
<p className='text-secondary fs-small mb-3 text-center'>
Donut variant — inner cutout for a secondary label or count.
</p>
<PieChart
data={STAGE_DATA}
colorMap={STAGE_COLOR_MAP}
height={240}
innerRadius={60}
outerRadius={100}
/>
</div>
</div>
),
],
}

export const SolidPie: Story = {
decorators: [
() => (
<div className='d-flex justify-content-center'>
<div style={{ width: 240 }}>
<p className='text-secondary fs-small mb-3 text-center'>
Solid pie — no inner radius.
</p>
<PieChart
data={STAGE_DATA}
colorMap={STAGE_COLOR_MAP}
height={240}
outerRadius={100}
/>
</div>
</div>
),
],
}

export const TwoSlices: Story = {
decorators: [
() => (
<div className='d-flex justify-content-center'>
<div style={{ width: 220 }}>
<p className='text-secondary fs-small mb-3 text-center'>
Minimal case — two slices for a released vs. in-progress split.
</p>
<PieChart
data={TWO_SLICE_DATA}
colorMap={TWO_SLICE_COLOR_MAP}
height={220}
innerRadius={60}
outerRadius={90}
/>
</div>
</div>
),
],
}

export const ManySlices: Story = {
decorators: [
() => (
<div className='d-flex justify-content-center'>
<div style={{ width: 300 }}>
<p className='text-secondary fs-small mb-3 text-center'>
Seven slices — stress-test of the colour palette.
</p>
<PieChart
data={SDK_DATA}
colorMap={SDK_COLOR_MAP}
height={380}
innerRadius={60}
outerRadius={100}
showLegend
/>
</div>
</div>
),
],
}

export const WithLegend: Story = {
decorators: [
() => (
<div className='d-flex justify-content-center'>
<div style={{ width: 300 }}>
<p className='text-secondary fs-small mb-3 text-center'>
Legend enabled — useful when slices are many or narrow.
</p>
<PieChart
data={STAGE_DATA}
colorMap={STAGE_COLOR_MAP}
height={340}
innerRadius={60}
outerRadius={100}
showLegend
/>
</div>
</div>
),
],
}
Loading
Loading