lost-sia 3.2.0-alpha1 → 3.2.0

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.
Files changed (57) hide show
  1. package/dist/Canvas/Canvas.js +1 -1
  2. package/dist/Canvas/LabelInput.js +1 -1
  3. package/dist/Sia.js +1 -1
  4. package/dist/Toolbar/ToolbarItems/ImageToolItems/ImageLabelInput.d.ts +2 -1
  5. package/dist/Toolbar/ToolbarItems/ImageToolItems/ImageLabelInput.js +1 -1
  6. package/dist/Toolbar/ToolbarItems/ImageToolItems/TagLabel.d.ts +2 -1
  7. package/dist/Toolbar/ToolbarItems/ImageToolItems/TagLabel.js +1 -1
  8. package/dist/Toolbar/ToolbarItems/ImageTools.js +1 -1
  9. package/dist/assets/SIA-CoOqcT1a.css +1 -0
  10. package/dist/assets/brand-icons-Cu_C0hZ4.svg +1008 -0
  11. package/dist/assets/brand-icons-F3SPCeH1.woff +0 -0
  12. package/dist/assets/brand-icons-XL9sxUpA.woff2 +0 -0
  13. package/dist/assets/brand-icons-sqJ2Pg7a.eot +0 -0
  14. package/dist/assets/brand-icons-ubhWoxly.ttf +0 -0
  15. package/dist/assets/flags-DOLqOU7Y.png +0 -0
  16. package/dist/assets/icons-BOCtAERH.woff +0 -0
  17. package/dist/assets/icons-CHzK1VD9.eot +0 -0
  18. package/dist/assets/icons-D29ZQHHw.ttf +0 -0
  19. package/dist/assets/icons-Du6TOHnR.woff2 +0 -0
  20. package/dist/assets/icons-RwhydX30.svg +1518 -0
  21. package/dist/assets/node_modules/semantic-ui-css/semantic.min-09YPtVE6.css +1 -0
  22. package/dist/assets/outline-icons-BfdLr8tr.svg +366 -0
  23. package/dist/assets/outline-icons-DD8jm0uy.ttf +0 -0
  24. package/dist/assets/outline-icons-DInHoiqI.woff2 +0 -0
  25. package/dist/assets/outline-icons-LX8adJ4n.eot +0 -0
  26. package/dist/assets/outline-icons-aQ88nltS.woff +0 -0
  27. package/dist/index.d.ts +0 -1
  28. package/dist/index.js +1 -1
  29. package/dist/stories/Canvas/Canvas.stories.d.ts +3 -3
  30. package/dist/stories/Canvas/CanvasWithOffset.stories.d.ts +6 -6
  31. package/dist/stories/Toolbar/ImageTools/TagLabel.stories.d.ts +2 -1
  32. package/dist/utils/index.d.ts +0 -1
  33. package/dist/utils/index.js +1 -1
  34. package/package.json +4 -3
  35. package/src/AnnoExampleViewer.jsx +69 -0
  36. package/src/Canvas/Canvas.tsx +45 -48
  37. package/src/Canvas/LabelInput.tsx +11 -9
  38. package/src/InfoBoxes/AnnoDetails.jsx +165 -0
  39. package/src/InfoBoxes/AnnoStats.jsx +106 -0
  40. package/src/InfoBoxes/InfoBox.jsx +76 -0
  41. package/src/InfoBoxes/InfoBoxArea.jsx +152 -0
  42. package/src/InfoBoxes/LabelInfo.jsx +107 -0
  43. package/src/SIA.scss +13 -0
  44. package/src/SIASettingButton.jsx +126 -0
  45. package/src/Sia.tsx +70 -54
  46. package/src/Toolbar/ToolbarItems/ImageToolItems/ImageLabelInput.tsx +74 -24
  47. package/src/Toolbar/ToolbarItems/ImageToolItems/TagLabel.tsx +33 -0
  48. package/src/Toolbar/ToolbarItems/ImageTools.tsx +1 -0
  49. package/src/index.ts +1 -1
  50. package/src/stories/exampleData/exampleExternalAnnotations.ts +22 -22
  51. package/src/utils/index.ts +0 -1
  52. package/dist/SiaViewer.d.ts +0 -19
  53. package/dist/SiaViewer.js +0 -1
  54. package/dist/assets/SIA-BV1tYu3P.css +0 -1
  55. package/dist/stories/SIA/SiaViewer.stories.d.ts +0 -52
  56. package/src/SiaViewer.tsx +0 -125
  57. package/src/stories/SIA/SiaViewer.stories.tsx +0 -105
