Skip to content

Commit c8754a3

Browse files
committed
Improve annotation editing experience
1 parent 370bbdd commit c8754a3

File tree

10 files changed

+3516
-3355
lines changed

10 files changed

+3516
-3355
lines changed

package-lock.json

Lines changed: 3261 additions & 3245 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@
158158
"polygon-offset": "0.3.1",
159159
"promise": "8.0.1",
160160
"prop-types": "15.6.0",
161-
"react-annotation": "2.0.0",
161+
"react-annotation": "2.1.5",
162162
"roughjs-es5": "0.1.0",
163163
"semiotic-mark": "0.3.1",
164164
"svg-path-bounding-box": "1.0.4"

src/components/Annotation.js

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ type Props = {
66
noteData: {
77
eventListeners: Object,
88
events: Object,
9+
onDragEnd?: Function,
10+
onDragStart?: Function,
11+
onDrag?: Function,
912
type: *,
1013
screenCoordinates: Array<Array<number>>,
1114
// What is this type supposed to be? It gets used only in a boolean context
@@ -20,15 +23,37 @@ type Props = {
2023
}
2124
}
2225

26+
const interactivityFns = ["onDragEnd", "onDragStart", "onDrag"]
27+
2328
class SemioticAnnotation extends React.Component<Props, null> {
2429
render() {
25-
const { noteData } = this.props
26-
const { screenCoordinates } = noteData
30+
const { noteData: baseNoteData } = this.props
31+
const { screenCoordinates } = baseNoteData
32+
33+
const noteData = { ...baseNoteData }
34+
35+
interactivityFns.forEach(d => {
36+
if (baseNoteData[d]) {
37+
delete noteData[d]
38+
const originalFn = baseNoteData[d]
39+
noteData[d] = updatedSettingsFromRA => {
40+
originalFn({
41+
originalSettings: baseNoteData,
42+
updatedSettings: updatedSettingsFromRA,
43+
noteIndex: baseNoteData.i
44+
})
45+
}
46+
}
47+
})
2748

2849
const Label =
2950
typeof noteData.type === "function" ? noteData.type : AnnotationLabel
3051

3152
const eventListeners = noteData.eventListeners || noteData.events || {}
53+
const finalStyle = {}
54+
if (noteData.events || noteData.eventListeners || noteData.editMode) {
55+
finalStyle.pointerEvents = "all"
56+
}
3257

3358
if (noteData.coordinates && screenCoordinates) {
3459
//Multisubject annotation
@@ -42,15 +67,9 @@ class SemioticAnnotation extends React.Component<Props, null> {
4267
nx: setNX,
4368
ny: setNY
4469
})
45-
4670
return <Label key={`multi-annotation-${i}`} {...subjectNote} />
4771
})
4872

49-
const finalStyle = {}
50-
if (noteData.events || noteData.eventListeners) {
51-
finalStyle.pointerEvents = "all"
52-
}
53-
5473
return (
5574
<g {...eventListeners} style={finalStyle}>
5675
{notes}
@@ -60,8 +79,8 @@ class SemioticAnnotation extends React.Component<Props, null> {
6079

6180
const finalAnnotation = <Label events={eventListeners} {...noteData} />
6281

63-
if (noteData.events) {
64-
return <g style={{ pointerEvents: "all" }}>{finalAnnotation}</g>
82+
if (finalStyle.pointerEvents) {
83+
return <g style={finalStyle}>{finalAnnotation}</g>
6584
}
6685

6786
return finalAnnotation

src/components/InteractionLayer.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,8 @@ class InteractionLayer extends React.Component<Props, State> {
214214
[xScale.invert(size[0]), yScale.invert(size[1])]
215215
]
216216
: actualBrush === "xBrush"
217-
? [xScale.invert(0), xScale.invert(size[0])]
218-
: [yScale.invert(0), yScale.invert(size[1])]
217+
? [xScale.invert(0), xScale.invert(size[0])]
218+
: [yScale.invert(0), yScale.invert(size[1])]
219219
} = interaction
220220

221221
if (extent.indexOf && extent.indexOf(undefined) !== -1) {
@@ -351,7 +351,8 @@ class InteractionLayer extends React.Component<Props, State> {
351351
this.props.overlay !== nextProps.overlay ||
352352
nextProps.points !== this.props.points ||
353353
this.props.xScale !== nextProps.xScale ||
354-
this.props.yScale !== nextProps.yScale
354+
this.props.yScale !== nextProps.yScale ||
355+
this.props.hoverAnnotation !== nextProps.hoverAnnotation
355356
) {
356357
this.setState({ overlayRegions: this.calculateOverlay(nextProps) })
357358
}
@@ -370,15 +371,16 @@ class InteractionLayer extends React.Component<Props, State> {
370371
overlay,
371372
interactionOverflow = { top: 0, bottom: 0, left: 0, right: 0 },
372373
customClickBehavior,
373-
customDoubleClickBehavior
374+
customDoubleClickBehavior,
375+
hoverAnnotation
374376
} = props
375377

376378
const pointerStyle =
377379
customClickBehavior || customDoubleClickBehavior
378380
? { cursor: "pointer" }
379381
: {}
380382

381-
if (points && props.hoverAnnotation && !overlay) {
383+
if (points && hoverAnnotation && !overlay) {
382384
const voronoiDataset = []
383385
const voronoiUniqueHash = {}
384386

src/components/annotationRules/baseRules.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ export const circleEnclosure = ({ d, i, circle }) => {
2626
subject: {
2727
radius: circle.r,
2828
radiusPadding
29-
}
29+
},
30+
i
3031
}
3132
)
3233

src/components/annotationRules/xyframeRules.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,8 @@ export const basicReactAnnotation = ({ screenCoordinates, d, i }) => {
213213
d,
214214
{
215215
type: d.type,
216-
screenCoordinates
216+
screenCoordinates,
217+
i
217218
}
218219
)
219220

@@ -240,7 +241,8 @@ export const svgXAnnotation = ({ screenCoordinates, d, i, adjustedSize }) => {
240241
x: screenCoordinates[0],
241242
y1: 0,
242243
y2: adjustedSize[1]
243-
}
244+
},
245+
i
244246
}
245247
)
246248
return <Annotation key={d.key || `annotation-${i}`} noteData={noteData} />
@@ -271,7 +273,8 @@ export const svgYAnnotation = ({
271273
y: screenCoordinates[1],
272274
x1: 0,
273275
x2: adjustedSize[0] + adjustedPosition[0]
274-
}
276+
},
277+
i
275278
}
276279
)
277280
return <Annotation key={d.key || `annotation-${i}`} noteData={noteData} />
@@ -311,7 +314,8 @@ export const svgBoundsAnnotation = ({
311314
subject: {
312315
width: Math.abs(x1Position - x0Position),
313316
height: Math.abs(y0Position - y1Position)
314-
}
317+
},
318+
i
315319
}
316320
)
317321
return <Annotation key={d.key || `annotation-${i}`} noteData={noteData} />
@@ -436,12 +440,12 @@ export const svgRectEncloseAnnotation = ({ d, i, screenCoordinates }) => {
436440
return rectangleEnclosure({ bboxNodes, d, i })
437441
}
438442

