This commit is contained in:
Jonathan Summers-Muir committed 2025-04-24 15:46:44 +08:00
1 parent 661e94b0fe
commit de3df1a1b0
7 files changed
+465

No files matched your search

@@ -0,0 +1,178 @@
'use client'
import { useState, ComponentProps } from 'react'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Rectangle } from 'recharts'
import { CHART_COLORS } from './Charts.constants'
import { CommonChartProps } from './Charts.types'
import { useChartSize, numberFormatter } from './Charts.utils'
import ChartHeader from './ChartHeader'
import NoDataPlaceholder from './NoDataPlaceholder'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export interface TimelineChartDatum {
id: string | number
label: string
start: number
duration: number
color?: string
[key: string]: string | number | undefined
}
export interface TimelineChartProps extends Omit<CommonChartProps<any>, 'data'> {
data?: TimelineChartDatum[]
xAxisDomain?: [number, number]
showGrid?: boolean
XAxisProps?: ComponentProps<typeof XAxis>
YAxisProps?: ComponentProps<typeof YAxis>
labelWidth?: number
onBarClick?: (entry: TimelineChartDatum) => void
emptyStateMessage?: string
}
const TimelineChart = ({
data = [],
title,
format,
highlightedValue,
highlightedLabel,
valuePrecision,
className = '',
size = 'normal',
emptyStateMessage,
xAxisDomain,
showGrid = true,
XAxisProps,
YAxisProps,
labelWidth = 120,
onBarClick,
}: TimelineChartProps) => {
const [focusedItemId, setFocusedItemId] = useState<string | number | null>(null)
const { Container } = useChartSize(size)
if (!data || data.length === 0) {
return <NoDataPlaceholder message={emptyStateMessage} size={size} />
}
// Calculate the max end time if domain not provided
const maxEndTime = xAxisDomain
? xAxisDomain[1]
: Math.max(...data.map((item) => item.start + item.duration))
// Create chart config for the timeline bars
const chartConfig: ChartConfig = data.reduce((config, item) => {
config[`item-${item.id}`] = {
label: item.label,
color: item.color || CHART_COLORS.GREEN_1,
}
return config
}, {} as ChartConfig)
// Custom shape renderer for timeline bars
const renderTimelineBar = (props: any) => {
const { x, y, width, height, fill, index } = props
// Calculate position based on start time and width based on duration
const item = data[index]
if (!item) return null
const barWidth = (item.duration / maxEndTime) * width
const barX = x + (item.start / maxEndTime) * width
return (
<g>
<Rectangle
x={barX}
y={y}
width={barWidth}
height={height}
fill={item.color || fill}
opacity={focusedItemId === null || focusedItemId === item.id ? 1 : 0.6}
className={onBarClick ? 'cursor-pointer' : ''}
rx={2}
ry={2}
/>
</g>
)
}
return (
<div className={`flex flex-col gap-y-3 ${className}`}>
{title && (
<ChartHeader
title={title}
format={format}
highlightedValue={highlightedValue}
highlightedLabel={highlightedLabel}
/>
)}
<ChartContainer config={chartConfig} className="min-h-[300px] w-full">
<BarChart
layout="vertical"
data={data}
margin={{ top: 5, right: 30, left: labelWidth, bottom: 5 }}
barSize={20}
barGap={0}
>
{showGrid && <CartesianGrid horizontal={false} stroke="var(--border)" />}
<XAxis
{...XAxisProps}
type="number"
domain={[0, maxEndTime]}
tickFormatter={(value) => `${value}${format ? ' ' + format : ''}`}
tickMargin={5}
/>
<YAxis
{...YAxisProps}
type="category"
dataKey="label"
width={labelWidth}
tickLine={false}
/>
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value, name, entry: any) => {
const item = entry.payload as TimelineChartDatum
return (
<div className="grid gap-1">
<div className="flex justify-between">
<span className="text-foreground-lighter mr-2">Start:</span>
<span>
{item.start} {format}
</span>
</div>
<div className="flex justify-between">
<span className="text-foreground-lighter mr-2">Duration:</span>
<span>
{item.duration} {format}
</span>
</div>
<div className="flex justify-between">
<span className="text-foreground-lighter mr-2">End:</span>
<span>
{item.start + item.duration} {format}
</span>
</div>
</div>
)
}}
/>
}
/>
<Bar
dataKey="duration"
shape={renderTimelineBar}
onClick={(entry: any) => {
onBarClick && onBarClick(entry as TimelineChartDatum)
}}
onMouseEnter={(entry: any) => setFocusedItemId(entry.id)}
onMouseLeave={() => setFocusedItemId(null)}
isAnimationActive={false}
/>
</BarChart>
</ChartContainer>
</div>
)
}
export default TimelineChart
@@ -0,0 +1,100 @@
'use client'
import { useState } from 'react'
import TimelineChart, { TimelineChartDatum } from './TimelineChart'
import { CHART_COLORS } from './Charts.constants'
// Sample timeline data for page load metrics
const sampleData: TimelineChartDatum[] = [
{ id: 1, label: 'documentLoad', start: 0, duration: 200, color: CHART_COLORS.GREEN_1 },
{ id: 2, label: 'documentFetch', start: 0, duration: 50, color: CHART_COLORS.GREEN_2 },
{ id: 3, label: 'resourceFetch', start: 30, duration: 30, color: '#3870FF' },
{ id: 4, label: 'resourceFetch', start: 40, duration: 25, color: '#3870FF' },
{ id: 5, label: 'resourceFetch', start: 100, duration: 50, color: '#3870FF' },
{ id: 6, label: 'resourceFetch', start: 150, duration: 30, color: '#3870FF' },
{ id: 7, label: 'DOMContentLoaded', start: 180, duration: 10, color: CHART_COLORS.RED_1 },
{ id: 8, label: 'onload', start: 195, duration: 5, color: CHART_COLORS.RED_2 },
]
// Sample data for performance metrics - waterfall style
const performanceData: TimelineChartDatum[] = [
{ id: 1, label: 'DNS Lookup', start: 0, duration: 15, color: '#9F7AEA' },
{ id: 2, label: 'TCP Connection', start: 15, duration: 20, color: '#4C51BF' },
{ id: 3, label: 'TLS Handshake', start: 35, duration: 30, color: '#3182CE' },
{ id: 4, label: 'Time to First Byte', start: 65, duration: 25, color: '#38B2AC' },
{ id: 5, label: 'Content Download', start: 90, duration: 60, color: '#48BB78' },
{ id: 6, label: 'DOM Processing', start: 150, duration: 40, color: '#ECC94B' },
{ id: 7, label: 'Render', start: 190, duration: 35, color: '#ED8936' },
]
const TimelineChartExample = () => {
const [selectedData, setSelectedData] = useState<TimelineChartDatum | null>(null)
const [activeDataset, setActiveDataset] = useState<'page' | 'performance'>('page')
// Handler for when a timeline item is clicked
const handleBarClick = (entry: TimelineChartDatum) => {
setSelectedData(entry)
}
return (
<div className="flex flex-col gap-6 p-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Timeline Chart</h2>
<div className="flex gap-2">
<button
className={`px-3 py-1 rounded ${
activeDataset === 'page' ? 'bg-foreground-light text-background' : 'bg-border'
}`}
onClick={() => setActiveDataset('page')}
>
Page Load
</button>
<button
className={`px-3 py-1 rounded ${
activeDataset === 'performance' ? 'bg-foreground-light text-background' : 'bg-border'
}`}
onClick={() => setActiveDataset('performance')}
>
Performance
</button>
</div>
</div>
<TimelineChart
data={activeDataset === 'page' ? sampleData : performanceData}
title={activeDataset === 'page' ? 'Page Load Timeline' : 'Performance Timeline'}
format="ms"
size="large"
xAxisDomain={[0, 250]}
showGrid={true}
onBarClick={handleBarClick}
/>
{selectedData && (
<div className="mt-4 p-3 border rounded">
<h3 className="font-medium mb-2">Selected Timeline Item</h3>
<div className="grid grid-cols-2 gap-2 text-sm">
<div>Task:</div>
<div className="font-mono">{selectedData.label}</div>
<div>Start Time:</div>
<div className="font-mono">{selectedData.start} ms</div>
<div>Duration:</div>
<div className="font-mono">{selectedData.duration} ms</div>
<div>End Time:</div>
<div className="font-mono">{selectedData.start + selectedData.duration} ms</div>
</div>
</div>
)}
<div className="mt-2 text-sm text-foreground-lighter">
<p>
This timeline chart displays {activeDataset === 'page' ? 'page load' : 'performance'}{' '}
metrics in milliseconds.
</p>
<p>Click on a bar to see detailed information.</p>
</div>
</div>
)
}
export default TimelineChartExample
@@ -0,0 +1,114 @@
'use client'
import React from 'react'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from 'recharts'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
// Sample data for the timeline chart
const data = [
{ name: 'documentLoad', start: 0, duration: 200 },
{ name: 'documentFetch', start: 0, duration: 50 },
{ name: 'resourceFetch', start: 30, duration: 30 },
{ name: 'resourceFetch', start: 40, duration: 25 },
{ name: 'resourceFetch', start: 100, duration: 50 },
{ name: 'resourceFetch', start: 150, duration: 30 },
{ name: 'DOMContentLoaded', start: 180, duration: 10 },
{ name: 'onload', start: 195, duration: 5 },
]
// Define chart configuration with colors for each item type
const chartConfig: ChartConfig = {
documentLoad: {
label: 'Document Load',
color: '#10B981', // green
},
documentFetch: {
label: 'Document Fetch',
color: '#10B981', // green
},
resourceFetch: {
label: 'Resource Fetch',
color: '#3B82F6', // blue
},
DOMContentLoaded: {
label: 'DOM Content Loaded',
color: '#EF4444', // red
},
onload: {
label: 'onLoad',
color: '#F97316', // orange
},
}
const TimelineExample = () => {
// Transform data for horizontal bar chart
const transformedData = data.map((item) => ({
...item,
// Create a property for each type to show correct colors
[item.name]: item.duration,
}))
// Compute max time for chart domain
const maxTime = Math.max(...data.map((item) => item.start + item.duration))
// Custom bar renderer to position bars at the correct start position
const renderCustomBar = (props: any) => {
const { x, y, width, height, fill, dataKey, index } = props
const item = data[index]
// Calculate position and width based on timeline data
const fullWidth = width / (maxTime || 1)
const barWidth = item.duration * fullWidth
const barX = x + item.start * fullWidth
return (
<g>
<rect x={barX} y={y} width={barWidth} height={height} fill={fill} rx={2} ry={2} />
</g>
)
}
return (
<div className="p-4">
<h2 className="text-lg font-medium mb-4">Page Load Timeline</h2>
<div className="mb-4" style={{ height: '300px' }}>
<ChartContainer config={chartConfig}>
<BarChart
layout="vertical"
data={transformedData}
margin={{ top: 20, right: 30, left: 120, bottom: 20 }}
barGap={4}
>
<CartesianGrid horizontal={false} />
<XAxis type="number" domain={[0, maxTime]} tickFormatter={(value) => `${value} ms`} />
<YAxis type="category" dataKey="name" width={120} tickLine={false} />
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value, name, entry) => (
<div>
<div>Start: {entry.payload.start} ms</div>
<div>Duration: {entry.payload.duration} ms</div>
<div>End: {entry.payload.start + entry.payload.duration} ms</div>
</div>
)}
/>
}
/>
{Object.keys(chartConfig).map((key) => (
<Bar key={key} dataKey={key} shape={renderCustomBar} isAnimationActive={false} />
))}
</BarChart>
</ChartContainer>
</div>
<div className="text-sm text-foreground-lighter">
<p>This timeline chart displays page load metrics in milliseconds.</p>
<p>Click on a bar to see detailed information.</p>
</div>
</div>
)
}
export default TimelineExample
+28
View File
@@ -0,0 +1,28 @@
export { default as BarChart } from './BarChart'
// LineChart missing, commenting out until it's implemented
// export { default as LineChart } from './LineChart'
export { default as AreaChart } from './AreaChart'
export { default as StackedBarChart } from './StackedBarChart'
export { default as ComposedChart } from './ComposedChart'
export { default as CandlestickChart } from './CandlestickChart'
export { default as TimelineChart } from './TimelineChart'
export { default as ChartHeader } from './ChartHeader'
// Fix wildcard exports by exporting specific items
// From Charts.types
export type { Datum, CommonChartProps } from './Charts.types'
// From Charts.utils
export {
numberFormatter,
useChartSize,
timestampFormatter,
precisionFormatter,
} from './Charts.utils'
// From Charts.constants
export { CHART_COLORS, DateTimeFormats, DEFAULT_STACK_COLORS } from './Charts.constants'
// From useChartHighlight
export { useChartHighlight } from './useChartHighlight'
export type { ChartHighlight } from './useChartHighlight'
+15
View File
@@ -0,0 +1,15 @@
import CandlestickChartExample from 'components/ui/Charts/CandlestickChartExample'
import { NextPage } from 'next'
const CandlestickDemoPage: NextPage = () => {
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-8">Candlestick Chart Demo</h1>
<div className="max-w-4xl mx-auto">
<CandlestickChartExample />
</div>
</div>
)
}
export default CandlestickDemoPage
+15
View File
@@ -0,0 +1,15 @@
import TimelineChartExample from 'components/ui/Charts/TimelineChartExample'
import { NextPage } from 'next'
const TimelineDemoPage: NextPage = () => {
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-8">Timeline Chart Demo</h1>
<div className="max-w-4xl mx-auto">
<TimelineChartExample />
</div>
</div>
)
}
export default TimelineDemoPage
+15
View File
@@ -0,0 +1,15 @@
import TimelineExample from 'components/ui/Charts/TimelineExample'
import { NextPage } from 'next'
const TimelineExamplePage: NextPage = () => {
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-8">Timeline Example</h1>
<div className="max-w-4xl mx-auto">
<TimelineExample />
</div>
</div>
)
}
export default TimelineExamplePage