go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/components/timeline/top_axis.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 { useTheme } from '@mui/material';
    16  import { axisTop, select } from 'd3';
    17  import { Duration } from 'luxon';
    18  import { useEffect, useRef } from 'react';
    19  
    20  import { displayDuration } from '@/common/tools/time_utils';
    21  
    22  import { TEXT_HEIGHT, TEXT_MARGIN, TOP_AXIS_HEIGHT } from './constants';
    23  import { useRulerState, useTimelineConfig } from './context';
    24  
    25  function TopAxisRuler() {
    26    const state = useRulerState();
    27    const config = useTimelineConfig();
    28    const startMs = config.startTimeMs;
    29    const selectedMs = state ? config.xScale.invert(state).getTime() : null;
    30  
    31    return (
    32      <>
    33        <line
    34          opacity={state === null ? 0 : 1}
    35          x1={state || 0}
    36          x2={state || 0}
    37          y2={-TOP_AXIS_HEIGHT}
    38          stroke="red"
    39          pointerEvents="none"
    40        />
    41        <text
    42          opacity={state === null ? 0 : 1}
    43          x={(state || 0) + TEXT_MARGIN}
    44          y={-TEXT_HEIGHT - TEXT_MARGIN}
    45          fill="red"
    46          textAnchor="start"
    47        >
    48          {selectedMs ? (
    49            selectedMs > startMs ? (
    50              <>
    51                {displayDuration(Duration.fromMillis(selectedMs - startMs))} since
    52                start
    53              </>
    54            ) : (
    55              <>
    56                {displayDuration(Duration.fromMillis(startMs - selectedMs))}{' '}
    57                before start
    58              </>
    59            )
    60          ) : (
    61            ''
    62          )}
    63        </text>
    64      </>
    65    );
    66  }
    67  
    68  export function TopAxis() {
    69    const theme = useTheme();
    70    const config = useTimelineConfig();
    71  
    72    const topAxisElement = useRef<SVGGElement>(null);
    73    useEffect(() => {
    74      const topAxis = axisTop(config.xScale).ticks(config.timeInterval);
    75      topAxis(select(topAxisElement.current!));
    76    }, [config.xScale, config.timeInterval]);
    77  
    78    return (
    79      <svg
    80        width={config.bodyWidth}
    81        height={TOP_AXIS_HEIGHT}
    82        css={{
    83          gridArea: 'top-axis',
    84          position: 'sticky',
    85          top: 0,
    86          zIndex: 1,
    87          background: theme.palette.background.default,
    88        }}
    89      >
    90        <g transform="translate(0.5, 0.5)">
    91          <g transform={`translate(0, ${TOP_AXIS_HEIGHT - 1})`}>
    92            <g ref={topAxisElement}></g>
    93            <TopAxisRuler />
    94          </g>
    95        </g>
    96        <path
    97          d={`m${config.bodyWidth - 0.5},0v${TOP_AXIS_HEIGHT}`}
    98          stroke="currentcolor"
    99        />
   100      </svg>
   101    );
   102  }