From de3df1a1b0d5a43e1ca6f0db8a1c312bb71484be Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Thu, 24 Apr 2025 15:46:44 +0800 Subject: [PATCH] init --- .../components/ui/Charts/TimelineChart.tsx | 178 ++++++++++++++++++ .../ui/Charts/TimelineChartExample.tsx | 100 ++++++++++ .../components/ui/Charts/TimelineExample.tsx | 114 +++++++++++ apps/studio/components/ui/Charts/index.ts | 28 +++ apps/studio/pages/candlestick-demo.tsx | 15 ++ apps/studio/pages/timeline-demo.tsx | 15 ++ apps/studio/pages/timeline-example.tsx | 15 ++ 7 files changed, 465 insertions(+) create mode 100644 apps/studio/components/ui/Charts/TimelineChart.tsx create mode 100644 apps/studio/components/ui/Charts/TimelineChartExample.tsx create mode 100644 apps/studio/components/ui/Charts/TimelineExample.tsx create mode 100644 apps/studio/components/ui/Charts/index.ts create mode 100644 apps/studio/pages/candlestick-demo.tsx create mode 100644 apps/studio/pages/timeline-demo.tsx create mode 100644 apps/studio/pages/timeline-example.tsx diff --git a/apps/studio/components/ui/Charts/TimelineChart.tsx b/apps/studio/components/ui/Charts/TimelineChart.tsx new file mode 100644 index 00000000000..5c08081ced6 --- /dev/null +++ b/apps/studio/components/ui/Charts/TimelineChart.tsx @@ -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, 'data'> { + data?: TimelineChartDatum[] + xAxisDomain?: [number, number] + showGrid?: boolean + XAxisProps?: ComponentProps + YAxisProps?: ComponentProps + 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(null) + const { Container } = useChartSize(size) + + if (!data || data.length === 0) { + return + } + + // 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 ( + + + + ) + } + + return ( +
+ {title && ( + + )} + + + {showGrid && } + `${value}${format ? ' ' + format : ''}`} + tickMargin={5} + /> + + { + const item = entry.payload as TimelineChartDatum + return ( +
+
+ Start: + + {item.start} {format} + +
+
+ Duration: + + {item.duration} {format} + +
+
+ End: + + {item.start + item.duration} {format} + +
+
+ ) + }} + /> + } + /> + { + onBarClick && onBarClick(entry as TimelineChartDatum) + }} + onMouseEnter={(entry: any) => setFocusedItemId(entry.id)} + onMouseLeave={() => setFocusedItemId(null)} + isAnimationActive={false} + /> +
+
+
+ ) +} + +export default TimelineChart diff --git a/apps/studio/components/ui/Charts/TimelineChartExample.tsx b/apps/studio/components/ui/Charts/TimelineChartExample.tsx new file mode 100644 index 00000000000..bd47a943696 --- /dev/null +++ b/apps/studio/components/ui/Charts/TimelineChartExample.tsx @@ -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(null) + const [activeDataset, setActiveDataset] = useState<'page' | 'performance'>('page') + + // Handler for when a timeline item is clicked + const handleBarClick = (entry: TimelineChartDatum) => { + setSelectedData(entry) + } + + return ( +
+
+

Timeline Chart

+
+ + +
+
+ + + + {selectedData && ( +
+

Selected Timeline Item

+
+
Task:
+
{selectedData.label}
+
Start Time:
+
{selectedData.start} ms
+
Duration:
+
{selectedData.duration} ms
+
End Time:
+
{selectedData.start + selectedData.duration} ms
+
+
+ )} + +
+

+ This timeline chart displays {activeDataset === 'page' ? 'page load' : 'performance'}{' '} + metrics in milliseconds. +

+

Click on a bar to see detailed information.

+
+
+ ) +} + +export default TimelineChartExample diff --git a/apps/studio/components/ui/Charts/TimelineExample.tsx b/apps/studio/components/ui/Charts/TimelineExample.tsx new file mode 100644 index 00000000000..66e305c19ed --- /dev/null +++ b/apps/studio/components/ui/Charts/TimelineExample.tsx @@ -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 ( + + + + ) + } + + return ( +
+

Page Load Timeline

+ +
+ + + + `${value} ms`} /> + + ( +
+
Start: {entry.payload.start} ms
+
Duration: {entry.payload.duration} ms
+
End: {entry.payload.start + entry.payload.duration} ms
+
+ )} + /> + } + /> + {Object.keys(chartConfig).map((key) => ( + + ))} +
+
+
+ +
+

This timeline chart displays page load metrics in milliseconds.

+

Click on a bar to see detailed information.

+
+
+ ) +} + +export default TimelineExample diff --git a/apps/studio/components/ui/Charts/index.ts b/apps/studio/components/ui/Charts/index.ts new file mode 100644 index 00000000000..95711596830 --- /dev/null +++ b/apps/studio/components/ui/Charts/index.ts @@ -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' diff --git a/apps/studio/pages/candlestick-demo.tsx b/apps/studio/pages/candlestick-demo.tsx new file mode 100644 index 00000000000..6916070c3bc --- /dev/null +++ b/apps/studio/pages/candlestick-demo.tsx @@ -0,0 +1,15 @@ +import CandlestickChartExample from 'components/ui/Charts/CandlestickChartExample' +import { NextPage } from 'next' + +const CandlestickDemoPage: NextPage = () => { + return ( +
+

Candlestick Chart Demo

+
+ +
+
+ ) +} + +export default CandlestickDemoPage diff --git a/apps/studio/pages/timeline-demo.tsx b/apps/studio/pages/timeline-demo.tsx new file mode 100644 index 00000000000..1f4179afe8d --- /dev/null +++ b/apps/studio/pages/timeline-demo.tsx @@ -0,0 +1,15 @@ +import TimelineChartExample from 'components/ui/Charts/TimelineChartExample' +import { NextPage } from 'next' + +const TimelineDemoPage: NextPage = () => { + return ( +
+

Timeline Chart Demo

+
+ +
+
+ ) +} + +export default TimelineDemoPage diff --git a/apps/studio/pages/timeline-example.tsx b/apps/studio/pages/timeline-example.tsx new file mode 100644 index 00000000000..f5aa3808377 --- /dev/null +++ b/apps/studio/pages/timeline-example.tsx @@ -0,0 +1,15 @@ +import TimelineExample from 'components/ui/Charts/TimelineExample' +import { NextPage } from 'next' + +const TimelineExamplePage: NextPage = () => { + return ( +
+

Timeline Example

+
+ +
+
+ ) +} + +export default TimelineExamplePage