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  }