
【项目推荐——文本标注 poplar-annotation 用法及相关问题解决】
·
一、项目介绍
poplar-annotation
是一款基于js的文本标注类库,支持react、vue框架,且与其框架版本没有关系,相对于react-text-annotations
库,需要依赖react 17以上版本,poplar-annotation
更有优势。
二、环境安装
npm i poplar-annotation
如果使用的是yarn:
yarn add poplar-annotation
三、基本语法
1.官网示例:
1.1 创建
import {Annotator} from 'poplar-annotation'
/**
* Create an Annotator object
* @param data can be JSON or string
* @param htmlElement the html element to bind to
* @param config config object
*/
new Annotator(data: string, htmlElement: HTMLElement, config?: Object)
1.2 官网中文手册
建议!!!先看官方文档!!!
2.个人项目搭建源码及解析
首先我将所有的dom绑定、鼠标监听选词、连线方法等封装在useCreateAnnotato()里面,数据与操作分离。
2.1useCreateAnnotato.js
文件
export const useCreateAnnotator = (data, config) => {
const annRef = useRef()
/**选择标签类型弹框 */
const [show, setShow] = useState(false)
/**选择连线类型弹框 */
const [visible,setVisible]=useState(false)
/**选词的起始坐标,结尾坐标。连线的首实体id,尾实体id*/
const [optionInfo, setOptionInfo] = useState({ startIndex: -1, endIndex: -1,firstId:-1, secondId:-1})
/**创建Annotator对象 */
const refHtml = useCallback(node => {
/**创建前判断,避免重新创建 */
if (node && !annRef.current) {annRef.current = new Annotator(data, node, config)}
}, [])
/**Events 事件操作*/
const textSelectedOp = () => {if(!annRef.current)return
/**选词 */
annRef.current.on("textSelected", (startIndex, endIndex) => {
setOptionInfo(pre=>{
const cpPre={...pre}
cpPre.startIndex=startIndex
cpPre.endIndex=endIndex
return cpPre
}
setShow(true)
});
/**删除选词 */
annRef.current.on('labelRightClicked', (id, x, y) => {
annRef.current.applyAction(Action.Label.Delete(id))
});
/**连线 */
annRef.current.on('twoLabelsClicked',(firstId,secondId)=>{
setOptionInfo(pre=>{
const cpPre={...pre}
cpPre.firstId=firstId
cpPre.secondId=secondId
return cpPre
})
setVisible(true)
})
/**删除连线 */
annRef.current.on('connectionRightClicked',(id,events)=>{
annRef.current.applyAction(Action.Connection.Delete(id))
})
}
/**选中标签类型 */
const addLabel = (categoryId, startIndex, endIndex) => {
if(!annRef.current)return
annRef.current.applyAction(Action.Label.Create(categoryId, startIndex, endIndex))
}
/**选中连线类型 */
const addConnect=(categoryId, fromId, toId)=>{
if(!annRef.current)return
annRef.current.applyAction(Action.Connection.Create(categoryId, fromId, toId))
}
/**移除Annotator实例 */
const remove=()=>{
console.log("remove")
if(annRef.current?.remove){
console.log("sucess");
annRef.current.remove()
annRef.current = null
}
return {
optionInfo,
textSelectedOp,
refHtml,
show,
setShow,
addLabel,
addConnect,
visible,
setVisible,
remove
}
}
}
2.2App.js
文件
function App() {
/**标签类型弹框信息 */
const [labelCategorieId, setLabelCategorieId] = useState(0)
const onChangeLabel = (e) => {
console.log('radio checked', e.target.value);
setLabelCategorieId(e.target.value);
};
const onLabelOk=()=>{
addLabel(labelCategorieId,optionInfo.startIndex,optionInfo.endIndex)
setShow(false)
}
/**连线类型弹框信息*/
const [connectionCategorieId, setConnectionCategorieId] = useState(0)
const onChangeConn = (e) => {
console.log('radio checked', e.target.value);
setConnectionCategorieId(e.target.value);
};
const onConnOk=()=>{
addConnect(connectionCategorieId,optionInfo.firstId,optionInfo.secondId)
setVisible(false)
}
const {
optionInfo,
textSelectedOp,
refHtml,
show,
setShow,
addLabel,
addConnect,
visible,
setVisible,remove
}=useCreateAnnotator(data)
useEffect(() => {
//挂载
textSelectedOp(),
//卸载
return ()=>{
remove && remove()
}
}, [])
return (
<AppContain>
<Wrapper
ref={refHtml}
>
</Wrapper>
<Modal title={'标签类型'} open={show} onCancel={()=>{setShow(false)}} onOk={onLabelOk}>
{data?.labelCategories?.map(e=>{
return(
<>
<Radio.Group value={labelCategorieId} onChange={onChangeLabel}>
<Radio value={e.id} key={e.id}>{e.text}</Radio>
</Radio.Group></>)})}
</Modal>
<Modal title={'连线类型'} open={visible} onCancel={()=>{setVisible(false)}} onOk={onConnOk}>
{data?.connectionCategories?.map(e=>{
return(
<>
<Radio.Group value={connectionCategorieId} onChange={onChangeConn}>
<Radio value={e.id} key={e.id}>{e.text}</Radio>
</Radio.Group></>)})}
</Modal>
</AppContain>
)
}
/** CSS */
const Wrapper = styled.div`
& > svg {
width: 100%;
}
/* 内容 */
.poplar-annotation-content {
font-family: 'PingFang SC', serif;
font-size: 20px;
}
/* Label */
.poplar-annotation-label {
font-family: 'PingFang SC', serif;
font-size: 14px;
}
/* Connection */
.poplar-annotation-connection {
font-family: 'PingFang SC', serif;
font-size: 12px;
}
.poplar-annotation-connection-line {
stroke: green; /* 有效 */
}
.poplar-annotation-connection-line.hover-from {
stroke: red;
}
.poplar-annotation-connection-line.hover-to {
stroke: blue;
}
.poplar-annotation-connection-line.hover {
stroke: yellow;
}
`
export default App
2.3data.js
JOSN数据
export const data = {
content:'北冥有鱼,其名为鲲。鲲之大,不知其几千里也;化而为鸟,其名为鹏。鹏之背,不知其几千里也;怒而飞,其翼若垂天之云。是鸟也,海运则将徙于南冥。南冥者,天池也。\n《齐谐》者,志怪者也。《谐》之言曰:“鹏之徙于南冥也,水击三千里,抟扶摇而上者九万里,去以六月息者也。”野马也,尘埃也,生物之以息相吹也。天之苍苍,其正色邪?其远而无所至极邪?其视下也,亦若是则已矣。且夫水之积也不厚,则其负大舟也无力。覆杯水于坳堂之上,则芥为之舟,置杯焉则胶,水浅而舟大也',
labelCategories: [
{
id: 0,
text: '名词',
color: '#eac0a2',
borderColor: '#a38671'
},
{
id: 1,
text: '动词',
color: '#619dff',
borderColor: '#436db2'
},
{
id: 2,
text: '形容词',
color: '#9d61ff',
borderColor: '#6d43b2'
},
{
id: 3,
text: '副词',
color: '#ff9d61',
borderColor: '#b26d43'
}
],
labels: [
{
id: 0,
categoryId: 0,
startIndex: 0,
endIndex: 2
},
{
id: 1,
categoryId: 0,
startIndex: 3,
endIndex: 4
},
{
id: 6,
categoryId: 0,
startIndex: 32,
endIndex: 33
},
{
id: 7,
categoryId: 1,
startIndex: 46,
endIndex: 47
},
{
id: 9,
categoryId: 1,
startIndex: 64,
endIndex: 65
},
{
id: 10,
categoryId: 0,
startIndex: 217,
endIndex: 218
},
{
id: 11,
categoryId: 0,
startIndex: 220,
endIndex: 221
},
{
id: 12,
categoryId: 2,
startIndex: 218,
endIndex: 219
},
{
id: 13,
categoryId: 2,
startIndex: 221,
endIndex: 222
},
{
id: 14,
categoryId: 0,
startIndex: 79,
endIndex: 81
},
{
id: 15,
categoryId: 2,
startIndex: 84,
endIndex: 86
}
],
connectionCategories: [
{
id: 0,
text: '修饰'
},
{
id: 1,
text: '限定'
},
{
id: 2,
text: '是...的动作'
}
],
connections: [
{
id: 0,
categoryId: 2,
fromId: 7,
toId: 6
},
{
id: 3,
categoryId: 2,
fromId: 9,
toId: 6
}
]
}
四、项目存在问题及相关优化
1.Annotator对象重复创建问题
解决思路:创建之前先对 annRef.current进行判断是否已经存在
const refHtml = useCallback(node => {
if (node && !annRef.current) {
annRef.current = new Annotator(data, node, config)
}
}, [])
2.跨行标注,标签及文字显示不出问题
跨行标注选择,文字会自动在一行,svg的长度超出屏幕的长度,文字就会隐藏;
删除也恢复不了文字显示
根据官网描述,他们在内部进行了换行标注的限制的,也就是说不能跨行标注,但是有些情况还是能触发。
解决思路:在选词前进行判断当前选词的长度是否小于整行的最大长度
/**选词 */
annRef.current.on("textSelected", (startIndex, endIndex) => {
if((endIndex-startIndex)*20 < annRef.current.view.lineMaxWidth){
setOptionInfo(pre=>{
const cpPre={...pre}
cpPre.startIndex=startIndex
cpPre.endIndex=endIndex
return cpPre
})
setShow(true)
}else{
return message.info('选择的文本超出长度限制')
}
});
annRef.current.view.lineMaxWidth
获取当前整行的最大长度(endIndex-startIndex)*20
选词的长度,这里暂定每个词长度为20,具体每个词的长度可以根据源码去研究。
3.主题颜色背景改变
该文本标注的整个样式,它已经脱离了CSS,是一个SVG,所以直接用CSS样式去修改,是没有效果的。
- 针对svg的颜色设置用fill属性填充
& > svg {
width: 100%;
fill: #fff;
background-color: #232323;
}
- 对连线背景的矩形颜色设置
rect节点和text节点是兄弟关系,rect上无法直接添加类,text上有类
解决思路::has
是一个 CSS 伪类选择器。提供了一种针对引用元素选择父元素或者先前的兄弟元素的方法。
& > svg {
width: 100%;
fill: #fff;
background-color: #232323;
rect:has(+ .poplar-annotation-connection){
fill: #6c6c6c;
}
}
4.导出Json数据
查询内部状态
annotator.store
里有各种对象的Repository,可以用来查询各种对象的内容:
annotator.store 的成员变量 |
---|
content |
labelCategoryRepo |
labelRepo |
connectionCategoryRepo |
connectionRepo |
annotator.store
有json
属性,直接取值即可得到json对象。
/**获取最终打标结果数据 */
const getTaggingData = () => {
if (!annRef.current.store) return
console.log(annRef.current?.store.json)
return annRef.current?.store.json
}
更多推荐
所有评论(0)