From b465ef38ef9c54609686d393f8fbc1482a73bd4a Mon Sep 17 00:00:00 2001 From: Andrew McNutt Date: Wed, 29 Apr 2026 10:26:23 -0600 Subject: [PATCH 1/2] tutorial fixes --- package.json | 1 + public/global.json | 8 +- .../example-brush-interactions/assets/Bar.tsx | 97 ++++++ .../assets/BrushPlot.tsx | 183 +++++++++++ .../assets/Paintbrush.tsx | 79 +++++ .../assets/Scatter.tsx | 293 ++++++++++++++++++ .../assets/XAxisBar.tsx | 83 +++++ .../assets/YAxis.tsx | 64 ++++ .../assets/YAxisBar.tsx | 75 +++++ .../assets/types.ts | 15 + yarn.lock | 87 +++++- 11 files changed, 979 insertions(+), 6 deletions(-) create mode 100644 src/public/example-brush-interactions/assets/Bar.tsx create mode 100644 src/public/example-brush-interactions/assets/BrushPlot.tsx create mode 100644 src/public/example-brush-interactions/assets/Paintbrush.tsx create mode 100644 src/public/example-brush-interactions/assets/Scatter.tsx create mode 100644 src/public/example-brush-interactions/assets/XAxisBar.tsx create mode 100644 src/public/example-brush-interactions/assets/YAxis.tsx create mode 100644 src/public/example-brush-interactions/assets/YAxisBar.tsx create mode 100644 src/public/example-brush-interactions/assets/types.ts diff --git a/package.json b/package.json index 32e4889a94..70ed95546f 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@types/hjson": "^2.4.6", "@types/node": "^24.5.2", "ajv": "^8.18.0", + "arquero": "^8.0.3", "crypto-js": "^4.2.0", "d3": "^7.9.0", "dayjs": "^1.11.18", diff --git a/public/global.json b/public/global.json index 00c60243ff..9f7e168ba1 100644 --- a/public/global.json +++ b/public/global.json @@ -1,5 +1,9 @@ { "$schema": "https://raw.githubusercontent.com/revisit-studies/study/v2.4.2/src/parser/GlobalConfigSchema.json", - "configsList": [], - "configs": {} + "configsList": ["tutorial"], + "configs": { + "tutorial": { + "path": "tutorial/config.json" + } + } } diff --git a/src/public/example-brush-interactions/assets/Bar.tsx b/src/public/example-brush-interactions/assets/Bar.tsx new file mode 100644 index 0000000000..08ef122462 --- /dev/null +++ b/src/public/example-brush-interactions/assets/Bar.tsx @@ -0,0 +1,97 @@ +import { useResizeObserver } from '@mantine/hooks'; +import { useMemo } from 'react'; +import ColumnTable from 'arquero/dist/types/table/column-table'; + +import * as d3 from 'd3'; +import { Loader } from '@mantine/core'; +import { XAxisBar } from './XAxisBar'; +import { YAxisBar } from './YAxisBar'; +import { BrushParams } from './types'; + +const margin = { + top: 15, + left: 100, + right: 50, + bottom: 50, +}; + +export function Bar({ barsTable, parameters, data } : {barsTable: ColumnTable | null, parameters: BrushParams, data: Record[]}) { + const [ref, { height: originalHeight, width: originalWidth }] = useResizeObserver(); + + const width = useMemo(() => originalWidth - margin.left - margin.right, [originalWidth]); + + const height = useMemo(() => originalHeight - margin.top - margin.bottom, [originalHeight]); + + const colorScale = useMemo(() => { + const categories = Array.from(new Set(data.map((car) => car[parameters.category]))); + return d3.scaleOrdinal(d3.schemeTableau10).domain(categories); + }, [data, parameters.category]); + + const xScale = useMemo(() => { + if (!barsTable) { + return null; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return d3.scaleLinear([margin.left, width + margin.left]).domain([0, d3.max(barsTable.objects().map((obj: any) => obj.count)) as any]).nice(); + }, [barsTable, width]); + + const yScale = useMemo(() => { + if (!barsTable) { + return null; + } + + return d3.scaleBand([margin.top, height + margin.top]).domain(barsTable.array(parameters.category).sort() as never).paddingInner(0.1); + }, [barsTable, height, parameters.category]); + + const rects = useMemo(() => { + if (!xScale || !yScale || !colorScale || !barsTable) { + return null; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (barsTable.objects() as any[]).map((car: any, i) => { + if (car[parameters.category] === null || car.count === null) { + return null; + } + + return ( + + + {car.count} + + ); + }); + }, [barsTable, colorScale, parameters.category, xScale, yScale]); + + return yScale && xScale ? ( + + ({ + value: value.toString(), + offset: xScale(value), + }))} + /> + + {yScale ? ( + ({ + value: country, + offset: yScale(country)! + yScale.bandwidth() / 2, + }))} + /> + ) : null } + { rects } + + ) : ; +} + +export default Bar; diff --git a/src/public/example-brush-interactions/assets/BrushPlot.tsx b/src/public/example-brush-interactions/assets/BrushPlot.tsx new file mode 100644 index 0000000000..ef2700f931 --- /dev/null +++ b/src/public/example-brush-interactions/assets/BrushPlot.tsx @@ -0,0 +1,183 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Loader, Stack } from '@mantine/core'; +import { + useCallback, useEffect, useMemo, useState, +} from 'react'; +import { from, escape } from 'arquero'; +import ColumnTable from 'arquero/dist/types/table/column-table'; +import { Registry, initializeTrrack } from '@trrack/core'; +import * as d3 from 'd3'; +import debounce from 'lodash.debounce'; +import { Scatter } from './Scatter'; +import { Bar } from './Bar'; +import { StimulusParams } from '../../../store/types'; +import { BrushParams, BrushState, SelectionType } from './types'; + +export function BrushPlot({ + parameters, setAnswer, provenanceState, updateState = () => null, +}: StimulusParams & {updateState: (b: BrushState) => void}) { + const [filteredTable, setFilteredTable] = useState(null); + const [brushState, setBrushState] = useState(provenanceState ? (provenanceState.all.brush || provenanceState.all) : { + hasBrush: false, x1: 0, y1: 0, x2: 0, y2: 0, ids: [], + }); + + useEffect(() => { + if (provenanceState) { + setBrushState(provenanceState.all.brush || provenanceState.all); + } else { + setBrushState({ + hasBrush: false, x1: 0, y1: 0, x2: 0, y2: 0, ids: [], + }); + } + }, [provenanceState]); + + const [data, setData] = useState(null); + + // load data + useEffect(() => { + d3.csv(`./data/${parameters.dataset}.csv`).then((_data) => { + setData(_data); + }); + }, [parameters]); + + const fullTable = useMemo(() => { + if (data) { + return from(data); + } + + return null; + }, [data]); + + // creating provenance tracking + const { actions, trrack } = useMemo(() => { + const reg = Registry.create(); + + const brush = reg.register('brush', (state, currBrush: BrushState) => { + state.all = { brush: currBrush }; + return state; + }); + + const brushMove = reg.register('brushMove', (state, currBrush: BrushState) => { + state.all = { brush: currBrush }; + return state; + }); + + const brushResize = reg.register('brushResize', (state, currBrush: BrushState) => { + state.all = { brush: currBrush }; + return state; + }); + + const clearBrush = reg.register('brushClear', (state, currBrush: BrushState) => { + state.all = { brush: currBrush }; + return state; + }); + + const trrackInst = initializeTrrack({ + registry: reg, + initialState: { + all: { + hasBrush: false, x1: null, x2: null, y1: null, y2: null, ids: [], + }, + }, + }); + + return { + actions: { + brush, + brushMove, + brushResize, + clearBrush, + }, + trrack: trrackInst, + }; + }, []); + + const moveBrushCallback = useCallback((selType: SelectionType, state: BrushState) => { + if (selType === 'drag') { + trrack.apply('Move Brush', actions.brushMove(state)); + } else if (selType === 'handle') { + trrack.apply('Brush', actions.brush(state)); + } + }, [actions, trrack]); + + // debouncing the trrack callback + const debouncedCallback = useMemo(() => debounce(moveBrushCallback, 100, { maxWait: 100 }), [moveBrushCallback]); + + // brush callback, updating state, finding the selected points, and pushing to trrack + const brushedSpaceCallback = useCallback((sel: [[number | null, number | null], [number | null, number | null]], xScale: any, yScale: any, selType: SelectionType, ids?: string[]) => { + if (!xScale || !yScale) { + return; + } + + const xMin = xScale.invert(sel[0][0] || brushState.x1); + const xMax = xScale.invert(sel[1][0] || brushState.x2); + + const yMin = yScale.invert(sel[1][1] || brushState.y2); + const yMax = yScale.invert(sel[0][1] || brushState.y1); + + let _filteredTable = null; + if (selType === 'clear') { + _filteredTable = fullTable; + } else if (ids) { + const idSet = new Set(ids); + _filteredTable = fullTable!.filter(escape((d: any) => idSet.has(d[parameters.ids]))); + } else if (parameters.brushType === 'Axis Selection') { + _filteredTable = fullTable!.filter(escape((d: any) => new Date(d[parameters.x]) >= new Date(xMin) && new Date(d[parameters.x]) <= new Date(xMax) && d[parameters.y] >= yMin && d[parameters.y] <= yMax)); + } else { + _filteredTable = fullTable!.filter(escape((d: any) => d[parameters.x] >= xMin && d[parameters.x] <= xMax && d[parameters.y] >= yMin && d[parameters.y] <= yMax)); + } + + const newState = { + x1: sel[0][0] || brushState?.x1 || 0, + x2: sel[1][0] || brushState?.x2 || 0, + y1: sel[0][1] || brushState?.y1 || 0, + y2: sel[1][1] || brushState?.y2 || 0, + hasBrush: selType !== 'clear', + ids: selType !== 'clear' ? _filteredTable?.array('id') as string[] : [], + }; + + setBrushState(newState); + + if (selType === 'drag' || selType === 'handle') { + debouncedCallback(selType, newState); + } else if (selType === 'clear') { + trrack.apply('Clear Brush', actions.clearBrush(newState)); + } + + setFilteredTable(_filteredTable); + + updateState(newState); + + setAnswer({ + status: true, + provenanceGraph: trrack.graph.backend, + answers: {}, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [brushState, fullTable, parameters, trrack, setAnswer, debouncedCallback, actions]); + + // Which table the bar chart uses, either the base or the filtered table if any selections + const barsTable = useMemo(() => { + if (filteredTable) { + return filteredTable?.groupby(parameters.category).count(); + } + if (fullTable) { + return fullTable?.groupby(parameters.category).count(); + } + return null; + }, [filteredTable, fullTable, parameters.category]); + + const filteredCallback = useCallback((c: ColumnTable | null) => { + setFilteredTable(c); + }, []); + + return data ? ( + + + + + ) : ; +} + +export default BrushPlot; diff --git a/src/public/example-brush-interactions/assets/Paintbrush.tsx b/src/public/example-brush-interactions/assets/Paintbrush.tsx new file mode 100644 index 0000000000..7b5b613354 --- /dev/null +++ b/src/public/example-brush-interactions/assets/Paintbrush.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from 'react'; +import * as d3 from 'd3'; +import { BrushParams, BrushState } from './types'; + +const BRUSH_SIZE = 15; + +export function Paintbrush( + { + xScale, + yScale, + setBrushedSpace, + params, + data, + brushState, + isSelect = true, + } : + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any[] + brushState: BrushState, + xScale: d3.ScaleLinear, + yScale: d3.ScaleLinear, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setBrushedSpace: (brush: [[number | null, number | null], [number | null, number | null]], _xScale: any, _yScale: any, selType: 'drag' | 'handle' | 'clear' | null, ids?: string[]) => void, + params: BrushParams, + isSelect?: boolean +}, +) { + const [brushPosition, setBrushPosition] = useState([0, 0]); + const [isBrushing, setIsBrushing] = useState(false); + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const svg = d3.select('#scatterSvgBrushStudy'); + + if (svg) { + const svgPos = svg.node()!.getBoundingClientRect(); + svg.on('mousemove', (e: React.MouseEvent) => { + const pos = [e.clientX - svgPos.x, e.clientY - svgPos.y]; + setBrushPosition(pos); + + if (isBrushing) { + const selected = data.filter((car) => Math.abs(xScale(car[params.x]) - pos[0]) < BRUSH_SIZE && Math.abs(yScale(car[params.y]) - pos[1]) < BRUSH_SIZE); + + if (e.ctrlKey || e.metaKey || !isSelect) { + const set = new Set(brushState.ids); + selected.forEach((sel) => { + if (set.has(sel[params.ids])) { + set.delete(sel[params.ids]); + } + }); + const newIds = Array.from(set); + setBrushedSpace([[brushPosition[0], brushPosition[1]], [brushPosition[0], brushPosition[1]]], xScale, yScale, newIds.length === 0 ? 'clear' : 'drag', newIds); + } else { + const newIds = Array.from(new Set([...brushState.ids, ...selected.map((car) => car[params.ids])])); + + if (newIds.length > 0) { + setBrushedSpace([[brushPosition[0], brushPosition[1]], [brushPosition[0], brushPosition[1]]], xScale, yScale, 'drag', newIds); + } + } + } + }); + + svg.on('mousedown', (e) => { + setIsBrushing(true); + e.stopPropagation(); + e.preventDefault(); + }); + + svg.on('mouseup', () => { + setIsBrushing(false); + }); + } + }, [brushPosition, brushState.ids, data, isBrushing, isSelect, params, setBrushedSpace, xScale, yScale]); + + return ( + + ); +} diff --git a/src/public/example-brush-interactions/assets/Scatter.tsx b/src/public/example-brush-interactions/assets/Scatter.tsx new file mode 100644 index 0000000000..728a303c9e --- /dev/null +++ b/src/public/example-brush-interactions/assets/Scatter.tsx @@ -0,0 +1,293 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { useResizeObserver } from '@mantine/hooks'; +import { useEffect, useMemo, useState } from 'react'; +import ColumnTable from 'arquero/dist/types/table/column-table'; +import * as d3 from 'd3'; +import { + Button, Group, RangeSlider, SegmentedControl, Stack, Text, +} from '@mantine/core'; +import { XAxisBar } from './XAxisBar'; +import { YAxis } from './YAxis'; +import { Paintbrush } from './Paintbrush'; +import { BrushNames, BrushParams, BrushState } from './types'; + +const margin = { + top: 15, + left: 100, + right: 15, + bottom: 70, +}; + +export function Scatter({ + setFilteredTable, + brushState, + setBrushedSpace, + brushType, + params, + data, + brushedPoints, +}: + { + brushedPoints: string[], + data: any[], + params: BrushParams, + setFilteredTable: (c: ColumnTable | null) => void, + brushState: BrushState, + setBrushedSpace: (brush: [[number | null, number | null], [number | null, number | null]], xScale: any, yScale: any, selType: 'drag' | 'handle' | 'clear' | null, ids?: string[]) => void, + brushType: BrushNames + }) { + const [ref, { height: originalHeight, width: originalWidth }] = useResizeObserver(); + + const [brushXRef] = useResizeObserver(); + const [brushYRef] = useResizeObserver(); + + const [isPaintbrushSelect, setIsPaintbrushSelect] = useState(true); + + const width = useMemo(() => originalWidth - margin.left - margin.right, [originalWidth]); + + const height = useMemo(() => originalHeight - margin.top - margin.bottom, [originalHeight]); + + const colorScale = useMemo(() => { + const cats = Array.from(new Set(data.map((d) => d[params.category]))); + return d3.scaleOrdinal(d3.schemeTableau10).domain(cats); + }, [data, params.category]); + + const { + xMin, yMin, xMax, yMax, + } = useMemo(() => { + const xData: number[] = data.map((d) => +d[params.x]).filter((val) => val !== null) as number[]; + const [_xMin, _xMax] = d3.extent(xData) as [number, number]; + + const yData: number[] = data.map((d) => +d[params.y]).filter((val) => val !== null) as number[]; + const [_yMin, _yMax] = d3.extent(yData) as [number, number]; + + return { + xMin: _xMin, + xMax: _xMax, + yMin: _yMin, + yMax: _yMax, + }; + }, [data, params.x, params.y]); + + const xScale = useMemo(() => { + const range = xMax - xMin; + if (width <= 0) { + return null; + } + + if (params.dataType === 'date') { + return d3.scaleTime([margin.left, width + margin.left]).domain([new Date('2014-12-20'), new Date('2016-01-10')]); + } + + return d3.scaleLinear([margin.left, width + margin.left]).domain([xMin - range / 10, xMax + range / 10]).nice(); + }, [params.dataType, width, xMax, xMin]); + + const yScale = useMemo(() => { + const range = yMax - yMin; + + if (height <= 0) { + return null; + } + + return d3.scaleLinear([height + margin.top, margin.top]).domain([yMin - range / 10, yMax + range / 10]).nice(); + }, [height, yMax, yMin]); + + // create brushes + const clearCallback = useMemo(() => { + if (!xScale || !yScale) { + return () => null; + } + + if (brushType === 'Axis Selection') { + const brushX = d3.brushX().extent([[margin.left, margin.top + height - 5], [margin.left + width, margin.top + height + 5]]).on('brush end', (e) => { + if (e.sourceEvent !== undefined) { + setBrushedSpace([[e.selection[0], null], [e.selection[1], null]], xScale, yScale, e.mode); + } + }); + + const brushY = d3.brushY().extent([[margin.left - 5, margin.top], [margin.left + 5, margin.top + height]]).on('brush end', (e) => { + if (e.sourceEvent !== undefined) { + setBrushedSpace([[null, e.selection[0]], [null, e.selection[1]]], xScale, yScale, e.mode); + } + }); + + if (brushXRef.current && brushYRef.current) { + d3.select(brushYRef.current).call(brushY); + d3.select(brushXRef.current).call(brushX); + + if (!brushState.hasBrush) { + d3.select(brushYRef.current).call(brushY.move, [yScale(yMax), yScale(yMin)]); + d3.select(brushXRef.current).call(brushX.move, [xScale(new Date('2015-01-02')), xScale(new Date('2015-12-31'))]); + setBrushedSpace([[xScale(new Date('2015-01-02')), yScale(yMax)], [xScale(new Date('2015-12-31')), yScale(yMin)]], xScale, yScale, 'drag'); + } + } + + return () => { + d3.select(brushYRef.current).call(brushY.move, [yScale(yMax), yScale(yMin)]); + d3.select(brushXRef.current).call(brushX.move, [xScale(new Date('2015-01-02')), xScale(new Date('2015-12-31'))]); + setBrushedSpace([[xScale(new Date('2015-01-02')), yScale(yMax)], [xScale(new Date('2015-12-31')), yScale(yMin)]], xScale, yScale, 'clear'); + }; + } + if (brushType === 'Rectangular Selection') { + const brush = d3.brush().extent([[margin.left, margin.top], [margin.left + width, margin.top + height]]).on('brush', (e) => { + if (e.sourceEvent !== undefined) { + setBrushedSpace([[e.selection[0][0], e.selection[0][1]], [e.selection[1][0], e.selection[1][1]]], xScale, yScale, e.mode); + } + }).on('end', (currData) => { + if (currData.selection === null && currData.sourceEvent !== undefined) { + d3.select(ref.current).call(brush.move, null); + setFilteredTable(null); + } + }); + + d3.select(ref.current).call(brush); + + return () => { + d3.select(ref.current).call(brush.move, null); + setBrushedSpace([[null, null], [null, null]], xScale, yScale, 'clear'); + }; + } + if (brushType === 'Slider Selection') { + if (!brushState.hasBrush) { + setBrushedSpace([[xScale(xMin), yScale(yMax)], [xScale(xMax), yScale(yMin)]], xScale, yScale, null); + } + + return () => setBrushedSpace([[xScale(xMin), yScale(yMax)], [xScale(xMax), yScale(yMin)]], xScale, yScale, 'clear'); + } + if (brushType === 'Paintbrush Selection') { + return () => setBrushedSpace([[xScale(xMin), yScale(yMax)], [xScale(xMax), yScale(yMin)]], xScale, yScale, 'clear', []); + } + + return () => null; + }, [brushState.hasBrush, brushType, brushXRef, brushYRef, height, ref, setBrushedSpace, setFilteredTable, width, xMax, xMin, xScale, yMax, yMin, yScale]); + + const brushedSet = useMemo(() => (brushedPoints.length === 0 ? null : new Set(brushedPoints)), [brushedPoints]); + + const circles = useMemo(() => { + if (!xScale || !yScale) { + return null; + } + + return data.map((d, i) => { + if (d[params.x] === null || d[params.y] === null) { + return null; + } + + const xVal = params.dataType === 'date' ? xScale(new Date(d[params.x])) : xScale(d[params.x]); + + return ; + }); + }, [brushedSet, colorScale, data, params.category, params.dataType, params.ids, params.x, params.y, xScale, yScale]); + + useEffect(() => { + if (brushType === 'Axis Selection') { + d3.selectAll('.handle').style('fill', 'darkgrey'); + } + }, [brushState, brushType]); + + return ( + + + + { params.brushType === 'Paintbrush Selection' + ? ( + setIsPaintbrushSelect(val === 'Select')} + data={[ + { label: 'Select', value: 'Select' }, + { label: 'De-Select', value: 'De-Select', disabled: brushedPoints.length === 0 }, + ]} + /> + ) : null} + + + + + + {xScale && yScale ? ( + + + ({ + value: value.toString(), + offset: xScale(value), + }))} + /> + + ) : null} + {circles} + + + {xScale && yScale && brushType === 'Paintbrush Selection' ? : null} + + {brushType === 'Slider Selection' && xScale && yScale + ? ( + + + {params.x} + { + setBrushedSpace([[xScale(value[0]), brushState.y1], [xScale(value[1]), brushState.y2]], xScale, yScale, 'drag'); + }} + style={{ width: '300px' }} + marks={ + xScale.ticks(5).map((t) => ({ + value: t, + label: t, + })) as any + } + value={[xScale.invert(brushState.x1), xScale.invert(brushState.x2)] as any} + /> + + + {params.y} + { + setBrushedSpace([[brushState.x1, yScale(value[1])], [brushState.x2, yScale(value[0])]], xScale, yScale, 'drag'); + }} + style={{ width: '300px' }} + marks={ + yScale.ticks(5).map((t) => ({ + value: t, + label: t, + })) +} + value={[yScale.invert(brushState.y2), yScale.invert(brushState.y1)]} + /> + + + ) : null} + + + ); +} + +export default Scatter; diff --git a/src/public/example-brush-interactions/assets/XAxisBar.tsx b/src/public/example-brush-interactions/assets/XAxisBar.tsx new file mode 100644 index 0000000000..54ff657fd9 --- /dev/null +++ b/src/public/example-brush-interactions/assets/XAxisBar.tsx @@ -0,0 +1,83 @@ +import { + Center, Group, Text, Tooltip, +} from '@mantine/core'; +import { useCallback, useMemo } from 'react'; +import * as d3 from 'd3'; + +// code taken from https://wattenberger.com/blog/react-and-d3 +export function XAxisBar({ + xScale, + yRange, + vertPosition, + label, + ticks, + isDate = false, + showLines = true, + compact = false, +}: { + showLines?: boolean; + isDate?: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + xScale: d3.ScaleTime | d3.ScaleLinear; + yRange: [number, number]; + vertPosition: number; + label: string; + ticks: { value: string; offset: number }[]; + compact?: boolean; +}) { + const tickWidth = useMemo(() => { + if (ticks.length > 1) { + return Math.abs(ticks[1].offset - ticks[0].offset); + } + + return xScale.range()[0] - xScale.range()[1]; + }, [ticks, xScale]); + + const format = useCallback((str: string | Date) => { + const myFormat = isDate ? d3.utcFormat('%B') : d3.format('.2s'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return myFormat(str as any); + }, [isDate]); + + return ( + <> + + +
+ + + {label} + + +
+
+
+ + + {showLines ? : null } + + {ticks.map(({ value, offset }) => ( + + + {showLines ? : null} + +
+ + + {+value === 0 ? 0 : format(isDate ? new Date(value) : value)} + + +
+
+
+ ))} + + ); +} diff --git a/src/public/example-brush-interactions/assets/YAxis.tsx b/src/public/example-brush-interactions/assets/YAxis.tsx new file mode 100644 index 0000000000..402db6876f --- /dev/null +++ b/src/public/example-brush-interactions/assets/YAxis.tsx @@ -0,0 +1,64 @@ +/* eslint-disable react/no-unused-prop-types */ +import { Center, Group, Text } from '@mantine/core'; +import * as React from 'react'; +import { useMemo } from 'react'; +import * as d3 from 'd3'; + +// code taken from https://wattenberger.com/blog/react-and-d3 +export function YAxis({ + yScale, xRange, horizontalPosition, label, +// eslint-disable-next-line @typescript-eslint/no-explicit-any +}: {yScale: any, xRange: any, horizontalPosition: any, label: string}) { + const ticks = useMemo( + () => yScale.ticks(5).map((value: unknown) => ({ + value, + yOffset: yScale(value), + })), + [yScale], + ); + + const format = useMemo(() => d3.format('.2s'), []); + + return ( + <> + + +
+ + + + {label} + + +
+
+
+ + + {ticks.map(({ value, yOffset }: {value: number, yOffset: number}) => ( + + + + + {format(value)} + + + ))} + + ); +} diff --git a/src/public/example-brush-interactions/assets/YAxisBar.tsx b/src/public/example-brush-interactions/assets/YAxisBar.tsx new file mode 100644 index 0000000000..20c66c1d05 --- /dev/null +++ b/src/public/example-brush-interactions/assets/YAxisBar.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { useMemo } from 'react'; +import { Center, Group, Text } from '@mantine/core'; + +// code taken from https://wattenberger.com/blog/react-and-d3 +export function YAxisBar({ + yScale, + xRange, + horizontalPosition, + label, + ticks, + showLines, + compact = false, +}: { + yScale: d3.ScaleBand + xRange: [number, number]; + horizontalPosition: number; + label: string; + ticks: { value: string; offset: number }[]; + showLines?: boolean; + compact?: boolean; +}) { + const labelSpacing = useMemo(() => { + const maxLabelLength = ticks.reduce((max, { value }) => { + const { length } = `${value}`; + return length > max ? length : max; + }, 0); + + return maxLabelLength > 10 ? 60 : maxLabelLength * 6; + }, [ticks]); + + return ( + <> + + +
+ + + + {label} + + +
+
+
+ + {showLines ? : null } + {ticks.map(({ value, offset }) => ( + + + {showLines ? : null} + + + + + {value} + + + + + + ))} + + ); +} diff --git a/src/public/example-brush-interactions/assets/types.ts b/src/public/example-brush-interactions/assets/types.ts new file mode 100644 index 0000000000..af1e7c1652 --- /dev/null +++ b/src/public/example-brush-interactions/assets/types.ts @@ -0,0 +1,15 @@ +export interface BrushState { + hasBrush: boolean; + x1: number; + x2: number; + y1: number; + y2: number; + + ids: string[]; + } + +export type SelectionType = 'drag' | 'handle' | 'clear' | null + +export type BrushNames = 'Rectangular Selection' | 'Axis Selection' | 'Slider Selection' | 'Paintbrush Selection' + +export interface BrushParams {brushType: BrushNames, dataset: string, x: string, y: string, category: string, ids: string, dataType?: 'date'} diff --git a/yarn.lock b/yarn.lock index 30a00e6dfe..37b2facbd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1198,6 +1198,20 @@ version "1.15.11" resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.11.tgz#8f52ab37b4d874b9cc1b1ae809778620b42dbf9f" integrity sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w== + dependencies: + "@swc/counter" "^0.1.3" + "@swc/types" "^0.1.25" + optionalDependencies: + "@swc/core-darwin-arm64" "1.15.11" + "@swc/core-darwin-x64" "1.15.11" + "@swc/core-linux-arm-gnueabihf" "1.15.11" + "@swc/core-linux-arm64-gnu" "1.15.11" + "@swc/core-linux-arm64-musl" "1.15.11" + "@swc/core-linux-x64-gnu" "1.15.11" + "@swc/core-linux-x64-musl" "1.15.11" + "@swc/core-win32-arm64-msvc" "1.15.11" + "@swc/core-win32-ia32-msvc" "1.15.11" + "@swc/core-win32-x64-msvc" "1.15.11" "@swc/counter@^0.1.3": version "0.1.3" @@ -1205,9 +1219,9 @@ integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== "@swc/types@^0.1.25": - version "0.1.25" - resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.25.tgz#b517b2a60feb37dd933e542d93093719e4cf1078" - integrity sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g== + version "0.1.26" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.26.tgz#2a976a1870caef1992316dda1464150ee36968b5" + integrity sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw== dependencies: "@swc/counter" "^0.1.3" @@ -1856,6 +1870,11 @@ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777" integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g== +"@uwdata/flechette@^2.0.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@uwdata/flechette/-/flechette-2.4.0.tgz#01c48f3adc1dc409078eaae433e8ef665c7361ef" + integrity sha512-cwxeYWe2iua4zPUopiqhYXkxaQWr0GK4Kbak7i0zemo3sgmNJ3IE+pNi6XD1dOK7q50SEXTLwieP5JycG6D56A== + "@vitejs/plugin-react-swc@^4.1.0": version "4.2.3" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-4.2.3.tgz#ab92c8a00aab280951a04c06d99731cb7768c964" @@ -1930,7 +1949,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.15.0: +acorn@^8.14.1, acorn@^8.15.0: version "8.16.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== @@ -1994,6 +2013,14 @@ aria-query@^5.3.2: resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== +arquero@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/arquero/-/arquero-8.0.3.tgz#ac72842df9143cbb3b9d215792344d3c6b14cef6" + integrity sha512-7YQwe/GPVBUiahaPwEwgvu6VHyuhX0Ut61JZlIJYsAobOH5unLBwTmh43BobhX/N4dW1sb8WyKlQ8GjFq2whzQ== + dependencies: + "@uwdata/flechette" "^2.0.0" + acorn "^8.14.1" + array-buffer-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" @@ -3156,6 +3183,33 @@ esbuild@^0.27.0: version "0.27.3" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8" integrity sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.27.3" + "@esbuild/android-arm" "0.27.3" + "@esbuild/android-arm64" "0.27.3" + "@esbuild/android-x64" "0.27.3" + "@esbuild/darwin-arm64" "0.27.3" + "@esbuild/darwin-x64" "0.27.3" + "@esbuild/freebsd-arm64" "0.27.3" + "@esbuild/freebsd-x64" "0.27.3" + "@esbuild/linux-arm" "0.27.3" + "@esbuild/linux-arm64" "0.27.3" + "@esbuild/linux-ia32" "0.27.3" + "@esbuild/linux-loong64" "0.27.3" + "@esbuild/linux-mips64el" "0.27.3" + "@esbuild/linux-ppc64" "0.27.3" + "@esbuild/linux-riscv64" "0.27.3" + "@esbuild/linux-s390x" "0.27.3" + "@esbuild/linux-x64" "0.27.3" + "@esbuild/netbsd-arm64" "0.27.3" + "@esbuild/netbsd-x64" "0.27.3" + "@esbuild/openbsd-arm64" "0.27.3" + "@esbuild/openbsd-x64" "0.27.3" + "@esbuild/openharmony-arm64" "0.27.3" + "@esbuild/sunos-x64" "0.27.3" + "@esbuild/win32-arm64" "0.27.3" + "@esbuild/win32-ia32" "0.27.3" + "@esbuild/win32-x64" "0.27.3" escalade@^3.1.1: version "3.2.0" @@ -5773,6 +5827,31 @@ rollup@^4.43.0: dependencies: "@types/estree" "1.0.8" optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.59.0" + "@rollup/rollup-android-arm64" "4.59.0" + "@rollup/rollup-darwin-arm64" "4.59.0" + "@rollup/rollup-darwin-x64" "4.59.0" + "@rollup/rollup-freebsd-arm64" "4.59.0" + "@rollup/rollup-freebsd-x64" "4.59.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.59.0" + "@rollup/rollup-linux-arm-musleabihf" "4.59.0" + "@rollup/rollup-linux-arm64-gnu" "4.59.0" + "@rollup/rollup-linux-arm64-musl" "4.59.0" + "@rollup/rollup-linux-loong64-gnu" "4.59.0" + "@rollup/rollup-linux-loong64-musl" "4.59.0" + "@rollup/rollup-linux-ppc64-gnu" "4.59.0" + "@rollup/rollup-linux-ppc64-musl" "4.59.0" + "@rollup/rollup-linux-riscv64-gnu" "4.59.0" + "@rollup/rollup-linux-riscv64-musl" "4.59.0" + "@rollup/rollup-linux-s390x-gnu" "4.59.0" + "@rollup/rollup-linux-x64-gnu" "4.59.0" + "@rollup/rollup-linux-x64-musl" "4.59.0" + "@rollup/rollup-openbsd-x64" "4.59.0" + "@rollup/rollup-openharmony-arm64" "4.59.0" + "@rollup/rollup-win32-arm64-msvc" "4.59.0" + "@rollup/rollup-win32-ia32-msvc" "4.59.0" + "@rollup/rollup-win32-x64-gnu" "4.59.0" + "@rollup/rollup-win32-x64-msvc" "4.59.0" fsevents "~2.3.2" rw@1: From f0df98ca75ed35348668cc4e2eafdf5344540496 Mon Sep 17 00:00:00 2001 From: Andrew McNutt Date: Wed, 29 Apr 2026 10:27:04 -0600 Subject: [PATCH 2/2] . --- .env | 17 +++++++++++++++++ .gitignore | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000000..098553a6f1 --- /dev/null +++ b/.env @@ -0,0 +1,17 @@ +VITE_BASE_PATH="/study/" +VITE_FIREBASE_CONFIG=' +{ + apiKey: "AIzaSyAm9QtUgx1lYPDeE0vKLN-lK17WfUGVkLo", + authDomain: "revisit-utah.firebaseapp.com", + projectId: "revisit-utah", + storageBucket: "revisit-utah.appspot.com", + messagingSenderId: "811568460432", + appId: "1:811568460432:web:995f6b4f1fc8042b5dde15" +} +' +VITE_STORAGE_ENGINE="localStorage" # "firebase" or "supabase" or "localStorage" or your own custom storage engine +VITE_RECAPTCHAV3TOKEN="6LdjOd0lAAAAAASvFfDZFWgtbzFSS9Y3so8rHJth" # recaptcha SITE KEY +VITE_REPO_URL="https://github.com/revisit-studies/study/tree/main/public/" # Set the url for the "view source" link on the front page + +VITE_SUPABASE_URL="https://supabase.revisit.dev" +VITE_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYwNjgwODAwLCJleHAiOjE5MTg0NDcyMDB9.IohiSvWUtjylJgn4gMrK3aYfnbz-hUmyb3h87DrQvTc" diff --git a/.gitignore b/.gitignore index 446cd48a69..26dd272055 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,3 @@ supabase/volumes/* !supabase/volumes/db/ supabase/volumes/db/data !supabase/volumes/api/ -.env