TanStack
Catalog

Streaming window preservation

interaction

396 lines · 3 files · 10.8 kB

cases/86-streaming-window-preservation/example.tsx288 lines · entry
cases/86-streaming-window-preservation/example.tsx
import { useEffect, useMemo, useRef, useState } from 'react'
import { defineChart, dot, lineY } from '@tanstack/charts'
import { decorative } from '@tanstack/charts/mark/decorative'
import { tooltip } from '@tanstack/charts/tooltip'
import { Chart } from '@tanstack/charts/react'
import { downloads } from '@tanstack/charts-data/downloads'
import { scaleLinear, scaleUtc } from 'd3-scale'
import { streamingData } from './selection'
import {
  formatStreamingDate,
  streamingStatus,
  streamingViewportForMode,
  streamingViewportLabel,
  visibleStreamingData,
} from './model'
import type { ChartScene } from '@tanstack/charts'
import type { DownloadsRow } from '@tanstack/charts-data/downloads'
import type { StreamingViewportMode } from './model'

const color = '#2563eb'

export function streamingWindowDefinition(
  rows: readonly DownloadsRow[],
  viewport: readonly [Date, Date],
  viewportMode: StreamingViewportMode,
) {
  const visibleRows = visibleStreamingData(rows, viewport)
  return defineChart({
    marks: [
      decorative(
        lineY(visibleRows, {
          id: 'stream-line',
          x: 'date',
          y: 'downloads',
          stroke: color,
          strokeWidth: 2.5,
        }),
      ),
      dot(visibleRows, {
        id: 'stream-points',
        x: 'date',
        y: 'downloads',
        fill: color,
        r: 3.5,
        stroke: '#ffffff',
        strokeWidth: 1,
      }),
    ],
    x: {
      scale: scaleUtc().domain(viewport),
      axis: {
        ticks: {
          format: (value) =>
            value.toLocaleDateString(undefined, {
              month: 'short',
              day: 'numeric',
              timeZone: 'UTC',
            }),
        },
        label: streamingViewportLabel(viewportMode),
      },
    },
    y: {
      scale: scaleLinear,
      grid: true,
      axis: { ticks: { count: 5 }, label: 'Downloads' },
    },
    margin: { top: 18, right: 24, bottom: 44, left: 58 },
    svgAnimation: false,
    keyboard: true,
    tooltip: {
      use: tooltip,
      format: (point) =>
        `${formatStreamingDate(point.datum.date)} · ${point.datum.downloads.toLocaleString()} downloads`,
    },
  })
}

function ControlButton({
  children,
  control,
  label,
  onClick,
  pressed,
}: {
  children: string
  control: 'append' | 'follow' | 'all'
  label: string
  onClick: () => void
  pressed?: boolean
}) {
  return (
    <button
      type="button"
      data-streaming-control={control}
      aria-label={label}
      aria-pressed={pressed}
      onClick={onClick}
      style={{
        minWidth: 0,
        minHeight: 44,
        padding: '0 10px',
        border: '1px solid color-mix(in srgb, CanvasText 22%, transparent)',
        borderRadius: 7,
        background: 'Canvas',
        color: 'CanvasText',
        font: '600 12px/1.15 system-ui, sans-serif',
        cursor: 'pointer',
      }}
    >
      {children}
    </button>
  )
}

export interface ExampleProps {
  width?: number
  height?: number
  revision?: number
}

