Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
import { inject, observer } from "mobx-react";
import { getType, types } from "mobx-state-tree";
import ColorScheme from "pleasejs";
 
import { Tooltip } from "@humansignal/ui";
import InfoModal from "../../components/Infomodal/Infomodal";
import { Label } from "../../components/Label/Label";
import Constants from "../../core/Constants";
import { customTypes } from "../../core/CustomTypes";
import { guidGenerator } from "../../core/Helpers";
import Registry from "../../core/Registry";
import Types from "../../core/Types";
import { AnnotationMixin } from "../../mixins/AnnotationMixin";
import ProcessAttrsMixin from "../../mixins/ProcessAttrs";
import { TagParentMixin } from "../../mixins/TagParentMixin";
import ToolsManager from "../../tools/Manager";
import Utils from "../../utils";
import { parseValue } from "../../utils/data";
import { sanitizeHtml } from "../../utils/html";
 
/**
 * The `Label` tag represents a single label. Use with the `Labels` tag, including `BrushLabels`, `EllipseLabels`, `HyperTextLabels`, `KeyPointLabels`, and other `Labels` tags to specify the value of a specific label.
 *
 * @example
 * <!--Basic named entity recognition labeling configuration for text-->
 * <View>
 *   <Labels name="type" toName="txt-1">
 *     <Label alias="B" value="Brand" />
 *     <Label alias="P" value="Product" />
 *   </Labels>
 *   <Text name="txt-1" value="$text" />
 * </View>
 * @name Label
 * @meta_title Label Tag for Single Label Tags
 * @meta_description Customize Label Studio with the Label tag to assign a single label to regions in a task for machine learning and data science projects.
 * @param {string} value                    - Value of the label
 * @param {boolean} [selected=false]        - Whether to preselect this label
 * @param {number} [maxUsages]              - Maximum number of times this label can be used per task
 * @param {string} [hint]                   - Hint for label on hover
 * @param {string} [hotkey]                 - Hotkey to use for the label. Automatically generated if not specified
 * @param {string} [alias]                  - Label alias
 * @param {boolean} [showAlias=false]       - Whether to show alias inside label text
 * @param {string} [aliasStyle=opacity:0.6] - CSS style for the alias
 * @param {string} [size=medium]            - Size of text in the label
 * @param {string} [background=#36B37E]     - Background color of an active label in hexadecimal
 * @param {string} [selectedColor=#ffffff]  - Color of text in an active label in hexadecimal
 * @param {symbol|word} [granularity]       - Set control based on symbol or word selection (only for Text)
 * @param {string} [html]                   - HTML code is used to display label button instead of raw text provided by `value` (should be properly escaped)
 * @param {int} [category]                  - Category is used in the export (in label-studio-converter lib) to make an order of labels for YOLO and COCO
 */
const TagAttrs = types.model({
  value: types.maybeNull(types.string),
  selected: types.optional(types.boolean, false),
  maxusages: types.maybeNull(types.string),
  alias: types.maybeNull(types.string),
  hint: types.maybeNull(types.string),
  hotkey: types.maybeNull(types.string),
  showalias: types.optional(types.boolean, false),
  aliasstyle: types.optional(types.string, "opacity: 0.6"),
  size: types.optional(types.string, "medium"),
  background: types.optional(customTypes.color, Constants.LABEL_BACKGROUND),
  selectedcolor: types.optional(customTypes.color, "#ffffff"),
  granularity: types.maybeNull(types.enumeration(["symbol", "word", "sentence", "paragraph"])),
  groupcancontain: types.maybeNull(types.string),
  // childrencheck: types.optional(types.enumeration(["any", "all"]), "any")
  html: types.maybeNull(types.string),
});
 