439-
export const svgEncloseAnnotation = ({ screenCoordinates, d }) => {
443+
export const svgEncloseAnnotation = ({ screenCoordinates, d, i }) => {
440444
const circle = packEnclose(
441445
screenCoordinates.map(p => ({ x: p[0], y: p[1], r: 2 }))
442446
)
443447

444-
return circleEnclosure({ d, circle })
448+
return circleEnclosure({ d, circle, i })
445449
}
446450

447451
export const svgHullEncloseAnnotation = ({ screenCoordinates, d, i }) => {

src/docs/components/AppleStockChart.js

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,21 @@ components.push({
1111
})
1212

1313
export default class AppleStockChart extends React.Component {
14+
constructor(props) {
15+
super(props)
16+
17+
this.state = {
18+
editMode: true,
19+
overridePosition: {}
20+
}
21+
}
1422
render() {
1523
const buttons = []
1624

1725
const examples = []
1826
examples.push({
1927
name: "Basic",
20-
demo: AppleStockChartRaw,
28+
demo: AppleStockChartRaw(this.state.editMode),
2129
source: `
2230
import { XYFrame, DividedLine } from "../../components"
2331
import { data } from '../sampledata/apple_stock'
@@ -91,6 +99,92 @@ const customTooltip = d => <div className="tooltip-content">
9199
`
92100
})
93101

102+
examples.push({
103+
name: "Editable Annotations",
104+
demo: (
105+
<div>
106+
<p>
107+
react-annotation already has built in functionality for adjusting
108+
the annotations it creates. You can activate the note editing
109+
control points by setting `editMode: true` on any of your
110+
annotations. With that in place, you can also set the drag,
111+
dragStart or dragEnd properties of the annotation to pass the new
112+
annotation position data to whatever you're using to manage state.
113+
In this simple example, I just override the dx/dy based on the new
114+
values but you could pass this back to a central annotation store or
115+
other method of saving changes.
116+
</p>
117+
<button
118+
style={{ color: "black" }}
119+
onClick={() => this.setState({ editMode: !this.state.editMode })}
120+
>
121+
{this.state.editMode ? "Turn off editMode" : "Turn on editMode"}
122+
</button>
123+
{AppleStockChartRaw(
124+
this.state.editMode,
125+
this.state.overridePosition,
126+
d => {
127+
this.setState({
128+
overridePosition: {
129+
...this.state.overridePosition,
130+
[d.noteIndex]: {
131+
dx: d.updatedSettings.dx,
132+
dy: d.updatedSettings.dy
133+
}
134+
}
135+
})
136+
}
137+
)}
138+
</div>
139+
),
140+
source: `
141+
constructor(props) {
142+
super(props)
143+
144+
this.state = {
145+
editMode: true,
146+
overridePosition: {}
147+
}
148+
}
149+
150+
render() {
151+
const annotations = [{
152+
type: "x",
153+
date: "7/9/1997",
154+
note: { label: "Steve Jobs Returns", align: "middle" },
155+
color: "rgb(0, 162, 206)",
156+
dy: -10,
157+
dx: 0,
158+
connector: { end: "none" },
159+
editMode,
160+
onDragEnd: annotationInfo => {
161+
annotationInfo => {
162+
this.setState({
163+
overridePosition: {
164+
...this.state.overridePosition,
165+
[annotationInfo.noteIndex]: {
166+
dx: annotationInfo.updatedSettings.dx,
167+
dy: annotationInfo.updatedSettings.dy
168+
}
169+
}
170+
})
171+
}
172+
173+
}
174+
}]
175+
176+
annotations.forEach((d, i) => {
177+
if (this.state.overridePosition[i]) {
178+
d.dx = overridePosition[i].dx
179+
d.dy = overridePosition[i].dy
180+
}
181+
})
182+
183+
return <XYFrame
184+
{...as above example}
185+
/>
186+
}`
187+
})
94188
return (
95189
<DocumentComponent
96190
name="Stock Chart with Annotations and Divided Line"
@@ -108,13 +202,18 @@ const customTooltip = d => <div className="tooltip-content">
108202
rel="noopener noreferrer"
109203
>
110204
Susie Lu's Apple stock chart
111-
</a>.
205+
</a>
206+
.
112207
</p>
113208
<p>
114209
It also uses a custom x scale using xScaleType to pass a scale built
115210
with D3's scaleTime, as well as tooltip processing rules using
116211
tooltipContent.
117212
</p>
213+
<p>
214+
(If you want to see how to allow your users to edit annotations, check
215+
out the second example below)
216+
</p>
118217
</DocumentComponent>
119218
)
120219
}

0 commit comments

Comments
 (0)