F: Implement association graph UI

This commit is contained in:
Ivan 2025-08-13 13:32:03 +03:00
parent 87c7e443e5
commit bd6f72aceb
15 changed files with 212 additions and 64 deletions

View File

@ -218,6 +218,7 @@
"Никанорова", "Никанорова",
"Номеноид", "Номеноид",
"номеноида", "номеноида",
"номеноидом",
"Номеноиды", "Номеноиды",
"операционализации", "операционализации",
"операционализированных", "операционализированных",

View File

@ -161,7 +161,6 @@ export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
export { TbEarScan as IconGraphInverse } from 'react-icons/tb'; export { TbEarScan as IconGraphInverse } from 'react-icons/tb';
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi'; export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
export { LuAtom as IconGraphCore } from 'react-icons/lu'; export { LuAtom as IconGraphCore } from 'react-icons/lu';
export { LuRotate3D as IconRotate3D } from 'react-icons/lu';
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md'; export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
export { RiFocus3Line as IconFocus } from 'react-icons/ri'; export { RiFocus3Line as IconFocus } from 'react-icons/ri';
export { LuSparkles as IconClustering } from 'react-icons/lu'; export { LuSparkles as IconClustering } from 'react-icons/lu';

View File

@ -6,9 +6,14 @@ export function HelpConceptRelations() {
<div className='text-justify'> <div className='text-justify'>
<h1>Связи между конституентами</h1> <h1>Связи между конституентами</h1>
<p> <p>
Конституенты связаны между собой через использование одних конституент при определении других. Такую связь в Наиболее общей связью между конституентами является ассоциация, устанавливаемая между номеноидом и относимыми к
общем случае называют <b>используется в определении</b>. Она является основой для построения <b>Графа термов</b> нему другими конституентами. Такая связь задается до установления точных определений и применяется для
, отображающего последовательность вывода понятий в концептуальной схеме. предварительной фиксации групп связанных конституент.
</p>
<p>
Конституенты также связаны между собой через использование одних конституент при определении других. Такую связь
в общем случае называют <b>используется в определении</b>. Она является основой для построения{' '}
<b>Графа термов</b>, отображающего последовательность вывода понятий в концептуальной схеме.
</p> </p>
<p> <p>

View File

@ -20,7 +20,6 @@ import {
IconOSS, IconOSS,
IconPredecessor, IconPredecessor,
IconReset, IconReset,
IconRotate3D,
IconText, IconText,
IconTypeGraph IconTypeGraph
} from '@/components/icons'; } from '@/components/icons';
@ -37,15 +36,15 @@ export function HelpRSGraphTerm() {
<h2>Настройка графа</h2> <h2>Настройка графа</h2>
<ul> <ul>
<li>Цвет покраска узлов</li> <li>Цвет покраска узлов</li>
<li>
Связи выбор типов <LinkTopic text='связей' topic={HelpTopic.CC_RELATIONS} />
</li>
<li> <li>
<IconText className='inline-icon' /> Отображение текста <IconText className='inline-icon' /> Отображение текста
</li> </li>
<li> <li>
<IconClustering className='inline-icon' /> Скрыть порожденные <IconClustering className='inline-icon' /> Скрыть порожденные
</li> </li>
<li>
<IconRotate3D className='inline-icon' /> Вращение 3D
</li>
</ul> </ul>
</div> </div>

View File

@ -7,11 +7,14 @@ import { type IConstituenta, type IRSForm } from '@/features/rsform';
import { TGEdgeTypes } from '@/features/rsform/components/term-graph/graph/tg-edge-types'; import { TGEdgeTypes } from '@/features/rsform/components/term-graph/graph/tg-edge-types';
import { TGNodeTypes } from '@/features/rsform/components/term-graph/graph/tg-node-types'; import { TGNodeTypes } from '@/features/rsform/components/term-graph/graph/tg-node-types';
import { SelectColoring } from '@/features/rsform/components/term-graph/select-coloring'; import { SelectColoring } from '@/features/rsform/components/term-graph/select-coloring';
import { SelectGraphType } from '@/features/rsform/components/term-graph/select-graph-type';
import { ToolbarFocusedCst } from '@/features/rsform/components/term-graph/toolbar-focused-cst'; import { ToolbarFocusedCst } from '@/features/rsform/components/term-graph/toolbar-focused-cst';
import { ViewHidden } from '@/features/rsform/components/term-graph/view-hidden';
import { applyLayout, produceFilteredGraph, type TGNodeData } from '@/features/rsform/models/graph-api'; import { applyLayout, produceFilteredGraph, type TGNodeData } from '@/features/rsform/models/graph-api';
import { useTermGraphStore } from '@/features/rsform/stores/term-graph'; import { useTermGraphStore } from '@/features/rsform/stores/term-graph';
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow'; import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
import { useFitHeight } from '@/stores/app-layout';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import ToolbarGraphFilter from './toolbar-graph-filter'; import ToolbarGraphFilter from './toolbar-graph-filter';
@ -35,6 +38,8 @@ export function TGReadonlyFlow({ schema }: TGReadonlyFlowProps) {
const filter = useTermGraphStore(state => state.filter); const filter = useTermGraphStore(state => state.filter);
const filteredGraph = produceFilteredGraph(schema, filter, focusCst); const filteredGraph = produceFilteredGraph(schema, filter, focusCst);
const hidden = schema.items.filter(cst => !filteredGraph.hasNode(cst.id)).map(cst => cst.id);
const hiddenHeight = useFitHeight('15.5rem + 2px', '4rem');
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]); const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges] = useEdgesState<Edge>([]); const [edges, setEdges] = useEdgesState<Edge>([]);
@ -102,7 +107,9 @@ export function TGReadonlyFlow({ schema }: TGReadonlyFlowProps) {
) : null} ) : null}
</div> </div>
<div className='absolute z-pop top-24 sm:top-16 left-2 sm:left-3 w-54 flex flex-col pointer-events-none'> <div className='absolute z-pop top-24 sm:top-16 left-2 sm:left-3 w-54 flex flex-col pointer-events-none'>
<SelectColoring schema={schema} /> <SelectColoring className='rounded-b-none' schema={schema} />
<SelectGraphType className='rounded-none border-t-0' />
<ViewHidden items={hidden} listHeight={hiddenHeight} schema={schema} setFocus={setFocusCst} />
</div> </div>
<DiagramFlow <DiagramFlow

