Skip to content

Commit

Permalink
Merge pull request #1520 from concord-consortium/185628921-graph-subp…
Browse files Browse the repository at this point in the history
…lot-mask
  • Loading branch information
pjanik authored Sep 27, 2024
2 parents b779c10 + b6672c4 commit 8bf1daa
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 29 deletions.
3 changes: 3 additions & 0 deletions v3/src/components/data-display/d3-types.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
// The data stored with each plot element (e.g. 'circle')
export type CaseData = { plotNum: number, caseID: string }

// The data stored with each plot element (e.g. 'circle') when subplots are used
export type CaseDataWithSubPlot = { plotNum: number; caseID: string; subPlotNum?: number }
7 changes: 5 additions & 2 deletions v3/src/components/data-display/data-display-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
} from "../../utilities/color-utils"
import {between} from "../../utilities/math-utils"
import { IBarCover } from "../graph/graphing-types"
import {isGraphDataConfigurationModel} from "../graph/models/graph-data-configuration-model"
import {ISetPointSelection} from "../graph/utilities/graph-utils"
import {
hoverRadiusFactor, kDataDisplayFont, Point, PointDisplayType, pointRadiusLogBase, pointRadiusMax, pointRadiusMin,
pointRadiusSelectionAddend, Rect, rTreeRect
} from "./data-display-types"
import {IDataConfigurationModel } from "./models/data-configuration-model"
import {IPixiPointStyle, PixiPoints} from "./pixi/pixi-points"
import {CaseDataWithSubPlot} from "./d3-types"