@@ -2,9 +2,9 @@ import { StoryObj } from '@storybook/react';
2
2
  import { default as AnnotationTool } from '../../models/AnnotationTool';
3
3
  import { UiConfig } from '../../types';
4
4
  export declare const ActionsData: {
5
- onAnnoEvent: import('@vitest/spy').Mock<(...args: any[]) => any>;
6
- onKeyDown: import('@vitest/spy').Mock<(...args: any[]) => any>;
7
- onKeyUp: import('@vitest/spy').Mock<(...args: any[]) => any>;
5
+ onAnnoEvent: any;
6
+ onKeyDown: any;
7
+ onKeyUp: any;
8
8
  };
9
9
  declare const meta: {
10
10
  title: string;
@@ -22,9 +22,9 @@ declare const meta: {
22
22
  tags: string[];
23
23
  excludeStories: RegExp;
24
24
  args: {
25
- onAnnoEvent: import('@vitest/spy').Mock<(...args: any[]) => any>;
26
- onKeyDown: import('@vitest/spy').Mock<(...args: any[]) => any>;
27
- onKeyUp: import('@vitest/spy').Mock<(...args: any[]) => any>;
25
+ onAnnoEvent: any;
26
+ onKeyDown: any;
27
+ onKeyUp: any;
28
28
  };
29
29
  };
30
30
  export default meta;
@@ -1,13 +1,14 @@
1
1
  import { StoryObj } from '@storybook/react';
2
2
  declare const meta: {
3
3
  title: string;
4
- component: ({ name, color, size: bodySize, style, triangleSize, onClick, }: {
4
+ component: ({ name, color, size: bodySize, style, triangleSize, onClick, onRemove, }: {
5
5
  name: string;
6
6
  color?: string;
7
7
  size?: number;
8
8
  style?: import('react').CSSProperties;
9
9
  triangleSize?: number;
10
10
  onClick?: () => void;
11
+ onRemove?: () => void;
11
12
  }) => import("react/jsx-runtime").JSX.Element;
12
13
  argTypes: {};
13
14
  parameters: {
@@ -1,3 +1,2 @@
1
1
  export { default as TimeUtils } from './TimeUtils';
2
2
  export { uiConfig, SIA_INITIAL_UI_CONFIG } from './uiConfig';
3
- export { getColor, getDefaultColor } from './color';
@@ -1 +1 @@
1
- import{default as r}from"./TimeUtils.js";import{SIA_INITIAL_UI_CONFIG as f,uiConfig as I}from"./uiConfig.js";import{getColor as i,getDefaultColor as m}from"./color.js";export{f as SIA_INITIAL_UI_CONFIG,r as TimeUtils,i as getColor,m as getDefaultColor,I as uiConfig};
1
+ import{default as e}from"./TimeUtils.js";import{SIA_INITIAL_UI_CONFIG as i,uiConfig as r}from"./uiConfig.js";export{i as SIA_INITIAL_UI_CONFIG,e as TimeUtils,r as uiConfig};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lost-sia",
3
- "version": "3.2.0-alpha1",
3
+ "version": "3.2.0",
4
4
  "description": "Single Image Annotation Tool",
5
5
  "license": "MIT",
6
6
  "repository": "l3p-cv/lost-sia",
@@ -63,7 +63,9 @@
63
63
  "react": "^19.2.1",
64
64
  "react-dom": "^19.2.1",
65
65
  "react-draggable": "^4.5.0",
66
- "sass": "^1.95.0"
66
+ "sass": "^1.95.0",
67
+ "semantic-ui-css": "2.5.0",
68
+ "semantic-ui-react": "^2.0.3"
67
69
  },
68
70
  "peerDependencies": {
69
71
  "@coreui/react": "^5.9.1",
@@ -79,7 +81,6 @@
79
81
  "@storybook/addon-links": "^10.1.6",
80
82
  "@storybook/react": "^10.1.6",
81
83
  "@storybook/react-vite": "^10.1.6",
82
- "@storybook/test": "^8.6.15",
83
84
  "@types/react": "^19.2.7",
84
85
  "@typescript-eslint/eslint-plugin": "^8.46.1",
85
86
  "@typescript-eslint/parser": "^8.46.1",
@@ -0,0 +1,69 @@
1
+ import Prompt from './Prompt'
2
+ import React from 'react'
3
+ import { Card, Image } from 'semantic-ui-react'
4
+
5
+ const LabelExampleViewer = (props) => {
6
+ const requestExample = (e) => {
7
+ e.stopPropagation()
8
+ if (props.onRequestExample) {
9
+ props.onRequestExample()
10
+ }
11
+ }
12
+ const renderContent = () => {
13
+ if (!props.lbl) return null
14
+ if (!props.exampleImg) return null
15
+ const description = (
16
+ <div>
17
+ {props.lbl.description}
18
+ <h4>Comment:</h4>
19
+ {props.exampleImg.anno ? props.exampleImg.anno.comment : 'no comment'}
20
+ </div>
21
+ )
22
+ return (
23
+ <div>
24
+ <Card
25
+
26
+ // image={props.exampleImg.img}
27
+ // description={props.lbl.description}
28
+ // header={props.lbl.label}
29
+ // // description={props.exampleImg.anno.comment}
30
+ // extra={props.exampleImg.anno.comment}
31
+ >
32
+ <Image
33
+ onClick={(e) => requestExample(e)}
34
+ src={props.exampleImg.img}
35
+ wrapped
36
+ ui={false}
37
+ />
38
+ <Card.Content>
39
+ <Card.Header>{props.lbl.label}</Card.Header>
40
+ {/* <Card.Description>{props.lbl.description}</Card.Description> */}
41
+ <Card.Description>{description}</Card.Description>
42
+ {/* <Card.Description>{props.exampleImg.anno.comment}</Card.Description> */}
43
+ </Card.Content>
44
+ </Card>
45
+ {/* <Header inverted>{props.lbl.label}</Header>
46
+ {props.lbl.description}
47
+ <Image src={props.exampleImg.img} centered size='medium'></Image> */}
48
+ </div>
49
+ )
50
+ }
51
+
52
+ const handlePromptClick = () => {
53
+ if (props.onClose) {
54
+ props.onClose()
55
+ }
56
+ }
57
+
58
+ return (
59
+ <Prompt
60
+ onClick={() => {
61
+ handlePromptClick()
62
+ }}
63
+ active={props.active}
64
+ content={renderContent()}
65
+ />
66
+ )
67
+ }
68
+
69
+ export default LabelExampleViewer
@@ -133,8 +133,12 @@ const Canvas = ({
133
133
  y: svgTranslation.y,
134
134
  }
135
135
 
136
- // label input will be opened if a position is set here
137
- const [labelInputPosition, setLabelInputPosition] = useState<Point>()
136
+ // stage coordinates of the annotation whose label popup is open.
137
+ // stored as Point[] so the pixel position can be recomputed on every render
138
+ // (zoom changes svgScale/svgTranslation, so we need a live recalculation each frame)
139
+ const [labelInputPosition, setLabelInputPosition] = useState<Point[] | undefined>(
140
+ undefined,
141
+ )
138
142
  const [isLabelInputVisible, setIsLabelInputVisible] = useState<boolean>(false)
139
143
 
140
144
  // svg element - used to trap focus
@@ -347,10 +351,31 @@ const Canvas = ({
347
351
  topLeftPoint,
348
352
  pageToStageOffset,
349
353
  svgScale,
350
- svgTranslation,
354
+ centeredSvgTranslation,
351
355
  )
352
356
  }
353
357
 
358
+ // Estimated max dimensions of the LabelInput dropdown (filter bar + ~8 label items).
359
+ // Used to clamp the popup anchor so it never escapes the canvas bounds.
360
+ const LABEL_POPUP_WIDTH = 250
361
+ const LABEL_POPUP_HEIGHT = 350
362
+
363
+ /** Clamps a viewport-space popup position so the dropdown stays inside the visible canvas area.
364
+ * Uses the canvas bounding rect so the popup is always reachable even when the annotation
365
+ * is outside the visible viewport (e.g. after zooming in while drawing).
366
+ */
367
+ const clampToViewport = (pos: Point): Point => {
368
+ const canvasRect = canvasRef.current?.getBoundingClientRect()
369
+ const minX = canvasRect?.left ?? 0
370
+ const minY = canvasRect?.top ?? 0
371
+ const maxX = (canvasRect?.right ?? window.innerWidth) - LABEL_POPUP_WIDTH
372
+ const maxY = (canvasRect?.bottom ?? window.innerHeight) - LABEL_POPUP_HEIGHT
373
+ return {
374
+ x: Math.min(Math.max(pos.x, minX), maxX),
375
+ y: Math.min(Math.max(pos.y, minY), maxY),
376
+ }
377
+ }
378
+
354
379
  const handleKeyAction = (keyAction: KeyAction) => {
355
380
  switch (keyAction) {
356
381
  case KeyAction.EDIT_LABEL:
@@ -361,7 +386,7 @@ const Canvas = ({
361
386
  imgSize,
362
387
  stageSize,
363
388
  )
364
- setLabelInputPosition(getAnnoTopLeftPagePosition(stageCoords))
389
+ setLabelInputPosition(stageCoords)
365
390
  setIsLabelInputVisible(true)
366
391
  }
367
392
  break
@@ -572,37 +597,6 @@ const Canvas = ({
572
597
  setImgSize({ x: width, y: height })
573
598
  }, [image, canvasSize])
574
599
 
575
- // Source of truth for the image's natural size.
576
- // The SVG <image> element above loads asynchronously, so reading
577
- // imageRef.getBoundingClientRect() (the effect above) runs before the
578
- // remote image has decoded and resolves to {0, 0}. That left imgSize at
579
- // 0, which trips the getFittedImageScale guard (returns 0), so stageSize
580
- // never got set and the <image> rendered at its natural (huge) pixel size.
581
- // Preloading via an HTMLImageElement gives us naturalWidth/Height as soon
582
- // as the bytes are decoded, regardless of the SVG <image> lifecycle.
583
- useEffect(() => {
584
- if (!image) {
585
- setImgSize({ x: -1, y: -1 })
586
- return
587
- }
588
-
589
- let cancelled = false
590
- const probe = new Image()
591
- probe.onload = () => {
592
- if (cancelled) return
593
- const { naturalWidth, naturalHeight } = probe
594
- if (naturalWidth > 0 && naturalHeight > 0) {
595
- setImgSize({ x: naturalWidth, y: naturalHeight })
596
- }
597
- }
598
- probe.src = image
599
-
600
- return () => {
601
- cancelled = true
602
- probe.onload = null
603
- }
604
- }, [image])
605
-
606
600
  useEffect(() => {
607
601
  if (imageToStageFactor === 0) return
608
602
 
@@ -810,10 +804,8 @@ const Canvas = ({
810
804
 
811
805
  onSelectAnnotation(percentagedAnnotation)
812
806
 
813
- // get top left point of annotation
814
- if (annotationSettings.canLabel) {
815
- setLabelInputPosition(getAnnoTopLeftPagePosition(annotation.coordinates))
816
- }
807
+ // store stage coords so popup position is recomputed live on every render
808
+ setLabelInputPosition(annotation.coordinates)
817
809
  }
818
810
 
819
811
  const handleOnAnnoChanged = (annotation: Annotation) => {
@@ -952,15 +944,21 @@ const Canvas = ({
952
944
  flexDirection: 'column',
953
945
  }}
954
946
  >
955
- {annotationSettings.canLabel && (
956
947
  <div
957
- style={{
958
- position: 'absolute',
959
- left: labelInputPosition?.x ?? 0,
960
- top: labelInputPosition?.y ?? 0,
961
- display: labelInputPosition?.y === undefined ? 'none' : 'inherit',
962
- zIndex: isLabelInputVisible ? 7000 : -1,
963
- }}
948
+ style={(() => {
949
+ // recompute pixel position on every render so the popup tracks the annotation
950
+ const pagePos = labelInputPosition
951
+ ? clampToViewport(getAnnoTopLeftPagePosition(labelInputPosition))
952
+ : undefined
953
+ return {
954
+ // position: fixed resolves left/top against the viewport.
955
+ position: 'fixed' as const,
956
+ left: pagePos ? pagePos.x - window.scrollX : 0,
957
+ top: pagePos ? pagePos.y - window.scrollY : 0,
958
+ display: pagePos === undefined ? 'none' : 'inherit',
959
+ zIndex: isLabelInputVisible ? 7000 : -1,
960
+ }
961
+ })()}
964
962
  >
965
963
  <LabelInput
966
964
  defaultLabelId={defaultLabelId}
@@ -1007,7 +1005,6 @@ const Canvas = ({
1007
1005
  }}
1008
1006
  />
1009
1007
  </div>
1010
- )}
1011
1008
 
1012
1009
  {isImageJunk && (
1013
1010
  <div
@@ -79,15 +79,17 @@ const LabelInput = ({
79
79
  />
80
80
  </div>
81
81
  <CDropdownDivider />
82
- {filteredLabels.length > 0 ? (
83
- filteredLabels.map((label: Label) => (
84
- <CDropdownItem key={label.id} onClick={() => updateSelectedLabels(label)}>
85
- {label.name}
86
- </CDropdownItem>
87
- ))
88
- ) : (
89
- <CDropdownItem disabled>No results</CDropdownItem>
90
- )}
82
+ <div style={{ maxHeight: '200px', overflowY: 'auto' }}>
83
+ {filteredLabels.length > 0 ? (
84
+ filteredLabels.map((label: Label) => (
85
+ <CDropdownItem key={label.id} onClick={() => updateSelectedLabels(label)}>
86
+ {label.name}
87
+ </CDropdownItem>
88
+ ))
89
+ ) : (
90
+ <CDropdownItem disabled>No results</CDropdownItem>
91
+ )}
92
+ </div>
91
93
  </CDropdownMenu>
92
94
  </CDropdown>
93
95
  </>
@@ -0,0 +1,165 @@
1
+ import React, { useState, useEffect, useRef } from 'react'
2
+ import { List, TextArea, Form, Label, Icon } from 'semantic-ui-react'
3
+ import InfoBox from './InfoBox'
4
+ import PopUp from '../SiaPopup'
5
+
6
+ const AnnoDetails = (props) => {
7
+ const [comment, setComment] = useState('')
8
+ const [example, setExample] = useState(false)
9
+ const [showSaveBtn, setShowSaveBtn] = useState(false)
10
+ const tARef = useRef()
11
+ useEffect(() => {
12
+ if (props.anno) {
13
+ if (props.anno.comment) {
14
+ setComment(props.anno.comment)
15
+ } else {
16
+ setComment('')
17
+ }
18
+ }
19
+ }, [props.anno])
20
+
21
+ useEffect(() => {
22
+ if (tARef.current) {
23
+ tARef.current.focus()
24
+ }
25
+ }, [props.commentInputTrigger])
26
+
27
+ const onDismiss = () => {
28
+ if (props.onDismiss) {
29
+ props.onDismiss()
30
+ }
31
+ }
32
+
33
+ const onCommentUpdate = () => {
34
+ if (props.onCommentUpdate) {
35
+ props.onCommentUpdate(comment)
36
+ }
37
+ setShowSaveBtn(false)
38
+ }
39
+
40
+ const onMarkExampleClick = () => {
41
+ // setExample(!example)
42
+ if (props.onMarkExample) {
43
+ props.onMarkExample(props.anno)
44
+ }
45
+ }
46
+
47
+ const renderSaveBtn = () => {
48
+ if (showSaveBtn) {
49
+ return <Label as="a" corner="right" icon="save" color="red"></Label>
50
+ }
51
+ }
52
+
53
+ const renderComment = () => {
54
+ return (
55
+ <div>
56
+ <Form onClick={() => {}}>
57
+ {renderSaveBtn()}
58
+ <TextArea
59
+ placeholder="Write a comment"
60
+ ref={tARef}
61
+ value={comment}
62
+ rows={2}
63
+ onBlur={() => onCommentUpdate()}
64
+ onFocus={() => setShowSaveBtn(true)}
65
+ onChange={(e) => setComment(e.target.value)}
66
+ ></TextArea>
67
+ </Form>
68
+ </div>
69
+ )
70
+ }
71
+ const renderLabels = () => {
72
+ if (props.anno) {
73
+ const selectedLabelIds = props.anno.labelIds
74
+ if (!selectedLabelIds) return 'No Label'
75
+
76
+ let lbls = ''
77
+ props.anno.labelIds.forEach((lbl, idx) => {
78
+ const labelObject = props.possibleLabels.find((el) => {
79
+ return el.id === lbl
80
+ })
81
+ if (idx > 0) lbls += ', '
82
+ lbls += labelObject.label
83
+ })
84
+ if (!lbls) return 'No Label'
85
+ return lbls
86
+ } else {
87
+ return 'No Label'
88
+ }
89
+ }
90
+
91
+ const renderExampleMark = () => {
92
+ if (!props.allowedToMarkExample) return null
93
+ let color = 'grey'
94
+ let iconName = 'bookmark outline'
95
+ if (props.anno) {
96
+ if (props.anno.isExample) {
97
+ color = 'yellow'
98
+ iconName = 'bookmark'
99
+ }
100
+ }
101
+ const mark = (
102
+ <Label
103
+ as="a"
104
+ color={color}
105
+ style={{ opacity: 0.8 }}
106
+ size="medium"
107
+ corner="left"
108
+ onClick={() => {
109
+ onMarkExampleClick()
110
+ }}
111
+ >
112
+ <Icon name={iconName} />
113
+ </Label>
114
+ )
115
+ return (
116
+ <PopUp
117
+ content="Mark this annotation as example for other annotators"
118
+ trigger={mark}
119
+ />
120
+ )
121
+ }
122
+
123
+ const renderDescription = () => {
124
+ if (props.anno) {
125
+ return (
126
+ <div>
127
+ {renderExampleMark()}
128
+ {/* <Header>
129
+ Labels
130
+ </Header>
131
+ <div>
132
+ {renderLabels()}
133
+ </div> */}
134
+ <List>
135
+ <List.Item icon="at" content={props.anno.id} />
136
+ <List.Item icon="tag" content={renderLabels()} />
137
+ <List.Item
138
+ icon="time"
139
+ content={`${props.anno.annoTime.toLocaleString('us', {
140
+ minimumFractionDigits: 2,
141
+ maximumFractionDigits: 2,
142
+ })} seconds`}
143
+ />
144
+ {/* <List.Item icon='time' content={props.anno.annoTime} /> */}
145
+ </List>
146
+ {renderComment()}
147
+ </div>
148
+ )
149
+ } else {
150
+ return 'No annotation selected!'
151
+ }
152
+ }
153
+
154
+ return (
155
+ <InfoBox
156
+ header={'Annotation Details'}
157
+ content={renderDescription()}
158
+ visible={props.visible}
159
+ defaultPos={props.defaultPos}
160
+ onDismiss={(e) => onDismiss()}
161
+ />
162
+ )
163
+ }
164
+
165
+ export default AnnoDetails
@@ -0,0 +1,106 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import { List, Label } from 'semantic-ui-react'
3
+ import InfoBox from './InfoBox'
4
+ import * as colorlut from '../utils/color'
5
+ // import actions from '../../../../actions'
6
+ // import * as transform from '../utils/transform'
7
+ // const { siaShowImgBar } = actions
8
+
9
+ const AnnoStats = (props) => {
10
+ const [hideList, setHideList] = useState([])
11
+
12
+ useEffect(() => {
13
+ setHideList([])
14
+ }, [props.imgLoadCount])
15
+
16
+ const onDismiss = () => {
17
+ if (props.onDismiss) {
18
+ props.onDismiss()
19
+ }
20
+ }
21
+
22
+ const onLblClick = (lbl) => {
23
+ let hideLbl = false
24
+ if (hideList.includes(lbl.id)) {
25
+ setHideList(hideList.filter((e) => e !== lbl.id))
26
+ hideLbl = false
27
+ } else {
28
+ // hideList.push(lbl.id)
29
+ setHideList([...hideList, lbl.id])
30
+ hideLbl = true
31
+ }
32
+ if (props.onHideLbl) {
33
+ props.onHideLbl(lbl, hideLbl)
34
+ }
35
+ }
36
+
37
+ const renderRow = (s) => {
38
+ const opacity = hideList.includes(s.id) ? 0.5 : 1.0
39
+ return (
40
+ <List.Item key={s.id}>
41
+ <List.Content>
42
+ <Label
43
+ as="a"
44
+ tag
45
+ style={{ background: s.color, opacity: opacity }}
46
+ onClick={() => onLblClick(s)}
47
+ >
48
+ {s.label}
49
+ <Label.Detail>{s.amount}</Label.Detail>
50
+ </Label>
51
+ </List.Content>
52
+ </List.Item>
53
+ )
54
+ }
55
+
56
+ const renderRows = () => {
57
+ let stats = {}
58
+ props.annos.forEach((a) => {
59
+ // // console.log('render rows', a)
60
+ if (a.status !== 'deleted') {
61
+ a.labelIds.forEach((lblId) => {
62
+ if (lblId in stats) {
63
+ stats[lblId] += 1
64
+ } else {
65
+ stats[lblId] = 1
66
+ }
67
+ })
68
+ if (a.labelIds.length === 0) {
69
+ if ((-1) in stats) {
70
+ stats[-1] += 1
71
+ } else {
72
+ stats[-1] = 1
73
+ }
74
+ }
75
+ }
76
+ })
77
+ const res = Object.entries(stats).map(([key, value]) => {
78
+ let lbl = props.possibleLabels.find((e) => {
79
+ return e.id === parseInt(key)
80
+ })
81
+ if (!lbl) {
82
+ lbl = { id: -1, label: 'No Label', color: colorlut.getDefaultColor() }
83
+ }
84
+
85
+ lbl.amount = value
86
+ // return renderRow({class:idToLbl[key], amount:value, color:idToColor[key]})
87
+ return renderRow(lbl)
88
+ })
89
+ return res
90
+ }
91
+ const renderDescription = () => {
92
+ return <List>{renderRows()}</List>
93
+ }
94
+
95
+ return (
96
+ <InfoBox
97
+ header={'Annotations per Label'}
98
+ content={renderDescription()}
99
+ visible={props.visible}
100
+ defaultPos={props.defaultPos}
101
+ onDismiss={(e) => onDismiss()}
102
+ />
103
+ )
104
+ }
105
+
106
+ export default AnnoStats
@@ -0,0 +1,76 @@
1
+ import React, { Component } from 'react'
2
+ import { Header, Message } from 'semantic-ui-react'
3
+ import Draggable from 'react-draggable'
4
+
5
+ class InfoBox extends Component {
6
+ constructor(props) {
7
+ super(props)
8
+ this.state = {
9
+ style: {
10
+ position: 'fixed',
11
+ top: 200,
12
+ left: 200,
13
+ width: 250,
14
+ },
15
+ }
16
+ }
17
+
18
+ componentDidMount() {
19
+ this.updateStyle()
20
+ }
21
+
22
+ componentDidUpdate(prevProps) {
23
+ if (this.props.defaultPos !== prevProps.defaultPos) {
24
+ this.updateStyle()
25
+ }
26
+ }
27
+
28
+ handleOnStop(e) {
29
+ if (this.props.onStop) {
30
+ this.props.onStop(e)
31
+ }
32
+ }
33
+
34
+ updateStyle() {
35
+ this.setState({
36
+ style: { ...this.state.style, ...this.props.defaultPos },
37
+ })
38
+ }
39
+
40
+ onDismiss(e) {
41
+ if (this.props.onDismiss) {
42
+ this.props.onDismiss(e)
43
+ }
44
+ }
45
+
46
+ render() {
47
+ if (!this.props.visible) return null
48
+ return (
49
+ <Draggable handle=".handle" onStop={(e) => this.handleOnStop(e)}>
50
+ <div style={this.state.style}>
51
+ <Message
52
+ style={{ opacity: 0.98 }}
53
+ onDismiss={(e) => {
54
+ this.onDismiss(e)
55
+ }}
56
+ size="small"
57
+ >
58
+ <Header
59
+ textAlign="center"
60
+ as="h5"
61
+ className="handle"
62
+ style={{ cursor: 'grab' }}
63
+ >
64
+ {this.props.header}
65
+ </Header>
66
+ {/* <Message.Header textAlign='center' className="handle" style={{cursor: 'grab'}}>{this.props.header}</Message.Header> */}
67
+ <hr />
68
+ <Message.Content>{this.props.content}</Message.Content>
69
+ </Message>
70
+ </div>
71
+ </Draggable>
72
+ )
73
+ }
74
+ }
75
+
76
+ export default InfoBox