export default function StreamingExample({
  width = 640,
  height = 480,
  revision = 0,
}: ExampleProps = {}) {
  const input = { width, height, revision, preview: false, interactive: true }
  const idPrefix = '86-streaming-window-preservation'
  const viewRef = useRef<HTMLDivElement>(null)

  const chartSurfaceRef = useRef<HTMLDivElement>(null)

  const sceneRef = useRef<ChartScene<DownloadsRow, Date, number> | null>(null)

  const [appended, setAppended] = useState(0)

  const [viewportMode, setViewportMode] =
    useState<StreamingViewportMode>('locked')

  const [announcement, setAnnouncement] = useState('')

  const rows = useMemo(
    () => streamingData(downloads, input.revision, appended),
    [appended, input.revision],
  )

  const viewport = useMemo(
    () => streamingViewportForMode(rows, viewportMode),
    [rows, viewportMode],
  )

  const stateRef = useRef({ rows, appended, viewport, viewportMode })

  stateRef.current = { rows, appended, viewport, viewportMode }

  const chartHeight = Math.max(180, input.height - 78)

  const definition = useMemo(
    () => streamingWindowDefinition(rows, viewport, viewportMode),
    [rows, viewport, viewportMode],
  )

  const status = streamingStatus({
    rows,
    viewport,
    viewportMode,
    announcement,
  })

  useEffect(() => {
    setAnnouncement('')
  }, [input.revision])

  const append = () => {
    const nextAppended = appended + 1
    const nextRows = streamingData(downloads, input.revision, nextAppended)
    const nextViewport = streamingViewportForMode(nextRows, viewportMode)
    const added = nextRows.at(-1)
    setAppended(nextAppended)
    setAnnouncement(
      added
        ? `Added ${formatStreamingDate(added.date)} (${added.downloads.toLocaleString()} downloads). ${
            visibleStreamingData([added], nextViewport).length
              ? 'The new sample is visible.'
              : `It is outside the locked viewport ending ${formatStreamingDate(nextViewport[1])}.`
          }`
        : '',
    )
  }

  return (
    <div
      ref={viewRef}
      data-conformance-view="main"
      style={{
        display: 'grid',
        gridTemplateRows: '78px minmax(0, 1fr)',
        width: input.width,
        height: input.height,
      }}
    >
      <div
        role="group"
        aria-label="Streaming viewport controls"
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
          gridTemplateRows: '44px 18px',
          alignItems: 'center',
          gap: '4px 8px',
          padding: '6px 10px',
          boxSizing: 'border-box',
          background: 'Canvas',
          color: 'CanvasText',
          font: '500 12px/1.2 system-ui, sans-serif',
        }}
      >
        <ControlButton
          control="append"
          label="Append one sample"
          onClick={append}
        >
          Append
        </ControlButton>
        <ControlButton
          control="follow"
          label="Follow the latest eight samples"
          pressed={viewportMode === 'latest'}
          onClick={() => {
            setViewportMode('latest')
            const nextViewport = streamingViewportForMode(rows, 'latest')
            setAnnouncement(
              `Following the latest samples through ${formatStreamingDate(nextViewport[1])}.`,
            )
          }}
        >
          Follow latest
        </ControlButton>
        <ControlButton
          control="all"
          label="Unlock the viewport and show every sample"
          pressed={viewportMode === 'all'}
          onClick={() => {
            setViewportMode('all')
            setAnnouncement(
              `Viewport unlocked. Showing all ${rows.length} samples.`,
            )
          }}
        >
          Show all
        </ControlButton>
        <output
          data-conformance-streaming-status
          aria-live="polite"
          aria-atomic="true"
          title={status}
          style={{
            gridColumn: '1 / -1',
            overflow: 'hidden',
            textOverflow: 'ellipsis',
            whiteSpace: 'nowrap',
            color: 'currentColor',
            opacity: 0.72,
          }}
        >
          {status}
        </output>
      </div>
      <div
        ref={chartSurfaceRef}
        style={{ minHeight: 0, width: input.width, height: chartHeight }}
      >
        <Chart
          idPrefix={idPrefix}
          definition={definition}
          width={input.width}
          height={chartHeight}
          ariaLabel={`Package downloads · ${streamingViewportLabel(
            viewportMode,
          )}`}
          onRender={({ scene }) => {
            sceneRef.current = scene
          }}
        />
      </div>
    </div>
  )
}
cases/86-streaming-window-preservation/model.ts90 lines · dependency
cases/86-streaming-window-preservation/model.ts
import type { DownloadsRow } from '@tanstack/charts-data/downloads'

export type StreamingViewportMode = 'locked' | 'latest' | 'all'

export const streamingViewportDomain: readonly [Date, Date] = [
  new Date('2018-10-05T00:00:00.000Z'),
  new Date('2018-10-12T00:00:00.000Z'),
]

const streamingViewportSpan =
  streamingViewportDomain[1].getTime() - streamingViewportDomain[0].getTime()

export function visibleStreamingData(
  rows: readonly DownloadsRow[],
  domain: readonly [Date, Date] = streamingViewportDomain,
) {
  const startTime = domain[0].getTime()
  const endTime = domain[1].getTime()
  return rows.filter((row) => {
    const time = row.date.getTime()
    return time >= startTime && time <= endTime
  })
}

export function streamingDateKey(date: Date) {
  return date.toISOString().slice(0, 10)
}

export function latestStreamingViewport(
  rows: readonly DownloadsRow[],
): readonly [Date, Date] {
  const end = rows.at(-1)?.date ?? streamingViewportDomain[1]
  return [new Date(end.getTime() - streamingViewportSpan), end]
}

export function fullStreamingViewport(
  rows: readonly DownloadsRow[],
): readonly [Date, Date] {
  return [
    rows[0]?.date ?? streamingViewportDomain[0],
    rows.at(-1)?.date ?? streamingViewportDomain[1],
  ]
}

export function streamingViewportForMode(
  rows: readonly DownloadsRow[],
  mode: StreamingViewportMode,
): readonly [Date, Date] {
  switch (mode) {
    case 'latest':
      return latestStreamingViewport(rows)
    case 'all':
      return fullStreamingViewport(rows)
    default:
      return streamingViewportDomain
  }
}

export function streamingViewportLabel(mode: StreamingViewportMode) {
  switch (mode) {
    case 'latest':
      return 'Following latest'
    case 'all':
      return 'All samples'
    default:
      return 'Locked viewport'
  }
}

export function streamingStatus(state: {
  rows: readonly DownloadsRow[]
  viewport: readonly [Date, Date]
  viewportMode: StreamingViewportMode
  announcement?: string
}) {
  if (state.announcement) return state.announcement
  return `${state.rows.length} samples · ${formatStreamingDate(
    state.viewport[0],
  )}${formatStreamingDate(state.viewport[1])} · ${streamingViewportLabel(
    state.viewportMode,
  )}`
}

export function formatStreamingDate(date: Date) {
  return date.toLocaleDateString(undefined, {
    month: 'short',
    day: 'numeric',
    timeZone: 'UTC',
  })
}
cases/86-streaming-window-preservation/selection.ts18 lines · dependency
cases/86-streaming-window-preservation/selection.ts
import type { DownloadsRow } from '@tanstack/charts-data/downloads'

const streamingStart = Date.parse('2018-10-01T00:00:00.000Z')
const streamingEnd = Date.parse('2018-10-31T00:00:00.000Z')

export function streamingData(
  sourceRows: readonly DownloadsRow[],
  revision = 0,
  appended = 0,
): readonly DownloadsRow[] {
  const streamingRows = sourceRows.filter(
    (row) =>
      row.date.getTime() >= streamingStart &&
      row.date.getTime() <= streamingEnd,
  )
  const advanced = Math.abs(Math.trunc(revision)) % 2
  return streamingRows.slice(advanced, advanced + 12 + appended)
}