Skip to content

Repository files navigation

Transfer Speed Graph

Transfer Speed Graph

Transfer Speed Graph homepage / NPM Package NPM Version

A lightweight, modular transfer-speed graph inspired by the Windows 8 file transfer dialog, with support for:

  • Real transfer integration (download/upload)
  • Deterministic and random fake transfer simulation
  • Pause/cancel/finish states
  • Highly configurable graph smoothing and dynamic Y-scale behavior

Real Example

Live homepage

Deterministic transfer at 50%

Quick Usage

Install

npm install @arijs/transfer-speed-graph

Then use the object:

import { TransferGraph } from '@arijs/transfer-speed-graph'

const graph = new TransferGraph()

CDN

If you prefer not to use a bundler, you can use either ES module imports or the UMD bundle.

ES module import via jsDelivr:

import { TransferGraph } from 'https://cdn.jsdelivr.net/npm/@arijs/transfer-speed-graph@latest/+esm'

const graph = new TransferGraph()

ES module import via unpkg:

import { TransferGraph } from 'https://unpkg.com/@arijs/transfer-speed-graph@latest?module'

const graph = new TransferGraph()

UMD script via jsDelivr:

<script src="https://cdn.jsdelivr.net/npm/@arijs/transfer-speed-graph@latest/dist/arijs-transfer-speed-graph.umd.js"></script>

UMD script via unpkg:

<script src="https://unpkg.com/@arijs/transfer-speed-graph@latest/dist/arijs-transfer-speed-graph.umd.js"></script>

HTML Script Tag

After loading one of the CDN scripts above, you can initialize the graph with a minimal HTML snippet:

<canvas id="graph" width="416" height="72"></canvas>
<script>
  const graph = new TransferSpeedGraph.TransferGraph()
  const canvasCtx = document.getElementById('graph').getContext('2d')

  graph.setRendererOptions({
    canvasCtx,
    canvasWidth: 416,
    canvasHeight: 72,
  })

  graph.startTransfer({ totalSize: 12 * 1024 * 1024 })
</script>

Download Example

import { TransferGraph } from '@arijs/transfer-speed-graph'

const controller = new TransferGraph()
controller.setOnFrame((view) => {
  // update your UI with view.progressInt, view.speedBps, etc.
})

const downloadUrl = 'https://httpbin.org/bytes/12582912?seed=2026'
const fallbackTotalSize = 12 * 1024 * 1024

controller.setRendererOptions({
  canvasCtx,
  canvasWidth: 416,
  canvasHeight: 72,
})

fetch(downloadUrl).then(async (res) => {
  if (!res.ok || !res.body) throw new Error('Download failed: ' + res.status)

  const totalHeader = parseInt(res.headers.get('content-length') || '0', 10)
  const totalSize = Number.isFinite(totalHeader) && totalHeader > 0
    ? totalHeader
    : fallbackTotalSize

  const reader = res.body.getReader()
  let transferredBytes = 0

  controller.startTransfer({ totalSize })

  while (true) {
    const chunk = await reader.read()
    if (chunk.done) break
    transferredBytes += chunk.value.byteLength
    controller.pushProgress({ transferredBytes, totalSize })
  }

  controller.finishTransfer({ transferredBytes, totalSize })
}).catch((err) => {
  console.warn(err)
  controller.cancel()
})

Source example: real/download-example.js

Upload Example

import { TransferGraph } from '@arijs/transfer-speed-graph'

const controller = new TransferGraph()
const uploadUrl = 'https://httpbin.org/post'
const uploadSize = 10 * 1024 * 1024

const xhr = new XMLHttpRequest()
const payload = new Blob([new Uint8Array(uploadSize)])

controller.setRendererOptions({
  canvasCtx,
  canvasWidth: 416,
  canvasHeight: 72,
})
controller.startTransfer({ totalSize: uploadSize })

xhr.upload.addEventListener('progress', (ev) => {
  if (!ev.lengthComputable) return
  controller.pushProgress({
    transferredBytes: ev.loaded,
    totalSize: ev.total,
  })
})

xhr.addEventListener('load', () => {
  controller.finishTransfer({
    transferredBytes: uploadSize,
    totalSize: uploadSize,
  })
})

xhr.addEventListener('error', () => {
  console.warn('upload failed')
  controller.cancel()
})

xhr.addEventListener('abort', () => {
  controller.cancel()
})

xhr.open('POST', uploadUrl)
xhr.send(payload)

Source example: real/upload-example.js

State Showcase

Paused state:

Paused transfer state

Cancelled state:

Cancelled transfer state

Why This Library Is Strong

Source Files

The published package is split by concern:

  • Graph core:
    • core/main.js public TransferGraph facade that coordinates model, renderer, and runtime APIs.
    • core/model.js transfer state machine plus series, timing, and control state management.
    • core/renderer.js renderer configuration layer that resolves options and delegates frame drawing.
    • core/frame.js canvas frame rendering, graph geometry, and draw-hook execution.
    • core/speed-series.js series utilities for averaging, speed calculations, and coordinate transforms.
  • Fake transfer subsystem:
  • Real transfer examples:

That separation keeps rendering, state management, fake generation, and transport examples independent and easy to evolve.

High Configurability

Graph behavior can be tuned at runtime with controls such as:

  • pixelAverageWindow
  • maxSpeedDecay
  • maxSpeedHeadroom
  • ignoreTrailingSpeedSample

Fake simulation is configurable through simulationOptions, including:

  • minFrame, maxFrame
  • minFrameRepeat, maxFrameRepeat
  • minSizeInc, maxSizeInc
  • deterministicSeed

This allows both realistic noisy profiles and stable deterministic test profiles.

Practical Runtime API

TransferGraph gives a straightforward app-facing API:

  • startTransfer

    Start a new transfer and reset the runtime state.

    Signature:

    startTransfer(config)
    • config.totalSize: required positive total size for the transfer.
    • config.nowMs: optional timestamp used as the transfer start time.

    Example:

    const graph = new TransferGraph()
    graph.startTransfer({ totalSize: 12 * 1024 * 1024 })
  • pushProgress

    Append a new progress update to the active transfer.

    Signature:

    pushProgress(update)
    • update.transferredBytes: current transferred byte count.
    • update.totalSize: optional total size override.
    • update.elapsedMs: optional elapsed time override.
    • update.nowMs: optional timestamp used to resolve elapsed time.

    Example:

    const graph = new TransferGraph()
    graph.pushProgress({ transferredBytes: 4 * 1024 * 1024 })
  • finishTransfer

    Mark the transfer as finished and capture the final point.

    Signature:

    finishTransfer(update)
    • update.transferredBytes: final transferred byte count.
    • update.totalSize: optional total size override.
    • update.elapsedMs: optional elapsed time override.
    • update.nowMs: optional timestamp used to resolve elapsed time.

    Example:

    const graph = new TransferGraph()
    graph.finishTransfer({ transferredBytes: 12 * 1024 * 1024 })
  • cancel

    Cancel the active transfer and freeze it in a finished state.

    Signature:

    cancel()

    Example:

    const graph = new TransferGraph()
    graph.cancel()
  • reset

    Reset the model back to its initial state.

    Signature:

    reset()

    Example:

    const graph = new TransferGraph()
    graph.reset()
  • pause

    Pause elapsed-time tracking without clearing transfer progress.

    Signature:

    pause(nowMs)
    • nowMs: optional timestamp used to freeze elapsed-time calculations.

    Example:

    const graph = new TransferGraph()
    graph.pause()
  • resume

    Resume elapsed-time tracking after a pause.

    Signature:

    resume(nowMs)
    • nowMs: optional timestamp used to resume elapsed-time calculations.

    Example:

    const graph = new TransferGraph()
    graph.resume()
  • toggleFinishedPauseVisual

    Toggle the paused visual state that is shown after a transfer finishes.

    Signature:

    toggleFinishedPauseVisual()

    Example:

    const graph = new TransferGraph()
    graph.toggleFinishedPauseVisual()
  • refreshGraphScale

    Force the renderer to recalculate the speed scale on the next frame.

    Signature:

    refreshGraphScale()

    Example:

    const graph = new TransferGraph()
    graph.refreshGraphScale()
  • setPixelAverageWindow

    Change the rolling average window used by the renderer.

    Signature:

    setPixelAverageWindow(nextWindow)
    • nextWindow: smoothing window size in rendered points.

    Example:

    const graph = new TransferGraph()
    graph.setPixelAverageWindow(16)
  • setMaxSpeedDecay

    Change the decay factor used for the dynamic speed scale.

    Signature:

    setMaxSpeedDecay(nextValue)
    • nextValue: new decay value, typically between 0.5 and 0.999.

    Example:

    const graph = new TransferGraph()
    graph.setMaxSpeedDecay(0.95)
  • setMaxSpeedHeadroom

    Change the headroom factor used for the dynamic speed scale.

    Signature:

    setMaxSpeedHeadroom(nextValue)
    • nextValue: new headroom value, typically between 1 and 2.

    Example:

    const graph = new TransferGraph()
    graph.setMaxSpeedHeadroom(1.08)
  • setOnFrame

    Register a callback that receives the computed frame view model.

    Signature:

    setOnFrame(onFrame)
    • onFrame(view): receives the computed frame view model.

    Example:

    const graph = new TransferGraph()
    graph.setOnFrame((view) => {
    console.log(view.progressInt)
    })
  • setOnControls

    Register a callback that receives the current controls state.

    Signature:

    setOnControls(onControls)
    • onControls(view): receives the current controls state.

    Example:

    const graph = new TransferGraph()
    graph.setOnControls((controls) => {
    console.log(controls.pixelAverageWindow)
    })
  • setOnStateChange

    Register a callback that receives the raw state snapshot.

    Signature:

    setOnStateChange(onStateChange)
    • onStateChange(state): receives the raw state snapshot.

    Example:

    const graph = new TransferGraph()
    graph.setOnStateChange((state) => {
    console.log(state.started, state.finished)
    })

Canvas Drawing Hooks

