Verified Commit f2260bb8 authored by Marek Veselý's avatar Marek Veselý
Browse files

refactor: use a slider to select time range for scatter plot

parent 3e256d68
Loading
Loading
Loading
Loading
+0 −2
Original line number Diff line number Diff line
@@ -31,7 +31,6 @@
    "@tanstack/react-router": "^1.109.2",
    "d3-array": "^3.2.4",
    "d3-axis": "^3.0.0",
    "d3-brush": "^3.0.0",
    "d3-format": "^3.1.0",
    "d3-hierarchy": "^3.1.2",
    "d3-polygon": "^3.0.1",
@@ -64,7 +63,6 @@
    "@types/d3": "^7.4.3",
    "@types/d3-array": "^3.2.1",
    "@types/d3-axis": "^3.0.6",
    "@types/d3-brush": "^3.0.6",
    "@types/d3-format": "^3.0.4",
    "@types/d3-hierarchy": "^3",
    "@types/d3-polygon": "^3",
+19 −6
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ import { ActionLogIcon } from '../ActionLogTitle/ActionLogIcon'
import useActionLogs from '../dataHooks/useActionLogs'
import { useTools } from '../dataHooks/useTools'
import type { SelectedAction, SelectedState } from '../Overview/selectedReducer'
import type { TimeRange } from '../Plots/TimeScatterPlot/types'

const centerChildren = css`
  display: flex;
@@ -30,6 +31,8 @@ interface ActionLogProps {
  teamLimit?: number
  displayAction: DisplayActionType
  teamIds: string[]
  selectedTimeRange: TimeRange
  earliestStartTime: number
}

export const ActionLog: FC<ActionLogProps> = ({
@@ -38,16 +41,26 @@ export const ActionLog: FC<ActionLogProps> = ({
  teamLimit,
  displayAction,
  teamIds,
  selectedTimeRange,
  earliestStartTime,
}) => {
  const tools = useTools()

  const actionLogs = useActionLogs({ teamLimit, teamIds }).filter(actionLog =>
  const actionLogs = useActionLogs({ teamLimit, teamIds })
    .filter(actionLog =>
      displayActionFilter({
        actionLog,
        displayAction,
        tools,
      })
    )
    .filter(actionLog => {
      const actionLogTime = new Date(actionLog.timestamp).getTime()
      return (
        actionLogTime >= earliestStartTime + selectedTimeRange.rangeStart &&
        actionLogTime <= earliestStartTime + selectedTimeRange.rangeEnd
      )
    })

  const getHandleClick = useCallback(
    (actionLog: IAnalystActionLogSimple) => () =>
+10 −0
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ import useAutoInjects from '../dataHooks/useAutoInjects'
import { ExerciseContext } from '../ExerciseContext'
import LegendText from '../Plots/LegendText'
import ScatterPlot from '../Plots/TimeScatterPlot'
import type { TimeRange } from '../Plots/TimeScatterPlot/types'
import type { ScatterPlotDataRenderer } from '../Plots/types'
import {
  LEGEND_ELEMENT_SIZE,
@@ -36,6 +37,9 @@ interface OverviewPlotProps {
  teams: Team[]
  showLegend: boolean
  setShowLegend: Dispatch<SetStateAction<boolean>>
  earliestStartTime: number
  latestStartTime: number
  selectedTimeRange: TimeRange
}

const OverviewPlot: FC<OverviewPlotProps> = ({
@@ -47,6 +51,9 @@ const OverviewPlot: FC<OverviewPlotProps> = ({
  teams,
  showLegend,
  setShowLegend,
  earliestStartTime,
  latestStartTime,
  selectedTimeRange,
}) => {
  const { exercise } = useContext(ExerciseContext)

@@ -173,6 +180,9 @@ const OverviewPlot: FC<OverviewPlotProps> = ({
      dataRenderer={dataRenderer}
      showLegend={showLegend}
      setShowLegend={setShowLegend}
      earliestStartTime={earliestStartTime}
      latestStartTime={latestStartTime}
      selectedTimeRange={selectedTimeRange}
    />
  )
}
+33 −67
Original line number Diff line number Diff line
import { Divider } from '@blueprintjs/core'
import { css, cx } from '@emotion/css'
import { css } from '@emotion/css'
import type { Team, VariablesOf } from '@inject/graphql'
import {
  AnalystActionLogsSimpleSubscription,
  useSubscription,
} from '@inject/graphql'
import { CenteredSpinner, HelpIcon } from '@inject/shared'
import { CenteredSpinner } from '@inject/shared'
import type { Dispatch, FC } from 'react'
import { Suspense, useState } from 'react'
import { ActionLog } from '../ActionLog'
import { ActionLogOptions } from '../ActionLogOptions'
import { useActionLogOptionsWithAutoProps } from '../ActionLogOptions/useActionLogOptionsProps'
import type { ActionLogOptionsProps } from '../ActionLogOptions'
import type { TimeRange } from '../Plots/TimeScatterPlot/types'
import Detail from './Detail'
import OverviewPlot from './OverviewPlot'
import type { SelectedAction, SelectedState } from './selectedReducer'
@@ -22,11 +22,6 @@ const wrapper = css`
  flex-direction: column;
`

const options = css`
  display: flex;
  justify-content: space-between;
