go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/components/timeline/timeline.tsx (about) 1 // Copyright 2024 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { Box, styled } from '@mui/material'; 16 import { scaleLinear, scaleTime, timeMillisecond } from 'd3'; 17 import { DateTime } from 'luxon'; 18 import { ReactNode, useMemo } from 'react'; 19 20 import { PREDEFINED_TIME_INTERVALS } from '@/common/constants/time'; 21 import { roundDown } from '@/generic_libs/tools/utils'; 22 23 import { V_GRID_LINE_MAX_GAP } from './constants'; 24 import { TimelineConfig, TimelineContextProvider } from './context'; 25 26 const Container = styled(Box)` 27 display: grid; 28 grid-template-areas: 29 'top-label top-axis' 30 'side-panel body' 31 'bottom-label bottom-axis'; 32 grid-template-columns: auto 1fr; 33 `; 34 35 export interface TimelineProps { 36 readonly startTime: DateTime; 37 readonly endTime: DateTime; 38 readonly itemCount: number; 39 readonly itemHeight: number; 40 readonly sidePanelWidth: number; 41 readonly bodyWidth: number; 42 readonly children: ReactNode; 43 } 44 45 export function Timeline({ 46 startTime, 47 endTime, 48 itemCount, 49 bodyWidth, 50 sidePanelWidth, 51 itemHeight, 52 children, 53 }: TimelineProps) { 54 const startTimeMs = startTime.toMillis(); 55 const endTimeMs = endTime.toMillis(); 56 57 const config = useMemo<TimelineConfig>(() => { 58 const maxInterval = 59 (endTimeMs - startTimeMs) / (bodyWidth / V_GRID_LINE_MAX_GAP); 60 61 return { 62 startTimeMs, 63 itemCount, 64 itemHeight, 65 sidePanelWidth, 66 bodyWidth, 67 xScale: scaleTime() 68 .domain([startTimeMs, endTimeMs]) 69 // Ensure the left border is not rendered. 70 .range([-1, bodyWidth]), 71 yScale: scaleLinear() 72 .domain([0, itemCount]) 73 // Ensure the top border is not rendered. 74 .range([-1, itemCount * itemHeight]), 75 timeInterval: timeMillisecond.every( 76 roundDown(maxInterval, PREDEFINED_TIME_INTERVALS), 77 )!, 78 }; 79 }, [ 80 startTimeMs, 81 endTimeMs, 82 itemCount, 83 itemHeight, 84 bodyWidth, 85 sidePanelWidth, 86 ]); 87 88 return ( 89 <TimelineContextProvider config={config}> 90 <Container>{children}</Container> 91 </TimelineContextProvider> 92 ); 93 }