View File

@ -5,7 +5,7 @@ import { TokenID } from './backend/types';
import { CstClass, ExpressionStatus, type IConstituenta } from './models/rsform'; import { CstClass, ExpressionStatus, type IConstituenta } from './models/rsform';
import { type ISyntaxTreeNode } from './models/rslang'; import { type ISyntaxTreeNode } from './models/rslang';
import { type TypificationGraphNode } from './models/typification-graph'; import { type TypificationGraphNode } from './models/typification-graph';
import { type GraphColoring } from './stores/term-graph'; import { type GraphColoring, type GraphType } from './stores/term-graph';
/** Represents Brackets highlights theme. */ /** Represents Brackets highlights theme. */
export const BRACKETS_THEME = { export const BRACKETS_THEME = {
@ -252,3 +252,14 @@ export function colorBgTMGraphNode(node: TypificationGraphNode): string {
} }
return APP_COLORS.bgOrange; return APP_COLORS.bgOrange;
} }
export function colorGraphEdge(edgeType: GraphType): string {
switch (edgeType) {
case 'full':
return APP_COLORS.bgGreen;
case 'definition':
return APP_COLORS.border;
case 'association':
return APP_COLORS.bgPurple;
}
}

View File

@ -20,9 +20,7 @@ export function SelectColoring({ className, schema }: SelectColoringProps) {
const setColoring = useTermGraphStore(state => state.setColoring); const setColoring = useTermGraphStore(state => state.setColoring);
return ( return (
<div <div className={cn('relative select-none bg-input border pointer-events-auto', className)}>
className={cn('relative border rounded-b-none select-none bg-input rounded-t-md pointer-events-auto', className)}
>
<div className='absolute z-pop right-10 h-9 flex items-center'> <div className='absolute z-pop right-10 h-9 flex items-center'>
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} contentClass='min-w-100' /> : null} {coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} contentClass='min-w-100' /> : null}
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} contentClass='min-w-100' /> : null} {coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} contentClass='min-w-100' /> : null}

View File