`

const plot = ({
  teamCount,
  showLegend,
@@ -35,58 +30,41 @@ const plot = ({
  showLegend: boolean
}) => css`
  /* make sure the legend is visible even for 1 team */
  min-height: ${(showLegend ? 20 : 10) + 1.5 * teamCount}rem;
  min-height: ${Math.max(showLegend ? 20 : 10, 10 + 1.5 * teamCount)}rem;
`

const footer = css`
  flex: 1;
  display: flex;
  overflow: auto;
`
const footerAllTeams = css`
  min-height: 20rem;
  min-height: 15rem;
`

const footerElement = css`
  flex: 1;
  overflow: auto;
  display: flex;
  min-height: 15rem;
`
const footerElementAllTeams = css`
  min-height: 20rem;
`

const HELP_TEXT = `\
ZOOM:

Select an area in the graph using the left mouse button\
 and pull it over the part of the graph you would like to see.
To reset, click on the button below the graph.

FILTERS:

Click on the filter icon in the top right corner\
 to open the filters menu.

SELECTION:

Click on an event to select it.
Selecting an event will highlight it in the graph.
Selecting an event will display its details below the graph.
Selecting an event will highlight other similar events.
To reset, click on the button below the graph or click on the\
 selected event again.`

interface OverviewProps {
  selectedState: SelectedState
  selectedDispatch: Dispatch<SelectedAction>
  teams: Team[]
  earliestStartTime: number
  latestStartTime: number
  selectedTimeRange: TimeRange
  actionLogOptionsProps: Required<ActionLogOptionsProps>
}

export const Overview: FC<OverviewProps> = ({
  selectedState,
  selectedDispatch,
  teams,
  earliestStartTime,
  latestStartTime,
  selectedTimeRange,
  actionLogOptionsProps,
}) => {
  useSubscription({
    document: AnalystActionLogsSimpleSubscription,
@@ -96,22 +74,12 @@ export const Overview: FC<OverviewProps> = ({
    pause: !teams.length,
  })

  const actionLogOptionsProps = useActionLogOptionsWithAutoProps({
    teamCount: teams.length,
    teamLimitDefault: 20,
    selectedTeamId: teams.length === 1 ? teams[0].id : null,
  })
  const { displayAuto, displayAction, teamLimit } = actionLogOptionsProps

  const [showLegend, setShowLegend] = useState(true)

  return (
    <div className={wrapper}>
      <div className={options}>
        <HelpIcon text={HELP_TEXT} />
        <ActionLogOptions {...actionLogOptionsProps} />
      </div>

      <div className={plot({ teamCount: teams.length, showLegend })}>
        <Suspense fallback={<CenteredSpinner />}>
          <OverviewPlot
@@ -123,34 +91,30 @@ export const Overview: FC<OverviewProps> = ({
            teams={teams}
            showLegend={showLegend}
            setShowLegend={setShowLegend}
            earliestStartTime={earliestStartTime}
            latestStartTime={latestStartTime}
            selectedTimeRange={selectedTimeRange}
          />
        </Suspense>
      </div>

      <Divider />
      <Divider
        className={css`
          margin: 0.5rem 1rem;
        `}
      />

      <div
        className={cx({
          [footer]: true,
          [footerAllTeams]: teams.length > 1,
        })}
      >
        <div
          className={cx({
            [footerElement]: true,
            [footerElementAllTeams]: teams.length > 1,
          })}
        >
      <div className={footer}>
        <div className={footerElement}>
          <Detail selectedState={selectedState} displayAuto={displayAuto} />
        </div>

        <Divider />
        <div
          className={cx({
            [footerElement]: true,
            [footerElementAllTeams]: teams.length > 1,
          })}
        >
        <Divider
          className={css`
            margin: 1rem 0.5rem;
          `}
        />
        <div className={footerElement}>
          <Suspense fallback={<CenteredSpinner />}>
            <ActionLog
              selectedState={selectedState}
@@ -158,6 +122,8 @@ export const Overview: FC<OverviewProps> = ({
              teamLimit={teamLimit}
              displayAction={displayAction}
              teamIds={teams.map(team => team.id)}
              selectedTimeRange={selectedTimeRange}
              earliestStartTime={earliestStartTime}
            />
          </Suspense>
        </div>
+0 −47
Original line number Diff line number Diff line
import { brushX } from 'd3-brush'
import { select } from 'd3-selection'
import type { FC } from 'react'
import { useEffect, useRef } from 'react'
import type { ScatterPlotTime, XScale } from './types'

interface BrushProps {
  setTime: ({ startTime, endTime }: ScatterPlotTime) => void
  width: number
  height: number
  xScale: XScale
}

const Brush: FC<BrushProps> = ({ setTime, width, height, xScale }) => {
  const ref = useRef<SVGGElement>(null)

  const brush = brushX()
    .extent([
      [0, 0],
      [width, Math.max(height - 1, 0)],
    ])
    // eslint-disable-next-line react-compiler/react-compiler
    .on('end', event => {
      if (event.selection == null) {
        return
      }

      if (ref.current) {
        select(ref.current).call(brush.move, null)
      }

      setTime({
        startTime: xScale.invert(event.selection[0]).getTime(),
        endTime: xScale.invert(event.selection[1]).getTime(),
      })
    })

  useEffect(() => {
    if (ref.current) {
      select(ref.current).call(brush)
    }
  }, [brush, xScale])

  return <g ref={ref} />
}

export default Brush
Loading