Table with a Scrollbar

An example of a vertical scrollbar in a simple one-column table. The scrollbar responds to dragging the scrollbar with a mouse, mouse wheel scrolling, and the Home, End, Page Up, Page Down, Arrow Up, and Arrow Down keys.

This Vega example made by Andrzej Leszkiewicz @avatorl

Vega JSON Specification <>

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "description": "An example of a simple table with a scrollbar",
  "width": 300,
  "height": 400,
  "padding": 5,
  "background": "#FFFFFF",
  "config": {
    "title": {"font": "Tahoma", "fontSize": 18},
    "text": {"font": "Tahoma", "fontSize": 16}
  },
  "signals": [
    {
      "name": "rowsToDisplay",
      "description": "The number of rows displayed in the table (visible without scrolling)",
      "value": 10
    },
    {"name": "rowHeight", "description": "Row height in pixels", "value": 40},
    {
      "name": "scrollAreaHeight",
      "description": "Scroll area height in pixels",
      "update": "rowHeight*rowsToDisplay"
    },
    {
      "name": "scrollBarWidth",
      "description": "Scrollbar width in pixels",
      "init": "12"
    },
    {
      "name": "scrollBarHeight",
      "description": "Scrollbar height: Dynamically calculated based on the percentage of visible rows out of all data rows, with a range limited to minimum and maximum values",
      "init": "clamp((rowHeight*rowsToDisplay)*rowsToDisplay/length(data('dataset-raw')),30,600)"
    },
    {
      "name": "scrollPositionMax",
      "update": "length(data('dataset-raw'))-rowsToDisplay+1"
    },
    {
      "name": "scrollbarMouseDragY",
      "init": "0",
      "on": [
        {
          "events": "[@rect-scrollbar:pointerdown, window:pointerup] > window:pointermove",
          "update": "clamp(y(),1,scrollAreaHeight)"
        }
      ]
    },
    {
      "name": "scrollPosition",
      "description": "Scrollbar Position: The scrollbar responds to dragging the scrollbar with a mouse, mouse wheel scrolling, and the Home, End, Page Up, Page Down, Arrow Up, and Arrow Down buttons",
      "value": 1,
      "on": [
        {
          "events": {"type": "wheel", "consume": true},
          "update": "clamp(round(scrollPosition+event.deltaY/abs(event.deltaY)*pow(1.0001, event.deltaY*pow(16, event.deltaMode)),0),1,scrollPositionMax)"
        },
        {
          "events": "window:keydown",
          "update": "event.code=='Home'?1:event.code=='End'?scrollPositionMax:scrollPosition"
        },
        {
          "events": "window:keydown",
          "update": "clamp(event.code=='PageDown'?(scrollPosition+rowsToDisplay):event.code=='PageUp'?(scrollPosition-rowsToDisplay):scrollPosition,1,scrollPositionMax)"
        },
        {
          "events": "window:keydown",
          "update": "clamp(event.code=='ArrowDown'?(scrollPosition+1):event.code=='ArrowUp'?(scrollPosition-1):scrollPosition,1,scrollPositionMax)"
        },
        {
          "events": "[@rect-scrollbar:pointerdown, window:pointerup] > window:pointermove",
          "update": "clamp(round(invert('scaleScrollBarY',scrollbarMouseDragY),0),1,scrollPositionMax)"
        }
      ]
    },
    {
      "name": "scrollbarFillOpacity",
      "value": 0.2,
      "on": [
        {"events": "view:pointerover", "update": "0.4"},
        {"events": "view:pointerout", "update": "0.2"}
      ]
    }
  ],
  "data": [
    {
      "name": "dataset-raw",
      "transform": [
        {"type": "sequence", "start": 1, "stop": 251, "step": 1, "as": "id"}
      ]
    },
    {
      "name": "dataset",
      "source": "dataset-raw",
      "transform": [
        {
          "type": "filter",
          "expr": "(datum.id>=scrollPosition)&&(datum.id<(scrollPosition+rowsToDisplay))"
        },
        {"type": "collect", "sort": {"field": "id", "order": "ascending"}}
      ]
    }
  ],
  "scales": [
    {
      "name": "scaleY",
      "type": "band",
      "domain": {"data": "dataset", "field": "id", "sort": true},
      "range": [
        {"signal": "0"},
        {"signal": "rowHeight*length(data('dataset'))"}
      ]
    },
    {
      "name": "scaleScrollBarY",
      "type": "linear",
      "domain": [1, {"signal": "scrollPositionMax"}],
      "range": [
        {"signal": "0"},
        {"signal": "rowHeight*rowsToDisplay-scrollBarHeight-1"}
      ]
    },
    {
      "name": "scaleRowStripeColors",
      "type": "ordinal",
      "domain": [0, 1],
      "range": ["#FFFFFF", "#EAEAEA"]
    }
  ],
  "title": {"text": "Scrollbar Example"},
  "marks": [
    {
      "name": "rule-scrolltrack-1",
      "type": "rule",
      "encode": {
        "update": {
          "x": {"signal": "width-scrollBarWidth-2"},
          "x2": {"signal": "width-scrollBarWidth-2"},
          "y": {"signal": "0"},
          "y2": {"signal": "scrollAreaHeight"},
          "stroke": {"signal": "'black'"},
          "strokeWidth": {"signal": "0.2"}
        }
      }
    },
    {
      "name": "rule-scrolltrack-2",
      "type": "rule",
      "encode": {
        "update": {
          "x": {"signal": "width"},
          "x2": {"signal": "width"},
          "y": {"signal": "0"},
          "y2": {"signal": "scrollAreaHeight"},
          "stroke": {"signal": "'black'"},
          "strokeWidth": {"signal": "0.2"}
        }
      }
    },
    {
      "name": "rect-scrollbar",
      "type": "rect",
      "encode": {
        "update": {
          "x": {"signal": "width-scrollBarWidth-1"},
          "y": {"scale": "scaleScrollBarY", "signal": "scrollPosition"},
          "width": {"signal": "scrollBarWidth"},
          "height": {"signal": "scrollBarHeight"},
          "fill": {"value": "#666666"},
          "fillOpacity": {"signal": "scrollbarFillOpacity"}
        }
      }
    },
    {
      "name": "rect-table-cell",
      "type": "rect",
      "from": {"data": "dataset"},
      "encode": {
        "update": {
          "x": {"signal": "0"},
          "x2": {"signal": "width-scrollBarWidth-3"},
          "y": {"scale": "scaleY", "field": "id", "band": 0},
          "height": {"signal": "rowHeight"},
          "fill": {
            "scale": "scaleRowStripeColors",
            "signal": "(ceil(datum.id/2,0)==datum.id/2)?1:0"
          }
        }
      }
    },
    {
      "name": "text-cell-content",
      "type": "text",
      "from": {"data": "dataset"},
      "encode": {
        "update": {
          "text": {"signal": "'Test Row #'+datum.id"},
          "dx": {"value": 5},
          "y": {"scale": "scaleY", "field": "id", "band": 0.5},
          "baseline": {"value": "middle"},
          "align": {"value": "left"}
        }
      }
    }
  ]
}