@ -0,0 +1,31 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/input/select';
import { cn } from '@/components/utils';
import { labelGraphType } from '../../labels';
import { graphTypes, useTermGraphStore } from '../../stores/term-graph';
interface SelectGraphTypeProps {
className?: string;
}
export function SelectGraphType({ className }: SelectGraphTypeProps) {
const graphType = useTermGraphStore(state => state.filter.graphType);
const setGraphType = useTermGraphStore(state => state.setGraphType);
return (
<div className={cn('relative border select-none bg-input pointer-events-auto', className)}>
<Select onValueChange={setGraphType} defaultValue={graphType}>
<SelectTrigger noBorder className='w-full'>
<SelectValue placeholder='Цветовая схема' />
</SelectTrigger>
<SelectContent alignOffset={-1} sideOffset={-4}>
{graphTypes.map(mode => (
<SelectItem key={`graphType-${mode}`} value={mode}>
{labelGraphType(mode)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}

View File

@ -4,35 +4,44 @@ import clsx from 'clsx';
import { MiniButton } from '@/components/control'; import { MiniButton } from '@/components/control';
import { IconDropArrow, IconDropArrowUp } from '@/components/icons'; import { IconDropArrow, IconDropArrowUp } from '@/components/icons';
import { useWindowSize } from '@/hooks/use-window-size';
import { useFitHeight } from '@/stores/app-layout';
import { globalIDs, prefixes } from '@/utils/constants'; import { globalIDs, prefixes } from '@/utils/constants';
import { colorBgGraphNode } from '../../../colors'; import { colorBgGraphNode } from '../../colors';
import { type IConstituenta } from '../../../models/rsform'; import { type IConstituenta, type IRSForm } from '../../models/rsform';
import { useCstTooltipStore } from '../../../stores/cst-tooltip'; import { useCstTooltipStore } from '../../stores/cst-tooltip';
import { useTermGraphStore } from '../../../stores/term-graph'; import { useTermGraphStore } from '../../stores/term-graph';
import { useRSEdit } from '../rsedit-context';
interface ViewHiddenProps { interface ViewHiddenProps {
items: number[]; items: number[];
listHeight?: string;
schema: IRSForm;
selected?: number[];
toggleSelect?: (id: number) => void;
setFocus: (cst: IConstituenta) => void;
onActivate?: (id: number) => void;
} }
export function ViewHidden({ items }: ViewHiddenProps) { export function ViewHidden({
const { isSmall } = useWindowSize(); items,
listHeight,
schema,
selected,
toggleSelect,
setFocus,
onActivate
}: ViewHiddenProps) {
const coloring = useTermGraphStore(state => state.coloring); const coloring = useTermGraphStore(state => state.coloring);
const { navigateCst, setFocus, schema, selected, toggleSelect } = useRSEdit();
const localSelected = items.filter(id => selected.includes(id)); const localSelected = selected ? items.filter(id => selected.includes(id)) : [];
const isFolded = useTermGraphStore(state => state.foldHidden); const isFolded = useTermGraphStore(state => state.foldHidden);
const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden); const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden);
const setActiveCst = useCstTooltipStore(state => state.setActiveCst); const setActiveCst = useCstTooltipStore(state => state.setActiveCst);
const hiddenHeight = useFitHeight(isSmall ? '10.4rem + 2px' : '12.5rem + 2px');
function handleClick(event: React.MouseEvent<Element>, cstID: number) { function handleClick(event: React.MouseEvent<Element>, cstID: number) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
toggleSelect(cstID); toggleSelect?.(cstID);
} }
function handleContextMenu(event: React.MouseEvent<HTMLElement>, target: IConstituenta) { function handleContextMenu(event: React.MouseEvent<HTMLElement>, target: IConstituenta) {
@ -56,7 +65,7 @@ export function ViewHidden({ items }: ViewHiddenProps) {
<div className={clsx('py-2 bg-input border-x', isFolded && 'border-b rounded-b-md')}> <div className={clsx('py-2 bg-input border-x', isFolded && 'border-b rounded-b-md')}>
<div className={clsx('w-fit select-none cc-view-hidden-header', !isFolded && 'open')}> <div className={clsx('w-fit select-none cc-view-hidden-header', !isFolded && 'open')}>
{`Скрытые [${localSelected.length} | ${items.length}]`} {localSelected ? `Скрытые [${localSelected.length} | ${items.length}]` : 'Скрытые'}
</div> </div>
</div> </div>
@ -70,7 +79,7 @@ export function ViewHidden({ items }: ViewHiddenProps) {
!isFolded && 'open' !isFolded && 'open'
)} )}
inert={isFolded} inert={isFolded}
style={{ maxHeight: hiddenHeight }} style={{ maxHeight: listHeight }}
> >
{items.map(cstID => { {items.map(cstID => {
const cst = schema.cstByID.get(cstID)!; const cst = schema.cstByID.get(cstID)!;
@ -87,7 +96,7 @@ export function ViewHidden({ items }: ViewHiddenProps) {
style={{ backgroundColor: colorBgGraphNode(cst, coloring) }} style={{ backgroundColor: colorBgGraphNode(cst, coloring) }}
onClick={event => handleClick(event, cstID)} onClick={event => handleClick(event, cstID)}
onContextMenu={event => handleContextMenu(event, cst)} onContextMenu={event => handleContextMenu(event, cst)}
onDoubleClick={() => navigateCst(cstID)} onDoubleClick={() => onActivate?.(cstID)}
data-tooltip-id={globalIDs.constituenta_tooltip} data-tooltip-id={globalIDs.constituenta_tooltip}
onMouseEnter={() => setActiveCst(cst)} onMouseEnter={() => setActiveCst(cst)}
> >

View File

@ -37,6 +37,13 @@ export function DlgGraphParams() {
name='noText' name='noText'
render={({ field }) => <Checkbox {...field} label='Скрыть текст' title='Не отображать термины' />} render={({ field }) => <Checkbox {...field} label='Скрыть текст' title='Не отображать термины' />}
/> />
<Controller
control={control}
name='foldDerived'
render={({ field }) => (
<Checkbox {...field} label='Скрыть порожденные' title='Не отображать порожденные понятия' />
)}
/>
<Controller <Controller
control={control} control={control}
name='noHermits' name='noHermits'
@ -64,13 +71,6 @@ export function DlgGraphParams() {
/> />
)} )}
/> />
<Controller
control={control}
name='foldDerived'
render={({ field }) => (
<Checkbox {...field} label='Свернуть порожденные' title='Не отображать порожденные понятия' />
)}
/>
</div> </div>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<h1 className='mb-1'>Типы конституент</h1> <h1 className='mb-1'>Типы конституент</h1>

View File

@ -14,7 +14,7 @@ import { Grammeme, ReferenceType } from './models/language';
import { CstClass, ExpressionStatus, type IConstituenta } from './models/rsform'; import { CstClass, ExpressionStatus, type IConstituenta } from './models/rsform';
import { type IArgumentInfo, type ISyntaxTreeNode } from './models/rslang'; import { type IArgumentInfo, type ISyntaxTreeNode } from './models/rslang';
import { CstMatchMode, DependencyMode } from './stores/cst-search'; import { CstMatchMode, DependencyMode } from './stores/cst-search';
import { type GraphColoring } from './stores/term-graph'; import { type GraphColoring, type GraphType } from './stores/term-graph';
// --- Records for label/describe functions --- // --- Records for label/describe functions ---
const labelCstTypeRecord: Record<CstType, string> = { const labelCstTypeRecord: Record<CstType, string> = {
@ -57,6 +57,12 @@ const labelColoringRecord: Record<GraphColoring, string> = {
schemas: 'Цвет: Схемы' schemas: 'Цвет: Схемы'
}; };
const labelGraphTypeRecord: Record<GraphType, string> = {
full: 'Связи: Все',
definition: 'Связи: Определения',
association: 'Связи: Ассоциации'
};
const labelCstMatchModeRecord: Record<CstMatchMode, string> = { const labelCstMatchModeRecord: Record<CstMatchMode, string> = {
[CstMatchMode.ALL]: 'фильтр', [CstMatchMode.ALL]: 'фильтр',
[CstMatchMode.EXPR]: 'выражение', [CstMatchMode.EXPR]: 'выражение',
@ -396,6 +402,11 @@ export function labelColoring(mode: GraphColoring): string {
return labelColoringRecord[mode] ?? `UNKNOWN COLORING: ${mode}`; return labelColoringRecord[mode] ?? `UNKNOWN COLORING: ${mode}`;
} }
/** Retrieves label for {@link GraphType}. */
export function labelGraphType(mode: GraphType): string {
return labelGraphTypeRecord[mode] ?? `UNKNOWN GRAPH TYPE: ${mode}`;
}
/** /**
* Retrieves label for {@link ExpressionStatus}. * Retrieves label for {@link ExpressionStatus}.
*/ */

View File

@ -7,7 +7,7 @@ import dagre from '@dagrejs/dagre';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { CstType } from '../backend/types'; import { CstType } from '../backend/types';
import { type GraphFilterParams } from '../stores/term-graph'; import { type GraphFilterParams, type GraphType } from '../stores/term-graph';
import { type IConstituenta, type IRSForm } from './rsform'; import { type IConstituenta, type IRSForm } from './rsform';
@ -57,8 +57,27 @@ export function applyLayout(nodes: Node<TGNodeState>[], edges: Edge[], subLabels
}); });
} }
export function inferEdgeType(schema: IRSForm, source: number, target: number): GraphType | null {
const isDefinition = schema.graph.hasEdge(source, target);
const isAssociation = schema.association_graph.hasEdge(source, target);
if (!isDefinition && !isAssociation) {
return null;
} else if (isDefinition && isAssociation) {
return 'full';
} else if (isDefinition) {
return 'definition';
} else {
return 'association';
}
}
export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | null) { export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | null) {
const filtered = schema.graph.clone(); const filtered =
params.graphType === 'full'
? schema.full_graph.clone()
: params.graphType === 'association'
? schema.association_graph.clone()
: schema.graph.clone();
const allowedTypes: CstType[] = (() => { const allowedTypes: CstType[] = (() => {
const result: CstType[] = []; const result: CstType[] = [];
if (params.allowBase) result.push(CstType.BASE); if (params.allowBase) result.push(CstType.BASE);
@ -73,9 +92,6 @@ export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams,
return result; return result;
})(); })();
if (params.noHermits) {
filtered.removeIsolated();
}
if (params.noTemplates) { if (params.noTemplates) {
schema.items.forEach(cst => { schema.items.forEach(cst => {
if (cst !== focusCst && cst.is_template) { if (cst !== focusCst && cst.is_template) {
@ -97,6 +113,9 @@ export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams,
} }
}); });
} }
if (params.noHermits) {
filtered.removeIsolated();
}
if (focusCst) { if (focusCst) {
const includes: number[] = [ const includes: number[] = [
focusCst.id, focusCst.id,

View File

@ -4,21 +4,24 @@ import { useEffect, useRef } from 'react';
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState, useOnSelectionChange } from 'reactflow'; import { type Edge, MarkerType, type Node, useEdgesState, useNodesState, useOnSelectionChange } from 'reactflow';
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow'; import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
import { useMainHeight } from '@/stores/app-layout'; import { useWindowSize } from '@/hooks/use-window-size';
import { useFitHeight, useMainHeight } from '@/stores/app-layout';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { withPreventDefault } from '@/utils/utils'; import { withPreventDefault } from '@/utils/utils';
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform'; import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
import { colorGraphEdge } from '../../../colors';
import { TGEdgeTypes } from '../../../components/term-graph/graph/tg-edge-types'; import { TGEdgeTypes } from '../../../components/term-graph/graph/tg-edge-types';
import { TGNodeTypes } from '../../../components/term-graph/graph/tg-node-types'; import { TGNodeTypes } from '../../../components/term-graph/graph/tg-node-types';
import { SelectColoring } from '../../../components/term-graph/select-coloring'; import { SelectColoring } from '../../../components/term-graph/select-coloring';
import { applyLayout, type TGNodeData } from '../../../models/graph-api'; import { SelectGraphType } from '../../../components/term-graph/select-graph-type';
import { ViewHidden } from '../../../components/term-graph/view-hidden';
import { applyLayout, inferEdgeType, type TGNodeData } from '../../../models/graph-api';
import { useTermGraphStore } from '../../../stores/term-graph'; import { useTermGraphStore } from '../../../stores/term-graph';
import { useRSEdit } from '../rsedit-context'; import { useRSEdit } from '../rsedit-context';
import { ToolbarTermGraph } from './toolbar-term-graph'; import { ToolbarTermGraph } from './toolbar-term-graph';
import { useFilteredGraph } from './use-filtered-graph'; import { useFilteredGraph } from './use-filtered-graph';
import { ViewHidden } from './view-hidden';
export const flowOptions = { export const flowOptions = {
fitView: true, fitView: true,
@ -31,17 +34,28 @@ export const flowOptions = {
} as const; } as const;
export function TGFlow() { export function TGFlow() {
const { isSmall } = useWindowSize();
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
const { fitView, viewportInitialized } = useReactFlow(); const { fitView, viewportInitialized } = useReactFlow();
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const { isContentEditable, schema, selected, setSelected, promptDeleteCst, focusCst, setFocus, navigateCst } = const {
useRSEdit(); isContentEditable,
schema,
selected,
setSelected,
promptDeleteCst,
focusCst,
setFocus,
toggleSelect,
navigateCst
} = useRSEdit();
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]); const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges] = useEdgesState<Edge>([]); const [edges, setEdges] = useEdgesState<Edge>([]);
const filter = useTermGraphStore(state => state.filter); const filter = useTermGraphStore(state => state.filter);
const { filteredGraph, hidden } = useFilteredGraph(); const { filteredGraph, hidden } = useFilteredGraph();
const hiddenHeight = useFitHeight(isSmall ? '15rem + 2px' : '13.5rem + 2px', '4rem');
function onSelectionChange({ nodes }: { nodes: Node[] }) { function onSelectionChange({ nodes }: { nodes: Node[] }) {
const ids = nodes.map(node => Number(node.id)); const ids = nodes.map(node => Number(node.id));
@ -73,17 +87,21 @@ export function TGFlow() {
let edgeID = 1; let edgeID = 1;
filteredGraph.nodes.forEach(source => { filteredGraph.nodes.forEach(source => {
source.outputs.forEach(target => { source.outputs.forEach(target => {
if (newNodes.find(node => node.id === String(target))) { const edgeType = inferEdgeType(schema, source.id, target);
if (edgeType && newNodes.find(node => node.id === String(target))) {
const color = filter.graphType === 'full' ? colorGraphEdge(edgeType) : colorGraphEdge(filter.graphType);
newEdges.push({ newEdges.push({
id: String(edgeID), id: String(edgeID),
source: String(source.id), source: String(source.id),
target: String(target), target: String(target),
type: 'termEdge', type: 'termEdge',
style: { stroke: color },
focusable: false, focusable: false,
markerEnd: { markerEnd: {
type: MarkerType.ArrowClosed, type: MarkerType.ArrowClosed,
width: 20, width: 20,
height: 20 height: 20,
color: color
} }
}); });
edgeID += 1; edgeID += 1;
@ -97,7 +115,17 @@ export function TGFlow() {
setEdges(newEdges); setEdges(newEdges);
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.minimalTimeout); setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.minimalTimeout);
}, [schema, filteredGraph, setNodes, setEdges, filter.noText, fitView, viewportInitialized, focusCst]); }, [
schema,
filteredGraph,
setNodes,
setEdges,
filter.noText,
fitView,
viewportInitialized,
focusCst,
filter.graphType
]);
const prevSelected = useRef<number[]>([]); const prevSelected = useRef<number[]>([]);
if ( if (
@ -150,8 +178,19 @@ export function TGFlow() {
<span className='px-2 pb-1 select-none whitespace-nowrap backdrop-blur-xs rounded-xl w-fit'> <span className='px-2 pb-1 select-none whitespace-nowrap backdrop-blur-xs rounded-xl w-fit'>
Выбор {selected.length} из {schema.stats?.count_all ?? 0} Выбор {selected.length} из {schema.stats?.count_all ?? 0}
</span> </span>
<SelectColoring schema={schema} />
<ViewHidden items={hidden} /> <SelectColoring className='rounded-b-none' schema={schema} />
<SelectGraphType className='rounded-none border-t-0' />
<ViewHidden
items={hidden}
listHeight={hiddenHeight}
schema={schema}
selected={selected}
toggleSelect={toggleSelect}
setFocus={setFocus}
onActivate={navigateCst}
/>
</div> </div>
<DiagramFlow <DiagramFlow

View File

@ -156,11 +156,6 @@ export function ToolbarTermGraph({ className }: ToolbarTermGraphProps) {
onClick={toggleClustering} onClick={toggleClustering}
/> />
<MiniButton
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
title='Граф ступеней'
onClick={handleShowTypeGraph}
/>
<BadgeHelp topic={HelpTopic.UI_GRAPH_TERM} contentClass='sm:max-w-160' offset={4} /> <BadgeHelp topic={HelpTopic.UI_GRAPH_TERM} contentClass='sm:max-w-160' offset={4} />
</div> </div>
<div className='cc-icons items-center'> <div className='cc-icons items-center'>
@ -198,6 +193,11 @@ export function ToolbarTermGraph({ className }: ToolbarTermGraphProps) {
disabled={!canDeleteSelected || isProcessing} disabled={!canDeleteSelected || isProcessing}
/> />
) : null} ) : null}
<MiniButton
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
title='Граф ступеней'
onClick={handleShowTypeGraph}
/>
</div> </div>
</div> </div>
); );

View File

@ -4,16 +4,18 @@ import { persist } from 'zustand/middleware';
import { CstType } from '../backend/types'; import { CstType } from '../backend/types';
export const graphColorings = ['none', 'status', 'type', 'schemas'] as const; export const graphColorings = ['none', 'status', 'type', 'schemas'] as const;
export const graphTypes = ['full', 'association', 'definition'] as const;
/** /** Represents graph node coloring scheme. */
* Represents graph node coloring scheme.
*/
export type GraphColoring = (typeof graphColorings)[number]; export type GraphColoring = (typeof graphColorings)[number];
/** /** Represents graph type. */
* Represents parameters for GraphEditor. export type GraphType = (typeof graphTypes)[number];
*/
/** Represents parameters for GraphEditor. */
export interface GraphFilterParams { export interface GraphFilterParams {
graphType: GraphType;
noHermits: boolean; noHermits: boolean;
noTransitive: boolean; noTransitive: boolean;
noTemplates: boolean; noTemplates: boolean;
@ -49,10 +51,12 @@ export const cstTypeToFilterKey: Record<CstType, keyof GraphFilterParams> = {
interface TermGraphStore { interface TermGraphStore {
filter: GraphFilterParams; filter: GraphFilterParams;
setFilter: (value: GraphFilterParams) => void; setFilter: (value: GraphFilterParams) => void;
setGraphType: (value: GraphType) => void;
toggleFocusInputs: () => void; toggleFocusInputs: () => void;
toggleFocusOutputs: () => void; toggleFocusOutputs: () => void;
toggleText: () => void; toggleText: () => void;
toggleClustering: () => void; toggleClustering: () => void;
toggleGraphType: () => void;
foldHidden: boolean; foldHidden: boolean;
toggleFoldHidden: () => void; toggleFoldHidden: () => void;
@ -65,6 +69,8 @@ export const useTermGraphStore = create<TermGraphStore>()(
persist( persist(
set => ({ set => ({
filter: { filter: {
graphType: 'full',
noTemplates: false, noTemplates: false,
noHermits: true, noHermits: true,
noTransitive: true, noTransitive: true,
@ -85,12 +91,25 @@ export const useTermGraphStore = create<TermGraphStore>()(
allowNominal: true allowNominal: true
}, },
setFilter: value => set({ filter: value }), setFilter: value => set({ filter: value }),
setGraphType: value => set(state => ({ filter: { ...state.filter, graphType: value } })),
toggleFocusInputs: () => toggleFocusInputs: () =>
set(state => ({ filter: { ...state.filter, focusShowInputs: !state.filter.focusShowInputs } })), set(state => ({ filter: { ...state.filter, focusShowInputs: !state.filter.focusShowInputs } })),
toggleFocusOutputs: () => toggleFocusOutputs: () =>
set(state => ({ filter: { ...state.filter, focusShowOutputs: !state.filter.focusShowOutputs } })), set(state => ({ filter: { ...state.filter, focusShowOutputs: !state.filter.focusShowOutputs } })),
toggleText: () => set(state => ({ filter: { ...state.filter, noText: !state.filter.noText } })), toggleText: () => set(state => ({ filter: { ...state.filter, noText: !state.filter.noText } })),
toggleClustering: () => set(state => ({ filter: { ...state.filter, foldDerived: !state.filter.foldDerived } })), toggleClustering: () => set(state => ({ filter: { ...state.filter, foldDerived: !state.filter.foldDerived } })),
toggleGraphType: () =>
set(state => ({
filter: {
...state.filter,
graphType:
state.filter.graphType === 'full'
? 'association'
: state.filter.graphType === 'association'
? 'definition'
: 'full'
}
})),
foldHidden: false, foldHidden: false,
toggleFoldHidden: () => set(state => ({ foldHidden: !state.foldHidden })), toggleFoldHidden: () => set(state => ({ foldHidden: !state.foldHidden })),
@ -99,7 +118,7 @@ export const useTermGraphStore = create<TermGraphStore>()(
setColoring: value => set({ coloring: value }) setColoring: value => set({ coloring: value })
}), }),
{ {
version: 1, version: 3,
name: 'portal.termGraph' name: 'portal.termGraph'
} }
) )