const Model = types
  .model({
    id: types.optional(types.identifier, guidGenerator),
    type: "label",
    visible: types.optional(types.boolean, true),
    _value: types.optional(types.string, ""),
    parentTypes: types.late(() =>
      Types.tagsTypes([
        "Labels",
        "EllipseLabels",
        "RectangleLabels",
        "PolygonLabels",
        "KeyPointLabels",
        "BrushLabels",
        "HyperTextLabels",
        "TimelineLabels",
        "TimeSeriesLabels",
        "ParagraphLabels",
        "BitmaskLabels",
        "VectorLabels",
        ...Registry.customTags.map((t) => t.tag).filter((tag) => tag.endsWith("Labels")),
      ]),
    ),
  })
  .volatile((self) => {
    return {
      initiallySelected: self.selected,
      isEmpty: false,
    };
  })
  .views((self) => ({
    get maxUsages() {
      return Number(self.maxusages || self.parent?.maxusages);
    },
 
    usedAlready() {
      const regions = self.annotation.regionStore.regions;
      // count all the usages among all the regions
      const used = regions.reduce((s, r) => s + r.hasLabel(self.value), 0);
 
      return used;
    },
 
    canBeUsed(count = 1) {
      if (!self.maxUsages) return true;
      return self.usedAlready() + count <= self.maxUsages;
    },
  }))
  .actions((self) => ({
    setEmpty() {
      self.isEmpty = true;
    },
    /**
     * Select label
     */
    toggleSelected() {
      let sameObjectSelectedRegions = [];
 
      // here we check if you click on label from labels group
      // connected to the region on the same object tag that is
      // right now highlighted, and if that region is readonly
 
      if (self.annotation.selectedDrawingRegions.length > 0) {
        /*  here we are validating if we are drawing a new region or if region is already closed
          the way that new drawing region and a finished regions work is similar, but new drawing region
          doesn't visualy select the polygons when you are drawing.
       */
        sameObjectSelectedRegions = self.annotation.selectedDrawingRegions.filter((region) => {
          return region.parent?.name === self.parent?.toname;
        });
      } else if (self.annotation.selectedRegions.length > 0) {
        sameObjectSelectedRegions = self.annotation.selectedRegions.filter((region) => {
          return region.parent?.name === self.parent?.toname;
        });
      }
 
      const affectedRegions = sameObjectSelectedRegions.filter((region) => {
        return !region.isReadOnly();
      });
 
      // one more check if that label can be selected
      if (self.annotation.isReadOnly()) return;
 
      if (sameObjectSelectedRegions.length > 0 && affectedRegions.length === 0) return;
 
      // don't select if it can not be used
      if (
        !!affectedRegions.length &&
        !self.selected &&
        !self.canBeUsed(affectedRegions.filter((region) => region.results).length)
      ) {
        InfoModal.warning(`You can't use ${self.value} more than ${self.maxUsages} time(s)`);
        return;
      }
 
      const labels = self.parent;
 
      // check if there is a region selected and if it is and user
      // is changing the label we need to make sure that region is
      // not going to end up without labels at all
      const applicableRegions = affectedRegions.filter((region) => {
        // if that's the only selected label, the only labelset assigned to region,
        // and we are trying to unselect it, then don't allow that
        // (except for rare labelsets that allow empty labels)
        if (
          labels.selectedLabels.length === 1 &&
          self.selected &&
          region.labelings.length === 1 &&
          (!labels?.allowempty || self.isEmpty)
        )
          return false;
 
        // @todo rewrite this check and add more named vars
        // @todo select only related specific labels
        // @todo unselect any label, but only if that won't leave region without specific labels!
        // @todo but check for regions created by tools
        // @todo lot of tests!
        if (self.selected) return true; // we are unselecting a label which is always ok
        if (labels.type === "labels") return true; // universal labels are fine to select
        if (labels.type.includes(region.type.replace(/region$/, ""))) return true; // region type is in label type
        if (labels.type.includes(region.results[0].type)) return true; // any result type of the region is in label type
 
        return false;
      });
 
      if (sameObjectSelectedRegions.length > 0 && applicableRegions.length === 0) return;
 
      // if we are going to select label and it would be the first in this labels group
      if (!labels.selectedLabels.length && !self.selected) {
        // unselect other tools if they exist and selected
        const manager = ToolsManager.getInstance({ name: self.parent.toname });
        const tool = Object.values(self.parent?.tools || {})[0];
 
        const selectedTool = manager.findSelectedTool();
        const sameType = tool && selectedTool ? getType(selectedTool).name === getType(tool).name : false;
        const sameLabel = selectedTool ? tool?.control?.name === selectedTool?.control?.name : false;
        const isNotSameTool = selectedTool && (!sameType || !sameLabel);
 
        if (tool && (isNotSameTool || !selectedTool)) {
          manager.selectTool(tool, true);
        }
      }
 
      if (self.isEmpty) {
        const selected = self.selected;
 
        labels.unselectAll();
        self.setSelected(!selected);
      } else {
        /**
         * Multiple
         */
        if (!labels.shouldBeUnselected) {
          self.setSelected(!self.selected);
        }
 
        /**
         * Single
         */
        if (labels.shouldBeUnselected) {
          /**
           * Current not selected
           */
          if (!self.selected) {
            labels.unselectAll();
            self.setSelected(!self.selected);
          } else {
            labels.unselectAll();
          }
        }
      }
 
      if (labels.allowempty && !self.isEmpty) {
        if (applicableRegions.length) {
          labels.findLabel().setSelected(!labels.selectedValues()?.length);
        } else {
          if (self.selected) {
            labels.findLabel().setSelected(false);
          }
        }
      }
 
      applicableRegions.forEach((region) => {
        if (region) {
          region.setValue(self.parent);
          region.notifyDrawingFinished();
          // hack to trigger RichText re-render the region
          region.updateSpans?.();
        }
      });
    },
 
    setVisible(val) {
      self.visible = val;
    },
 
    /**
     *
     * @param {boolean} value
     */
    setSelected(value) {
      self.selected = value;
    },
 
    onHotKey() {
      return self.onLabelInteract();
    },
 
    onClick() {
      self.onLabelInteract();
      return false;
    },
 
    onLabelInteract() {
      return self.toggleSelected();
    },
 
    _updateBackgroundColor(val) {
      if (self.background === Constants.LABEL_BACKGROUND) self.background = ColorScheme.make_color({ seed: val })[0];
    },
 
    afterCreate() {
      self._updateBackgroundColor(self._value || self.value);
    },
 
    updateValue(store) {
      self._value = parseValue(self.value, store.task.dataObj) || Constants.EMPTY_LABEL;
    },
  }));
 
const LabelModel = types.compose("LabelModel", TagParentMixin, TagAttrs, ProcessAttrsMixin, Model, AnnotationMixin);
 
const HtxLabelView = inject("store")(
  observer(({ item, store }) => {
    const hotkey =
      (store.settings.enableTooltips || store.settings.enableLabelTooltips) &&
      store.settings.enableHotkeys &&
      item.hotkey;
 
    const label = (
      <Label
        color={item.background}
        margins
        empty={item.isEmpty}
        hotkey={hotkey}
        hidden={!item.visible}
        selected={item.selected}
        onClick={item.onClick}
      >
        {item.html ? (
          <div title={item._value} dangerouslySetInnerHTML={{ __html: sanitizeHtml(item.html) }} />
        ) : (
          item._value
        )}
        {item.showalias === true && item.alias && (
          <span style={Utils.styleToProp(item.aliasstyle)}>&nbsp;{item.alias}</span>
        )}
      </Label>
    );
 
    return item.hint ? <Tooltip title={item.hint}>{label}</Tooltip> : label;
  }),
);
 
Registry.addTag("label", LabelModel, HtxLabelView);
 
export { HtxLabelView, LabelModel };