Skip to content
This repository has been archived by the owner on Apr 18, 2024. It is now read-only.

Commit

Permalink
feat: LSDV-5451: LSDV-5452: Improved Taxonomy (#1526)
Browse files Browse the repository at this point in the history
## 1. Async Taxonomy

Allow Taxonomy to load data from remote api set in `apiUrl`. API should accept optional array `path` param: `apiUrl?path[]=root&path[]=child1` to return only nested children of `root/child1` node. API should return array of items with required `value` property plus optional array `children` for list of nested items and boolean `isLeaf` to indicate that there are more items to load.

FF `fflag_feat_front_lsdv_5451_async_taxonomy_110823_short`

## 2. Taxonomy as Labeling tool

Allow Taxonomy to be used as a top-level labeling tool, so users can select Taxonomy item(s) first and then select a region on text. This region will have taxonomy items as labels displayed in region, in regions list, in info panel. Result will have type `taxonomy`.

This feature is enabled by `labeling="true"` attribute.

FF `fflag_feat_front_lsdv_5452_taxonomy_labeling_110823_short`

* First try on Taxonomy as labeling tool

* feat: LSDV-5451: Async Taxonomy data from remote

* Fix default color + linting + comment

* Use NewTaxonomy and load data async

* Change dev example to Taxonomy

* Use isLabeling and params from Taxonomy

So checks and usage are universal now

* Fix perRegion and isLabeling usage

correct FF appliance

* apiUrl as a parameter

* Fix URL operations; remove commented out code

* Fix blocks and styles

But BEM is still can't be used because of global "taxonomy" class

* Add footnote about FF to apiUrl

* Reorder imports

* Use apiUrl from task; add error handling; updateValue

updateValue() is a usual way to init data

* Fix `get result()`

* More checks for Taxonomy Async FF

* Fix selector in test

* Fix updateValue() with DynamicChildren

* Enable back test for readonly Taxonomy - it works

* failsafe when async taxonomy on, but dynamic off
  • Loading branch information
hlomzik authored Aug 16, 2023
1 parent 74fbeb2 commit d0dd212
Show file tree
Hide file tree
Showing 14 changed files with 331 additions and 54 deletions.
2 changes: 1 addition & 1 deletion e2e/fragments/AtTaxonomy.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { I } = inject();

class Taxonomy {
rootBase = '//div[./child::*[starts-with(./@class, "taxonomy")]]';
rootBase = '//div[contains(concat(" ", @class, " "), " taxonomy ")]'; // [./child::*[class*="taxonomy--"]]
input = '[class*="taxonomy--"]';
selectedList = '[class*="taxonomy__selected"]';
item = '[class*="taxonomy__item"]';
Expand Down
5 changes: 2 additions & 3 deletions e2e/tests/taxonomy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Feature('Taxonomy');

Before(({ LabelStudio }) => {
LabelStudio.setFeatureFlags({
fflag_feat_front_lsdv_5451_async_taxonomy_110823_short: false,
ff_dev_2007_dev_2008_dynamic_tag_children_250322_short: true,
fflag_fix_front_dev_3617_taxonomy_memory_leaks_fix: true,
ff_front_dev_1536_taxonomy_user_labels_150222_long: true,
Expand Down Expand Up @@ -320,9 +321,7 @@ Scenario('Taxonomy read only in history', async ({ I, LabelStudio, AtTaxonomy })
AtTaxonomy.seeCheckedItemByText('c');
});

/* todo: looks like we have a bug */
// eslint-disable-next-line
xScenario('Taxonomy readonly result', async ({ I, LabelStudio, AtTaxonomy }) => {
Scenario('Taxonomy readonly result', async ({ I, LabelStudio, AtTaxonomy }) => {
I.amOnPage('/');
LabelStudio.init({
config: `
Expand Down
98 changes: 98 additions & 0 deletions src/components/NewTaxonomy/NewTaxonomy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { TreeSelect } from 'antd';
import React, { useCallback, useEffect, useState } from 'react';

type TaxonomyPath = string[];
type onAddLabelCallback = (path: string[]) => any;
type onDeleteLabelCallback = (path: string[]) => any;

type TaxonomyItem = {
label: string,
path: TaxonomyPath,
depth: number,
isLeaf?: boolean, // only in new async taxonomy
children?: TaxonomyItem[],
origin?: 'config' | 'user' | 'session',
hint?: string,
};

type AntTaxonomyItem = {
title: string,
value: string,
key: string,
isLeaf?: boolean,
children?: AntTaxonomyItem[],
};

type TaxonomyOptions = {
leafsOnly?: boolean,
showFullPath?: boolean,
pathSeparator: string,
maxUsages?: number,
maxWidth?: number,
minWidth?: number,
placeholder?: string,
};

type TaxonomyProps = {
items: TaxonomyItem[],
selected: TaxonomyPath[],
onChange: (node: any, selected: TaxonomyPath[]) => any,
onLoadData?: (item: TaxonomyPath) => any,
onAddLabel?: onAddLabelCallback,
onDeleteLabel?: onDeleteLabelCallback,
options: TaxonomyOptions,
isEditable?: boolean,
};

const convert = (items: TaxonomyItem[], separator: string): AntTaxonomyItem[] => {
return items.map(item => ({
title: item.label,
value: item.path.join(separator),
key: item.path.join(separator),
isLeaf: item.isLeaf !== false && !item.children,
children: item.children ? convert(item.children, separator) : undefined,
}));
};

const NewTaxonomy = ({
items,
selected,
onChange,
onLoadData,
// @todo implement user labels
// onAddLabel,
// onDeleteLabel,
options,
// @todo implement readonly mode
// isEditable = true,
}: TaxonomyProps) => {
const [treeData, setTreeData] = useState<AntTaxonomyItem[]>([]);
const separator = options.pathSeparator;
const style = { minWidth: options.minWidth ?? 200, maxWidth: options.maxWidth };

useEffect(() => {
setTreeData(convert(items, separator));
}, [items]);

const loadData = useCallback(async (node: any) => {
return onLoadData?.(node.value.split(separator));
}, []);

return (
<TreeSelect
treeData={treeData}
value={selected.map(path => path.join(separator))}
onChange={items => onChange(null, items.map(item => item.value.split(separator)))}
loadData={loadData}
treeCheckable
treeCheckStrictly
showCheckedStrategy={TreeSelect.SHOW_ALL}
treeExpandAction="click"
placeholder={options.placeholder || 'Click to add...'}
style={style}
className="htx-taxonomy"
/>
);
};

export { NewTaxonomy };
3 changes: 1 addition & 2 deletions src/components/SidePanels/DetailsPanel/RegionLabels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { observer } from 'mobx-react';
import { Block } from '../../../utils/bem';

export const RegionLabels: FC<{region: LSFRegion}> = observer(({ region }) => {
const labelsInResults = region.results
.filter(result => result.type.endsWith('labels'))
const labelsInResults = region.labelings
.map((result: any) => result.selectedLabels || []);
const labels: any[] = [].concat(...labelsInResults);

Expand Down
15 changes: 11 additions & 4 deletions src/mixins/AreaMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { guidGenerator } from '../core/Helpers';
import Result from '../regions/Result';
import { PER_REGION_MODES } from './PerRegion';
import { ReadOnlyRegionMixin } from './ReadOnlyMixin';
import { FF_LSDV_4930, isFF } from '../utils/feature-flags';
import { FF_LSDV_4930, FF_TAXONOMY_LABELING, isFF } from '../utils/feature-flags';

let ouid = 1;

Expand All @@ -25,17 +25,17 @@ export const AreaMixinBase = types
* @return {Result[]} all results with labeling (created by *Labels control)
*/
get labelings() {
return self.results.filter(r => r.type.endsWith('labels'));
return self.results.filter(r => r.from_name.isLabeling);
},

/**
* @return {Result?} first result with labels (usually it's the only one, but not always)
*/
get labeling() {
if (!isAlive(self)) {
return void 0;
return undefined;
}
return self.results.find(r => r.type.endsWith('labels') && r.hasValue);
return self.results.find(r => r.from_name.isLabeling && r.hasValue);
},

get emptyLabel() {
Expand Down Expand Up @@ -66,6 +66,13 @@ export const AreaMixinBase = types
return self.annotation.toNames.get(self.object.name)?.filter(tag => tag.perregion) || [];
},

// special tags that can be used for labeling (only <Taxonomy isLabeling/> for now)
get labelingTags() {
if (!isFF(FF_TAXONOMY_LABELING)) return [];

return self.annotation.toNames.get(self.object.name)?.filter(tag => tag.classification && tag.isLabeling) || [];
},

get perRegionDescControls() {
return self.perRegionTags.filter(tag => tag.displaymode === PER_REGION_MODES.REGION_LIST);
},
Expand Down
8 changes: 2 additions & 6 deletions src/mixins/HighlightMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,13 +278,9 @@ export const HighlightMixin = types
},

getLabelColor() {
let labelColor = self.parent.highlightcolor || (self.style || self.tag || defaultStyle).fillcolor;
const labelColor = self.parent.highlightcolor || (self.style || self.tag || defaultStyle).fillcolor;

if (labelColor) {
labelColor = Utils.Colors.convertToRGBA(labelColor, LABEL_COLOR_ALPHA);
}

return labelColor;
return Utils.Colors.convertToRGBA(labelColor ?? '#DA935D', LABEL_COLOR_ALPHA);
},

find(span) {
Expand Down
8 changes: 8 additions & 0 deletions src/regions/Result.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ const Result = types
},

get selectedLabels() {
if (self.type === 'taxonomy') {
const sep = self.from_name.pathseparator;
const join = self.from_name.showfullpath;

return (self.mainValue || [])
.map(v => join ? v.join(sep) : v.at(-1))
.map(v => ({ value: v, id: v }));
}
if (self.mainValue?.length === 0 && self.from_name.allowempty) {
return self.from_name.findLabel(null);
}
Expand Down
4 changes: 2 additions & 2 deletions src/stores/Annotation/Annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -881,8 +881,8 @@ export const Annotation = types
setTimeout(() => isAlive(area) && self.selectArea(area));
}
} else {
// unselect labels after use, but consider "keep labels selected" settings
if (control.type.includes('labels')) self.unselectAll(true);
// unselect labeling tools after use, but consider "keep labels selected" settings
if (control.isLabeling) self.unselectAll(true);
}
},

Expand Down
2 changes: 2 additions & 0 deletions src/stores/RegionStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ const SelectionMap = types.model(
// @todo some backward compatibility, should be rewritten to state handling
// @todo but there are some actions should be performed like scroll to region
self.highlighted.perRegionTags.forEach(tag => tag.updateFromResult?.(undefined));
// special case for Taxonomy as labeling tool
self.highlighted.labelingTags.forEach(tag => tag.updateFromResult?.(undefined));
updateResultsFromSelection();
} else {
updateResultsFromSelection();
Expand Down
3 changes: 3 additions & 0 deletions src/tags/control/Labels/Labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ const Model = LabelMixin.views(self => ({
get defaultChildType() {
return 'label';
},
get isLabeling() {
return true;
},
})).actions(self => ({
afterCreate() {
if (self.allowempty) {
Expand Down
Loading

0 comments on commit d0dd212

Please sign in to comment.