export const maxWidthOfStringsD3 = (strings: Iterable<string>) => {
let maxWidth = 0
Expand Down Expand Up @@ -85,7 +87,9 @@ export interface IMatchCirclesProps {
export function matchCirclesToData(props: IMatchCirclesProps) {
const { dataConfiguration, pixiPoints, startAnimation, pointRadius, pointColor, pointStrokeColor,
pointDisplayType = "points" } = props
const allCaseData = dataConfiguration.joinedCaseDataArrays
const allCaseData: CaseDataWithSubPlot[] = isGraphDataConfigurationModel(dataConfiguration)
? dataConfiguration.caseDataWithSubPlot
: dataConfiguration.joinedCaseDataArrays

startAnimation()

Expand Down Expand Up @@ -212,4 +216,3 @@ export function rectToTreeRect(rect: Rect) {
h: rect.height
}
}

4 changes: 2 additions & 2 deletions v3/src/components/data-display/hooks/use-connecting-lines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const useConnectingLines = (props: IProps) => {

const handleConnectingLinesMouseOver = useCallback((mouseOverProps: IMouseOverProps) => {
const { caseIDs, event, parentAttrName, primaryAttrValue } = mouseOverProps
if (pixiPoints) pixiPoints.canvas.style.cursor = "pointer"
if (pixiPoints?.canvas) pixiPoints.canvas.style.cursor = "pointer"
// TODO: In V2, the tool tip is only shown when there is a parent attribute. V3 should always show the tool tip,
// but the text needs to be different when there is no parent attribute. We'll need to work out how to handle the
// localization for this. When a parent attribute is present, the tool tip should look like:
Expand All @@ -90,7 +90,7 @@ export const useConnectingLines = (props: IProps) => {
}, [dataTip, dataset?.name, pixiPoints])

const handleConnectingLinesMouseOut = useCallback(() => {
if (pixiPoints) pixiPoints.canvas.style.cursor = ""
if (pixiPoints?.canvas) pixiPoints.canvas.style.cursor = ""
dataTip.hide()
}, [dataTip, pixiPoints])

Expand Down
91 changes: 71 additions & 20 deletions v3/src/components/data-display/pixi/pixi-points.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as PIXI from "pixi.js"
import { CaseData } from "../d3-types"
import { CaseData, CaseDataWithSubPlot } from "../d3-types"
import { PixiTransition, TransitionPropMap, TransitionProp } from "./pixi-transition"
import { hoverRadiusFactor, transitionDuration } from "../data-display-types"
import { isFiniteNumber } from "../../../utilities/math-utils"
Expand Down Expand Up @@ -76,6 +76,7 @@ export class PixiPoints {
stage = new PIXI.Container()
pointsContainer = new PIXI.Container()
background = new PIXI.Sprite(PIXI.Texture.EMPTY)
subPlotMasks: PIXI.Graphics[] = []
ticker = new PIXI.Ticker()
tickerStopTimeoutId: number | undefined

Expand Down Expand Up @@ -109,21 +110,26 @@ export class PixiPoints {
// The function will prioritize the WebGL renderer as it is the most tested safe API to use. In the near future as
// WebGPU becomes more stable and ubiquitous, it will be prioritized over WebGL.
// See: https://pixijs.download/release/docs/rendering.html#autoDetectRenderer
this.renderer = await PIXI.autoDetectRenderer({
resolution: window.devicePixelRatio,
autoDensity: true,
backgroundAlpha: 0,
antialias: true,
// `passive` is more performant and will be used by default in the future Pixi.JS versions
eventMode: "passive",
eventFeatures: {
move: true,
click: true,
// disables the global move events which can be very expensive in large scenes
globalMove: false,
wheel: false
}
})
try {
this.renderer = await PIXI.autoDetectRenderer({
resolution: window.devicePixelRatio,
autoDensity: true,
backgroundAlpha: 0,
antialias: true,
// `passive` is more performant and will be used by default in the future Pixi.JS versions
eventMode: "passive",
eventFeatures: {
move: true,
click: true,
// disables the global move events which can be very expensive in large scenes
globalMove: false,
wheel: false
}
})
} catch (e) {
console.error("PixiPoints failed to initialize renderer")
return
}

this.ticker.add(this.tick.bind(this))
this.stage.addChild(this.background)
Expand All @@ -146,8 +152,8 @@ export class PixiPoints {
}
}

get canvas() {
return this.renderer?.view.canvas as HTMLCanvasElement
get canvas(): HTMLCanvasElement | null {
return this.renderer?.view.canvas as HTMLCanvasElement ?? null
}

get points() {
Expand All @@ -172,7 +178,7 @@ export class PixiPoints {
this.renderer?.render(this.stage)
}

resize(width: number, height: number) {
resize(width: number, height: number, numColumns?: number, numRows?: number) {
// We only set the background size if the width and height are valid. If we ever set width/height of background to
// negative values, the background won't be able to detect pointer events.
if (width > 0 && height > 0) {
Expand All @@ -181,6 +187,24 @@ export class PixiPoints {
this.background.height = height
this.startRendering()
}

if (numColumns !== undefined && numRows !== undefined) {
this.subPlotMasks = []
const maskWidth = width / numColumns
const maskHeight = height / numRows
// These two for loops follow order of the subPlots ordering. Subplots seem to be ordered by columns first (left
// to right), then rows (bottom to top).
for (let c = 0; c < numColumns; c++) {
for (let r = numRows - 1; r >= 0; r--) {
const mask = new PIXI.Graphics()
.rect(c * maskWidth, r * maskHeight, maskWidth, maskHeight)
.fill(0xffffff)
this.subPlotMasks.push(mask)
}
}
} else {
this.subPlotMasks = []
}
}

setVisibility(isVisible: boolean) {
Expand Down Expand Up @@ -365,6 +389,11 @@ export class PixiPoints {
this.startRendering()
}

setPointSubPlot(point: PIXI.Sprite, subPlotIndex: number) {
point.mask = this.subPlotMasks[subPlotIndex]
this.startRendering()
}

transition(callback: () => void, options: { duration: number }) {
const { duration } = options
if (duration === 0) {
Expand Down Expand Up @@ -638,7 +667,10 @@ export class PixiPoints {
})
}

matchPointsToData(datasetID:string, caseData: CaseData[], _displayType: string, style: IPixiPointStyle) {
matchPointsToData(datasetID:string, caseData: CaseDataWithSubPlot[], _displayType: string, style: IPixiPointStyle) {
if (!this.renderer) {
return
}
// If the display type has changed, we need to prepare for the transition between types
// For now, the only display type values PixiPoints supports are "points" and "bars", so
// all other display type values passed to this method will be treated as "points".
Expand Down Expand Up @@ -702,6 +734,8 @@ export class PixiPoints {
}
}

this.setPointsMask(caseData)

// Before rendering, reset the scale for all points. This may be necessary if the scale was modified
// during a transition immediately before matchPointsToData is called. For example, when the Connecting
// Lines graph adornment is activated or deactivated.
Expand All @@ -710,6 +744,23 @@ export class PixiPoints {
this.startRendering()
}

setPointsMask(allCaseData: CaseDataWithSubPlot[]) {
if (!this.renderer || (window as any).Cypress) {
// This method causes Cypress tests to fail in the GitHub Actions environment, so we skip it in that case.
// The exact reason is unclear, but it seems likely that the WebGL (or WebGPU) renderer initialized in GitHub
// Actions is somehow faulty or incomplete, and using masking features causes it to break entirely. This isn't
// a feature that can be tested using Cypress anyway, so it's safe to skip it in this case.
return
}
allCaseData.forEach((caseData, i) => {
const point = this.getPointForCaseData(caseData)
if (point) {
const subPlotNum = caseData.subPlotNum
point.mask = subPlotNum !== undefined ? this.subPlotMasks[subPlotNum] : null
}
})
}

dispose() {
this.ticker.destroy()
this.renderer?.destroy()
Expand Down
16 changes: 13 additions & 3 deletions v3/src/components/graph/components/graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const Graph = observer(function Graph({graphController, graphRef, pixiPoi
xAttrID = graphModel.getAttributeID('x'),
yAttrID = graphModel.getAttributeID('y')

if (pixiPoints && pixiContainerRef.current && pixiContainerRef.current.children.length === 0) {
if (pixiPoints?.canvas && pixiContainerRef.current && pixiContainerRef.current.children.length === 0) {
pixiContainerRef.current.appendChild(pixiPoints.canvas)
pixiPoints.setupBackgroundEventDistribution({
elementToHide: pixiContainerRef.current
Expand Down Expand Up @@ -110,9 +110,19 @@ export const Graph = observer(function Graph({graphController, graphRef, pixiPoi
.attr("width", `${Math.max(0, layout.plotWidth)}px`)
.attr("height", `${Math.max(0, layout.plotHeight)}px`)

pixiPoints?.resize(layout.plotWidth, layout.plotHeight)
pixiPoints?.resize(layout.plotWidth, layout.plotHeight, layout.numColumns, layout.numRows)
pixiPoints?.setPointsMask(graphModel.dataConfiguration.caseDataWithSubPlot)
}
}, [dataset, layout, layout.plotHeight, layout.plotWidth, pixiPoints, xScale])
}, [dataset, graphModel.dataConfiguration, layout, layout.plotHeight, layout.plotWidth, pixiPoints, xScale])

useEffect(function handleSubPlotsUpdate() {
return mstReaction(
() => graphModel.dataConfiguration.caseDataWithSubPlot,
() => {
pixiPoints?.setPointsMask(graphModel.dataConfiguration.caseDataWithSubPlot)
}, {name: "Graph.handleSubPlotsUpdate"}, graphModel
)
}, [graphModel, graphModel.dataConfiguration, pixiPoints])

useEffect(function handleAttributeConfigurationChange() {
// Handles attribute configuration changes from undo/redo, for instance, among others.
Expand Down
17 changes: 16 additions & 1 deletion v3/src/components/graph/models/graph-data-configuration-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {AttributeDescription, DataConfigurationModel, IAttributeDescriptionSnaps
import {AttrRole, GraphAttrRole, graphPlaceToAttrRole, PrimaryAttrRoles} from "../../data-display/data-display-types"
import {updateCellKey} from "../adornments/adornment-utils"
import { isFiniteNumber } from "../../../utilities/math-utils"
import { CaseData } from "../../data-display/d3-types"
import { CaseData, CaseDataWithSubPlot } from "../../data-display/d3-types"

export const kGraphDataConfigurationType = "graphDataConfigurationType"

Expand Down Expand Up @@ -512,6 +512,21 @@ export const GraphDataConfigurationModel = DataConfigurationModel
}
})
}))
.views(self => ({
get caseDataWithSubPlot() {
const allCaseData: CaseDataWithSubPlot[] = self.joinedCaseDataArrays
const caseIDToSubPlot: Record<string, number> = {}
self.getAllCellKeys().forEach((cellKey, cellIndex) => {
self.subPlotCases(cellKey).forEach(caseID => {
caseIDToSubPlot[caseID] = cellIndex
})
})
allCaseData.forEach((caseData) => {
caseData.subPlotNum = caseIDToSubPlot[caseData.caseID]
})
return allCaseData
}
}))
.views(self => ({
casesInRange(min: number, max: number, attrId: string, cellKey: Record<string, string>, inclusiveMax = true) {
return self.subPlotCases(cellKey)?.filter(caseId => {
Expand Down
2 changes: 1 addition & 1 deletion v3/src/components/map/components/map-point-layer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export const MapPointLayer = observer(function MapPointLayer({mapLayerModel, set
}, [dataConfiguration.dataset, mapModel, pixiPoints])

useEffect(() => {
if (pixiPoints != null && pixiContainerRef.current && pixiContainerRef.current.children.length === 0) {
if (pixiPoints?.canvas && pixiContainerRef.current && pixiContainerRef.current.children.length === 0) {
pixiContainerRef.current.appendChild(pixiPoints.canvas)
pixiPoints.resize(layout.contentWidth, layout.contentHeight)
}
Expand Down

0 comments on commit 8bf1daa

Please sign in to comment.