Skip to content

Instantly share code, notes, and snippets.

@Giammaria
Last active February 25, 2025 12:51
Show Gist options
  • Save Giammaria/d2b890b92c8591e1351e19869fe80723 to your computer and use it in GitHub Desktop.
Save Giammaria/d2b890b92c8591e1351e19869fe80723 to your computer and use it in GitHub Desktop.
20250219_pointer_&_screen_input_testing_v1_v
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"description": "Zooming and panning in this visualization are controlled through both pointer and touch interactions. Zooming occurs via the mouse wheel or pinch gestures. When using the wheel, the zoom level is adjusted based on the scroll direction and smoothed when the direction remains consistent. The zoom range is clamped between 0.6 and 1.8 to prevent excessive zooming. When performing a pinch gesture, the zoom level updates based on the ratio between the current and initial pinch distances. Zooming is centered on the pointer location unless the Control key is pressed, in which case it zooms around the center of the data domain. Panning occurs when the user clicks and drags or performs a single-finger touch drag. The pan movement is calculated as the difference between the initial and current pointer positions, adjusting the x and y domains accordingly. Panning is disabled when more than one touch is active to prevent conflicts with pinch zooming. The visualization ensures that zoom and pan interactions smoothly update without abrupt jumps or unexpected domain changes.",
"autosize": "pad",
"padding": 0,
"signals": [
{
"name": "width",
"description": "The width of the visualization, initialized to 75% of the window width.",
"init": "windowSize()[0]*0.75"
},
{
"name": "height",
"description": "The height of the visualization, initialized to 75% of the window height.",
"init": "windowSize()[1]*0.75"
},
{
"name": "ctrlPressed",
"description": "Tracks whether the Control key is currently pressed. It updates to 'true' on keydown if the Control key is pressed and resets to 'false' on keyup.",
"value": false,
"on": [
{"events": "window:keydown[event.ctrlKey]{0, 100}", "update": "true"},
{"events": "window:keyup{0, 100}", "update": "false"}
]
},
{
"name": "xRange",
"description": "The pixel range for the x-axis, spanning from 0 to width.",
"update": "[0, width]"
},
{
"name": "yRange",
"description": "The pixel range for the y-axis, spanning from height (top) to 0 (bottom).",
"update": "[height, 0]"
},
{
"name": "xOffset",
"description": "Offset for positioning the x-axis label, determined by height and bottom padding.",
"update": "-(height + padding.bottom)"
},
{
"name": "yOffset",
"description": "Offset for positioning the y-axis label, determined by width and left padding.",
"update": "-(width + padding.left)"
},
{
"name": "pointerDownCoordinates",
"description": "Stores the coordinates where the pointer was pressed. On pointerdown, stores the pointer coordinates (xy(group())). On touchstart, stores the coordinates only if there is exactly one finger.",
"value": null,
"on": [
{"events": "pointerdown", "update": "xy(group())"},
{
"events": "touchstart",
"update": "event.touches.length === 1 ? xy() : pointerDownCoordinates"
}
]
},
{
"name": "singleTouch",
"description": "Tracks whether exactly one finger is touching the screen. On touchstart, updates to true if one finger is touching. On touchend, updates to false.",
"value": false,
"on": [
{"events": "touchstart", "update": "event.touches.length === 1"},
{"events": "touchend", "update": "false"}
]
},
{
"name": "dragDelta",
"description": "Tracks the change in position during drag movements (x, y). Updates when the pointer moves after being pressed or when a single touch moves. Calculates delta relative to the initial press point.",
"value": [0, 0],
"on": [
{
"events": "[pointerdown, pointerup] > mousemove",
"consume": true,
"update": "pointerDownCoordinates ? [pointerDownCoordinates[0] - x(), y() - pointerDownCoordinates[1]] : [0, 0]"
},
{
"events": "[touchstart, touchend] > touchmove",
"consume": true,
"update": "singleTouch && pointerDownCoordinates ? [pointerDownCoordinates[0] - x(), y() - pointerDownCoordinates[1]] : [0, 0]"
}
]
},
{
"name": "touchCount",
"description": "Tracks the number of active touch points on the screen. Updates on 'touchmove' based on event.touches.length and resets to null on 'touchend'.",
"value": null,
"on": [
{
"events": "[touchstart, touchend] > touchmove",
"consume": true,
"update": " event.touches.length"
},
{"events": "touchend", "update": " null"}
]
},
{
"name": "pinchDistanceInitial",
"description": "Stores the initial distance between two touch points at the start of a pinch gesture. Updates on 'touchstart' with two fingers and smooths values on 'touchmove'. Resets to null on 'touchend'.",
"value": null,
"on": [
{
"events": {
"type": "touchstart",
"filter": "event.touches.length === 2"
},
"update": "sqrt(pow(event.touches[0].clientX - event.touches[1].clientX, 2) + pow(event.touches[0].clientY - event.touches[1].clientY, 2))"
},
{
"events": {
"type": "touchmove",
"filter": "event.touches.length === 2"
},
"update": "pinchDistanceInitial !== null ? pinchDistanceInitial * 0.98 + pinchDistance * 0.02 : pinchDistance"
},
{"events": "touchend", "update": "null"}
]
},
{
"name": "pinchDistance",
"description": "Tracks the current distance between two fingers. On touchmove, calculates the Euclidean distance between touch points.",
"value": null,
"on": [
{
"events": {
"type": "touchmove",
"consume": true,
"filter": "event.touches.length === 2"
},
"update": "clamp(sqrt(pow(event.touches[0].clientX - event.touches[1].clientX, 2) + pow(event.touches[0].clientY - event.touches[1].clientY, 2)), 0, width/2)"
}
]
},
{
"name": "zoomDelta",
"description": "Stores the delta value of the scroll wheel movement, representing the zoom direction and magnitude. Updates on 'wheel' with the negative deltaY value.",
"value": 0,
"on": [{"events": "wheel!", "update": "-event.deltaY"}]
},
{
"name": "deltaMode",
"description": "change in event direction",
"value": 0,
"on": [{"events": "wheel!", "update": "event.deltaMode"}]
},
{
"name": "prevZoomDelta",
"description": "Stores the previous zoom delta value to track direction changes in zooming. Resets when zoom direction changes.",
"value": 0,
"on": [{"events": {"signal": "zoom"}, "update": "zoomDelta"}]
},
{
"name": "zoom",
"description": "Controls the zoom level of the visualization. Clamped between 0.6 and 1.8. Updates dynamically on wheel zooming and pinch gestures, with smoothing applied when zoom direction is consistent.",
"value": 1,
"on": [
{
"events": "wheel!{0,1}",
"update": "clamp((zoomDelta * prevZoomDelta > 0 ? zoom * 0.7 + (zoom * pow(1.0005, zoomDelta)) * 0.3 : zoom * pow(1.0005, zoomDelta)), 0.6, 1.8)"
},
{
"events": {"signal": "pinchDistance"},
"update": "clamp(isValid(pinchDistanceInitial) && (zoomDelta * prevZoomDelta > 0 ? zoom * 0.85 + (zoom * pow(pinchDistance / pinchDistanceInitial, 0.03)) * 0.15 : zoom * pow(pinchDistance / pinchDistanceInitial, 0.03)), 0.6, 1.8)"
}
]
},
{
"name": "zoomAnchor",
"description": "Determines the anchor point for zooming based on user interactions. When the Control key is pressed, zooms around the center. Otherwise, zooms based on pointer location or touch gestures.",
"value": [0, 0],
"on": [
{
"events": "wheel",
"update": "ctrlPressed ? [(xDomain[0] + xDomain[1]) / 2, (yDomain[0] + yDomain[1]) / 2] : [invert('xScale', x()), invert('yScale', y())]"
},
{
"events": "[pointerdown, pointerup] > mousemove",
"consume": true,
"update": "[invert('xScale', x()), invert('yScale', y())]"
},
{
"events": {
"type": "touchstart",
"filter": "event.touches.length === 1"
},
"update": "[(xDomain[0] + xDomain[1]) / 2, (yDomain[0] + yDomain[1]) / 2]"
}
]
},
{
"name": "xDomainInitial",
"description": "Stores the initial x-domain",
"init": "slice(xRange)"
},
{
"name": "yDomainInitial",
"description": "Stores the initial x-domain",
"init": "slice(yRange)"
},
{
"name": "xDomainInstanceInitial",
"description": "Stores the initial x-domain when interaction begins. On pointerdown (if no touch is active), stores the xDomain. On touchstart (if two fingers are used), stores the xDomain.",
"init": "[xRange]",
"on": [
{
"events": "pointerdown",
"filter": "event.touches.length === 0",
"update": "slice(xDomain)"
},
{
"events": {
"type": "touchstart",
"filter": "event.touches.length === 2"
},
"update": "slice(xDomain)"
}
]
},
{
"name": "yDomainInstanceInitial",
"description": "Stores the initial y-domain when interaction begins. On pointerdown (if no touch is active), stores the yDomain. On touchstart (if two fingers are used), stores the yDomain.",
"init": "slice(yDomain)",
"on": [
{
"events": "pointerdown",
"filter": "event.touches.length === 0",
"update": "slice(yDomain)"
},
{
"events": {
"type": "touchstart",
"filter": "event.touches.length === 2"
},
"update": "slice(yDomain)"
}
]
},
{
"name": "xDomain",
"description": "The current x-axis domain, updated during panning and zooming. On dragDelta, pans the domain unless more than one touch is active. On zoom, zooms in/out around zoomAnchor. On On touchmove with 2 fingers down, updates when a pinch-zoom occurs.",
"init": "slice(xDomainInitial)",
"on": [
{
"events": {"signal": "dragDelta"},
"update": "touchCount > 1 ? xDomain : [xDomainInstanceInitial[0] + span(xDomainInstanceInitial) * dragDelta[0] / width, xDomainInstanceInitial[1] + span(xDomainInstanceInitial) * dragDelta[0] / width]"
},
{
"events": {"signal": "zoom"},
"update": "[zoomAnchor[0] + (xDomain[0] - zoomAnchor[0]) / zoom, zoomAnchor[0] + (xDomain[1] - zoomAnchor[0]) / zoom]"
}
]
},
{
"name": "yDomain",
"description": "The current y-axis domain, updated during panning and zooming. On dragDelta, pans the domain unless more than one touch is active. On zoom, zooms in/out around zoomAnchor. On touchmove with 2 fingers down, updates when a pinch-zoom occurs.",
"update": "slice(yDomainInitial)",
"on": [
{
"events": {"signal": "dragDelta"},
"update": "touchCount > 1 ? yDomain : [yDomainInstanceInitial[0] + span(yDomainInstanceInitial) * dragDelta[1] / height, yDomainInstanceInitial[1] + span(yDomainInstanceInitial) * dragDelta[1] / height]"
},
{
"events": {"signal": "zoom"},
"update": "[zoomAnchor[1] + (yDomain[0] - zoomAnchor[1]) / zoom, zoomAnchor[1] + (yDomain[1] - zoomAnchor[1]) / zoom]"
}
]
},
{
"name": "initialPointSize",
"description": "The initial size of points in the visualization based on a reference domain span.",
"init": "span(xDomainInitial)"
},
{
"name": "pointSize",
"description": "Determines the size of points in the visualization based on actual zoom level. Clamps the value to prevent extreme scaling.",
"update": "initialPointSize * pow(zoom, 10)"
},
{
"name": "pointSizeRadius",
"description": "Radius of point in pixels",
"update": "sqrt(pointSize/PI)"
}
],
"marks": [
{
"type": "group",
"encode": {
"update": {
"width": {"signal": "width"},
"height": {"signal": "height"},
"clip": {"value": true}
}
},
"axes": [
{
"scale": "xScale",
"orient": "bottom",
"offset": {"signal": "xOffset"},
"tickCount": 10,
"format": ".2f"
},
{
"scale": "yScale",
"orient": "right",
"offset": {"signal": "yOffset"},
"tickCount": 10,
"format": ".2f"
}
],
"marks": [
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "50"},
"y": {"signal": "50"},
"text": {"signal": "pluck(data('dataset'), 'text')"},
"font": {"value": "monospace"},
"fontSize": {"value": 14}
}
}
},
{
"type": "symbol",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "scale('xScale', width/2)"},
"y": {"signal": "scale('yScale', height/2)"},
"size": {"signal": "pointSize"},
"opacity": {"value": 0.25},
"fill": {"value": "#000"}
}
}
}
]
}
],
"scales": [
{
"name": "xScale",
"zero": false,
"clamp": true,
"domain": {"signal": "xDomain"},
"range": {"signal": "xRange"}
},
{
"name": "yScale",
"zero": false,
"domain": {"signal": "yDomain"},
"range": {"signal": "yRange"}
}
],
"data": [
{
"name": "ctrlPressed",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'ctrlPressed'", "as": "label"},
{"type": "formula", "expr": "ctrlPressed", "as": "values"}
]
},
{
"name": "singleTouch",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'singleTouch'", "as": "label"},
{"type": "formula", "expr": "singleTouch", "as": "values"}
]
},
{
"name": "pointerDownCoordinates",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'pointerDownCoordinates'", "as": "label"},
{"type": "formula", "expr": "pointerDownCoordinates", "as": "values"}
]
},
{
"name": "dragDelta",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'dragDelta'", "as": "label"},
{"type": "formula", "expr": "dragDelta", "as": "values"}
]
},
{
"name": "touchCount",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'touchCount'", "as": "label"},
{"type": "formula", "expr": "touchCount", "as": "values"}
]
},
{
"name": "pinchDistanceInitial",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'pinchDistanceInitial'", "as": "label"},
{"type": "formula", "expr": "pinchDistanceInitial", "as": "values"}
]
},
{
"name": "pinchDistance",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'pinchDistance'", "as": "label"},
{"type": "formula", "expr": "pinchDistance", "as": "values"}
]
},
{
"name": "zoomDelta",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'zoomDelta'", "as": "label"},
{"type": "formula", "expr": "zoomDelta", "as": "values"}
]
},
{
"name": "prevZoomDelta",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'prevZoomDelta'", "as": "label"},
{"type": "formula", "expr": "prevZoomDelta", "as": "values"}
]
},
{
"name": "zoom",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'zoom'", "as": "label"},
{"type": "formula", "expr": "zoom", "as": "values"}
]
},
{
"name": "zoomAnchor",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'zoomAnchor'", "as": "label"},
{
"type": "formula",
"expr": "[round(zoomAnchor[0]), round(zoomAnchor[1])]",
"as": "values"
}
]
},
{
"name": "xDomainInstanceInitial",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'xDomainInstanceInitial'", "as": "label"},
{"type": "formula", "expr": "xDomainInstanceInitial", "as": "values"}
]
},
{
"name": "xDomain",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'xDomain'", "as": "label"},
{"type": "formula", "expr": "xDomain", "as": "values"}
]
},
{
"name": "yDomainInstanceInitial",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'yDomainInstanceInitial'", "as": "label"},
{"type": "formula", "expr": "yDomainInstanceInitial", "as": "values"}
]
},
{
"name": "yDomain",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'yDomain'", "as": "label"},
{"type": "formula", "expr": "yDomain", "as": "values"}
]
},
{
"name": "pointSize",
"values": [{}],
"transform": [
{"type": "formula", "expr": "'pointSize'", "as": "label"},
{"type": "formula", "expr": "pointSize", "as": "values"}
]
},
{
"name": "dataset",
"source": [
"ctrlPressed",
"singleTouch",
"pointerDownCoordinates",
"dragDelta",
"touchCount",
"pinchDistanceInitial",
"pinchDistance",
"zoom",
"zoomDelta",
"prevZoomDelta",
"zoomAnchor",
"xDomainInstanceInitial",
"xDomain",
"yDomainInstanceInitial",
"yDomain",
"pointSize"
],
"transform": [
{
"type": "formula",
"expr": "datum.label+': ' + (datum.values || '')",
"as": "text"
}
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment