Skip to content

Commit 74352bb

Browse files
committed
Canvas interactivity
1 parent d9b0265 commit 74352bb

File tree

13 files changed

+54282
-56
lines changed

13 files changed

+54282
-56
lines changed

src/components/Axis.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class Axis extends React.Component {
4343
.map(l => (l.getBBox && l.getBBox()) || { height: 30, width: 30 })
4444
.map(d => d[positionType])
4545
) + 25
46+
4647
return axisLabelMax
4748
}
4849

@@ -169,7 +170,6 @@ class Axis extends React.Component {
169170
lineHeight = height + 25
170171
break
171172
case "bottom":
172-
position = [position[0], position[1]]
173173
position = [position[0], 0]
174174
hoverWidth = width
175175
hoverHeight = 50

src/components/Frame.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,16 @@ class Frame extends React.Component<Props, State> {
9999
canvasContext = null
100100

101101
componentDidMount() {
102-
this.setState({ canvasContext: this.canvasContext })
102+
this.setState({
103+
canvasContext: this.canvasContext
104+
})
103105
}
104106

105107
componentDidUpdate() {
106108
if (this.canvasContext !== this.state.canvasContext)
107-
this.setState({ canvasContext: this.canvasContext })
109+
this.setState({
110+
canvasContext: this.canvasContext
111+
})
108112
}
109113

110114
setVoronoi = (d: Object) => {
@@ -344,6 +348,7 @@ class Frame extends React.Component<Props, State> {
344348
)}
345349
</svg>
346350
</SpanOrDiv>
351+
347352
<InteractionLayer
348353
useSpans={useSpans}
349354
hoverAnnotation={hoverAnnotation}
@@ -356,6 +361,7 @@ class Frame extends React.Component<Props, State> {
356361
customHoverBehavior={customHoverBehavior}
357362
customDoubleClickBehavior={customDoubleClickBehavior}
358363
points={points}
364+
canvasRendering={canvasRendering}
359365
position={adjustedPosition}
360366
margin={margin}
361367
size={adjustedSize}

src/components/InteractionLayer.js

Lines changed: 156 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,99 @@ type Props = {
4242
customDoubleClickBehavior?: Function,
4343
customClickBehavior?: Function,
4444
customHoverBehavior?: Function,
45-
voronoiHover: Function
45+
voronoiHover: Function,
46+
canvasRendering?: boolean
4647
}
4748

4849
type State = {
49-
overlayRegions: Node
50+
overlayRegions: Array<Node>,
51+
interactionCanvas: Node
5052
}
5153

5254
class InteractionLayer extends React.Component<Props, State> {
5355
constructor(props: Props) {
5456
super(props)
5557

58+
// $FlowFixMe
5659
this.state = {
57-
overlayRegions: this.calculateOverlay(props)
60+
overlayRegions: this.calculateOverlay(props),
61+
interactionCanvas: (
62+
<canvas
63+
className="frame-canvas-interaction"
64+
ref={canvasContext => {
65+
if (canvasContext) {
66+
canvasContext.onmousemove = e => {
67+
const interactionContext = canvasContext.getContext("2d")
68+
const hoverPoint = interactionContext.getImageData(
69+
e.offsetX,
70+
e.offsetY,
71+
1,
72+
1
73+
)
74+
75+
const mostCommonRGB = `rgba(${hoverPoint.data[0]},${
76+
hoverPoint.data[1]
77+
},${hoverPoint.data[2]},255)`
78+
79+
// $FlowFixMe
80+
let overlay = this.state.overlayRegions[
81+
this.canvasMap.get(mostCommonRGB)
82+
]
83+
if (!overlay) {
84+
const hoverArea = interactionContext.getImageData(
85+
e.offsetX - 2,
86+
e.offsetY - 2,
87+
5,
88+
5
89+
)
90+
let x = 0
91+
92+
while (!overlay && x < 100) {
93+
// $FlowFixMe
94+
overlay = this.state.overlayRegions[
95+
this.canvasMap.get(
96+
`rgba(${hoverArea.data[x]},${hoverArea.data[x + 1]},${
97+
hoverArea.data[x + 2]
98+
},255)`
99+
)
100+
]
101+
x += 4
102+
}
103+
}
104+
105+
// $FlowFixMe
106+
if (overlay && overlay.props) {
107+
overlay.props.onMouseEnter()
108+
} else {
109+
this.changeVoronoi()
110+
}
111+
}
112+
}
113+
this.interactionContext = canvasContext
114+
}}
115+
style={{
116+
position: "absolute",
117+
left: `0px`,
118+
top: `0px`,
119+
imageRendering: "pixelated",
120+
pointerEvents: "all",
121+
opacity: 0
122+
}}
123+
width={props.svgSize[0]}
124+
height={props.svgSize[1]}
125+
/>
126+
)
58127
}
59128
}
60129

130+
static defaultProps = {
131+
svgSize: [500, 500]
132+
}
133+
134+
interactionContext = null
135+
136+
canvasMap: Map<string, number> = new Map()
137+
61138
changeVoronoi = (d?: Object, customHoverTypes?: CustomHoverType) => {
62139
//Until semiotic 2
63140
const dataObject = d && d.data ? { ...d.data, ...d } : d
@@ -427,6 +504,55 @@ class InteractionLayer extends React.Component<Props, State> {
427504
}
428505
}
429506

507+
componentDidMount() {
508+
this.canvasRendering()
509+
}
510+
511+
componentDidUpdate(prevProps: Props, prevState: State) {
512+
if (this.state.overlayRegions !== prevState.overlayRegions) {
513+
this.canvasRendering()
514+
}
515+
}
516+
517+
canvasRendering = () => {
518+
if (this.interactionContext === null || !this.state.overlayRegions) return
519+
520+
const { svgSize, margin } = this.props
521+
const { overlayRegions } = this.state
522+
523+
this.canvasMap.clear()
524+
525+
// $FlowFixMe
526+
const interactionContext = this.interactionContext.getContext("2d")
527+
528+
interactionContext.imageSmoothingEnabled = false
529+
interactionContext.setTransform(1, 0, 0, 1, margin.left, margin.top)
530+
interactionContext.clearRect(
531+
-margin.left,
532+
-margin.top,
533+
svgSize[0],
534+
svgSize[1]
535+
)
536+
537+
interactionContext.lineWidth = 1
538+
539+
overlayRegions.forEach((overlay, oi) => {
540+
const interactionRGBA = `rgba(${parseInt(Math.random() * 255)},${parseInt(
541+
Math.random() * 255
542+
)},${parseInt(Math.random() * 255)},255)`
543+
544+
this.canvasMap.set(interactionRGBA, oi)
545+
546+
interactionContext.fillStyle = interactionRGBA
547+
interactionContext.strokeStyle = interactionRGBA
548+
549+
// $FlowFixMe
550+
const p = new Path2D(overlay.props.d)
551+
interactionContext.stroke(p)
552+
interactionContext.fill(p)
553+
})
554+
}
555+
430556
createColumnsBrush = (interaction: Object) => {
431557
const { projection, rScale, size, oColumns } = this.props
432558

@@ -510,7 +636,13 @@ class InteractionLayer extends React.Component<Props, State> {
510636

511637
render() {
512638
let semioticBrush = null
513-
const { interaction, svgSize, margin, useSpans = false } = this.props
639+
const {
640+
interaction,
641+
svgSize,
642+
margin,
643+
useSpans = false,
644+
canvasRendering
645+
} = this.props
514646
const { overlayRegions } = this.state
515647
let { enabled } = this.props
516648

@@ -527,6 +659,11 @@ class InteractionLayer extends React.Component<Props, State> {
527659
return null
528660
}
529661

662+
const interactionCanvas =
663+
canvasRendering &&
664+
this.state.overlayRegions &&
665+
this.state.interactionCanvas
666+
530667
return (
531668
<SpanOrDiv
532669
span={useSpans}
@@ -537,20 +674,22 @@ class InteractionLayer extends React.Component<Props, State> {
537674
pointerEvents: "none"
538675
}}
539676
>
540-
<svg
541-
height={svgSize[1]}
542-
width={svgSize[0]}
543-
style={{ background: "none", pointerEvents: "none" }}
544-
>
545-
<g
546-
className="interaction-overlay"
547-
transform={`translate(${margin.left},${margin.top})`}
548-
style={{ pointerEvents: enabled ? "all" : "none" }}
677+
{interactionCanvas || (
678+
<svg
679+
height={svgSize[1]}
680+
width={svgSize[0]}
681+
style={{ background: "none", pointerEvents: "none" }}
549682
>
550-
<g className="interaction-regions">{overlayRegions}</g>
551-
{semioticBrush}
552-
</g>
553-
</svg>
683+
<g
684+
className="interaction-overlay"
685+
transform={`translate(${margin.left},${margin.top})`}
686+
style={{ pointerEvents: enabled ? "all" : "none" }}
687+
>
688+
<g className="interaction-regions">{overlayRegions}</g>
689+
{semioticBrush}
690+
</g>
691+
</svg>
692+
)}
554693
</SpanOrDiv>
555694
)
556695
}

src/components/InteractionLayer.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe("InteractionLayer", () => {
2323
yScale={scaleLinear()
2424
.domain([0, 1200])
2525
.range([400, 0])}
26+
disableCanvas={true}
2627
interaction={{
2728
brush: "xyBrush",
2829
end: xyEndFunction,

src/components/visualizationLayerBehavior/axis.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export const axisLabels = ({ axisParts, tickFormat, rotate = 0 }) => {
134134
key={i}
135135
pointerEvents="none"
136136
transform={`translate(${axisPart.tx},${axisPart.ty})rotate(${rotate})`}
137+
className="axis-label"
137138
>
138139
{renderedValue}
139140
</g>

src/docs/Documentation.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import JoyPlot from "./components/JoyPlot"
3030
import WaterfallChart from "./components/WaterfallChart"
3131
import BulletChart from "./components/BulletChart"
3232
import NeighborhoodMap from "./components/NeighborhoodMap"
33+
import CanvasInteraction from "./components/CanvasInteraction"
3334
import BaseballMap from "./components/BaseballMap"
3435
import WordCloud from "./components/WordCloud"
3536
import SwarmBrush from "./components/SwarmBrush"
@@ -94,6 +95,7 @@ const components = {
9495
annotations: { docs: AppleStockChart, parent: "xyframe" },
9596
homerunmap: { docs: BaseballMap, parent: "xyframe" },
9697
neighborhoodmap: { docs: NeighborhoodMap, parent: "xyframe" },
98+
canvasinteraction: { docs: CanvasInteraction, parent: "xyframe" },
9799
realtimeline: { docs: RealtimeXYFrame, parent: "xyframe" },
98100
minimap: { docs: Minimap, parent: "xyframe" },
99101
linebrush: { docs: LineBrush, parent: "xyframe" },
@@ -347,7 +349,8 @@ class Documentation extends React.Component {
347349
<div className={"drawer-title"}>
348350
<IconButton onClick={this.handleDrawerClose}>
349351
<ChevronLeftIcon />
350-
</IconButton>Semiotic
352+
</IconButton>
353+
Semiotic
351354
</div>
352355
<Divider />
353356
<List className={classes.list}>{allDocs}</List>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from "react"
2+
import DocumentComponent from "../layout/DocumentComponent"
3+
import CanvasInteractionRaw from "./CanvasInteractionRaw"
4+
5+
const components = []
6+
7+
components.push({
8+
name: "CanvasInteraction"
9+
})
10+
11+
export default class CanvasInteractionDocs extends React.Component {
12+
render() {
13+
const examples = []
14+
examples.push({
15+
name: "Basic",
16+
demo: CanvasInteractionRaw,
17+
source: `
18+
<XYFrame
19+
points={parsedDiamonds}
20+
size={[700, 700]}
21+
xAccessor="x"
22+
yAccessor="y"
23+
pointStyle={d => ({ fill: d.color })}
24+
canvasPoints={true}
25+
axes={[
26+
{ orient: "left" },
27+
{
28+
orient: "bottom",
29+
tickFormat: d => <text transform="rotate(45)">{d}</text>
30+
}
31+
]}
32+
margin={50}
33+
hoverAnnotation={true}
34+
tooltipContent={d => (
35+
<div className="tooltip-content">
36+
<p>Price: {d.x}</p>
37+
<p>Carat: {d.y}</p>
38+
<p>
39+
{d.coincidentPoints.length}
40+
</p>
41+
</div>
42+
)}
43+
/>
44+
`
45+
})
46+
47+
return (
48+
<DocumentComponent
49+
name="Canvas Interaction Layer Map"
50+
components={components}
51+
examples={examples}
52+
buttons={[]}
53+
>
54+
<p>
55+
In XYFrame if you have points or lines rendered with canvas then the
56+
interaction voronoi will also be rendered with canvas, allowing for
57+
large-scale viz like this scatterplot of 50,000+ points. Canvas
58+
interaction layers aren't currently supported in OrdinalFrame and
59+
NetworkFrame.
60+
</p>
61+
</DocumentComponent>
62+
)
63+
}
64+
}
65+
66+
CanvasInteractionDocs.title = "Canvas Interaction"

0 commit comments

Comments
 (0)