diff --git a/src/components/Document/index.js b/src/components/Document/index.js index 940d936..739664b 100644 --- a/src/components/Document/index.js +++ b/src/components/Document/index.js @@ -6,13 +6,14 @@ import type { Relationship } from "../../types" import { styled } from "@material-ui/styles" -import stringToSequence from "../../string-to-sequence.js" -import Tooltip from "@material-ui/core/Tooltip" import RelationshipArrows from "../RelationshipArrows" import colors from "../../colors" import ArrowToMouse from "../ArrowToMouse" import { useTimeout, useWindowSize } from "react-use" +import SequenceItem from "../SequenceItem" import classNames from "classnames" +import stringToSequence from "../../string-to-sequence" +import useEventCallback from "use-event-callback" const Container = styled("div")(({ relationshipsOn }) => ({ lineHeight: 1.5, @@ -21,50 +22,6 @@ const Container = styled("div")(({ relationshipsOn }) => ({ flexWrap: "wrap" })) -const SequenceItem = styled("span")(({ color, relationshipsOn }) => ({ - display: "inline-flex", - cursor: "pointer", - backgroundColor: color, - color: "#fff", - padding: 4, - margin: 4, - marginBottom: relationshipsOn ? 64 : 4, - paddingLeft: 10, - paddingRight: 10, - borderRadius: 4, - userSelect: "none", - boxSizing: "border-box", - "&.unlabeled": { - color: "#333", - paddingTop: 4, - paddingBottom: 4, - paddingLeft: 2, - paddingRight: 2, - ".notSpace:hover": { - paddingTop: 2, - paddingBottom: 2, - paddingLeft: 0, - paddingRight: 0, - border: `2px dashed #ccc` - } - } -})) - -const LabeledText = styled("div")({ - display: "inline-flex", - cursor: "pointer", - alignSelf: "center", - fontSize: 11, - width: 18, - height: 18, - alignItems: "center", - justifyContent: "center", - marginLeft: 4, - borderRadius: 9, - color: "#fff", - backgroundColor: "rgba(0,0,0,0.2)" -}) - type Props = { sequence: Array, relationships: Array, @@ -126,6 +83,16 @@ export default function Document({ highlightedItems.push(i) } + const onRemoveLabel = useEventCallback(sequenceItemIndex => { + onSequenceChange( + sequence + .flatMap((s, i) => + i !== sequenceItemIndex ? s : stringToSequence(s.text) + ) + .filter(s => s.text.length > 0) + ) + }) + return ( {sequence.map((seq, i) => ( { - if (!elm) return - sequenceItemPositionsRef.current[seq.textId] = { - offset: { - left: elm.offsetLeft, - top: elm.offsetTop, - width: elm.offsetWidth, - height: elm.offsetHeight - } - } - }} + {...seq} + sequenceItemIndex={i} + sequenceItemPositionsRef={sequenceItemPositionsRef} relationshipsOn={Boolean(relationships)} - onMouseUp={e => { - if (!createRelationshipsMode) return - if (!secondSequenceItem) { - setFirstSequenceItem(null) - setSecondSequenceItem(null) - onCreateEmptyRelationship([firstSequenceItem, seq.textId]) - } else { - setFirstSequenceItem(null) - setSecondSequenceItem(null) - } - }} - onMouseDown={() => { - if (createRelationshipsMode) { - if (!firstSequenceItem) { - setFirstSequenceItem(seq.textId) - } - } else { - if (seq.label) return - changeHighlightedRange([i, i]) - } - }} - onMouseMove={() => { - if (!mouseDown) return - if (!createRelationshipsMode) { - if (seq.label) return - if (i !== lastSelected) { - changeHighlightedRange([ - firstSelected === null ? i : firstSelected, - i - ]) - } - } - }} - className={classNames( - seq.label ? "label" : "unlabeled", - seq.text.trim().length > 0 && "notSpace" - )} - color={ - seq.label - ? seq.color || colorLabelMap[seq.label] || "#333" - : !createRelationshipsMode && - seq.text !== " " && - highlightedItems.includes(i) - ? "#ccc" - : "inherit" - } + createRelationshipsMode={createRelationshipsMode} + onChangeFirstSequenceItem={setFirstSequenceItem} + onChangeSecondSequenceItem={setSecondSequenceItem} + onCreateEmptyRelationship={onCreateEmptyRelationship} + onChangeHighlightedRange={changeHighlightedRange} + firstSequenceItem={firstSequenceItem} + secondSequenceItem={secondSequenceItem} + mouseDown={mouseDown} + firstSelected={firstSelected} + lastSelected={lastSelected} + isHighlighted={highlightedItems.includes(i)} + onRemoveLabel={onRemoveLabel} + color={seq.color || colorLabelMap[seq.label]} key={i} - > - {seq.label ? ( - -
{seq.text}
-
- ) : ( -
{seq.text}
- )} - {seq.label && !createRelationshipsMode && ( - { - e.stopPropagation() - onSequenceChange( - sequence - .flatMap(s => (s !== seq ? s : stringToSequence(s.text))) - .filter(s => s.text.length > 0) - ) - }} - > - {"\u2716"} - - )} -
+ /> ))} {firstSequenceItem && !secondSequenceItem && ( )) @@ -41,15 +41,15 @@ storiesOf("Document", module) Math.random() < 0.9 ? { text: text + " ", textId: `l${i}` } : { - text: text + " ", - textId: `l${i}`, - label: - "somelabel" + - Math.random() - .toString() - .slice(-4), - color: "#9638F9" - } + text: text + " ", + textId: `l${i}`, + label: + "somelabel" + + Math.random() + .toString() + .slice(-4), + color: "#9638F9" + } )} relationships={[ { @@ -60,3 +60,15 @@ storiesOf("Document", module) ]} /> )) + .add("Character Sequence", () => ( + ({ + text: c + })) + } + /> + )) \ No newline at end of file diff --git a/src/components/DocumentLabeler/index.js b/src/components/DocumentLabeler/index.js index 9731f34..c339de4 100644 --- a/src/components/DocumentLabeler/index.js +++ b/src/components/DocumentLabeler/index.js @@ -12,9 +12,10 @@ export default function DocumentLabeler(props: LabelDocumentProps) { const [selectedLabels, changeSelectedLabels] = useState( props.initialLabels || (props.initialLabel ? [props.initialLabel] : []) ) - const sequence = useMemo(() => stringToSequence(props.document), [ - props.document - ]) + const sequence = useMemo( + () => stringToSequence(props.document, props.separatorRegex), + [props.document] + ) return (
@@ -58,7 +59,11 @@ export default function DocumentLabeler(props: LabelDocumentProps) { ) })}
- +
) diff --git a/src/components/NLPAnnotator/index.story.js b/src/components/NLPAnnotator/index.story.js index 5551269..2b4a5fc 100644 --- a/src/components/NLPAnnotator/index.story.js +++ b/src/components/NLPAnnotator/index.story.js @@ -32,6 +32,30 @@ storiesOf("NLPAnnotator", module) ]} /> )) + .add("Sequence Labeler with Custom Regex", () => ( + + )) .add("Document Labeler", () => (
{ diff --git a/src/components/SequenceAnnotator/index.js b/src/components/SequenceAnnotator/index.js index 0f16b77..fdb0907 100644 --- a/src/components/SequenceAnnotator/index.js +++ b/src/components/SequenceAnnotator/index.js @@ -18,7 +18,7 @@ export default function SequenceAnnotator(props: SequenceAnnotatorProps) { ? [entity] : stringToSequence(entity.text, props.separatorRegex) ) - : stringToSequence(props.document) + : stringToSequence(props.document, props.separatorRegex) ) const colorLabelMap = useMemo( () => @@ -71,6 +71,7 @@ export default function SequenceAnnotator(props: SequenceAnnotatorProps) {
diff --git a/src/components/SequenceItem/index.js b/src/components/SequenceItem/index.js new file mode 100644 index 0000000..b151342 --- /dev/null +++ b/src/components/SequenceItem/index.js @@ -0,0 +1,153 @@ +import React, { memo } from "react" +import classNames from "classnames" +import { styled, Tooltip } from "@material-ui/core" +import stringToSequence from "../../string-to-sequence.js" + +const SequenceItemContainer = styled("span")(({ color, relationshipsOn }) => ({ + display: "inline-flex", + cursor: "pointer", + backgroundColor: color, + color: "#fff", + padding: 4, + margin: 4, + marginBottom: relationshipsOn ? 64 : 4, + paddingLeft: 10, + paddingRight: 10, + borderRadius: 4, + userSelect: "none", + boxSizing: "border-box", + "&.unlabeled": { + color: "#333", + paddingTop: 4, + paddingBottom: 4, + paddingLeft: 2, + paddingRight: 2, + ".notSpace:hover": { + paddingTop: 2, + paddingBottom: 2, + paddingLeft: 0, + paddingRight: 0, + border: `2px dashed #ccc` + } + } +})) + +const XContainer = styled("div")({ + display: "inline-flex", + cursor: "pointer", + alignSelf: "center", + fontSize: 11, + width: 18, + height: 18, + alignItems: "center", + justifyContent: "center", + marginLeft: 4, + borderRadius: 9, + color: "#fff", + backgroundColor: "rgba(0,0,0,0.2)" +}) + +export const SequenceItem = ({ + textId, + text, + label, + color, + sequenceItemIndex, + sequenceItemPositionsRef, + relationshipsOn, + createRelationshipsMode, + onChangeFirstSequenceItem, + onChangeSecondSequenceItem, + onCreateEmptyRelationship, + onChangeHighlightedRange, + firstSequenceItem, + secondSequenceItem, + mouseDown, + firstSelected, + lastSelected, + colorLabelMap, + isHighlighted, + onRemoveLabel +}) => { + return ( + { + if (!elm) return + sequenceItemPositionsRef.current[textId] = { + offset: { + left: elm.offsetLeft, + top: elm.offsetTop, + width: elm.offsetWidth, + height: elm.offsetHeight + } + } + }} + relationshipsOn={relationshipsOn} + onMouseUp={e => { + if (!createRelationshipsMode) return + if (!secondSequenceItem) { + onChangeFirstSequenceItem(null) + onChangeSecondSequenceItem(null) + onCreateEmptyRelationship([firstSequenceItem, textId]) + } else { + onChangeFirstSequenceItem(null) + onChangeSecondSequenceItem(null) + } + }} + onMouseDown={() => { + if (createRelationshipsMode) { + if (!firstSequenceItem) { + onChangeFirstSequenceItem(textId) + } + } else { + if (label) return + onChangeHighlightedRange([sequenceItemIndex, sequenceItemIndex]) + } + }} + onMouseMove={() => { + if (!mouseDown) return + if (!createRelationshipsMode) { + if (label) return + if (sequenceItemIndex !== lastSelected) { + onChangeHighlightedRange([ + firstSelected === null ? sequenceItemIndex : firstSelected, + sequenceItemIndex + ]) + } + } + }} + className={classNames( + label ? "label" : "unlabeled", + text.trim().length > 0 && "notSpace" + )} + color={ + label + ? color || "#333" + : !createRelationshipsMode && text !== " " && isHighlighted + ? "#ccc" + : "inherit" + } + > + {label ? ( + +
{text}
+
+ ) : ( +
{text}
+ )} + {label && !createRelationshipsMode && ( + { + e.stopPropagation() + onRemoveLabel(sequenceItemIndex) + }} + > + + + )} +
+ ) +} + +export default memo(SequenceItem) diff --git a/src/components/SequenceItem/index.story.js b/src/components/SequenceItem/index.story.js new file mode 100644 index 0000000..ae54c0c --- /dev/null +++ b/src/components/SequenceItem/index.story.js @@ -0,0 +1,33 @@ +// @flow + +import React from "react" + +import { storiesOf } from "@storybook/react" +import { action } from "@storybook/addon-actions" +import SequenceItem from "./" + +storiesOf("SequenceItem", module).add("Basic", () => { + return ( + + ) +}) diff --git a/src/string-to-sequence.js b/src/string-to-sequence.js index 395e513..a4385a9 100644 --- a/src/string-to-sequence.js +++ b/src/string-to-sequence.js @@ -1,8 +1,11 @@ // @flow -const stringToSequence = (doc: string, sepRe: RegExp = /[a-zA-ZÀ-ÿ]+/g) => { +const stringToSequence = ( + doc: string, + sepRe: RegExp | string = /[a-zA-ZÀ-ÿ]+/g +) => { if (typeof sepRe === "string") { - sepRe = new RegExp(sepRe) + sepRe = new RegExp(sepRe, "g") } let m let indices = [0] @@ -15,7 +18,6 @@ const stringToSequence = (doc: string, sepRe: RegExp = /[a-zA-ZÀ-ÿ]+/g) => { } while (m) indices = indices.concat([doc.length]) return indices - .filter((_, i) => indices[i] !== indices[i + 1]) .map((_, i) => ({ text: doc.slice(indices[i], indices[i + 1]), textId: Math.random() diff --git a/src/types.js b/src/types.js index c952c54..19f49de 100644 --- a/src/types.js +++ b/src/types.js @@ -32,6 +32,7 @@ export type LabelDocumentProps = { multipleLabels?: boolean, document: string, initialLabels?: Array, + separatorRegex?: string, onChange: (Array | string | null) => any }