Skip to content

Commit 48169ba

Browse files
authored
Merge pull request UniversalDataTool#9 from UniversalDataTool/feat/workspace-and-more
Integrate React Material Workspace and Introduce Entity Relations
2 parents 2efb72c + 6d211be commit 48169ba

File tree

15 files changed

+1406
-108
lines changed

15 files changed

+1406
-108
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
"version": "0.2.0",
44
"homepage": "https://waoai.github.io/react-nlp-annotate/",
55
"dependencies": {
6+
"@material-ui/lab": "^4.0.0-alpha.56",
7+
"color-alpha": "^1.0.4",
68
"react-hotkeys": "^2.0.0",
79
"react-material-workspace-layout": "^0.1.6",
10+
"react-use": "^15.3.3",
811
"use-event-callback": "^0.1.0"
912
},
1013
"scripts": {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// @flow
2+
3+
import React, { useRef } from "react"
4+
import { styled } from "@material-ui/core/styles"
5+
import { useMouse } from "react-use"
6+
7+
const Container = styled("div")({
8+
position: "absolute",
9+
left: 0,
10+
top: 0,
11+
right: 0,
12+
bottom: 0,
13+
pointerEvents: "none"
14+
})
15+
16+
export const ArrowToMouse = ({ startAt } = {}) => {
17+
const ref = useRef(null)
18+
let { elX: mx, elY: my } = useMouse(ref)
19+
let dx, dy
20+
if (mx === 0 && my === 0) {
21+
dx = 0
22+
dy = 0
23+
mx = startAt.left + startAt.width / 2
24+
my = startAt.top + startAt.height / 2
25+
} else {
26+
const a = mx - startAt.left
27+
const b = my - startAt.top
28+
const c = Math.sqrt(a * a + b * b)
29+
const sf = c < 100 ? (c / 100) ** 2 : 1
30+
dx = (a / c) * sf
31+
dy = (b / c) * sf
32+
}
33+
return (
34+
<Container ref={ref}>
35+
<svg
36+
width={Math.max(mx, startAt.left + startAt.width + 8)}
37+
height={Math.max(my, startAt.top + startAt.height + 8)}
38+
>
39+
<defs>
40+
<marker
41+
id={"arrowhead"}
42+
markerWidth="5"
43+
markerHeight="5"
44+
refX="0"
45+
refY="2.5"
46+
orient="auto"
47+
>
48+
<polygon fill={"rgba(255,0,0,0.75)"} points="0 0, 6 2.5, 0 5" />
49+
</marker>
50+
</defs>
51+
<rect
52+
x={startAt.left - 5}
53+
y={startAt.top - 5}
54+
width={startAt.width + 10}
55+
height={startAt.height + 10}
56+
stroke="rgba(255,0,0,0.75)"
57+
stroke-dasharray="10 5"
58+
fill="none"
59+
/>
60+
<line
61+
x1={startAt.left + startAt.width / 2}
62+
y1={startAt.top + startAt.height / 2}
63+
x2={mx - dx * 30}
64+
y2={my - dy * 30}
65+
marker-end={`url(#arrowhead)`}
66+
stroke-width={3}
67+
stroke="rgba(255,0,0,0.75)"
68+
fill="none"
69+
/>
70+
</svg>
71+
</Container>
72+
)
73+
}
74+
75+
export default ArrowToMouse

src/components/Document/index.js

Lines changed: 180 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,73 @@
11
// @flow
22

3-
import React, { useState } from "react"
4-
import type { SequenceItem } from "../../types"
5-
import { makeStyles } from "@material-ui/styles"
3+
import React, { useState, useRef, useEffect } from "react"
4+
import type {
5+
SequenceItem as SequenceItemData,
6+
Relationship
7+
} from "../../types"
8+
import { styled } from "@material-ui/styles"
69
import stringToSequence from "../../string-to-sequence.js"
710
import Tooltip from "@material-ui/core/Tooltip"
11+
import RelationshipArrows from "../RelationshipArrows"
12+
import colors from "../../colors"
13+
import ArrowToMouse from "../ArrowToMouse"
14+
import { useTimeout, useWindowSize } from "react-use"
15+
import classNames from "classnames"
816

9-
const useStyles = makeStyles({})
17+
const Container = styled("div")(({ relationshipsOn }) => ({
18+
lineHeight: 1.5,
19+
marginTop: relationshipsOn ? 64 : 0,
20+
display: "flex",
21+
flexWrap: "wrap"
22+
}))
23+
24+
const SequenceItem = styled("span")(({ color, relationshipsOn }) => ({
25+
display: "inline-flex",
26+
cursor: "pointer",
27+
backgroundColor: color,
28+
color: "#fff",
29+
padding: 4,
30+
margin: 4,
31+
marginBottom: relationshipsOn ? 64 : 4,
32+
paddingLeft: 10,
33+
paddingRight: 10,
34+
borderRadius: 4,
35+
userSelect: "none",
36+
boxSizing: "border-box",
37+
"&.unlabeled": {
38+
color: "#333",
39+
paddingTop: 4,
40+
paddingBottom: 4,
41+
paddingLeft: 2,
42+
paddingRight: 2,
43+
".notSpace:hover": {
44+
paddingTop: 2,
45+
paddingBottom: 2,
46+
paddingLeft: 0,
47+
paddingRight: 0,
48+
border: `2px dashed #ccc`
49+
}
50+
}
51+
}))
52+
53+
const LabeledText = styled("div")({
54+
display: "inline-flex",
55+
cursor: "pointer",
56+
alignSelf: "center",
57+
fontSize: 11,
58+
width: 18,
59+
height: 18,
60+
alignItems: "center",
61+
justifyContent: "center",
62+
marginLeft: 4,
63+
borderRadius: 9,
64+
color: "#fff",
65+
backgroundColor: "rgba(0,0,0,0.2)"
66+
})
1067

1168
type Props = {
12-
sequence: Array<SequenceItem>,
69+
sequence: Array<SequenceItemData>,
70+
relationships: Array<Relationship>,
1371
canModifySequence?: boolean,
1472
onSequenceChange?: (Array<SequenceItem>) => any,
1573
onHighlightedChanged?: (Array<number>) => any,
@@ -19,17 +77,39 @@ type Props = {
1977

2078
export default function Document({
2179
sequence,
80+
relationships,
2281
onHighlightedChanged = () => null,
82+
onCreateEmptyRelationship = () => null,
2383
onSequenceChange = () => null,
84+
onRelationshipsChange = () => null,
2485
nothingHighlighted = false,
86+
createRelationshipsMode = false,
2587
colorLabelMap = {}
2688
}: Props) {
89+
const sequenceItemPositionsRef = useRef({})
2790
const [mouseDown, changeMouseDown] = useState()
91+
const [timeoutCalled, cancelTimeout, resetTimeout] = useTimeout(30) // Force rerender after mounting
92+
const windowSize = useWindowSize()
93+
useEffect(() => {
94+
resetTimeout()
95+
}, [windowSize])
2896
const [
2997
[firstSelected, lastSelected],
3098
changeHighlightedRangeState
3199
] = useState([null, null])
100+
101+
const [firstSequenceItem, setFirstSequenceItem] = useState(null)
102+
const [secondSequenceItem, setSecondSequenceItem] = useState(null)
103+
104+
useEffect(() => {
105+
setFirstSequenceItem(null)
106+
setSecondSequenceItem(null)
107+
changeHighlightedRangeState([null, null])
108+
changeMouseDown(false)
109+
}, [createRelationshipsMode])
110+
32111
const changeHighlightedRange = ([first, last]) => {
112+
if (createRelationshipsMode) return
33113
changeHighlightedRangeState([first, last])
34114
const highlightedItems = []
35115
for (let i = Math.min(first, last); i <= Math.max(first, last); i++)
@@ -47,55 +127,79 @@ export default function Document({
47127
}
48128

49129
return (
50-
<div
130+
<Container
131+
relationshipsOn={Boolean(relationships)}
51132
onMouseDown={() => changeMouseDown(true)}
52-
onMouseUp={() => changeMouseDown(false)}
133+
onMouseUp={() => {
134+
if (createRelationshipsMode && firstSequenceItem) {
135+
setFirstSequenceItem(null)
136+
if (secondSequenceItem) {
137+
setSecondSequenceItem(null)
138+
}
139+
}
140+
changeMouseDown(false)
141+
}}
53142
>
54143
{sequence.map((seq, i) => (
55-
<span
56-
key={i}
144+
<SequenceItem
145+
key={seq.textId || i}
146+
ref={elm => {
147+
if (!elm) return
148+
sequenceItemPositionsRef.current[seq.textId] = {
149+
offset: {
150+
left: elm.offsetLeft,
151+
top: elm.offsetTop,
152+
width: elm.offsetWidth,
153+
height: elm.offsetHeight
154+
}
155+
}
156+
}}
157+
relationshipsOn={Boolean(relationships)}
158+
onMouseUp={e => {
159+
if (!createRelationshipsMode) return
160+
if (!secondSequenceItem) {
161+
setFirstSequenceItem(null)
162+
setSecondSequenceItem(null)
163+
onCreateEmptyRelationship([firstSequenceItem, seq.textId])
164+
} else {
165+
setFirstSequenceItem(null)
166+
setSecondSequenceItem(null)
167+
}
168+
}}
57169
onMouseDown={() => {
58-
if (seq.label) return
59-
changeHighlightedRange([i, i])
170+
if (createRelationshipsMode) {
171+
if (!firstSequenceItem) {
172+
setFirstSequenceItem(seq.textId)
173+
}
174+
} else {
175+
if (seq.label) return
176+
changeHighlightedRange([i, i])
177+
}
60178
}}
61179
onMouseMove={() => {
62-
if (seq.label) return
63-
if (mouseDown && i !== lastSelected) {
64-
changeHighlightedRange([
65-
firstSelected === null ? i : firstSelected,
66-
i
67-
])
180+
if (!mouseDown) return
181+
if (!createRelationshipsMode) {
182+
if (seq.label) return
183+
if (i !== lastSelected) {
184+
changeHighlightedRange([
185+
firstSelected === null ? i : firstSelected,
186+
i
187+
])
188+
}
68189
}
69190
}}
70-
style={
191+
className={classNames(
192+
seq.label ? "label" : "unlabeled",
193+
seq.text.trim().length > 0 && "notSpace"
194+
)}
195+
color={
71196
seq.label
72-
? {
73-
display: "inline-flex",
74-
backgroundColor:
75-
seq.color || colorLabelMap[seq.label] || "#333",
76-
color: "#fff",
77-
padding: 4,
78-
margin: 4,
79-
paddingLeft: 10,
80-
paddingRight: 10,
81-
borderRadius: 4,
82-
userSelect: "none"
83-
}
84-
: {
85-
display: "inline-flex",
86-
backgroundColor:
87-
seq.text !== " " && highlightedItems.includes(i)
88-
? "#ccc"
89-
: "inherit",
90-
color: "#333",
91-
marginTop: 4,
92-
marginBottom: 4,
93-
paddingTop: 4,
94-
paddingBottom: 4,
95-
paddingLeft: 2,
96-
paddingRight: 2,
97-
userSelect: "none"
98-
}
197+
? seq.color || colorLabelMap[seq.label] || "#333"
198+
: !createRelationshipsMode &&
199+
seq.text !== " " &&
200+
highlightedItems.includes(i)
201+
? "#ccc"
202+
: "inherit"
99203
}
100204
key={i}
101205
>
@@ -106,35 +210,46 @@ export default function Document({
106210
) : (
107211
<div>{seq.text}</div>
108212
)}
109-
{seq.label && (
110-
<div
111-
onClick={() => {
213+
{seq.label && !createRelationshipsMode && (
214+
<LabeledText
215+
onClick={e => {
216+
e.stopPropagation()
112217
onSequenceChange(
113218
sequence
114219
.flatMap(s => (s !== seq ? s : stringToSequence(s.text)))
115220
.filter(s => s.text.length > 0)
116221
)
117222
}}
118-
style={{
119-
display: "inline-flex",
120-
cursor: "pointer",
121-
alignSelf: "center",
122-
fontSize: 11,
123-
width: 18,
124-
height: 18,
125-
alignItems: "center",
126-
justifyContent: "center",
127-
marginLeft: 4,
128-
borderRadius: 9,
129-
color: "#fff",
130-
backgroundColor: "rgba(0,0,0,0.2)"
131-
}}
132223
>
133224
<span>{"\u2716"}</span>
134-
</div>
225+
</LabeledText>
135226
)}
136-
</span>
227+
</SequenceItem>
137228
))}
138-
</div>
229+
{firstSequenceItem && !secondSequenceItem && (
230+
<ArrowToMouse
231+
startAt={
232+
((sequenceItemPositionsRef.current || {})[firstSequenceItem] || {})
233+
.offset
234+
}
235+
/>
236+
)}
237+
{relationships && (
238+
<RelationshipArrows
239+
onClickArrow={({ label, from, to }) => {
240+
onRelationshipsChange(
241+
relationships.filter(
242+
r => !(r.from === from && r.to === to && r.label === label)
243+
)
244+
)
245+
}}
246+
positions={sequenceItemPositionsRef.current}
247+
arrows={relationships.map((a, i) => ({
248+
...a,
249+
color: a.color || colors[i % colors.length]
250+
}))}
251+
/>
252+
)}
253+
</Container>
139254
)
140255
}

0 commit comments

Comments
 (0)