The renderer also exposes optional hooks for custom canvas drawing. If you do not provide any of them, the default implementation keeps the current visual behavior.

Pass these through new TransferGraph({ ... }) or controller.setRendererOptions({ ... }):

Common argument properties

Every draw hook receives these shared properties:

  • canvasCtx: the active 2D canvas context for the current frame.

  • canvasWidth: the canvas width in pixels.

  • canvasHeight: the canvas height in pixels.

  • drawProgressBar

    Signature:

    drawProgressBar({ canvasCtx, canvasWidth, canvasHeight, lastX, backgroundValue, createDefaultPath })
    • lastX: the resolved filled width of the progress bar in canvas coordinates.
    • backgroundValue: the value used to compute the background fill width.
    • createDefaultPath(): builds the current progress-bar path with beginPath() + rect().

    Default behavior: the renderer calls createDefaultPath(), then fill() and stroke().

    const graph = new TransferGraph({
    drawProgressBar({ canvasCtx, createDefaultPath }) {
      createDefaultPath()
      canvasCtx.fill()
      canvasCtx.stroke()
    },
    })
  • drawGrid

    Signature:

    drawGrid({ canvasCtx, canvasWidth, canvasHeight, gridCols, gridRows, createDefaultPath })
    • gridCols: number of vertical grid divisions.
    • gridRows: number of horizontal grid divisions.
    • createDefaultPath(): builds the current grid paths for both directions.

    Default behavior: the renderer calls createDefaultPath(), then stroke().

    const graph = new TransferGraph({
    drawGrid({ canvasCtx, createDefaultPath }) {
      createDefaultPath()
      canvasCtx.stroke()
    },
    })
  • drawSpeedOverlay

    Signature:

    drawSpeedOverlay({ canvasCtx, canvasWidth, canvasHeight, lastX, endX, avgWithSpeeds, renderPointCount, pixelsPerValue, maxAvgSpeed, createDefaultPath })
    • lastX: the resolved filled width of the progress area.
    • endX: the right edge where the overlay path should continue until the filled progress ends.
    • avgWithSpeeds: the calculated speed points used to draw the overlay.
    • renderPointCount: number of points used when rendering the visible speed curve.
    • pixelsPerValue: the conversion factor from value units to canvas X pixels.
    • maxAvgSpeed: the resolved speed scale used to normalize the overlay height.
    • createDefaultPath(): builds the current speed-area path and returns the resolved point position via the internal path logic.

    Default behavior: the renderer calls createDefaultPath(), then fill().

    const graph = new TransferGraph({
    drawSpeedOverlay({ canvasCtx, createDefaultPath }) {
      createDefaultPath()
      canvasCtx.fill()
    },
    })
  • drawSpeedLineLabel

    Signature:

    drawSpeedLineLabel({ canvasCtx, canvasWidth, canvasHeight, guideY, textX, labelBottomY, labelTopY, labelLeftX, labelWidth, labelPaddingX, labelPaddingY, speedLabel, speedLabelColor, speedLabelBackgroundColor, speedGuideColor, createDefaultLinePath, createDefaultLabelBackgroundPath, fillDefaultLabelText })
    • guideY: the Y position of the horizontal guide line.
    • textX: the right-aligned X position used for the label text.
    • labelBottomY: the baseline Y position for the label text.
    • labelTopY: the top Y position of the background box.
    • labelLeftX: the left X position of the background box.
    • labelWidth: measured width of the label text.
    • labelPaddingX: horizontal padding used by the default label box.
    • labelPaddingY: vertical padding used by the default label box.
    • speedLabel: the formatted label string.
    • speedLabelColor: the current text color.
    • speedLabelBackgroundColor: the current background fill color.
    • speedGuideColor: the current guide-line stroke color.
    • createDefaultLinePath(): builds the current guide-line path.
    • createDefaultLabelBackgroundPath(): builds the current label background path with beginPath() + rect().
    • fillDefaultLabelText(options): draws the current text using fillText(). You can override the fill style or text alignment through options.

    Default behavior: the renderer calls the default line path, strokes it, calls the default background path, fills it, and then calls fillDefaultLabelText().

    const graph = new TransferGraph({
    drawSpeedLineLabel({
      canvasCtx,
      createDefaultLinePath,
      createDefaultLabelBackgroundPath,
      fillDefaultLabelText,
    }) {
      createDefaultLinePath()
      canvasCtx.stroke()
      createDefaultLabelBackgroundPath()
      canvasCtx.fill()
      fillDefaultLabelText()
    },
    })
  • drawBorder

    Signature:

    drawBorder({ canvasCtx, canvasWidth, canvasHeight, borderColor, createDefaultPath })
    • borderColor: the current border stroke color.
    • createDefaultPath(): builds the current canvas border path with beginPath() + rect().

    Default behavior: the renderer calls createDefaultPath(), then stroke().

    const graph = new TransferGraph({
    drawBorder({ canvasCtx, createDefaultPath }) {
      createDefaultPath()
      canvasCtx.stroke()
    },
    })

Development

Install dependencies:

npm install

Run tests / snapshot checks:

npm test

Open the demo UI by loading index.html in a browser.