tycho-components 0.40.6 → 0.40.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configs/Localization.js +2 -0
- package/dist/configs/localization/ConlluGraphTexts.d.ts +35 -0
- package/dist/configs/localization/ConlluGraphTexts.js +35 -0
- package/dist/features/ConlluGraphViewer/ConlluGraphViewer.d.ts +12 -0
- package/dist/features/ConlluGraphViewer/ConlluGraphViewer.js +115 -0
- package/dist/features/ConlluGraphViewer/ConlluGraphViewerRenderer.d.ts +25 -0
- package/dist/features/ConlluGraphViewer/ConlluGraphViewerRenderer.js +195 -0
- package/dist/features/ConlluGraphViewer/index.d.ts +2 -0
- package/dist/features/ConlluGraphViewer/index.js +1 -0
- package/dist/features/ConlluGraphViewer/style.scss +73 -0
- package/dist/features/index.d.ts +2 -0
- package/dist/features/index.js +1 -0
- package/dist/functions/conllu/ConlluUtils.d.ts +5 -0
- package/dist/functions/conllu/ConlluUtils.js +49 -0
- package/dist/functions/conllu/updateDependencyInConllu.d.ts +4 -0
- package/dist/functions/conllu/updateDependencyInConllu.js +52 -0
- package/dist/functions/index.d.ts +1 -0
- package/dist/functions/index.js +1 -0
- package/package.json +3 -1
|
@@ -11,6 +11,7 @@ import { HelpTexts } from "./localization/HelpTexts";
|
|
|
11
11
|
import { HistoryTexts } from "./localization/HistoryTexts";
|
|
12
12
|
import { ParametersTexts } from "./localization/ParametersTexts";
|
|
13
13
|
import { SentenceTexts } from "./localization/SentenceTexts";
|
|
14
|
+
import { ConlluGraphTexts } from "./localization/ConlluGraphTexts";
|
|
14
15
|
import { TreeTexts } from "./localization/TreeTexts";
|
|
15
16
|
import { UploadTexts } from "./localization/UploadTexts";
|
|
16
17
|
function buildResources(namespaces) {
|
|
@@ -31,6 +32,7 @@ const CORE_NAMESPACES = {
|
|
|
31
32
|
};
|
|
32
33
|
const FEATURE_NAMESPACES = {
|
|
33
34
|
comments: CommentsTexts,
|
|
35
|
+
'conllu-graph': ConlluGraphTexts,
|
|
34
36
|
tree: TreeTexts,
|
|
35
37
|
sentence: SentenceTexts,
|
|
36
38
|
parameters: ParametersTexts,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const ConlluGraphTexts: {
|
|
2
|
+
en: {
|
|
3
|
+
'button.tooltip.zoom.in': string;
|
|
4
|
+
'button.tooltip.zoom.out': string;
|
|
5
|
+
'button.tooltip.recenter': string;
|
|
6
|
+
'button.tooltip.download': string;
|
|
7
|
+
'popover.id': string;
|
|
8
|
+
'popover.form': string;
|
|
9
|
+
'popover.pos': string;
|
|
10
|
+
'popover.xpos': string;
|
|
11
|
+
'popover.lemma': string;
|
|
12
|
+
};
|
|
13
|
+
'pt-BR': {
|
|
14
|
+
'button.tooltip.zoom.in': string;
|
|
15
|
+
'button.tooltip.zoom.out': string;
|
|
16
|
+
'button.tooltip.recenter': string;
|
|
17
|
+
'button.tooltip.download': string;
|
|
18
|
+
'popover.id': string;
|
|
19
|
+
'popover.form': string;
|
|
20
|
+
'popover.pos': string;
|
|
21
|
+
'popover.xpos': string;
|
|
22
|
+
'popover.lemma': string;
|
|
23
|
+
};
|
|
24
|
+
it: {
|
|
25
|
+
'button.tooltip.zoom.in': string;
|
|
26
|
+
'button.tooltip.zoom.out': string;
|
|
27
|
+
'button.tooltip.recenter': string;
|
|
28
|
+
'button.tooltip.download': string;
|
|
29
|
+
'popover.id': string;
|
|
30
|
+
'popover.form': string;
|
|
31
|
+
'popover.pos': string;
|
|
32
|
+
'popover.xpos': string;
|
|
33
|
+
'popover.lemma': string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const ConlluGraphTexts = {
|
|
2
|
+
en: {
|
|
3
|
+
'button.tooltip.zoom.in': 'Zoom in',
|
|
4
|
+
'button.tooltip.zoom.out': 'Zoom out',
|
|
5
|
+
'button.tooltip.recenter': 'Recenter',
|
|
6
|
+
'button.tooltip.download': 'Download',
|
|
7
|
+
'popover.id': 'ID',
|
|
8
|
+
'popover.form': 'Form',
|
|
9
|
+
'popover.pos': 'POS tag',
|
|
10
|
+
'popover.xpos': 'XPOS tag',
|
|
11
|
+
'popover.lemma': 'Lemma',
|
|
12
|
+
},
|
|
13
|
+
'pt-BR': {
|
|
14
|
+
'button.tooltip.zoom.in': 'Aumentar zoom',
|
|
15
|
+
'button.tooltip.zoom.out': 'Diminuir zoom',
|
|
16
|
+
'button.tooltip.recenter': 'Recentralizar',
|
|
17
|
+
'button.tooltip.download': 'Baixar',
|
|
18
|
+
'popover.id': 'ID',
|
|
19
|
+
'popover.form': 'Forma',
|
|
20
|
+
'popover.pos': 'Etiqueta POS',
|
|
21
|
+
'popover.xpos': 'Etiqueta XPOS',
|
|
22
|
+
'popover.lemma': 'Lema',
|
|
23
|
+
},
|
|
24
|
+
it: {
|
|
25
|
+
'button.tooltip.zoom.in': 'Zoom avanti',
|
|
26
|
+
'button.tooltip.zoom.out': 'Zoom indietro',
|
|
27
|
+
'button.tooltip.recenter': 'Ricentra',
|
|
28
|
+
'button.tooltip.download': 'Scarica',
|
|
29
|
+
'popover.id': 'ID',
|
|
30
|
+
'popover.form': 'Forma',
|
|
31
|
+
'popover.pos': 'Etichetta POS',
|
|
32
|
+
'popover.xpos': 'Etichetta XPOS',
|
|
33
|
+
'popover.lemma': 'Lemma',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import '@svgdotjs/svg.panzoom.js';
|
|
2
|
+
import { Conllu } from '../../configs/types/Conllu';
|
|
3
|
+
import { ConlluViewerItem } from '../../functions/conllu/ConlluViewerGraph';
|
|
4
|
+
import './style.scss';
|
|
5
|
+
export type ConlluGraphViewerProps = {
|
|
6
|
+
conllu: Conllu;
|
|
7
|
+
editable?: boolean;
|
|
8
|
+
onConlluChange?: (conllu: Conllu) => void;
|
|
9
|
+
onTokenClick?: (item: ConlluViewerItem) => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
export default function ConlluGraphViewer({ conllu, editable, onConlluChange, onTokenClick, className, }: ConlluGraphViewerProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Point, SVG as dotSVG } from '@svgdotjs/svg.js';
|
|
3
|
+
import '@svgdotjs/svg.panzoom.js';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
import { useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { IconButton } from 'tycho-storybook';
|
|
8
|
+
import ConlluUtils from '../../functions/conllu/ConlluUtils';
|
|
9
|
+
import ConlluViewerConverter from '../../functions/conllu/ConlluViewerConverter';
|
|
10
|
+
import { conlluViewerConfigurations } from '../../functions/conllu/ConlluViewerConstants';
|
|
11
|
+
import { drawDependencies, drawItems, drawMultis, } from './ConlluGraphViewerRenderer';
|
|
12
|
+
import './style.scss';
|
|
13
|
+
const ZOOM_FACTOR = 0.1;
|
|
14
|
+
const ZOOM_STEP = 1.1;
|
|
15
|
+
const DEFAULT_ZOOM = 1;
|
|
16
|
+
const applyDefaultView = (svg, graph) => {
|
|
17
|
+
svg.viewbox(0, 0, graph.width, graph.height);
|
|
18
|
+
const fitZoom = svg.zoom();
|
|
19
|
+
const initialZoom = Math.min(DEFAULT_ZOOM, fitZoom);
|
|
20
|
+
svg.zoom(initialZoom, new Point(graph.width / 2, graph.height / 2));
|
|
21
|
+
};
|
|
22
|
+
export default function ConlluGraphViewer({ conllu, editable = false, onConlluChange, onTokenClick, className, }) {
|
|
23
|
+
const { t } = useTranslation('conllu-graph');
|
|
24
|
+
const svgRef = useRef(null);
|
|
25
|
+
const svgDotRef = useRef(null);
|
|
26
|
+
const [graph, setGraph] = useState();
|
|
27
|
+
const [activeItem, setActiveItem] = useState();
|
|
28
|
+
const [draggedDep, setDraggedDep] = useState(null);
|
|
29
|
+
const [dragOverItem, setDragOverItem] = useState(null);
|
|
30
|
+
const handleGenerateImage = () => {
|
|
31
|
+
if (!svgRef.current || !graph) {
|
|
32
|
+
throw new Error('SVG element not found');
|
|
33
|
+
}
|
|
34
|
+
const svg = svgDotRef.current;
|
|
35
|
+
const previousViewbox = svg?.viewbox();
|
|
36
|
+
svg?.viewbox(0, 0, graph.width, graph.height);
|
|
37
|
+
try {
|
|
38
|
+
ConlluUtils.generateImageFromSVG(svgRef.current);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
if (svg && previousViewbox) {
|
|
42
|
+
svg.viewbox(previousViewbox.x, previousViewbox.y, previousViewbox.width, previousViewbox.height);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const handleZoomIn = () => {
|
|
47
|
+
const svg = svgDotRef.current;
|
|
48
|
+
if (!svg)
|
|
49
|
+
return;
|
|
50
|
+
svg.zoom(svg.zoom() * ZOOM_STEP);
|
|
51
|
+
};
|
|
52
|
+
const handleZoomOut = () => {
|
|
53
|
+
const svg = svgDotRef.current;
|
|
54
|
+
if (!svg)
|
|
55
|
+
return;
|
|
56
|
+
svg.zoom(svg.zoom() / ZOOM_STEP);
|
|
57
|
+
};
|
|
58
|
+
const handleRecenter = () => {
|
|
59
|
+
const svg = svgDotRef.current;
|
|
60
|
+
if (!svg || !graph)
|
|
61
|
+
return;
|
|
62
|
+
applyDefaultView(svg, graph);
|
|
63
|
+
};
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
try {
|
|
66
|
+
setGraph(new ConlluViewerConverter().execute(conllu));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.error(err);
|
|
70
|
+
setGraph(undefined);
|
|
71
|
+
}
|
|
72
|
+
}, [conllu]);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!graph || !svgRef.current)
|
|
75
|
+
return;
|
|
76
|
+
const svg = dotSVG(svgRef.current);
|
|
77
|
+
svg.panZoom({ zoomFactor: ZOOM_FACTOR });
|
|
78
|
+
applyDefaultView(svg, graph);
|
|
79
|
+
svgDotRef.current = svg;
|
|
80
|
+
return () => {
|
|
81
|
+
svg.panZoom(false);
|
|
82
|
+
svgDotRef.current = null;
|
|
83
|
+
};
|
|
84
|
+
}, [graph?.width, graph?.height]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const svg = svgDotRef.current;
|
|
87
|
+
if (!svg)
|
|
88
|
+
return;
|
|
89
|
+
svg.panZoom({
|
|
90
|
+
zoomFactor: ZOOM_FACTOR,
|
|
91
|
+
panning: draggedDep == null,
|
|
92
|
+
});
|
|
93
|
+
}, [draggedDep]);
|
|
94
|
+
if (!graph) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const rendererProps = {
|
|
98
|
+
graph,
|
|
99
|
+
activeItem,
|
|
100
|
+
setActiveItem,
|
|
101
|
+
editable,
|
|
102
|
+
conllu,
|
|
103
|
+
onTokenClick,
|
|
104
|
+
...(editable
|
|
105
|
+
? {
|
|
106
|
+
onDependencyChange: onConlluChange,
|
|
107
|
+
draggedDep,
|
|
108
|
+
setDraggedDep,
|
|
109
|
+
dragOverItem,
|
|
110
|
+
setDragOverItem,
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
113
|
+
};
|
|
114
|
+
return (_jsxs("div", { className: classNames('conllu-graph-viewer-container', className), children: [_jsxs("div", { className: "floating-buttons", children: [_jsx(IconButton, { name: "zoom_in", title: t('button.tooltip.zoom.in'), size: "small", mode: "ghost", onClick: handleZoomIn }), _jsx(IconButton, { name: "zoom_out", title: t('button.tooltip.zoom.out'), size: "small", mode: "ghost", onClick: handleZoomOut }), _jsx(IconButton, { name: "center_focus_weak", title: t('button.tooltip.recenter'), size: "small", mode: "ghost", onClick: handleRecenter }), _jsx(IconButton, { name: "download", title: t('button.tooltip.download'), size: "small", mode: "ghost", onClick: handleGenerateImage })] }), _jsxs("svg", { ref: svgRef, id: conlluViewerConfigurations.svgID, width: "100%", height: "100%", preserveAspectRatio: "xMidYMid meet", children: [drawDependencies(rendererProps), drawItems(rendererProps), drawMultis(rendererProps)] })] }));
|
|
115
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { Conllu } from '../../configs/types/Conllu';
|
|
3
|
+
import { ConlluViewerGraph, ConlluViewerItem } from '../../functions/conllu/ConlluViewerGraph';
|
|
4
|
+
export type ConlluGraphViewerRendererProps = {
|
|
5
|
+
graph: ConlluViewerGraph;
|
|
6
|
+
activeItem: ConlluViewerItem | undefined;
|
|
7
|
+
setActiveItem: (item: ConlluViewerItem | undefined) => void;
|
|
8
|
+
editable?: boolean;
|
|
9
|
+
onDependencyChange?: (updatedConllu: Conllu) => void;
|
|
10
|
+
conllu: Conllu;
|
|
11
|
+
draggedDep?: {
|
|
12
|
+
depIndex: number;
|
|
13
|
+
variant: number;
|
|
14
|
+
} | null;
|
|
15
|
+
setDraggedDep?: (dep: {
|
|
16
|
+
depIndex: number;
|
|
17
|
+
variant: number;
|
|
18
|
+
} | null) => void;
|
|
19
|
+
dragOverItem?: number | null;
|
|
20
|
+
setDragOverItem?: (itemIndex: number | null) => void;
|
|
21
|
+
onTokenClick?: (item: ConlluViewerItem) => void;
|
|
22
|
+
};
|
|
23
|
+
export declare const drawDependencies: ({ graph, activeItem, editable, draggedDep, setDraggedDep, }: ConlluGraphViewerRendererProps) => ReactElement | null;
|
|
24
|
+
export declare const drawItems: ({ graph, activeItem, setActiveItem, editable, onDependencyChange, conllu, dragOverItem, setDragOverItem, draggedDep, setDraggedDep, onTokenClick, }: ConlluGraphViewerRendererProps) => ReactElement | null;
|
|
25
|
+
export declare const drawMultis: ({ graph, }: ConlluGraphViewerRendererProps) => ReactElement | null;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Tooltip } from '@mui/material';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import ConlluUtils from '../../functions/conllu/ConlluUtils';
|
|
5
|
+
import { calculateConlluTextWidth, throwConlluError, } from '../../functions/conllu/ConlluViewerConverter';
|
|
6
|
+
import { conlluViewerConfigurations } from '../../functions/conllu/ConlluViewerConstants';
|
|
7
|
+
import updateDependencyInConllu from '../../functions/conllu/updateDependencyInConllu';
|
|
8
|
+
const PopoverItem = ({ item }) => {
|
|
9
|
+
const { t } = useTranslation('conllu-graph');
|
|
10
|
+
return (_jsx("table", { children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("td", { children: t('popover.id') }), _jsx("td", { children: item.lineno + 1 })] }), _jsxs("tr", { children: [_jsx("td", { children: t('popover.form') }), _jsx("td", { children: item.word })] }), _jsxs("tr", { children: [_jsx("td", { children: t('popover.pos') }), _jsx("td", { children: item.postag })] }), _jsxs("tr", { children: [_jsx("td", { children: t('popover.xpos') }), _jsx("td", { children: item.xpostag })] }), _jsxs("tr", { children: [_jsx("td", { children: t('popover.lemma') }), _jsx("td", { children: item.lemma })] }), _jsx("tr", { children: _jsx("td", { colSpan: 2, children: item.attribs }) })] }) }));
|
|
11
|
+
};
|
|
12
|
+
export const drawDependencies = ({ graph, activeItem, editable = false, draggedDep, setDraggedDep, }) => {
|
|
13
|
+
const results = [];
|
|
14
|
+
for (let variant = 0; variant < 2; variant++) {
|
|
15
|
+
let e = 'n';
|
|
16
|
+
if (variant === 1) {
|
|
17
|
+
if (!graph.hasEnhanced) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
e = 'e';
|
|
21
|
+
}
|
|
22
|
+
const lines = [];
|
|
23
|
+
const arrows = [];
|
|
24
|
+
const whites = [];
|
|
25
|
+
const texts = [];
|
|
26
|
+
graph.dependencies.forEach((dep, i) => {
|
|
27
|
+
if (dep.rel[variant] === '') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let i1 = dep.end - 1;
|
|
31
|
+
let i2 = dep.headpos - 1;
|
|
32
|
+
if (dep.headpos === 0) {
|
|
33
|
+
i2 = i1;
|
|
34
|
+
}
|
|
35
|
+
let d1 = graph.items[i1].x2 - graph.items[i1].x1 - 20;
|
|
36
|
+
let x1 = graph.items[i1].x1 +
|
|
37
|
+
10 +
|
|
38
|
+
Math.floor(d1 * ConlluUtils.calculateAnchor(graph, i1, i2, dep.lvl));
|
|
39
|
+
let d2 = graph.items[i2].x2 - graph.items[i2].x1 - 20;
|
|
40
|
+
let x2 = graph.items[i2].x1 +
|
|
41
|
+
10 +
|
|
42
|
+
Math.floor(d2 * ConlluUtils.calculateAnchor(graph, i2, i1, dep.lvl));
|
|
43
|
+
let y1 = conlluViewerConfigurations.margin +
|
|
44
|
+
conlluViewerConfigurations.edgeFontSize +
|
|
45
|
+
conlluViewerConfigurations.edgeFontOffset +
|
|
46
|
+
conlluViewerConfigurations.lvlHeight * (graph.maxlvl + 1);
|
|
47
|
+
let y2 = conlluViewerConfigurations.margin +
|
|
48
|
+
conlluViewerConfigurations.edgeFontSize +
|
|
49
|
+
conlluViewerConfigurations.edgeFontOffset +
|
|
50
|
+
conlluViewerConfigurations.lvlHeight * (graph.maxlvl - dep.lvl);
|
|
51
|
+
if (dep.headpos === 0) {
|
|
52
|
+
y2 =
|
|
53
|
+
conlluViewerConfigurations.margin +
|
|
54
|
+
conlluViewerConfigurations.edgeFontSize +
|
|
55
|
+
conlluViewerConfigurations.edgeFontOffset;
|
|
56
|
+
lines.push(_jsx("path", { className: `e${e}${dep.end} q${dep.end}q${dep.headpos}`, d: `M${x1} ${y1} L${x1} ${y2}` }, `deps_${i}`));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
lines.push(_jsx("path", { className: `e${e}${dep.end} e${e}${dep.headpos} q${dep.end}q${dep.headpos} ${ConlluUtils.isActiveDependency(dep, activeItem)
|
|
60
|
+
? 'active-line'
|
|
61
|
+
: ''}`, d: `M${x1} ${y1} L${x1} ${y2 + conlluViewerConfigurations.edgeDrop} C${x1} ${y2} ${x1} ${y2} ${(x1 + x2) / 2} ${y2} C${x2} ${y2} ${x2} ${y2} ${x2} ${y2 + conlluViewerConfigurations.edgeDrop} L${x2} ${y1}` }, `deps_${i}`));
|
|
62
|
+
}
|
|
63
|
+
const isDragging = editable &&
|
|
64
|
+
draggedDep?.depIndex === i &&
|
|
65
|
+
draggedDep?.variant === variant;
|
|
66
|
+
const diamondSize = 6;
|
|
67
|
+
const diamondOffset = 8;
|
|
68
|
+
const angle = 90;
|
|
69
|
+
const diamondY = y1 - diamondOffset;
|
|
70
|
+
arrows.push(_jsxs("g", { children: [dep.headpos !== 0 && (_jsx("polygon", { className: `e${e}${dep.end} e${e}${dep.headpos} q${dep.end}q${dep.headpos} ${ConlluUtils.isActiveDependency(dep, activeItem)
|
|
71
|
+
? 'active-arrow'
|
|
72
|
+
: ''} ${isDragging ? 'dragging' : ''}`, points: `${x2},${diamondY - diamondSize} ${x2 + diamondSize},${diamondY} ${x2},${diamondY + diamondSize} ${x2 - diamondSize},${diamondY}`, transform: `rotate(${angle} ${x2} ${diamondY})`, style: editable ? { cursor: 'grab' } : undefined, onMouseDownCapture: editable && setDraggedDep
|
|
73
|
+
? (e) => {
|
|
74
|
+
if (e.button === 0) {
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
const dragData = JSON.stringify({
|
|
78
|
+
depIndex: i,
|
|
79
|
+
variant,
|
|
80
|
+
end: dep.end,
|
|
81
|
+
});
|
|
82
|
+
const dragElement = document.createElement('div');
|
|
83
|
+
dragElement.style.position = 'absolute';
|
|
84
|
+
dragElement.style.left = '-9999px';
|
|
85
|
+
dragElement.setAttribute('data-dependency', dragData);
|
|
86
|
+
document.body.appendChild(dragElement);
|
|
87
|
+
setDraggedDep({ depIndex: i, variant });
|
|
88
|
+
const handleMouseUp = () => {
|
|
89
|
+
document.body.removeChild(dragElement);
|
|
90
|
+
setDraggedDep(null);
|
|
91
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
92
|
+
};
|
|
93
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
: undefined })), _jsx("path", { className: `e${e}${dep.end} e${e}${dep.headpos} q${dep.end}q${dep.headpos} ${ConlluUtils.isActiveDependency(dep, activeItem)
|
|
97
|
+
? 'active-arrow'
|
|
98
|
+
: ''} ${isDragging ? 'dragging' : ''}`, d: `M${x1} ${y1} l3 -14 l-6 0 Z` })] }, `arrow-group-${i}_${variant}`));
|
|
99
|
+
const { w, h, l } = calculateConlluTextWidth(dep.rel[variant] + 'i', conlluViewerConfigurations.edgeFontSize, true);
|
|
100
|
+
whites.push(_jsx("rect", { x: `${(x1 + x2 - w) / 2 - conlluViewerConfigurations.edgeFontWhiteMargin}`, y: `${y2 -
|
|
101
|
+
l -
|
|
102
|
+
conlluViewerConfigurations.edgeFontOffset -
|
|
103
|
+
conlluViewerConfigurations.edgeFontWhiteMargin}`, width: `${w + 2 * conlluViewerConfigurations.edgeFontWhiteMargin}`, height: `${h + 2 * conlluViewerConfigurations.edgeFontWhiteMargin}` }, `whites_${i}`));
|
|
104
|
+
texts.push(_jsx("text", { className: `l${e}${dep.end} l${e}${dep.headpos} p${dep.end}p${dep.headpos}`, x: (x1 + x2) / 2, y: y2 - conlluViewerConfigurations.edgeFontOffset, children: ConlluUtils.escapeHTML(dep.rel[variant]) }, `texts_${i}`));
|
|
105
|
+
});
|
|
106
|
+
results.push(_jsxs("g", { className: variant === 0 ? 'normal' : 'enhnaced', style: variant === 0 && graph.hasEnhanced
|
|
107
|
+
? { visibility: 'hidden' }
|
|
108
|
+
: undefined, children: [_jsx("g", { fill: "none", stroke: "black", strokeWidth: "1", children: lines }), _jsx("g", { fill: "black", stroke: "black", strokeWidth: "1", children: arrows }), _jsx("g", { fill: conlluViewerConfigurations.edgeLblBackground, stroke: conlluViewerConfigurations.edgeLblBackground, strokeWidth: "1", opacity: conlluViewerConfigurations.edgeLblOpacity, children: whites }), _jsx("g", { fontFamily: conlluViewerConfigurations.font, fontSize: conlluViewerConfigurations.edgeFontSize, textAnchor: "middle", children: texts })] }, variant));
|
|
109
|
+
}
|
|
110
|
+
return results.length > 0 ? _jsx(_Fragment, { children: results }) : null;
|
|
111
|
+
};
|
|
112
|
+
export const drawItems = ({ graph, activeItem, setActiveItem, editable = false, onDependencyChange, conllu, dragOverItem, setDragOverItem, draggedDep, setDraggedDep, onTokenClick, }) => {
|
|
113
|
+
const items = graph.items.map((item, i) => {
|
|
114
|
+
const color = item.enhanced ? '#ffe0e0' : '';
|
|
115
|
+
const isTooltipOpen = activeItem && activeItem.lineno === item.lineno;
|
|
116
|
+
const isDragging = draggedDep !== null && draggedDep !== undefined;
|
|
117
|
+
return (_jsx(Tooltip, { placement: "bottom", open: !!isTooltipOpen, title: _jsx(PopoverItem, { item: item }), arrow: true, children: _jsxs("g", { className: "pointer", onMouseDownCapture: editable && isDragging
|
|
118
|
+
? (e) => {
|
|
119
|
+
e.stopPropagation();
|
|
120
|
+
}
|
|
121
|
+
: undefined, onClick: () => {
|
|
122
|
+
if (!editable || !isDragging) {
|
|
123
|
+
onTokenClick?.(item);
|
|
124
|
+
}
|
|
125
|
+
}, onMouseEnter: () => {
|
|
126
|
+
setActiveItem(item);
|
|
127
|
+
if (editable && isDragging && setDragOverItem) {
|
|
128
|
+
setDragOverItem(i);
|
|
129
|
+
}
|
|
130
|
+
}, onMouseLeave: () => {
|
|
131
|
+
setActiveItem(undefined);
|
|
132
|
+
if (editable && setDragOverItem) {
|
|
133
|
+
setDragOverItem(null);
|
|
134
|
+
}
|
|
135
|
+
}, onMouseUp: editable && isDragging
|
|
136
|
+
? (e) => {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
setDragOverItem?.(null);
|
|
140
|
+
const depIndex = draggedDep.depIndex;
|
|
141
|
+
const variant = draggedDep.variant;
|
|
142
|
+
const newHeadpos = item.end;
|
|
143
|
+
if (onDependencyChange) {
|
|
144
|
+
const updatedConllu = updateDependencyInConllu(conllu, graph, depIndex, newHeadpos, variant);
|
|
145
|
+
onDependencyChange(updatedConllu);
|
|
146
|
+
}
|
|
147
|
+
setDraggedDep?.(null);
|
|
148
|
+
}
|
|
149
|
+
: undefined, children: [_jsx("rect", { className: `${ConlluUtils.isActiveItem(item, activeItem) ? 'active-item' : ''} ${editable && dragOverItem === i ? 'drag-over' : ''}`, x: item.x1, y: graph.offset, width: item.x2 - item.x1, height: conlluViewerConfigurations.nodeHeight, rx: "5", ry: "5", fill: color }), _jsx("text", { className: `item-postag ${item.enhanced ? 'enhanced' : ''}`, x: (item.x1 + item.x2) / 2, y: graph.offset +
|
|
150
|
+
conlluViewerConfigurations.nodeTweek +
|
|
151
|
+
conlluViewerConfigurations.nodeHeight / 4 +
|
|
152
|
+
graph.lower, textAnchor: "middle", children: ConlluUtils.escapeHTML(item.postag) }, `texts_2_${i}`), _jsx("text", { className: `item-text ${item.enhanced ? 'enhanced' : ''}`, x: (item.x1 + item.x2) / 2, y: graph.offset -
|
|
153
|
+
conlluViewerConfigurations.nodeTweek +
|
|
154
|
+
(conlluViewerConfigurations.nodeHeight * 3) / 4 +
|
|
155
|
+
graph.lower, textAnchor: "middle", children: ConlluUtils.escapeHTML(item.word) }, `texts_3_${i}`)] }) }, `items_${i}`));
|
|
156
|
+
});
|
|
157
|
+
return (_jsx("g", { fill: "#D0E0FF", stroke: "black", strokeWidth: "1", children: items }));
|
|
158
|
+
};
|
|
159
|
+
export const drawMultis = ({ graph, }) => {
|
|
160
|
+
const boxes = [];
|
|
161
|
+
const labels = [];
|
|
162
|
+
graph.multis.forEach((multi, i) => {
|
|
163
|
+
const aa = multi.id.split('-');
|
|
164
|
+
if (aa.length !== 2) {
|
|
165
|
+
throwConlluError(new Error(`Invalid range ${multi.id}`), multi.lineno);
|
|
166
|
+
}
|
|
167
|
+
let x1 = 0;
|
|
168
|
+
let x2 = 0;
|
|
169
|
+
let found1 = false;
|
|
170
|
+
let found2 = false;
|
|
171
|
+
graph.items.forEach((item) => {
|
|
172
|
+
if (aa[0] === item.here) {
|
|
173
|
+
x1 = item.x1;
|
|
174
|
+
found1 = true;
|
|
175
|
+
}
|
|
176
|
+
if (aa[1] === item.here) {
|
|
177
|
+
x2 = item.x2;
|
|
178
|
+
found2 = true;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
if (!(found1 && found2)) {
|
|
183
|
+
throwConlluError(new Error(`Invalid range ${multi.id}`), multi.lineno);
|
|
184
|
+
}
|
|
185
|
+
boxes.push(_jsx("rect", { x: x1, y: graph.offset +
|
|
186
|
+
conlluViewerConfigurations.nodeHeight +
|
|
187
|
+
conlluViewerConfigurations.multiSkip, width: x2 - x1, height: conlluViewerConfigurations.multiHeight, rx: "5", ry: "5" }, `boxes_${i}`));
|
|
188
|
+
labels.push(_jsx("text", { x: (x1 + x2) / 2, y: graph.offset +
|
|
189
|
+
conlluViewerConfigurations.nodeHeight +
|
|
190
|
+
conlluViewerConfigurations.multiSkip +
|
|
191
|
+
conlluViewerConfigurations.multiHeight / 2 +
|
|
192
|
+
graph.lower, children: ConlluUtils.escapeHTML(multi.word) }, `labels_${i}`));
|
|
193
|
+
});
|
|
194
|
+
return (_jsxs(_Fragment, { children: [_jsx("g", { fill: "#D0E0FF", stroke: "black", strokeWidth: "1", children: boxes }), _jsx("g", { fontFamily: conlluViewerConfigurations.font, fontSize: conlluViewerConfigurations.nodeFontSize, fontStyle: "italic", textAnchor: "middle", children: labels })] }));
|
|
195
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './ConlluGraphViewer';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
.conllu-graph-viewer-container {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
text-align: center;
|
|
7
|
+
position: relative;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
background-color: white;
|
|
10
|
+
|
|
11
|
+
.floating-buttons {
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: var(--spacing-100, 10px);
|
|
14
|
+
right: var(--spacing-100, 10px);
|
|
15
|
+
z-index: 1069;
|
|
16
|
+
display: flex;
|
|
17
|
+
gap: var(--spacing-50, 4px);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#canvas {
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 100%;
|
|
23
|
+
display: block;
|
|
24
|
+
background-color: white;
|
|
25
|
+
|
|
26
|
+
.item-postag {
|
|
27
|
+
fill: #000;
|
|
28
|
+
font-size: var(--font-size-body);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.item-text {
|
|
32
|
+
fill: #000;
|
|
33
|
+
font-size: var(--font-size-body);
|
|
34
|
+
font-style: italic;
|
|
35
|
+
font-weight: var(--font-weight-light);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.active-arrow {
|
|
39
|
+
stroke: var(--color-primary);
|
|
40
|
+
fill: var(--color-primary);
|
|
41
|
+
stroke-width: 3px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.active-line {
|
|
45
|
+
stroke: var(--color-primary);
|
|
46
|
+
stroke-width: 3px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.active-item {
|
|
50
|
+
stroke: var(--color-primary);
|
|
51
|
+
stroke-width: 3px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.drag-over {
|
|
55
|
+
stroke: var(--color-success, #28a745);
|
|
56
|
+
stroke-width: 3px;
|
|
57
|
+
fill: rgba(40, 167, 69, 0.1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.dragging {
|
|
61
|
+
opacity: 0.5;
|
|
62
|
+
cursor: grabbing !important;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
g[style*='cursor: grab'] {
|
|
66
|
+
cursor: grab !important;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
g[style*='cursor: grab']:active {
|
|
70
|
+
cursor: grabbing !important;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/features/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { default as CommentService } from "./Comments/CommentService";
|
|
|
5
5
|
export { default as Parameters } from "./Parameters";
|
|
6
6
|
export { default as SentenceView } from "./SentenceView";
|
|
7
7
|
export { default as TreeView } from "./TreeView";
|
|
8
|
+
export { default as ConlluGraphViewer } from "./ConlluGraphViewer";
|
|
9
|
+
export type { ConlluGraphViewerProps } from "./ConlluGraphViewer";
|
|
8
10
|
export { default as CytoscapeTreeConverter } from "./TreeView/cytoscape/CytoscapeTreeConverter";
|
|
9
11
|
export type { CytoscapeTreeConverterOptions } from "./TreeView/cytoscape/CytoscapeTreeConverter";
|
|
10
12
|
export { default as SyntreesCytoscape } from "./TreeView/cytoscape/SyntreesCytoscape";
|
package/dist/features/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { default as CommentService } from "./Comments/CommentService";
|
|
|
4
4
|
export { default as Parameters } from "./Parameters";
|
|
5
5
|
export { default as SentenceView } from "./SentenceView";
|
|
6
6
|
export { default as TreeView } from "./TreeView";
|
|
7
|
+
export { default as ConlluGraphViewer } from "./ConlluGraphViewer";
|
|
7
8
|
export { default as CytoscapeTreeConverter } from "./TreeView/cytoscape/CytoscapeTreeConverter";
|
|
8
9
|
export { default as SyntreesCytoscape } from "./TreeView/cytoscape/SyntreesCytoscape";
|
|
9
10
|
export { default as TreeViewSearch } from "./TreeView/TreeViewSearch/TreeViewSearch";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EdgeDefinition, NodeDefinition, Position } from 'cytoscape';
|
|
2
2
|
import { Conllu, ConlluToken, Struct } from '../../configs';
|
|
3
|
+
import { ConlluViewerDependency, ConlluViewerGraph, ConlluViewerItem } from './ConlluViewerGraph';
|
|
3
4
|
export type CytoscapeTree = {
|
|
4
5
|
nodes: NodeDefinition[];
|
|
5
6
|
edges: EdgeDefinition[];
|
|
@@ -16,5 +17,9 @@ declare const ConlluUtils: {
|
|
|
16
17
|
deleteToken: (conllu: Conllu, tokenIndex: number) => Conllu;
|
|
17
18
|
getConlluTranslationFromStruct: (struct: Struct | undefined, lang: string) => string;
|
|
18
19
|
generateImageFromSVG: (svgElement: SVGSVGElement) => void;
|
|
20
|
+
isActiveDependency: (dep: ConlluViewerDependency, activeItem: ConlluViewerItem | undefined) => boolean;
|
|
21
|
+
isActiveItem: (item: ConlluViewerItem, activeItem: ConlluViewerItem | undefined) => boolean;
|
|
22
|
+
calculateAnchor: (graph: ConlluViewerGraph, i1: number, i2: number, lvl: number) => number;
|
|
23
|
+
escapeHTML: (str: string) => string;
|
|
19
24
|
};
|
|
20
25
|
export default ConlluUtils;
|
|
@@ -1,4 +1,49 @@
|
|
|
1
1
|
import { saveAs } from 'file-saver';
|
|
2
|
+
const escapeHTML = (str) => {
|
|
3
|
+
return str || '';
|
|
4
|
+
// return str.replace(/[&<>"'`]/g, (match) => {
|
|
5
|
+
// const escapeMap: Record<string, string> = {
|
|
6
|
+
// '&': '&',
|
|
7
|
+
// '<': '<',
|
|
8
|
+
// '>': '>',
|
|
9
|
+
// '"': '"',
|
|
10
|
+
// "'": ''',
|
|
11
|
+
// '`': '`',
|
|
12
|
+
// };
|
|
13
|
+
// return escapeMap[match];
|
|
14
|
+
// });
|
|
15
|
+
};
|
|
16
|
+
const calculateAnchor = (graph, i1, i2, lvl) => {
|
|
17
|
+
const a = graph.anchors[i1];
|
|
18
|
+
if (a.length === 1) {
|
|
19
|
+
if (i1 < i2) {
|
|
20
|
+
return 0.75;
|
|
21
|
+
}
|
|
22
|
+
return 0.25;
|
|
23
|
+
}
|
|
24
|
+
let n = 0;
|
|
25
|
+
for (let i = 0; i < a.length; i++) {
|
|
26
|
+
const v = a[i];
|
|
27
|
+
if (v.dist === i2 - i1 && v.level === lvl) {
|
|
28
|
+
n = i;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return (n + 0.5) / a.length;
|
|
33
|
+
};
|
|
34
|
+
const isActiveDependency = (dep, activeItem) => {
|
|
35
|
+
if (!activeItem)
|
|
36
|
+
return false;
|
|
37
|
+
return (dep.headpos === Number(activeItem.here) ||
|
|
38
|
+
dep.end === Number(activeItem.here));
|
|
39
|
+
};
|
|
40
|
+
const isActiveItem = (item, activeItem) => {
|
|
41
|
+
if (!activeItem)
|
|
42
|
+
return false;
|
|
43
|
+
return (activeItem.here === item.there ||
|
|
44
|
+
activeItem.there === item.here ||
|
|
45
|
+
activeItem.lineno === item.lineno);
|
|
46
|
+
};
|
|
2
47
|
const convertStructToConllu = (struct) => {
|
|
3
48
|
return {
|
|
4
49
|
uid: struct.uid,
|
|
@@ -267,5 +312,9 @@ const ConlluUtils = {
|
|
|
267
312
|
deleteToken,
|
|
268
313
|
getConlluTranslationFromStruct,
|
|
269
314
|
generateImageFromSVG,
|
|
315
|
+
isActiveDependency,
|
|
316
|
+
isActiveItem,
|
|
317
|
+
calculateAnchor,
|
|
318
|
+
escapeHTML,
|
|
270
319
|
};
|
|
271
320
|
export default ConlluUtils;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Conllu } from '../../configs/types/Conllu';
|
|
2
|
+
import { ConlluViewerGraph } from './ConlluViewerGraph';
|
|
3
|
+
declare const updateDependencyInConllu: (conllu: Conllu, graph: ConlluViewerGraph, depIndex: number, newHeadpos: number, variant: number) => Conllu;
|
|
4
|
+
export default updateDependencyInConllu;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const updateDependencyInConllu = (conllu, graph, depIndex, newHeadpos, variant) => {
|
|
2
|
+
const dep = graph.dependencies[depIndex];
|
|
3
|
+
if (!dep)
|
|
4
|
+
return conllu;
|
|
5
|
+
const endItem = graph.items.find((item) => item.end === dep.end);
|
|
6
|
+
if (!endItem)
|
|
7
|
+
return conllu;
|
|
8
|
+
const tokenIndex = endItem.lineno;
|
|
9
|
+
const token = conllu.tokens[tokenIndex];
|
|
10
|
+
if (!token)
|
|
11
|
+
return conllu;
|
|
12
|
+
const updatedConllu = {
|
|
13
|
+
...conllu,
|
|
14
|
+
tokens: [...conllu.tokens],
|
|
15
|
+
};
|
|
16
|
+
if (variant === 0) {
|
|
17
|
+
const newHeadItem = graph.items.find((item) => item.end === newHeadpos);
|
|
18
|
+
if (newHeadItem) {
|
|
19
|
+
updatedConllu.tokens[tokenIndex] = {
|
|
20
|
+
...token,
|
|
21
|
+
head: newHeadItem.here,
|
|
22
|
+
deprel: dep.rel[0],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const newHeadItem = graph.items.find((item) => item.end === newHeadpos);
|
|
28
|
+
if (newHeadItem) {
|
|
29
|
+
const enhancedDeps = graph.dependencies
|
|
30
|
+
.map((d, idx) => ({ dep: d, index: idx }))
|
|
31
|
+
.filter(({ dep }) => {
|
|
32
|
+
const depEndItem = graph.items.find((item) => item.end === dep.end);
|
|
33
|
+
return (depEndItem && depEndItem.lineno === tokenIndex && dep.rel[1] !== '');
|
|
34
|
+
});
|
|
35
|
+
const updatedDeps = enhancedDeps
|
|
36
|
+
.map(({ dep, index }) => {
|
|
37
|
+
if (index === depIndex) {
|
|
38
|
+
return `${newHeadItem.here}:${dep.rel[1]}`;
|
|
39
|
+
}
|
|
40
|
+
const headItem = graph.items.find((item) => item.end === dep.headpos);
|
|
41
|
+
return headItem ? `${headItem.here}:${dep.rel[1]}` : '';
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
updatedConllu.tokens[tokenIndex] = {
|
|
45
|
+
...token,
|
|
46
|
+
deps: updatedDeps.length > 0 ? updatedDeps.join('|') : '_',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return updatedConllu;
|
|
51
|
+
};
|
|
52
|
+
export default updateDependencyInConllu;
|
|
@@ -11,5 +11,6 @@ export { default as UsabilityUtils } from './UsabilityUtils';
|
|
|
11
11
|
export { conlluViewerConfigurations } from './conllu/ConlluViewerConstants';
|
|
12
12
|
export { default as ConlluUtils } from './conllu/ConlluUtils';
|
|
13
13
|
export { default as ConlluViewerConverter, calculateConlluTextWidth, throwConlluError, } from './conllu/ConlluViewerConverter';
|
|
14
|
+
export { default as updateDependencyInConllu } from './conllu/updateDependencyInConllu';
|
|
14
15
|
export type { ConlluViewerAnchor, ConlluViewerDependency, ConlluViewerGraph, ConlluViewerItem, ConlluViewerMulti, } from './conllu/ConlluViewerGraph';
|
|
15
16
|
export { normalizeCorpusFromApi } from './normalizeCorpusFromApi';
|
package/dist/functions/index.js
CHANGED
|
@@ -10,4 +10,5 @@ export { default as UsabilityUtils } from './UsabilityUtils';
|
|
|
10
10
|
export { conlluViewerConfigurations } from './conllu/ConlluViewerConstants';
|
|
11
11
|
export { default as ConlluUtils } from './conllu/ConlluUtils';
|
|
12
12
|
export { default as ConlluViewerConverter, calculateConlluTextWidth, throwConlluError, } from './conllu/ConlluViewerConverter';
|
|
13
|
+
export { default as updateDependencyInConllu } from './conllu/updateDependencyInConllu';
|
|
13
14
|
export { normalizeCorpusFromApi } from './normalizeCorpusFromApi';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tycho-components",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.40.
|
|
4
|
+
"version": "0.40.8",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@hookform/resolvers": "^5.2.2",
|
|
51
|
+
"@svgdotjs/svg.js": "^3.2.4",
|
|
52
|
+
"@svgdotjs/svg.panzoom.js": "^2.1.2",
|
|
51
53
|
"@tanstack/react-table": "^8.20.6",
|
|
52
54
|
"classnames": "^2.5.1",
|
|
53
55
|
"cytoscape": "^3.28.1",
|