diff --git a/libs/shared/lib/components/DesignGuides/styleGuide.mdx b/libs/shared/lib/components/DesignGuides/styleGuide.mdx index 00c29bc5a1683ad01e93e52e651dae0bb23971bb..a7e82a621d89cdcbff0378515b42343a5d69e227 100644 --- a/libs/shared/lib/components/DesignGuides/styleGuide.mdx +++ b/libs/shared/lib/components/DesignGuides/styleGuide.mdx @@ -645,7 +645,7 @@ GraphPolaris uses [Material UI](https://mui.com/material-ui/material-icons/) thr </div> ```jsx -import Icon from '@graphpolaris/shared/lib/components/icon'; +import { Icon } from '@graphpolaris/shared/lib/components/icon'; <Icon name="ArrowBack" size={32} />; ``` diff --git a/libs/shared/lib/components/buttons/Button.tsx b/libs/shared/lib/components/buttons/Button.tsx index f4e5c04dd148f17fc07899461b50a14f150a2d88..f607db885c5faf598e0fcfc20f3f0922440ada1b 100644 --- a/libs/shared/lib/components/buttons/Button.tsx +++ b/libs/shared/lib/components/buttons/Button.tsx @@ -1,6 +1,6 @@ import React, { ReactElement, ReactPropTypes, useMemo } from 'react'; import styles from './buttons.module.scss'; -import Icon, { Sizes } from '../icon'; +import { Icon, Sizes } from '../icon'; import { forwardRef } from 'react'; type ButtonProps = { diff --git a/libs/shared/lib/components/dropdowns/index.tsx b/libs/shared/lib/components/dropdowns/index.tsx index be780115dfe688aaa70531a18a15341176fb18a6..8fb237c6e7ff776987d8b56aaa33b89243d44cf4 100644 --- a/libs/shared/lib/components/dropdowns/index.tsx +++ b/libs/shared/lib/components/dropdowns/index.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef, ReactNode } from 'react'; import styles from './dropdowns.module.scss'; -import Icon from '../icon'; +import { Icon } from '../icon'; import { ArrowDropDown } from '@mui/icons-material'; import { PopoverContent, PopoverTrigger, Popover, PopoverOptions } from '../layout/Popover'; @@ -45,7 +45,7 @@ export function DropdownTrigger({ const inner = children || ( <div - className={`inline-flex w-full truncate justify-between items-center gap-x-1.5 ${variantClass} ${textSizeClass} ${paddingClass} text-secondary-900 shadow-sm hover:bg-secondary-50 disabled:bg-secondary-100 disabled:cursor-not-allowed disabled:text-secondary-400 pl-1 truncate${className ? ` ${className}` : ''}`} + className={`inline-flex w-full truncate justify-between items-center gap-x-1.5 ${variantClass} ${textSizeClass} ${paddingClass} text-secondary-900 shadow-sm hover:bg-secondary-50 disabled:bg-secondary-100 disabled:cursor-not-allowed disabled:text-secondary-400 pl-1 truncate cursor-pointer${className ? ` ${className}` : ''}`} > <span className={`text-${size}`}>{title}</span> <Icon component={<ArrowDropDown />} size={16} /> @@ -70,6 +70,7 @@ type DropdownItemContainerProps = { }; export const DropdownItemContainer = React.forwardRef<HTMLDivElement, DropdownItemContainerProps>(({ children, className }, ref) => { + if (!children || !React.Children.count(children)) return null; return ( <PopoverContent ref={ref} @@ -91,9 +92,10 @@ type DropdownItemProps = { onClick?: (value: string) => void; submenu?: React.ReactNode; selected?: boolean; + children?: ReactNode; }; -export function DropdownItem({ value, disabled, className, onClick, submenu, selected }: DropdownItemProps) { +export function DropdownItem({ value, disabled, className, onClick, submenu, selected, children }: DropdownItemProps) { const itemRef = useRef(null); const submenuRef = useRef(null); const [isSubmenuOpen, setIsSubmenuOpen] = useState(false); @@ -109,7 +111,7 @@ export function DropdownItem({ value, disabled, className, onClick, submenu, sel onMouseEnter={() => setIsSubmenuOpen(true)} onMouseLeave={() => setIsSubmenuOpen(false)} > - { value } + {value} {submenu && isSubmenuOpen && <DropdownSubmenuContainer ref={submenuRef}>{submenu}</DropdownSubmenuContainer>} </li> ); diff --git a/libs/shared/lib/components/icon/icon.stories.tsx b/libs/shared/lib/components/icon/icon.stories.tsx index b57f7c09eb64b72801e5339ffbe0a3227294fe79..cad42fd6e227cbf69997acff02fb34b11bad6f57 100644 --- a/libs/shared/lib/components/icon/icon.stories.tsx +++ b/libs/shared/lib/components/icon/icon.stories.tsx @@ -1,5 +1,5 @@ import { StoryObj, Meta } from '@storybook/react'; -import Icon from '../icon'; +import { Icon } from '../icon'; import { ArrowBack, DeleteOutline, KeyboardArrowLeft, Settings } from '@mui/icons-material'; const Component: Meta<typeof Icon> = { diff --git a/libs/shared/lib/components/icon/index.tsx b/libs/shared/lib/components/icon/index.tsx index 6ea37b5712c9645c01f2ef9c982f5a1b6e8fd93c..4f2695cb556abfa1d7749e39146bcd663e1e8563 100644 --- a/libs/shared/lib/components/icon/index.tsx +++ b/libs/shared/lib/components/icon/index.tsx @@ -5,9 +5,10 @@ export type Sizes = 12 | 14 | 16 | 20 | 24 | 28 | 32 | 40; export type IconProps = SVGProps<SVGSVGElement> & { component: ReactElement<any>; size?: Sizes; + color?: string; }; -export const Icon: React.FC<IconProps> = ({ component, size = 24, ...props }) => { +export const Icon: React.FC<IconProps> = ({ component, size = 24, color, ...props }) => { if (!component) { console.error(`No icon found`); return <div></div>; @@ -15,5 +16,3 @@ export const Icon: React.FC<IconProps> = ({ component, size = 24, ...props }) => return React.cloneElement(component, { style: { fontSize: size }, width: size, height: size, ...props }); }; - -export default Icon; diff --git a/libs/shared/lib/components/icon/overview.mdx b/libs/shared/lib/components/icon/overview.mdx index dc7299e9e2fd0769da3a27e3f6f5bdac33a22adc..d03354ff28ea7465d3ffb386b366ca3f3f2cbf4f 100644 --- a/libs/shared/lib/components/icon/overview.mdx +++ b/libs/shared/lib/components/icon/overview.mdx @@ -1,6 +1,6 @@ import { Canvas, Meta, Story } from '@storybook/blocks'; import * as IconStories from './icon.stories'; -import Icon from '.'; +import { Icon } from '.'; <Meta title="Components/Icon" component={Icon} /> diff --git a/libs/shared/lib/components/info/index.tsx b/libs/shared/lib/components/info/index.tsx index 6bb08d1234fbf3a2b9b37cb6b976903daf62dc45..20b158aae418700b04bc87421f476538fa52af16 100644 --- a/libs/shared/lib/components/info/index.tsx +++ b/libs/shared/lib/components/info/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Icon from '../icon'; +import { Icon } from '../icon'; import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'; import { InfoOutlined } from '@mui/icons-material'; diff --git a/libs/shared/lib/components/selectors/entityPillSelector.stories.tsx b/libs/shared/lib/components/selectors/entityPillSelector.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..931e1410a4c92ffc837e465792add76bb58ac05d --- /dev/null +++ b/libs/shared/lib/components/selectors/entityPillSelector.stories.tsx @@ -0,0 +1,19 @@ +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import EntityPillSelector, { EntityPillSelectorProps } from './entityPillSelector'; + +const metaPillDropdown: Meta<typeof EntityPillSelector> = { + component: EntityPillSelector, + title: 'Components/Selectors/Entity', + decorators: [(story) => <div className="flex items-center justify-center m-11 p-11">{story()}</div>], +}; + +export default metaPillDropdown; + +type Story = StoryObj<typeof EntityPillSelector>; + +export const entity: Story = { + args: { + dropdownNodes: ['kamerleden', 'commissies'], + }, +}; diff --git a/libs/shared/lib/components/selectors/entityPillSelector.tsx b/libs/shared/lib/components/selectors/entityPillSelector.tsx new file mode 100644 index 0000000000000000000000000000000000000000..998294d8837c8421200919d810ee826c11876580 --- /dev/null +++ b/libs/shared/lib/components/selectors/entityPillSelector.tsx @@ -0,0 +1,56 @@ +import React, { useRef, useState } from 'react'; +import { Button } from '../buttons'; +import { ArrowDropDown } from '@mui/icons-material'; +import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill'; +import { DropdownContainer, DropdownItemContainer, DropdownTrigger, DropdownItem } from '../dropdowns'; + +export type EntityPillSelectorProps = { + selectedNode?: string; + dropdownNodes: string[]; + onSelectOption: (option: string) => void; +}; + +export function EntityPillSelector({ dropdownNodes, onSelectOption, selectedNode }: EntityPillSelectorProps) { + const [isCollapsed, setIsCollapsed] = useState(true); + // const [initialNamePill, setInitialNamePill] = useState('Choose a node:'); + + const handleButtonClick = () => { + setIsCollapsed(!isCollapsed); + }; + + const handleOptionClick = (option: string) => { + setIsCollapsed(true); + onSelectOption(option); + }; + + return ( + <DropdownContainer placement="bottom"> + <DropdownTrigger title={selectedNode || 'Choose a node:'} size="sm"> + <EntityPill + className="cursor-pointer" + title={ + <div className="flex flex-row items-center justify-between pointer-events-none"> + <span>{selectedNode || 'Choose a node:'}</span> + <Button variantType="secondary" variant="ghost" size="xs" iconComponent={<ArrowDropDown />} onClick={handleButtonClick} /> + </div> + } + /> + </DropdownTrigger> + <DropdownItemContainer> + {dropdownNodes + .map((node, index) => ( + <DropdownItem + className="my-0 cursor-pointer" + selected={selectedNode === node} + onClick={() => handleOptionClick(node)} + key={'entity_' + index + '-' + node} + value={node} + > + <EntityPill title={node} /> + </DropdownItem> + )) + .filter((node) => node.props.value !== selectedNode)} + </DropdownItemContainer> + </DropdownContainer> + ); +} diff --git a/libs/shared/lib/components/selectors/index.ts b/libs/shared/lib/components/selectors/index.ts index 14964560cc76dc9467199fcebe6dd68fa21064b0..04c48ea22477b6d7a78bf5124d1bd01c69f1037a 100644 --- a/libs/shared/lib/components/selectors/index.ts +++ b/libs/shared/lib/components/selectors/index.ts @@ -3,6 +3,7 @@ import Shape from './shape'; import Size from './size'; import Axis from './axis'; import Opacity from './opacity'; +import EntityPill from './entityPillSelector'; export const EncodingSelector = { Color: Color, @@ -10,6 +11,7 @@ export const EncodingSelector = { Size: Size, Axis: Axis, Opacity: Opacity, + EntityPill: EntityPill, }; // Inspiration: https://uwdata.github.io/visualization-curriculum/altair_marks_encoding.html diff --git a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts index 48968c579985a1812012773a260111f91961f944..95e2f6bb2afe89609e0f5ab1a9f1ef93aec5eae1 100755 --- a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts +++ b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts @@ -49,7 +49,7 @@ export type Edge = { // Define a type for the slice state export type GraphQueryResult = { - metaData: GraphMetadata; + metaData?: GraphMetadata; nodes: Node[]; edges: Edge[]; queryingBackend: boolean; @@ -57,7 +57,7 @@ export type GraphQueryResult = { // Define the initial state using that type export const initialState: GraphQueryResult = { - metaData: { nodes: { labels: [], types: {} }, edges: { labels: [], types: {} } }, + metaData: undefined, nodes: [], edges: [], queryingBackend: false, @@ -139,14 +139,14 @@ export const graphQueryResultSlice = createSlice({ const { metaData, nodes, edges } = graphQueryBackend2graphQuery(payload); // Assign new state - state.metaData = extractStatistics(metaData); + state.metaData = metaData; state.nodes = nodes; state.edges = edges; state.queryingBackend = false; }, resetGraphQueryResults: (state) => { // Assign new state - state.metaData = { nodes: { labels: [], types: {} }, edges: { labels: [], types: {} } }; + state.metaData = undefined; state.nodes = []; state.edges = []; state.queryingBackend = false; diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts index 8e52b38f41aa88622b10f0b62be23cffabccee24..9f1ab6ce9a0d0cb1ad0b4e1019d41c6c1633022d 100644 --- a/libs/shared/lib/data-access/store/hooks.ts +++ b/libs/shared/lib/data-access/store/hooks.ts @@ -39,7 +39,7 @@ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; /** Gives the graphQueryResult from the store */ export const useGraphQueryResult: () => GraphQueryResult = () => useAppSelector(selectGraphQueryResult); -export const useGraphQueryResultMeta: () => GraphMetadata = () => useAppSelector(selectGraphQueryResultMetaData); +export const useGraphQueryResultMeta: () => GraphMetadata | undefined = () => useAppSelector(selectGraphQueryResultMetaData); // Gives the schema export const useSchemaGraph: () => SchemaGraph = () => useAppSelector(schemaGraph); diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx index 85c84843a5cfcf552dda97d5bd8d713dbaebd8ef..07caaabbf76bed3384b3992afe6a4a15f0102242 100644 --- a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx +++ b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx @@ -2,7 +2,7 @@ import { useMemo, ReactElement, useState, useContext } from 'react'; import { NodeAttribute, QueryGraphEdges, SchemaReactflowEntityNode, handleDataFromReactflowToDataId, toHandleId } from '../../model'; import { Handle, Position, useUpdateNodeInternals } from 'reactflow'; import { Abc, CalendarToday, Map, Numbers, Place, QuestionMarkOutlined } from '@mui/icons-material'; -import Icon from '@graphpolaris/shared/lib/components/icon'; +import { Icon } from '@graphpolaris/shared/lib/components/icon'; import { PillHandle } from '@graphpolaris/shared/lib/components/pills/PillHandle'; import { pillDropdownPadding } from '@graphpolaris/shared/lib/components/pills/pill.const'; import { Button, TextInput, useAppDispatch, useQuerybuilderAttributesShown } from '../../..'; diff --git a/libs/shared/lib/vis/components/VisualizationPanel.tsx b/libs/shared/lib/vis/components/VisualizationPanel.tsx index c7446a261d91974ff5c412ad92eb9a04314716cc..881dafbb61a549b4a030e4bd5cb4e624670ef15c 100644 --- a/libs/shared/lib/vis/components/VisualizationPanel.tsx +++ b/libs/shared/lib/vis/components/VisualizationPanel.tsx @@ -108,7 +108,8 @@ export const VisualizationPanel = ({ fullSize }: { fullSize: () => void }) => { {!!viz && activeVisualizationIndex !== -1 && openVisualizationArray?.[activeVisualizationIndex] && - viz.id === openVisualizationArray[activeVisualizationIndex].id && ( + viz.id === openVisualizationArray[activeVisualizationIndex].id && + graphMetadata && ( <viz.component data={graphQueryResult} schema={schema} diff --git a/libs/shared/lib/vis/components/config/VisualizationSettings.tsx b/libs/shared/lib/vis/components/config/VisualizationSettings.tsx index ddfe63d150694b40601cb23bf78dc63240cee40b..ad46863543ba051af8d2e042d47059c3e4ce0ab7 100644 --- a/libs/shared/lib/vis/components/config/VisualizationSettings.tsx +++ b/libs/shared/lib/vis/components/config/VisualizationSettings.tsx @@ -102,7 +102,7 @@ export function VisualizationSettings({}: Props) { label="Name" inline /> - {activeVisualization && ( + {activeVisualization && graphMetadata && ( <> <SettingsHeader name="Visualization Settings" /> <Suspense fallback={<div>Loading...</div>}> diff --git a/libs/shared/lib/vis/components/config/panel.tsx b/libs/shared/lib/vis/components/config/panel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/libs/shared/lib/vis/visualizations/paohvis/components/RowLabels.tsx b/libs/shared/lib/vis/visualizations/paohvis/components/RowLabels.tsx index 425472216e37bd60ab2cb374ef403cb21898f595..096883c8feba637bff53064ae29f5f314f7129e0 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/components/RowLabels.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/components/RowLabels.tsx @@ -23,7 +23,7 @@ export const RowLabels = ({ rowHeight, yOffset, rowLabelColumnWidth, - classTopTextColumns: classTopTextColums, + classTopTextColumns, marginText, sortState, headerState, @@ -100,7 +100,7 @@ export const RowLabels = ({ fill={indexRows % 2 === 0 ? 'hsl(var(--clr-sec--50))' : 'hsl(var(--clr-sec--0))'} strokeWidth={0} ></rect> - <text x={row.width * marginText} y={rowHeight / 2} dy="0" dominantBaseline="middle" className={classTopTextColums}> + <text x={row.width * marginText} y={rowHeight / 2} dy="0" dominantBaseline="middle" className={classTopTextColumns}> {row.data[indexRows]} </text> </g> @@ -153,7 +153,7 @@ export const RowLabels = ({ }} > <rect width={row.width} height={rowHeight} fill={'hsl(var(--clr-sec--200))'} opacity={1.0} strokeWidth={0}></rect> - <text x={marginText * row.width} y={0.5 * rowHeight} dy="0" dominantBaseline="middle" className={classTopTextColums}> + <text x={marginText * row.width} y={0.5 * rowHeight} dy="0" dominantBaseline="middle" className={classTopTextColumns}> {row.header} </text> {iconComponents[indexRows] && isHovered && ( diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/Overview.mdx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/Overview.mdx new file mode 100644 index 0000000000000000000000000000000000000000..2ae6f0b46d5422844979a533227b488a67442585 --- /dev/null +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/Overview.mdx @@ -0,0 +1,21 @@ +import { Meta, Unstyled } from '@storybook/blocks'; + +<Meta title="Visualizations/Implementation" /> + +# Variables + +## Related to Scatterplots + +- appState: used to render the scatterplots. Contains dataRegions: data that builds the scatterplots, and scatterplot: what renders the scatterplot + +- idBrush: used to keep track of the brush idBrush + +- computedData: build when the scatterplot finish the jitter. Only two positions: + --region1 for R0, only the first scatterplot + --region2 for RX, when a scatterplot finish the jitter process it saves the result here + +## Related to Edges + +- edgeState: used to render the scatterplots. Eg. contains positions of the data points in the scatterplot +- informationEdges contains edge labels and IDs from the edges. +- arrayConnections contains IDs from the edges. diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/ConfigPanel.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/ConfigPanel.tsx deleted file mode 100644 index 3aadfdeeef1292248c889e421d1043888a1de9e1..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/ConfigPanel.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Node, DataConfig, AugmentedNodeAttributes } from './types'; -import { Button } from '../../../../components/buttons'; - -function getUniqueValues(arr: any[]): any[] { - return [...new Set(arr)]; -} - -interface ConfigPanelProps { - data: AugmentedNodeAttributes[]; - onUpdateData: (data: DataConfig) => void; -} - -const ConfigPanel: React.FC<ConfigPanelProps> = ({ data, onUpdateData }) => { - const [state, setState] = useState<{ - entityVertical: string; - attributeEntity: string; - attributeValueSelected: string; - orderNameXaxis: string; - orderNameYaxis: string; - isButtonEnabled: boolean; - }>({ - entityVertical: '', - attributeEntity: '', - attributeValueSelected: '', - orderNameXaxis: '', - orderNameYaxis: '', - isButtonEnabled: true, - }); - - const nodeLabels: string[] = data.map((node: any) => node.label); - - const uniqueNodeLabels = getUniqueValues(nodeLabels); - - const entityOptions = [...uniqueNodeLabels].map((value, index) => ( - <option key={`option${index}`} value={value}> - {value} - </option> - )); - - // Extract unique attributeEntity values from the entire data array - const [attributeEntityMenuItems, setattributeEntityMenuItems] = useState<string[]>([]); - // Filter the data based on the selected entity (label) - const [filteredData, setFilteredData] = useState<AugmentedNodeAttributes[]>([]); - const [attributeOptions, setAttributeOptions] = useState<any[]>([]); - - useEffect(() => { - if (state.entityVertical) { - const selectedEntity = state.entityVertical; - // Filter the data based on the selected entity (label) - const filteredData: AugmentedNodeAttributes[] = data.filter((item) => item.label === selectedEntity); - - setFilteredData(filteredData); - } else { - setFilteredData([]); - } - }, [state.entityVertical, data]); - - useEffect(() => { - if (filteredData.length > 0) { - const attributes: object = filteredData[0].attributes; - - if (attributes) { - const keys = Object.keys(attributes); - setattributeEntityMenuItems(keys); - } - } else { - setattributeEntityMenuItems([]); // Clear the attributeEntityMenuItems when there's no filtered data - } - }, [filteredData]); - - useEffect(() => { - // Update attributeOptions when attributeEntity changes - if (filteredData.length > 0) { - const filteredAAttributes: any[] = (filteredData as AugmentedNodeAttributes[]).map((item: AugmentedNodeAttributes) => { - const attributeValueSelected = item.attributes[state.attributeEntity]; - - if (Array.isArray(attributeValueSelected)) { - if (attributeValueSelected.length === 1) { - return attributeValueSelected[0]; - } else { - return attributeValueSelected.join('-'); - } - } else if (typeof attributeValueSelected === 'string' || typeof attributeValueSelected === 'number') { - return attributeValueSelected; - } else { - return null; // Return null for other types - } - }); - - if (filteredAAttributes) { - // Extract unique values from the relation's attributes - const uniqueValues = Array.from(new Set(filteredAAttributes)); - const firstElement = uniqueValues[0]; - - if (typeof firstElement === 'number') { - // Sort numbers in descending order - const sortedValues = uniqueValues.slice().sort((a, b) => a - b); - - setAttributeOptions(sortedValues); - } else if (typeof firstElement === 'string') { - // Sort strings in descending order - // localCompare is useful to take into account local language consideration. - // but breaks for comparing an URL - const sortedValues = uniqueValues.slice().sort((a, b) => a - b); - setAttributeOptions(sortedValues); - } else { - // Handle other data types as needed - setAttributeOptions(uniqueValues); // Clear attributeOptions for unsupported data types - } - } - } else { - setAttributeOptions([]); // Clear attributeOptions when there's no filtered data - } - }, [state.attributeEntity, filteredData]); - - const onClickMakeButton = () => { - // Retrieve the selected values - - const { entityVertical, attributeEntity, attributeValueSelected, orderNameXaxis, orderNameYaxis, isButtonEnabled } = state; - const isAxisSelected = orderNameXaxis !== '' || orderNameYaxis !== ''; - - // Call the callback to send the data to the parent component (VisSemanticSubstrates) - if (isAxisSelected) { - onUpdateData({ - entityVertical, - attributeEntity, - attributeValueSelected, - orderNameXAxis: orderNameXaxis, - orderNameYAxis: orderNameYaxis, - isButtonEnabled, - }); - } - }; - - return ( - <div className="nav card w-full"> - <div className="card-body flex flex-row overflow-y-auto max-w-[60vw] self-center items-center"> - <div className="select-container"> - <label className="select-label">Entity:</label> - <select - className="select" - id="standard-select-entity" - value={state.entityVertical} - onChange={(e) => setState({ ...state, entityVertical: e.target.value })} - > - <option value="" disabled> - Select an entity - </option> - {entityOptions} - </select> - </div> - - <div className="select-container"> - <label className="select-label">Attribute:</label> - <select - className={`select ${attributeEntityMenuItems.length === 0 ? 'select-disabled' : ''}`} - id="standard-select-relation" - value={state.attributeEntity} - onChange={(e) => setState({ ...state, attributeEntity: e.target.value, attributeValueSelected: '', orderNameXaxis: '' })} - > - <option value="" disabled> - Select a relation - </option> - {attributeEntityMenuItems.map((value, index) => ( - <option key={`option${index}`} value={value}> - {value} - </option> - ))} - </select> - </div> - - <div className="select-container"> - <label className="select-label">Selected Attribute:</label> - <select - className={`select ${attributeOptions.length === 0 ? 'select-disabled' : ''}`} - id="standard-select-attribute" - value={state.attributeValueSelected} - onChange={(e) => setState({ ...state, attributeValueSelected: e.target.value, orderNameXaxis: '', orderNameYaxis: '' })} - > - <option value="" disabled> - Select an attribute - </option> - {attributeOptions.map((value, index) => ( - <option key={`option${index}`} value={value}> - {value} - </option> - ))} - </select> - </div> - - <div className="select-container"> - <label className="select-label">X-axis:</label> - <select - className={`select ${attributeEntityMenuItems.length === 0 ? 'select-disabled' : ''}`} - id="standard-select-relation" - value={state.orderNameXaxis} - onChange={(e) => setState({ ...state, orderNameXaxis: e.target.value })} - > - <option value="" disabled> - Select a x axis - </option> - {attributeEntityMenuItems.map((value, index) => ( - <option key={`option${index}`} value={value}> - {value} - </option> - ))} - </select> - </div> - - <div className="select-container"> - <label className="select-label">Y-axis:</label> - <select - className={`select ${attributeEntityMenuItems.length === 0 ? 'select-disabled' : ''}`} - id="standard-select-relation" - value={state.orderNameYaxis} - onChange={(e) => setState({ ...state, orderNameYaxis: e.target.value })} - > - <option value="" disabled> - Select a y axis - </option> - {attributeEntityMenuItems.map((value, index) => ( - <option key={`option${index}`} value={value}> - {value} - </option> - ))} - </select> - </div> - - <Button - label="Make" - variantType="secondary" - variant="solid" - disabled={!state.isButtonEnabled || (!state.orderNameXaxis && !state.orderNameYaxis)} - onClick={onClickMakeButton} - /> - </div> - </div> - ); -}; - -export default ConfigPanel; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/EdgesLayer.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/EdgesLayer.tsx index 5adafe3499e91f1deadd9d6a5152364e86e82808..bea37982d5a3ea3383f2372ba82eb238eb57bc06 100644 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/EdgesLayer.tsx +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/EdgesLayer.tsx @@ -1,17 +1,14 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useMemo } from 'react'; import { DataConnection, VisualRegionConfig, RegionData, VisualEdgesConfig, DataPoint } from './types'; -import { select } from 'd3'; - -import { isNumeric } from './utils'; +import { index, select } from 'd3'; export type EdgesLayerProps = { dataConnections: DataConnection[]; visualConfig: React.MutableRefObject<VisualEdgesConfig>; + visualScatterplot: VisualRegionConfig; data1: DataPoint[]; data2: DataPoint[]; nameEdges: string; - nameRegions: string[]; - width: number; }; export type KeyedEdgesLayerProps = EdgesLayerProps & { @@ -68,10 +65,10 @@ function edgeGenerator(dataPoint: dataPointEdge): string { return path; } -const EdgesLayer: React.FC<EdgesLayerProps> = ({ dataConnections, visualConfig, data1, data2, nameEdges, width }) => { +const EdgesLayer: React.FC<EdgesLayerProps> = ({ dataConnections, visualConfig, data1, data2, nameEdges, visualScatterplot }) => { const svgRef = useRef(null); - useEffect(() => { + const [dataVis, dataEdgeIds] = useMemo(() => { const data1_id = data1.map((item) => item.id); const data1_x = data1.map((item) => item.x); const data1_y = data1.map((item) => item.y); @@ -80,64 +77,61 @@ const EdgesLayer: React.FC<EdgesLayerProps> = ({ dataConnections, visualConfig, const data2_x = data2.map((item) => item.x); const data2_y = data2.map((item) => item.y); - const svg = select(svgRef.current); const heightRegion = visualConfig.current.configRegion.height; - const svgToRegion1 = [visualConfig.current.configRegion.margin.left, visualConfig.current.configRegion.margin.top + 0 * heightRegion]; - const svgToRegion2 = [visualConfig.current.configRegion.margin.left, visualConfig.current.configRegion.margin.top + 1 * heightRegion]; + const svgToRegion1 = [visualScatterplot.margin.left, visualConfig.current.configRegion.margin.top + 0 * heightRegion]; + const svgToRegion2 = [visualScatterplot.margin.left, visualConfig.current.configRegion.margin.top + 1 * heightRegion]; const dataVis: dataPointEdge[] = []; const dataEdgeIds: string[] = []; - dataConnections.forEach(function (value: DataConnection) { - // Get FROM - // ID - // what happens if indexID_region1 is not found - const indexID_region1 = data1_id.findIndex((idInstance) => { - return idInstance == value.from; - }); + + dataConnections.forEach((value: DataConnection) => { + const indexID_region1 = data1_id.findIndex((idInstance) => idInstance === value.from); const startX_region1 = data1_x[indexID_region1] + svgToRegion1[0]; const startY_region1 = data1_y[indexID_region1] + svgToRegion1[1]; - // GET TO - const indexID_region2 = data2_id.findIndex((idInstance) => { - return idInstance == value.to; - }); + const indexID_region2 = data2_id.findIndex((idInstance) => idInstance === value.to); const startX_region2 = data2_x[indexID_region2] + svgToRegion2[0]; const startY_region2 = data2_y[indexID_region2] + svgToRegion2[1] + visualConfig.current.offsetY; + dataVis.push({ start: [startX_region1, startY_region1], end: [startX_region2, startY_region2] }); - let from_stringModified = value.from.replace('/', '_'); // / is not css valid - const to_stringModified = value.to.replace('/', '_'); // / is not css valid + let from_stringModified = value.from.replace('/', '_'); + const to_stringModified = value.to.replace('/', '_'); - if (isNumeric(from_stringModified)) { + if (!isNaN(parseInt(from_stringModified))) { from_stringModified = 'idAdd_' + from_stringModified; } dataEdgeIds.push(`${from_stringModified}_fromto_${to_stringModified}`); }); - - const groupEdges = svg.append('g').attr('class', nameEdges); - - groupEdges - .selectAll('edgesInside') - .data(dataVis) - .enter() - .append('path') - .attr('d', (d) => edgeGenerator(d)) - .attr('class', (d, i) => dataEdgeIds[i]) - /* - .attr('stroke', 'bg-secondary-600') - .attr('stroke-width', 2) - .style('stroke-opacity', 0.7) - */ - .attr('stroke', visualConfig.current.stroke) - .attr('stroke-width', visualConfig.current.strokeWidth) - .style('stroke-opacity', visualConfig.current.strokeOpacity) - .attr('fill', 'none'); + return [dataVis, dataEdgeIds]; }, [dataConnections, visualConfig, data1, data2, nameEdges]); - return <svg ref={svgRef} width={600} height={visualConfig.current.height} />; + return ( + <svg + ref={svgRef} + width={visualScatterplot.width} + height={visualConfig.current.height} + //preserveAspectRatio="xMidYMid meet" + //viewBox={`0 0 ${visualScatterplot.width} ${visualConfig.current.height}`} + > + <g className={nameEdges}> + {dataVis.map((edgeData, index) => ( + <path + key={dataEdgeIds[index]} + d={edgeGenerator(edgeData)} + className={dataEdgeIds[index]} + fill="none" + stroke={visualConfig.current.stroke} + strokeWidth={visualConfig.current.strokeWidth} + strokeOpacity={visualConfig.current.strokeOpacity} + /> + ))} + </g> + </svg> + ); }; export default EdgesLayer; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/Scatterplot.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/Scatterplot.tsx index cb1016e219a01b4df68dd7557ab09e131cec54b2..2f1b415031785b3b15854f82d8b99955257906ce 100644 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/Scatterplot.tsx +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/Scatterplot.tsx @@ -1,33 +1,51 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { select, scaleBand, axisBottom, scaleLinear, forceX, forceY, brush, forceCollide, format, axisLeft, forceSimulation } from 'd3'; +import React, { useEffect, useRef, useState, useMemo } from 'react'; +import { + select, + scaleBand, + axisBottom, + scaleLinear, + forceX, + forceY, + brush, + forceCollide, + format, + axisLeft, + forceSimulation, + Axis, + NumberValue, + Selection, + ScaleBand, + ScaleLinear, +} from 'd3'; import { VisualRegionConfig, RegionData, DataPoint, DataPointXY } from './types'; -import { calcTextWidth, calcTextWidthCanvas } from './utils'; -import { ArrowRightAlt } from '@mui/icons-material'; -import Icon from '@graphpolaris/shared/lib/components/icon'; +import { calcTextWidth } from './utils'; +import { ArrowForward } from '@mui/icons-material'; +import { Icon } from '@graphpolaris/shared/lib/components/icon'; +import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill'; +import { noDataRange } from '../utils'; -export type ScatterPlotProps = { +export type ScatterplotProps = { data: RegionData; visualConfig: VisualRegionConfig; xScaleRange: string[] | number[]; yScaleRange: string[] | number[]; - width: number; onBrushUpdate: (idElements: string[], selectedElement: string) => void; - onBrushClear: (selectedElement: string, idData: string[]) => void; - onResultJitter: (data: DataPoint[]) => void; + onBrushClear: (selectedElement: string) => void; + onResultJitter: (data: DataPoint[], idScatterplot: number) => void; }; -export type KeyedScatterplotProps = ScatterPlotProps & { +export type KeyedScatterplotProps = ScatterplotProps & { key: number; }; function computeRadiusPoints(width: number, numPoints: number): number { - const radius: number = numPoints >= 170 ? width * 0.0042 : width * 0.007; + const radius: number = numPoints >= 170 ? width * 0.0032 : width * 0.004; return radius; } -export const ScatterPlot = ({ +export const Scatterplot: React.FC<ScatterplotProps> = ({ data, visualConfig, xScaleRange, @@ -35,24 +53,41 @@ export const ScatterPlot = ({ onBrushUpdate, onBrushClear, onResultJitter, - width, -}: ScatterPlotProps) => { +}) => { const svgRef = useRef(null); const groupMarginRef = useRef<SVGGElement>(null as any); - const brushRef = useRef<SVGGElement>(null as any); - useEffect(() => { - const maxLengthAllowedAxisY: number = 85; - const maxLengthAllowedAxisX: number = 50; - const styleTextXAxisLabel = { + const [textXLabel, setTextXLabel] = useState(''); + const [textYLabel, setTextYLabel] = useState(''); + + const idScatterplot = useMemo((): number => { + return parseInt(data.name.split('_')[1], 10); + }, [data.name]); + + const configStyle = useMemo(() => { + return { + colorText: 'hsl(var(--clr-sec--800))', + colorTextUnselect: 'hsl(var(--clr-sec--400))', + colorLinesHyperEdge: 'hsl(var(--clr-black))', + }; + }, []); + + const styleTextXaxisLabel = useMemo( + () => ({ classTextXAxis: 'font-inter font-secondary font-semibold text-right capitalize text-xs', x: 1.01 * visualConfig.widthMargin + visualConfig.margin.right, y: visualConfig.heightMargin + 1.25 * visualConfig.margin.bottom, textAnchor: 'start', dominantBaseline: 'middle', maxLengthText: 90, - }; + }), + [visualConfig.widthMargin, visualConfig.margin.right, visualConfig.heightMargin, visualConfig.margin.bottom], + ); + + useEffect(() => { + const maxLengthAllowedAxisY: number = 85; + const maxLengthAllowedAxisX: number = 50; const svg = select(svgRef.current); @@ -65,35 +100,124 @@ export const ScatterPlot = ({ let tickCount = 0; let dataCircles: DataPointXY[] = []; - let dataCirclesXTemp: number[] = []; - let dataCirclesYTemp: number[] = []; - let xOffset: number; + let dataCirclesXtemp: number[] = []; + let dataCirclesYtemp: number[] = []; + let xOffset: number = 0; + let yOffset: number = 0; + + let xAxis: Axis<string> | ((selection: Selection<SVGGElement, unknown, null, undefined>) => void) | Axis<NumberValue>; + let yAxis; + + let xAxisType: string = 'none'; //'linear', 'band','none' + let yAxisType: string = 'none'; //'linear', 'band','none' + + let xScaleTemp: ScaleBand<string> | ScaleLinear<number, number>; + let yScaleTemp: ScaleBand<string> | ScaleLinear<number, number>; + + // for updating axis. + let yAxisGroup = groupMargin.select<SVGGElement>(`.${data.name}yAxis`); + if (yAxisGroup.empty()) { + yAxisGroup = groupMargin.append('g').attr('class', `${data.name}yAxis`); + } + + let xAxisGroup = groupMargin.select<SVGGElement>(`.${data.name}xAxis`); + if (xAxisGroup.empty()) { + xAxisGroup = groupMargin + .append('g') + .attr('class', `${data.name}xAxis`) + .attr('transform', `translate(0, ${visualConfig.heightMargin})`); + } + + if (!data.xAxisName && !data.yAxisName) { + //console.log('case1 ', data.xAxisName, data.yAxisName); + + dataCirclesXtemp = Array(data.xData.length).fill(visualConfig.widthMargin * 0.5); // place dots at the center of the svg + dataCirclesYtemp = Array(data.yData.length).fill(visualConfig.heightMargin * 0.5); + dataCircles = data.xData.map((value, index) => ({ x: dataCirclesXtemp[index], y: dataCirclesYtemp[index] })); + + const radius = computeRadiusPoints(visualConfig.width, data.xData.length); + + const simulation = forceSimulation<DataPointXY>(dataCircles) + .force('x', forceX<DataPointXY>((d) => d.x).strength(0.1)) + .force('y', forceY<DataPointXY>((d) => d.y).strength(4)) + .force('collide', forceCollide(radius * 1.25).strength(2)); + + const circles = groupMargin.selectAll('circle').data(dataCircles); + + circles + .enter() + .append('circle') + .attr('class', (d, i) => `${data.idData[i]}`) + .attr('cx', (d) => d.x) + .attr('cy', (d) => d.y) + .attr('r', radius) + .attr('stroke', data.colorNodesStroke) + .attr('fill', data.colorNodes); + + circles.exit().remove(); + + simulation.on('tick', function () { + groupMargin + .selectAll<SVGCircleElement, DataPointXY>('circle') + .attr('cx', (d: DataPointXY) => d.x) + .attr('cy', (d: DataPointXY) => d.y); + + tickCount++; + if (tickCount > maxComputations) { + const dataSimulation: DataPoint[] = dataCircles.map(({ x, y }, i) => ({ + x, + y, + id: data.idData[i], + })); + onResultJitter(dataSimulation, idScatterplot); + + simulation.stop(); + } + }); + + xScaleTemp = scaleBand<string>() + .domain(xScaleRange as string[]) + .range([0, visualConfig.widthMargin]) + .paddingOuter(0); + + yScaleTemp = scaleBand<string>() + .domain(yScaleRange as string[]) + .range([visualConfig.heightMargin, 0]) + .paddingOuter(0); + + xAxisType = 'none'; + yAxisType = 'none'; + yAxis = axisLeft(yScaleTemp).tickValues([]); + xAxis = axisBottom(xScaleTemp).tickValues([]); + yAxisGroup.call(yAxis); + xAxisGroup.attr('transform', 'translate(0,' + visualConfig.heightMargin + ')').call(xAxis); + } else if (!!data.xAxisName && !!data.yAxisName) { + //console.log('case2 ', data.xAxisName, data.yAxisName); - if (data.xData.length != 0 && data.yData.length != 0) { if (typeof data.xData[0] != 'number') { - let xScaleTemp = scaleBand<string>() + xScaleTemp = scaleBand<string>() .domain(xScaleRange as string[]) .range([0, visualConfig.widthMargin]) .paddingOuter(0); + xAxisType = 'band'; xOffset = 0.5 * xScaleTemp.bandwidth(); - dataCirclesXTemp = data.xData.map((value, index) => { - const scaledValue = typeof value === 'number' ? xScaleTemp(value.toString()) : xScaleTemp(value); + dataCirclesXtemp = data.xData.map((value, index) => { + const scaledValue = xScaleTemp(value); if (scaledValue !== undefined) { return scaledValue + xOffset; } else { return 0; } }); + const TextTicks = calcTextWidth(xScaleRange as string[], maxLengthAllowedAxisX, styleTextXaxisLabel.classTextXAxis); - const textTicks = calcTextWidth(xScaleRange as string[], maxLengthAllowedAxisX, styleTextXAxisLabel.classTextXAxis); - - let xAxis = axisBottom(xScaleTemp) - .tickFormat((d, i) => textTicks[i]) + xAxis = axisBottom(xScaleTemp) + .tickFormat((d, i) => TextTicks[i]) .tickSizeOuter(0); - groupMargin - .append('g') + + xAxisGroup .attr('transform', 'translate(0,' + visualConfig.heightMargin + ')') .call(xAxis) .selectAll('text') @@ -101,80 +225,95 @@ export const ScatterPlot = ({ .attr('x', '10') .attr('y', '0') .attr('dy', '0') - .style('dominant-baseline', styleTextXAxisLabel.dominantBaseline) + .style('dominant-baseline', styleTextXaxisLabel.dominantBaseline) .attr('transform', 'rotate(90)'); } else { - let xScaleTemp = scaleLinear<number>() + xScaleTemp = scaleLinear<number>() .domain(xScaleRange as number[]) .range([0, visualConfig.widthMargin]); + xAxisType = 'linear'; + dataCirclesXtemp = data.xData.map((value, index) => { + const scaledValue = xScaleTemp(value); - dataCirclesXTemp = data.xData.map((value, index) => { - const numericValue = typeof value === 'string' ? parseFloat(value) : value; - return xScaleTemp(numericValue); + if (scaledValue !== undefined) { + return scaledValue; + } else { + return 0; + } }); const [minValueX, maxValueX]: number[] = xScaleTemp.domain(); const averageMinMaxX: number = Math.round((minValueX + maxValueX) / 2.0); - const xAxis = axisBottom(xScaleTemp) + xAxis = axisBottom(xScaleTemp) .tickValues([minValueX, 0.5 * (minValueX + averageMinMaxX), averageMinMaxX, 0.5 * (maxValueX + averageMinMaxX), maxValueX]) .tickFormat(format('.2s')); - groupMargin - .append('g') - .attr('transform', 'translate(0,' + visualConfig.heightMargin + ')') - .call(xAxis); + xAxisGroup.attr('transform', 'translate(0,' + visualConfig.heightMargin + ')').call(xAxis); } if (typeof data.yData[0] != 'number') { - let yScaleTemp = scaleBand<string>() + yScaleTemp = scaleBand<string>() .domain(yScaleRange as string[]) .range([visualConfig.heightMargin, 0]) .paddingOuter(0); + yAxisType = 'band'; + yOffset = 0.5 * yScaleTemp.bandwidth(); - xOffset = 0.5 * yScaleTemp.bandwidth(); + dataCirclesYtemp = data.yData.map((value, index) => { + //const scaledValue = typeof value === 'number' ? yScaleTemp(value.toString()) : yScaleTemp(value); + const scaledValue = yScaleTemp(value); - dataCirclesYTemp = data.yData.map((value, index) => { - const scaledValue = typeof value === 'number' ? yScaleTemp(value.toString()) : yScaleTemp(value); if (scaledValue !== undefined) { - return scaledValue + xOffset; + return scaledValue + yOffset; } else { return 0; } }); - const textTicks = calcTextWidth(yScaleRange as string[], maxLengthAllowedAxisY, styleTextXAxisLabel.classTextXAxis); + const textTicks = calcTextWidth(yScaleRange as string[], maxLengthAllowedAxisY, styleTextXaxisLabel.classTextXAxis); - let yAxis = axisLeft(yScaleTemp) + yAxis = axisLeft(yScaleTemp) .tickFormat((d, i) => textTicks[i]) .tickSizeOuter(0); - groupMargin.append('g').call(yAxis).selectAll('text'); + yAxisGroup.call(yAxis).selectAll('text'); } else { - let yScaleTemp = scaleLinear<number>() + yScaleTemp = scaleLinear<number>() .domain(yScaleRange as number[]) .range([visualConfig.heightMargin, 0]); + yAxisType = 'linear'; + dataCirclesYtemp = data.yData.map((value, index) => { + const scaledValue = yScaleTemp(value); - dataCirclesYTemp = data.yData.map((value, index) => { - const numericValue = typeof value === 'string' ? parseFloat(value) : value; - return yScaleTemp(numericValue); + if (scaledValue !== undefined) { + return scaledValue; + } else { + return 0; + } }); const [minValueX, maxValueX]: number[] = yScaleTemp.domain(); const averageMinMaxX: number = Math.round((minValueX + maxValueX) / 2.0); - const yAxis = axisLeft(yScaleTemp) + yAxis = axisLeft(yScaleTemp) .tickValues([minValueX, 0.5 * (minValueX + averageMinMaxX), averageMinMaxX, 0.5 * (maxValueX + averageMinMaxX), maxValueX]) .tickFormat(format('.2s')); - groupMargin.append('g').call(yAxis); + yAxisGroup.call(yAxis); } - dataCircles = data.xData.map((value, index) => ({ x: dataCirclesXTemp[index], y: dataCirclesYTemp[index] })); - + dataCircles = data.xData.map((value, index) => ({ x: dataCirclesXtemp[index], y: dataCirclesYtemp[index] })); const radius = computeRadiusPoints(visualConfig.width, data.xData.length); - groupMargin - .selectAll('circle') - .data(dataCircles) + + const circles = groupMargin.selectAll('circle').data(dataCircles); + circles + .attr('cx', (d) => d.x) + .attr('cy', (d) => d.y) + .attr('r', radius) + .attr('stroke', data.colorNodesStroke) + .attr('fill', data.colorNodes); + + circles .enter() .append('circle') .attr('class', (d, i) => `${data.idData[i]}`) @@ -184,73 +323,74 @@ export const ScatterPlot = ({ .attr('stroke', data.colorNodesStroke) .attr('fill', data.colorNodes); + circles.exit().remove(); + const dataSimulation: DataPoint[] = dataCircles.map(({ x, y }, i) => ({ x: x, y: y, id: data.idData[i], })); - onResultJitter(dataSimulation); + onResultJitter(dataSimulation, idScatterplot); + } else if (!!data.yAxisName) { + //console.log('case3 ', data.xAxisName, data.yAxisName); - const textLabelAxis = calcTextWidth(data.xAxisName, styleTextXAxisLabel.maxLengthText, styleTextXAxisLabel.classTextXAxis); - - svg - .append('text') - .attr('x', styleTextXAxisLabel.x) - .attr('y', styleTextXAxisLabel.y) - .text(textLabelAxis[0]) - .style('text-anchor', styleTextXAxisLabel.textAnchor) - .style('dominant-baseline', styleTextXAxisLabel.dominantBaseline) - .attr('class', styleTextXAxisLabel.classTextXAxis); - } else if (data.yData.length != 0) { if (typeof data.yData[0] != 'number') { - let yScaleTemp = scaleBand<string>() + yScaleTemp = scaleBand<string>() .domain(yScaleRange as string[]) .range([visualConfig.heightMargin, 0]) .paddingOuter(0); - xOffset = 0.5 * yScaleTemp.bandwidth(); + yOffset = 0.5 * yScaleTemp.bandwidth(); + yAxisType = 'band'; + dataCirclesYtemp = data.yData.map((value, index) => { + //const scaledValue = typeof value === 'number' ? yScaleTemp(value.toString()) : yScaleTemp(value); + const scaledValue = yScaleTemp(value); - dataCirclesYTemp = data.yData.map((value, index) => { - const scaledValue = typeof value === 'number' ? yScaleTemp(value.toString()) : yScaleTemp(value); if (scaledValue !== undefined) { - return scaledValue + xOffset; + return scaledValue + yOffset; } else { return 0; } }); - const textTicks = calcTextWidth(yScaleRange as string[], maxLengthAllowedAxisY, styleTextXAxisLabel.classTextXAxis); + const textTicks = calcTextWidth(yScaleRange as string[], maxLengthAllowedAxisY, styleTextXaxisLabel.classTextXAxis); - let yAxis = axisLeft(yScaleTemp) + yAxis = axisLeft(yScaleTemp) .tickFormat((d, i) => textTicks[i]) .tickSizeOuter(0); - groupMargin.append('g').call(yAxis).selectAll('text'); + yAxisGroup.call(yAxis).selectAll('text'); } else { - let yScaleTemp = scaleLinear<number>() + yScaleTemp = scaleLinear<number>() .domain(yScaleRange as number[]) .range([visualConfig.heightMargin, 0]); + yAxisType = 'linear'; + dataCirclesYtemp = data.yData.map((value, index) => { + const scaledValue = yScaleTemp(value); - dataCirclesYTemp = data.yData.map((value, index) => { - const numericValue = typeof value === 'string' ? parseFloat(value) : value; - return yScaleTemp(numericValue); + if (scaledValue !== undefined) { + return scaledValue; + } else { + return 0; + } }); const [minValueX, maxValueX]: number[] = yScaleTemp.domain(); const averageMinMaxX: number = Math.round((minValueX + maxValueX) / 2.0); - const xAxis = axisLeft(yScaleTemp) + yAxis = axisLeft(yScaleTemp) .tickValues([minValueX, 0.5 * (minValueX + averageMinMaxX), averageMinMaxX, 0.5 * (maxValueX + averageMinMaxX), maxValueX]) .tickFormat(format('.2s')); - groupMargin.append('g').call(xAxis); + yAxisGroup.call(yAxis); } - let xScaleTemp = scaleLinear<number>() - .domain(xScaleRange as number[]) + xScaleTemp = scaleLinear<number, number>() + .domain(noDataRange as number[]) .range([0, visualConfig.widthMargin]); - dataCircles = data.yData.map((value, index) => ({ x: xScaleTemp(0.0), y: dataCirclesYTemp[index] })); + const valueXMiddle = visualConfig.widthMargin / 2; + dataCircles = data.yData.map((value, index) => ({ x: valueXMiddle, y: dataCirclesYtemp[index] })); const radius = computeRadiusPoints(visualConfig.width, data.yData.length); const simulation = forceSimulation<DataPointXY>(dataCircles) @@ -258,9 +398,14 @@ export const ScatterPlot = ({ .force('y', forceY<DataPointXY>((d) => d.y).strength(4)) .force('collide', forceCollide(radius * 1.25).strength(0.5)); - const circles = groupMargin - .selectAll('circle') - .data(dataCircles) + const circles = groupMargin.selectAll('circle').data(dataCircles); + circles + .attr('cx', (d) => d.x) + .attr('cy', (d) => d.y) + .attr('r', radius) + .attr('stroke', data.colorNodesStroke) + .attr('fill', data.colorNodes); + circles .enter() .append('circle') .attr('class', (d, i) => `${data.idData[i]}`) @@ -269,9 +414,13 @@ export const ScatterPlot = ({ .attr('r', radius) .attr('stroke', data.colorNodesStroke) .attr('fill', data.colorNodes); + circles.exit().remove(); simulation.on('tick', function () { - circles.attr('cx', (d, i) => d.x as number).attr('cy', (d) => d.y as number); + groupMargin + .selectAll<SVGCircleElement, DataPointXY>('circle') + .attr('cx', (d: DataPointXY) => d.x) + .attr('cy', (d: DataPointXY) => d.y); tickCount++; if (tickCount > maxComputations) { @@ -280,22 +429,27 @@ export const ScatterPlot = ({ y, id: data.idData[i], })); - onResultJitter(dataSimulation); + onResultJitter(dataSimulation, idScatterplot); simulation.stop(); } }); - } else if (data.xData.length != 0) { + + xAxis = axisBottom(xScaleTemp).tickValues([]); + xAxisGroup.attr('transform', 'translate(0,' + visualConfig.heightMargin + ')').call(xAxis); + } else if (!!data.xAxisName) { + //console.log('case4 ', data.xAxisName, data.yAxisName); if (typeof data.xData[0] != 'number') { - let xScaleTemp = scaleBand<string>() + xScaleTemp = scaleBand<string>() .domain(xScaleRange as string[]) .range([0, visualConfig.widthMargin]) .paddingOuter(0); xOffset = 0.5 * xScaleTemp.bandwidth(); + xAxisType = 'band'; + dataCirclesXtemp = data.xData.map((value, index) => { + const scaledValue = xScaleTemp(value); - dataCirclesXTemp = data.xData.map((value, index) => { - const scaledValue = typeof value === 'number' ? xScaleTemp(value.toString()) : xScaleTemp(value); if (scaledValue !== undefined) { return scaledValue + xOffset; } else { @@ -303,13 +457,12 @@ export const ScatterPlot = ({ } }); - const textTicks = calcTextWidth(xScaleRange as string[], maxLengthAllowedAxisX, styleTextXAxisLabel.classTextXAxis); + const textTicks = calcTextWidth(xScaleRange as string[], maxLengthAllowedAxisX, styleTextXaxisLabel.classTextXAxis); - let xAxis = axisBottom(xScaleTemp) + xAxis = axisBottom(xScaleTemp) .tickFormat((d, i) => textTicks[i]) .tickSizeOuter(0); - groupMargin - .append('g') + xAxisGroup .attr('transform', 'translate(0,' + visualConfig.heightMargin + ')') .call(xAxis) .selectAll('text') @@ -317,57 +470,68 @@ export const ScatterPlot = ({ .attr('x', '10') .attr('y', '0') .attr('dy', '0') - .style('dominant-baseline', styleTextXAxisLabel.dominantBaseline) + .style('dominant-baseline', styleTextXaxisLabel.dominantBaseline) .attr('transform', 'rotate(90)'); } else { - let xScaleTemp = scaleLinear<number>() + xScaleTemp = scaleLinear<number>() .domain(xScaleRange as number[]) .range([0, visualConfig.widthMargin]); - - dataCirclesXTemp = data.xData.map((value, index) => { - const numericValue = typeof value === 'string' ? parseFloat(value) : value; - return xScaleTemp(numericValue); + xAxisType = 'linear'; + dataCirclesXtemp = data.xData.map((value, index) => { + const scaledValue = xScaleTemp(value); + if (scaledValue !== undefined) { + return scaledValue; + } else { + return 0; + } }); const [minValueX, maxValueX]: number[] = xScaleTemp.domain(); const averageMinMaxX: number = Math.round((minValueX + maxValueX) / 2.0); - const xAxis = axisBottom(xScaleTemp) + xAxis = axisBottom(xScaleTemp) .tickValues([minValueX, 0.5 * (minValueX + averageMinMaxX), averageMinMaxX, 0.5 * (maxValueX + averageMinMaxX), maxValueX]) .tickFormat(format('.2s')); - - groupMargin - .append('g') - .attr('transform', 'translate(0,' + visualConfig.heightMargin + ')') - .call(xAxis); + xAxisGroup.attr('transform', 'translate(0,' + visualConfig.heightMargin + ')').call(xAxis); } - let yScaleTemp = scaleLinear<number>() - .domain(yScaleRange as number[]) + yScaleTemp = scaleLinear<number>() + .domain(noDataRange as number[]) .range([visualConfig.heightMargin, 0]); - dataCircles = data.xData.map((value, index) => ({ x: dataCirclesXTemp[index], y: yScaleTemp(0) })); + const valueYMiddle = visualConfig.heightMargin / 2; + dataCircles = data.xData.map((value, index) => ({ x: dataCirclesXtemp[index], y: valueYMiddle })); const radius = computeRadiusPoints(visualConfig.width, data.xData.length); + const simulation = forceSimulation<DataPointXY>(dataCircles) .force('x', forceX<DataPointXY>((d) => d.x).strength(4)) .force('y', forceY<DataPointXY>((d) => d.y).strength(0.1)) .force('collide', forceCollide(radius * 1.25).strength(0.5)); - const circles = groupMargin - .selectAll('circle') - .data(dataCircles) + const circles = groupMargin.selectAll('circle').data(dataCircles); + circles + .attr('cx', (d) => d.x) + .attr('cy', (d) => d.y) + .attr('r', radius) + .attr('stroke', data.colorNodesStroke) + .attr('fill', data.colorNodes); + circles .enter() .append('circle') .attr('class', (d, i) => `${data.idData[i]}`) - .attr('cx', (d) => d.x || 0) - .attr('cy', (d) => d.y || 0) + .attr('cx', (d) => d.x) + .attr('cy', (d) => d.y) .attr('r', radius) .attr('stroke', data.colorNodesStroke) .attr('fill', data.colorNodes); + circles.exit().remove(); simulation.on('tick', function () { - circles.attr('cx', (d, i) => d.x as number).attr('cy', (d) => d.y as number); + groupMargin + .selectAll<SVGCircleElement, DataPointXY>('circle') + .attr('cx', (d: DataPointXY) => d.x) + .attr('cy', (d: DataPointXY) => d.y); tickCount++; if (tickCount > maxComputations) { @@ -377,35 +541,24 @@ export const ScatterPlot = ({ id: data.idData[i], })); - onResultJitter(dataSimulation); + onResultJitter(dataSimulation, idScatterplot); simulation.stop(); } }); - const textLabelAxis = calcTextWidth(data.xAxisName, styleTextXAxisLabel.maxLengthText, styleTextXAxisLabel.classTextXAxis); - - svg - .append('text') - .attr('x', styleTextXAxisLabel.x) - .attr('y', styleTextXAxisLabel.y) - .text(textLabelAxis[0]) - .style('text-anchor', styleTextXAxisLabel.textAnchor) - .style('dominant-baseline', styleTextXAxisLabel.dominantBaseline) - .attr('class', styleTextXAxisLabel.classTextXAxis); + + yAxis = axisLeft(yScaleTemp).tickValues([]); + yAxisGroup.call(yAxis).selectAll('text'); } - svg - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', visualConfig.width) - .attr('height', visualConfig.height) - .attr('fill', 'none') - .attr('stroke', 'gray'); + const textLabelAxis = calcTextWidth(data.xAxisName, styleTextXaxisLabel.maxLengthText, styleTextXaxisLabel.classTextXAxis); + setTextXLabel(textLabelAxis[0]); - svg.selectAll('.domain').style('stroke', 'hsl(var(--clr-sec--400))'); - svg.selectAll('.tick line').style('stroke', 'hsl(var(--clr-sec--400))'); - svg.selectAll('.tick text').attr('class', 'font-mono').style('stroke', 'none').style('fill', 'hsl(var(--clr-sec--500))'); + const textLabelYAxis = calcTextWidth(data.yAxisName, styleTextXaxisLabel.maxLengthText, styleTextXaxisLabel.classTextXAxis); + setTextYLabel(textLabelYAxis[0]); + + // axis style + svg.selectAll('.tick text').attr('class', 'font-inter').style('stroke', 'none').style('fill', configStyle.colorText); // BRUSH LOGIC const myBrush: any = brush() @@ -416,7 +569,9 @@ export const ScatterPlot = ({ .on('brush', brushed) .on('end', function (event: any) { if (event.selection === null) { - onBrushClear(data.name, data.idData); + onBrushClear(data.name); + xAxisGroup.selectAll('.tick text').style('fill', configStyle.colorText); + yAxisGroup.selectAll('.tick text').style('fill', configStyle.colorText); } }); @@ -426,6 +581,7 @@ export const ScatterPlot = ({ if (!event.selection) { } else { const [[x0, y0], [x1, y1]] = event.selection; + dataCircles.forEach((d, i) => { if (d.x >= x0 && d.x <= x1 && d.y >= y0 && d.y <= y1) { selectedDataIds.push(data.idData[i]); @@ -434,14 +590,46 @@ export const ScatterPlot = ({ onBrushUpdate(selectedDataIds, data.name); selectedDataIds = []; + + // new stuff + + xAxisGroup.selectAll('.tick text').style('fill', configStyle.colorTextUnselect); + xAxisGroup.selectAll('.tick text').each(function (d: any) { + if (xAxisType != 'none') { + const xValueFromD = xScaleTemp(d); + + if (xValueFromD != undefined) { + select(this).style( + 'fill', + xValueFromD >= x0 - xOffset && xValueFromD <= x1 - xOffset ? configStyle.colorText : configStyle.colorTextUnselect, + ); + } + } + }); + + yAxisGroup.selectAll('.tick text').style('fill', configStyle.colorTextUnselect); + + yAxisGroup.selectAll('.tick text').each(function (d: any) { + if (yAxisType != 'none') { + const yValueFromD = yScaleTemp(d); + + if (yValueFromD != undefined) { + select(this).style( + 'fill', + yValueFromD >= y0 - yOffset && yValueFromD <= y1 - yOffset ? configStyle.colorText : configStyle.colorTextUnselect, + ); + } + } + }); } } const brushGroup = select(brushRef.current); - brushGroup.attr('transform', `translate(${visualConfig.margin.left},${visualConfig.margin.top})`).attr('class', 'brushingElem'); + brushGroup + .attr('transform', `translate(${visualConfig.margin.left},${visualConfig.margin.top})`) + .attr('class', 'brushingElem_' + data.name); brushGroup.call(myBrush); - brushGroup .select('.selection') .style('fill', data.colorBrush) @@ -449,20 +637,51 @@ export const ScatterPlot = ({ .style('stroke', data.colorBrushStroke) .style('stroke-width', 2); + brushGroup.call(myBrush.move, null); + brushGroup.selectAll('.handle').style('stroke', 'none'); - }, [data, visualConfig, xScaleRange, yScaleRange, width]); + brushGroup.selectAll('.overlay').style('stroke', 'none'); + }, [data, visualConfig, xScaleRange, yScaleRange]); return ( - <div className="w-full border border-secondary-200"> - <div className="w-full border-light font-secondary group bg-secondary-200 truncate text-left absolute"> - <div className="mx-1 w-full flex items-center"> - <span>{data.nodeName}</span> <Icon component={<ArrowRightAlt />} size={32} /> - <span>{data.attributeName}</span> <span> : </span> - <span>{data.attributeSelected}</span> + <div className="w-full"> + <div className="flex absolute mx-20 my-2 w-22 items-center gap-1"> + <EntityPill title={data.nodeName} /> + <div> + {data.attributeName && ( + <div className="flex items-center gap-1"> + <Icon component={<ArrowForward />} size={16} color="text-secondary-300" /> + <span className="text-secondary-700 text-sm m-0.5">{data.attributeName}</span> + {data.attributeSelected && ( + <div className="flex items-center gap-1"> + <Icon component={<ArrowForward />} size={16} color="text-secondary-300" /> + <span className="text-secondary-700 text-sm m-0.5">{data.attributeSelected}</span> + </div> + )} + </div> + )} </div> </div> <div className="w-full flex flex-row justify-center"> - <svg ref={svgRef} width={600} height={visualConfig.height}> + <svg ref={svgRef} width={visualConfig.width} height={visualConfig.height}> + <text + x={styleTextXaxisLabel.x} + y={styleTextXaxisLabel.y} + dominantBaseline={styleTextXaxisLabel.dominantBaseline} + className={styleTextXaxisLabel.classTextXAxis} + > + {textXLabel} + </text> + + <text + x={styleTextXaxisLabel.x * 0.05} + y={styleTextXaxisLabel.y * 0.5} + textAnchor={styleTextXaxisLabel.textAnchor} + dominantBaseline={styleTextXaxisLabel.dominantBaseline} + className={styleTextXaxisLabel.classTextXAxis} + > + {textYLabel} + </text> <g ref={groupMarginRef} /> <g ref={brushRef} /> </svg> diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/SelectionEdges.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/SelectionEdges.tsx deleted file mode 100644 index 1d387b9dd55fe228d7f8b615180919f58def6bea..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/SelectionEdges.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import Icon from '@graphpolaris/shared/lib/components/icon'; -import * as d3 from 'd3'; -import { ArrowRightAlt } from '@mui/icons-material'; - -type EdgeArrayItem = { - nameRegions: string[]; -}; - -export type SelectionEdgesProps = EdgeArrayItem[]; - -function handleCheckbox(index: number, checkboxStates: boolean[]) { - const visibility = !checkboxStates[index] ? 'block' : 'none'; - d3.selectAll(`.edge_region0_to_region${index + 1}`).style('display', visibility); -} - -const SelectionEdges: React.FC<SelectionEdgesProps> = (props) => { - const edgesArray = Array.isArray(props) ? props : (Object.values(props) as EdgeArrayItem[]); - - const [checkboxStates, setCheckboxStates] = useState<boolean[]>([]); - - useEffect(() => { - // To initialize states - setCheckboxStates((prevStates) => { - const newStates = [...prevStates]; - edgesArray.forEach((_, index) => { - if (newStates[index] === undefined) { - newStates[index] = true; - } - }); - return newStates; - }); - }, [props]); - - const handleCheckboxChange = (index: number) => { - setCheckboxStates((prevStates) => { - const newStates = [...prevStates]; - newStates[index] = !prevStates[index]; - return newStates; - }); - handleCheckbox(index, checkboxStates); - }; - return ( - <div className="border border-gray p-4 bg-white rounded flex items-center"> - <ul> - {edgesArray.map((element: EdgeArrayItem, index: number) => ( - <li key={index}> - <div className="flex items-center space-x-2"> - <input - type="checkbox" - checked={checkboxStates[index]} - className="checkbox checkbox-sm" - onChange={() => handleCheckboxChange(index)} - ></input> - <div> - <span>{element.nameRegions[0]}</span> - <Icon component={<ArrowRightAlt />} size={32} /> - <span>{element.nameRegions[1]}</span> - </div> - </div> - </li> - ))} - </ul> - </div> - ); -}; - -export default SelectionEdges; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/types.ts b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/types.ts index 07c8ecc37e9ea4a720861b6c07eb47cb1cb80fe6..10a4ebb740501e714f14b55b0e05741ba98524a2 100644 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/types.ts +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/types.ts @@ -5,6 +5,12 @@ export interface NodesGraphology { attributes: object[]; } +export interface DataFromPanel { + id: number; + settingsOpen: boolean; + data: DataPanelConfig; +} + export interface EdgesGraphology { key: string; attributes: object[]; @@ -18,12 +24,12 @@ export interface GraphData { export interface UserSelection { name: string; - nodeName: string; - attributeAsRegion: string; - attributeAsRegionSelection: string; + nodeName?: string; + attributeAsRegion?: string; + attributeAsRegionSelection?: string; placement: { - xAxis: string; - yAxis: string; + xAxis?: string; + yAxis?: string; colorNodes: string; colorNodesStroke: string; colorFillBrush: string; @@ -42,19 +48,19 @@ export interface DataPointXY { y: number; } -export interface ConnectionFromTo { +export interface connectionFromTo { to: string; from: string; } -export interface EdgeVisibility { +export interface edgeVisibility { _id: string; to: boolean; from: boolean; visibility: boolean; } -export interface IdConnectionsObjects { +export interface idConnectionsObjects { from: IdConnections; to: IdConnections; } @@ -86,22 +92,21 @@ export interface AugmentedEdgeAttributes { attributes: NodeAttributes; from: string; to: string; - _id: string; + id: string; label: string; } -export interface DataConfig { - entityVertical: string; - attributeEntity: string; - attributeValueSelected: string; - orderNameXAxis: string; - orderNameYAxis: string; - isButtonEnabled: boolean; +export interface DataPanelConfig { + entitySelected?: string; + attributeSelected?: string; + attributeValueSelected?: string; + xAxisSelected?: string; + yAxisSelected?: string; } export interface Edge { from: string; - _id: string; + id: string; attributes: object[]; label: string; _key: string; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/utils.ts b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/utils.ts index 639634acac330f2e4c51d7f421497fa5d27c9fe1..88ea1a75e45357e5d803066403a7b705207a52df 100644 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/utils.ts +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/utils.ts @@ -1,6 +1,7 @@ -import { UserSelection, RegionData, AugmentedNodeAttributes, ConnectionFromTo, IdConnections, EdgeVisibility } from './types'; -import * as d3 from 'd3'; +import { UserSelection, RegionData, AugmentedNodeAttributes, connectionFromTo, IdConnections, edgeVisibility } from './types'; +import { ScaleBand, ScaleLinear, extent } from 'd3'; import { RefObject } from 'react'; +import { visualizationColors } from 'config'; import Graph, { MultiGraph } from 'graphology'; @@ -9,7 +10,8 @@ export function findConnectionsNodes( originIDs: string[], graphStructure: MultiGraph, labelNode: string, -): [ConnectionFromTo[], string[]] { + invert: boolean = false, +): [connectionFromTo[], string[]] { const neighborMap: IdConnections = {}; originIDs.forEach((nodeId) => { @@ -30,8 +32,8 @@ export function findConnectionsNodes( neighborMap[nodeId] = Array.from(tempSet); }); - const edgeStrings = wrapperForEdge(neighborMap); - const edgeStrings2 = wrapperForEdgeString(neighborMap); + const edgeStrings = wrapperForEdge(neighborMap, invert); + const edgeStrings2 = wrapperForEdgeString(neighborMap, invert); return [edgeStrings, edgeStrings2]; } @@ -61,15 +63,26 @@ export function getRegionData(nodes: AugmentedNodeAttributes[], regionUserSelect // then regionUserSelection.attributeAsRegionSelection will be a string of the elements joined by "-" // that is why item.attributes[regionUserSelection.attributeAsRegion] is join with ("-") - let filteredData: AugmentedNodeAttributes[] = nodes.filter((item: AugmentedNodeAttributes) => { - return ( - item.label === regionUserSelection.nodeName && - item.attributes && - (Array.isArray(item.attributes[regionUserSelection.attributeAsRegion]) - ? (item.attributes[regionUserSelection.attributeAsRegion] as string[]).join('-') === regionUserSelection.attributeAsRegionSelection - : item.attributes[regionUserSelection.attributeAsRegion] === regionUserSelection.attributeAsRegionSelection) - ); - }); + let filteredData: AugmentedNodeAttributes[] = []; + + if (!regionUserSelection.attributeAsRegion) { + filteredData = nodes.filter((item: AugmentedNodeAttributes) => { + return item.label === regionUserSelection.nodeName; + }); + } else { + filteredData = nodes.filter((item: AugmentedNodeAttributes) => { + return ( + item.label === regionUserSelection.nodeName && + item.attributes && + regionUserSelection.attributeAsRegion && + (Array.isArray(item.attributes[regionUserSelection.attributeAsRegion]) + ? (item.attributes[regionUserSelection.attributeAsRegion] as string[]).join('-') === + regionUserSelection.attributeAsRegionSelection + : item.attributes[regionUserSelection.attributeAsRegion] === regionUserSelection.attributeAsRegionSelection) + ); + }); + } + if (filteredData.length === 0) filteredData = []; const idData: string[] = filteredData.map((item) => item._id); @@ -98,11 +111,11 @@ export function getRegionData(nodes: AugmentedNodeAttributes[], regionUserSelect colorNodesStroke: regionUserSelection.placement.colorNodesStroke, colorBrush: regionUserSelection.placement.colorFillBrush, colorBrushStroke: regionUserSelection.placement.colorStrokeBrush, - nodeName: regionUserSelection.nodeName, - attributeName: regionUserSelection.attributeAsRegion, - attributeSelected: regionUserSelection.attributeAsRegionSelection, - xAxisName: regionUserSelection.placement.xAxis, - yAxisName: regionUserSelection.placement.yAxis, + nodeName: regionUserSelection.nodeName as string, + attributeName: regionUserSelection.attributeAsRegion as string, + attributeSelected: regionUserSelection.attributeAsRegionSelection as string, + xAxisName: regionUserSelection.placement.xAxis as string, + yAxisName: regionUserSelection.placement.yAxis as string, label: filteredData?.[0]?.label || nodes[0].label, }; @@ -110,7 +123,7 @@ export function getRegionData(nodes: AugmentedNodeAttributes[], regionUserSelect } export function setExtension(margin: number, data: number[]): [number, number] { - const extentData: [number, number] = d3.extent(data) as [number, number]; + const extentData: [number, number] = extent(data) as [number, number]; if (extentData[0] >= 0.0 && extentData[1] > 0.0) { return [extentData[0] * (1.0 - margin), extentData[1] * (1.0 + margin)]; @@ -129,30 +142,38 @@ export function findSimilarElements(array1: string[], array2: string[]): string[ return array1.filter((element) => array2.includes(element)); } -export function wrapperForEdge(data: IdConnections): ConnectionFromTo[] { +export function wrapperForEdge(data: IdConnections, invert: boolean = false): connectionFromTo[] { const keysData = Object.keys(data); - const resultsDas: ConnectionFromTo[] = []; + const resultsDas: connectionFromTo[] = []; const results = keysData.forEach((item) => { const r = data[item].forEach((itemConnected) => { - resultsDas.push({ from: item, to: itemConnected }); + if (!invert) { + resultsDas.push({ from: item, to: itemConnected }); + } else { + resultsDas.push({ from: itemConnected, to: item }); + } }); }); return resultsDas; } -export function wrapperForEdgeString(data: IdConnections): string[] { +export function wrapperForEdgeString(data: IdConnections, invert: boolean = false): string[] { const keysData = Object.keys(data); const resultsDas: string[] = []; const results = keysData.forEach((item) => { const r = data[item].forEach((itemConnected) => { - resultsDas.push(item + '_fromto_' + itemConnected); + if (!invert) { + resultsDas.push(item + '_fromto_' + itemConnected); + } else { + resultsDas.push(itemConnected + '_fromto_' + item); + } }); }); return resultsDas; } -export function wrapperEdgeVisibility(data: ConnectionFromTo[]): EdgeVisibility[] { - let transformedArray: EdgeVisibility[] = data.map(function (item) { +export function wrapperEdgeVisibility(data: connectionFromTo[]): edgeVisibility[] { + let transformedArray: edgeVisibility[] = data.map(function (item) { const from_stringModified = item.from.replace('/', '_'); const to_stringModified = item.to.replace('/', '_'); const elementString = `${from_stringModified}_fromto_${to_stringModified}`; @@ -271,3 +292,9 @@ export function calcTextWidthCanvas(rowLabels: string[], maxLengthAllowed: numbe return rowLabels; } + +export function nodeColorHex(num: number) { + //let entityColors = Object.values(visualizationColors.GPSeq.colors[9]); + const col = visualizationColors.GPCat.colors[14][num % visualizationColors.GPCat.colors[14].length]; + return col; +} diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/SemSubsConfigPanel.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/SemSubsConfigPanel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a6df7b0f335c83a01c7618dd114a9677e256220c --- /dev/null +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/SemSubsConfigPanel.tsx @@ -0,0 +1,243 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { DeleteOutline, ArrowDropDown, SubdirectoryArrowRight, ArrowRight } from '@mui/icons-material'; +import { Button } from '@graphpolaris/shared/lib/components/buttons'; +import { Icon } from '@graphpolaris/shared/lib/components/icon'; +import { DataFromPanel, DataPanelConfig } from '../components/types'; +import { GraphMetadata } from '@graphpolaris/shared/lib/data-access/statistics'; + +import { EntityPillSelector } from '@graphpolaris/shared/lib/components/selectors/entityPillSelector'; +import { Input } from '@graphpolaris/shared/lib/components'; + +export type SemSubsConfigPanelProps = { + dataFromPanel: DataFromPanel; + graphMetaData: GraphMetadata; + colorNode: string; + onUpdateData: (data: Partial<DataPanelConfig>) => void; + onCollapse: (isOpen: boolean) => void; + onDelete: () => void; + isFirstPanel: boolean; +}; + +export const SemSubsConfigPanel: React.FC<SemSubsConfigPanelProps> = ({ + dataFromPanel, + graphMetaData, + colorNode, + isFirstPanel, + onCollapse, + onUpdateData, + onDelete, +}) => { + const [stateConfigPanelOptions, setStateConfigPanelOptions] = useState<{ + entitySelectedOptions: string[]; + attributeSelectedOptions: (undefined | string)[]; + attributeValueSelectedOptions: any[]; + xAxisSelectedOptions: (undefined | string)[]; + yAxisSelectedOptions: (undefined | string)[]; + }>({ + entitySelectedOptions: [], + attributeSelectedOptions: [], + attributeValueSelectedOptions: [], + xAxisSelectedOptions: [], + yAxisSelectedOptions: [], + }); + + const [allowToSelectEntity, setAllowToSelectEntity] = useState(false); + const data = dataFromPanel.data; + const [isNotCollapsed, setIsNotCollapsed] = useState(true); + + const handleButtonCollapseSubsratedPanel = () => { + //onCollapse(!dataFromPanel.settingsOpen); + //setAllowToSelectEntity(!allowToSelectEntity); + setIsNotCollapsed(!isNotCollapsed); + }; + + // data processing + + useEffect(() => { + if (graphMetaData) { + setStateConfigPanelOptions((prevState) => ({ + ...prevState, + entitySelectedOptions: graphMetaData.nodes.labels, + })); + } + }, [graphMetaData]); + + useEffect(() => { + if (!data.entitySelected) return; + if (!graphMetaData.nodes.types[data.entitySelected]) return; + + onCollapse(true); + + // field values of nummerical data are empty. Need to have access to raw data. For now just use categorical + const categoricalKeys: (undefined | string)[] = []; + + categoricalKeys.push(undefined); + + for (const key in graphMetaData.nodes.types[data.entitySelected].attributes) { + const values = graphMetaData.nodes.types[data.entitySelected].attributes[key].values; + if (values && values.length > 0 && values[0].toString() !== '[object Object]') { + if ( + graphMetaData.nodes.types[data.entitySelected].attributes.hasOwnProperty(key) && + graphMetaData.nodes.types[data.entitySelected].attributes[key].dimension === 'categorical' + ) { + categoricalKeys.push(key as string); + } + } + } + + setStateConfigPanelOptions((prevState) => ({ + ...prevState, + attributeSelectedOptions: categoricalKeys, + })); + + onUpdateData({ + attributeSelected: undefined, + attributeValueSelected: undefined, + xAxisSelected: undefined, + yAxisSelected: undefined, + }); + }, [data.entitySelected]); + + useEffect(() => { + if (!data.entitySelected || !data.attributeSelected) return; + + if (data.attributeSelected && graphMetaData.nodes.types[data.entitySelected].attributes[data.attributeSelected]) { + let arrayAttributeValues = graphMetaData.nodes.types[data.entitySelected].attributes[data.attributeSelected].values; + if (arrayAttributeValues !== undefined) { + if (arrayAttributeValues[0].toString() != '[object Object]') { + setStateConfigPanelOptions((prevState) => ({ + ...prevState, + attributeValueSelectedOptions: arrayAttributeValues, + })); + + onUpdateData({ + attributeValueSelected: undefined, + }); + } + } + } + }, [data.attributeSelected]); + + useEffect(() => { + if (!!data.entitySelected && graphMetaData.nodes.types[data.entitySelected]) { + const attributes = Object.keys(graphMetaData.nodes.types[data.entitySelected].attributes); + + const attributesForAxis: (string | undefined)[] = []; + + attributesForAxis.push(undefined); + attributesForAxis.push(...attributes); + + setStateConfigPanelOptions((prevState) => ({ + ...prevState, + xAxisSelectedOptions: attributesForAxis, + yAxisSelectedOptions: attributesForAxis, + })); + } + }, [data.entitySelected]); + + useEffect(() => { + if (!data.attributeSelected) { + onUpdateData({ attributeValueSelected: undefined }); + } + }, [data.attributeSelected]); + + return ( + <div className="flex flex-col w-full"> + <div className="flex my-2 items-center"> + <Button + onClick={handleButtonCollapseSubsratedPanel} + variantType="secondary" + variant="ghost" + size="xs" + iconComponent={dataFromPanel.settingsOpen ? <ArrowDropDown /> : <ArrowRight />} + /> + + <EntityPillSelector + selectedNode={data.entitySelected} + dropdownNodes={stateConfigPanelOptions.entitySelectedOptions} + onSelectOption={function (option: string): void { + onUpdateData({ entitySelected: option }); + }} + /> + + <div className="flex justify-center items-center ml-auto"> + <div className="grow-0 shrink-0 w-6 h-6 flex justify-center items-center"> + <div className={`h-4 w-4 border border-secondary-150 rounded-sm`} style={{ backgroundColor: colorNode }}></div> + {/* TODO: change this to color selector */} + </div> + <Button + variantType="secondary" + variant="ghost" + size="xs" + disabled={isFirstPanel} + iconComponent={<DeleteOutline />} + onClick={onDelete} + /> + </div> + </div> + {isNotCollapsed && ( + <div className="ml-4 gap-2 flex flex-col"> + <div className="flex justify-between items-center gap-1"> + <Icon component={<SubdirectoryArrowRight />} size={16} color="text-secondary-300" /> + <Input + size="xs" + type="dropdown" + label="X-axis" + value={data.xAxisSelected || 'None'} + disabled={!data.entitySelected} + options={stateConfigPanelOptions.xAxisSelectedOptions.map((option) => option || 'None')} + onChange={(option: string | number) => { + onUpdateData({ xAxisSelected: String(option) }); + }} + /> + </div> + + <div className="flex justify-between items-center gap-1"> + <Icon component={<SubdirectoryArrowRight />} size={16} color="text-secondary-300" /> + <Input + size="xs" + type="dropdown" + inline + label="Y-axis" + disabled={!data.entitySelected} + value={data.yAxisSelected || 'None'} + options={stateConfigPanelOptions.yAxisSelectedOptions.map((option) => option || 'None')} + onChange={(option: string | number) => { + onUpdateData({ yAxisSelected: String(option) }); + }} + /> + </div> + <div className="flex justify-between items-center gap-1"> + <Icon component={<SubdirectoryArrowRight />} size={16} color="text-secondary-300" /> + <Input + size="xs" + type="dropdown" + label="Attribute" + disabled={!data.entitySelected} + value={data.attributeSelected || 'None'} + options={stateConfigPanelOptions.attributeSelectedOptions.map((option) => option || 'None')} + onChange={(option: string | number) => { + onUpdateData({ attributeSelected: String(option) }); + }} + /> + </div> + + <div className="flex justify-between items-center ml-3 gap-1"> + <Icon component={<SubdirectoryArrowRight />} size={16} color="text-secondary-300" /> + <Input + size="xs" + type="dropdown" + disabled={!data.attributeSelected} + label="Value" + value={data.attributeValueSelected || 'None'} + options={stateConfigPanelOptions.attributeValueSelectedOptions.map((option) => option || 'None')} + onChange={(option: string | number) => { + onUpdateData({ attributeValueSelected: String(option) }); + }} + /> + </div> + </div> + )} + </div> + ); +}; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/index.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bcc2b4d70d280536724213642dd2e7088efeb5fc --- /dev/null +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/index.tsx @@ -0,0 +1 @@ +export { SemSubsConfigPanel } from './SemSubsConfigPanel'; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/semSubsConfigPanel.stories.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/semSubsConfigPanel.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dbc8e86aa6ed36ad43e56299a38ac5509c3af4bf --- /dev/null +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/semSubsConfigPanel.stories.tsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import SemSubsConfigPanel, { SemSubsConfigPanelProps } from '.'; + +const metaPillDropdown: Meta<typeof SemSubsConfigPanel> = { + component: SemSubsConfigPanel, + title: 'Visualizations/SemanticSubstrates/configpanel', + decorators: [(story) => <div className="flex items-center justify-center w-20 m-11">{story()}</div>], +}; + +export default metaPillDropdown; + +type Story = StoryObj<typeof SemSubsConfigPanel>; + +export const userAddsData: Story = { + args: { + entitySelectedOptions: ['kamerleden', 'commissies'], + attributeSelectedOptions: ['Partij', 'Leeftijd', 'Woonplaats'], + valueAttributeSelectedOptions: ['D66', 'VVD', 'PVV', 'DierenPartij'], + xAxisSelectedOptions: ['Partij', 'Leeftijd', 'Woonplaats'], + yAxisSelectedOptions: ['Partij', 'Leeftijd', 'Woonplaats'], + colorNode: '#FFA500', + }, +}; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx index 40e03c4eea3553a5c2ef8483ca2110a098f13b14..b542d9847b675d76396d09f2b9f5840571bf81e8 100644 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx @@ -1,118 +1,183 @@ import React, { useRef, useState, useMemo, useEffect } from 'react'; -import { ScatterPlot, KeyedScatterplotProps as KeyedScatterPlotProps } from './components/Scatterplot'; -import { GraphMetadata } from '@graphpolaris/shared/lib/data-access/statistics'; +import { Scatterplot, KeyedScatterplotProps } from './components/Scatterplot'; import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; - -import { visualizationColors } from 'config/src/colors'; +import { Button } from '@graphpolaris/shared/lib/components/buttons'; +import { Add } from '@mui/icons-material'; +import { select, selectAll } from 'd3'; import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common'; import { findConnectionsNodes, getRegionData, setExtension, filterArray, getUniqueValues } from './components/utils'; - +import { cloneDeep, isEqual } from 'lodash-es'; import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; import { UserSelection, RegionData, - DataConfig, + DataPanelConfig, AugmentedNodeAttributes, VisualEdgesConfig, - ConnectionFromTo, + connectionFromTo, AugmentedEdgeAttributes, DataPoint, + DataFromPanel, + VisualRegionConfig, } from './components/types'; -import ConfigPanel from './components/ConfigPanel'; -import EdgesLayer, { KeyedEdgesLayerProps } from './components/EdgesLayer'; +import EdgesLayer, { KeyedEdgesLayerProps } from './components/EdgesLayer'; import { MultiGraph } from 'graphology'; -import { select, selectAll } from 'd3'; - -import { buildGraphology, configVisualRegion, config, numColorsCategorical, marginAxis, isColorCircleFix } from './utils'; -import { Input } from '../../..'; +import { buildGraphology, config, numColorsCategorical, marginAxis, isColorCircleFix, noDataRange, noSelection } from './utils'; +import { SemSubsConfigPanel } from './configPanel'; +import { nodeColorHex } from './components/utils'; export type SemSubstrProps = { showColor: boolean; + dataPanels: DataFromPanel[]; }; const settings: SemSubstrProps = { showColor: true, + dataPanels: [], }; const displayName = 'SemSubstrVis'; -export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstrProps>) => { - const nodes = data.nodes; - const edges = data.edges; - +export const VisSemanticSubstrates = ({ data, graphMetadata, settings, updateSettings }: VisualizationPropTypes<SemSubstrProps>) => { + // for sizing the vis const divRef = useRef<HTMLDivElement>(null); - const idEdges = useRef<string[][]>([]); - const nameEdges = useRef<string[]>([]); - const idBrushed = useRef<string[][]>([]); - const [divSize, setDivSize] = useState({ width: 0, height: 0 }); + + // avoid mount iteration + const isMounted = useRef(false); + + // to render scatterplots const [appState, setAppState] = useState({ - isButtonClicked: false, - scatterPlots: [] as KeyedScatterPlotProps[], + scatterplots: [] as KeyedScatterplotProps[], dataRegions: [] as RegionData[], }); + // to render edges layer const [stateEdges, setStateEdges] = useState({ edgePlotted: [] as KeyedEdgesLayerProps[], }); - const [arrayConnections, setarrayConnections] = useState<ConnectionFromTo[][]>([]); + + // for connecting with config panel + const idScatterplotsFromConfig = useRef<number[]>([]); + const prevDataPanelsRef = useRef<DataFromPanel[]>([]); + + // for preserve brush information + //const idBrushed = useRef<string[][]>([]); + const idBrushed = useRef<{ idScatterplot: number; data: string[] }[]>([]); + + // keep track what is the new scatterplot + const newScatterplotIs = useRef<string>(''); + + // To know the correspondence between scatterplot IDs and edge IDs + const IDScatterplot2IDEdge = useRef<number[]>([]); + const indexNewEdge = useRef<number>(0); + const IDEdgeUpdating = useRef<number>(0); + const IDScatterplotR0 = useRef<number>(0); + const arrayIDScatterplotR0 = useRef<string[]>([]); + + const IDScatterplotUpdating = useRef<number>(0); + // information about edges, useful for brushing + const informationEdges = useRef<{ nameEdges: string[]; idEdges: string[][] }>({ + nameEdges: [], + idEdges: [], + }); + + // information about edges + const arrayConnections = useRef<connectionFromTo[][]>([]); + + // to syncronized the data from scatterplots simulation and create the edges const [computedData, setComputedData] = useState({ - region1: [] as DataPoint[], - region2: [] as DataPoint[], + region1: [] as DataPoint[], // always R0 + region2: [] as DataPoint[], // last scatterplot added }); + // data structure to handle node/edge information const augmentedNodes: AugmentedNodeAttributes[] = useMemo(() => { - return nodes.map((node: Node) => ({ + return data.nodes.map((node: Node) => ({ _id: node._id, attributes: node.attributes, label: node.label, })); - }, [nodes]); + }, [data]); const augmentedEdges: AugmentedEdgeAttributes[] = useMemo(() => { - return edges.map((edge: any) => ({ - _id: edge.id, + return data.edges.map((edge: any) => ({ + id: edge.id, to: edge.to, from: edge.from, attributes: edge.attributes, label: edge.label, })); - }, [edges]); - - const attributesArray = nodes.map((node) => node.attributes); + }, [data]); const graphologyGraph: MultiGraph = useMemo( () => buildGraphology({ nodes: Object.values(augmentedNodes), edges: Object.values(augmentedEdges) }), [augmentedNodes, augmentedEdges], ); - const pushElement = (newElement: ConnectionFromTo[]) => { - setarrayConnections((prevArray) => [...prevArray, newElement]); - }; + const configVisualRegion = useMemo<VisualRegionConfig>(() => { + const baseConfig = { + marginPercentage: { top: 0.14, right: 0.15, bottom: 0.2, left: 0.15 }, + width: divSize.width, + widthPercentage: 0.4, + height: 300, + }; + + const margin = { + top: baseConfig.marginPercentage.top * baseConfig.height, + right: baseConfig.marginPercentage.right * baseConfig.width, + bottom: baseConfig.marginPercentage.bottom * baseConfig.height, + left: baseConfig.marginPercentage.left * baseConfig.width, + }; + + const widthMargin = baseConfig.width - margin.right - margin.left; + const heightMargin = baseConfig.height - margin.top - margin.bottom; + + return { + ...baseConfig, + margin, + widthMargin, + heightMargin, + }; + }, [divSize]); const configVisualEdges = useRef<VisualEdgesConfig>({ - width: configVisualRegion.width, - height: 2 * configVisualRegion.height, + width: divSize.width, + height: configVisualRegion.height, configRegion: configVisualRegion, offsetY: 0, - stroke: 'TBD', + stroke: config.edges.stroke, strokeWidth: config.edges.strokeWidth, strokeOpacity: config.edges.strokeOpacity, }); const handleBrushClear = useMemo(() => { - return (selectedElement: string, idData: string[]): void => { - let modifiedString: number = +selectedElement.replace('region', ''); - idBrushed.current[modifiedString] = []; - - idEdges.current.forEach((edgesR0RN, index) => { - if (edgesR0RN.length != 0) { - if (idBrushed.current[0].length == 0 && idBrushed.current[index + 1].length == 0) { - select(`.edge_region0_to_region${index + 1}`) + return (selectedElement: string): void => { + let modifiedString: number = +selectedElement.replace('region_', ''); + //idBrushed.current[modifiedString] = []; + + const indexBrushedR0 = idBrushed.current.findIndex((obj) => obj.idScatterplot === modifiedString); + idBrushed.current[indexBrushedR0].data = []; + + informationEdges.current.idEdges.forEach((edgesR0RN, index) => { + if (edgesR0RN.length !== 0) { + //if (idBrushed.current[0]?.length === 0 && idBrushed.current[index + 1]?.length === 0) { + if ( + idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length === 0 && + idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data.length === 0 + ) { + select(`.edge_region0_to_region_${index + 1}`) .selectAll('path') .style('stroke-opacity', 1); } else { - const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_'); + //const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_'); + + const edgesVisible = filterArray( + idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [], + idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data ?? [], + edgesR0RN, + '_fromto_', + ); const edgesVisibleSlash = edgesVisible.map((element) => element.replace(/\//g, '_')); @@ -125,7 +190,7 @@ export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstr }); const edgesVisibleSlashDot: string = edgesVisibleSlashNotNum.map((edgeClass) => `.${edgeClass}`).join(','); - select(`.edge_region0_to_region${index + 1}`) + select(`.edge_region0_to_region_${index + 1}`) .selectAll('path') .style('stroke-opacity', 0); selectAll(edgesVisibleSlashDot).style('stroke-opacity', 1); @@ -137,18 +202,33 @@ export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstr const handleBrushUpdate = useMemo(() => { return (idElements: string[], selectedElement: string): void => { - let modifiedString: number = +selectedElement.replace('region', ''); - idBrushed.current[modifiedString] = idElements; + let modifiedString: number = +selectedElement.replace('region_', ''); + + //idBrushed.current[modifiedString] = idElements; + const indexBrushedR0 = idBrushed.current.findIndex((obj) => obj.idScatterplot === modifiedString); + idBrushed.current[indexBrushedR0].data = idElements; // iterate over the region pairs: r0-r1, r0-r2, and update visibility of edges based on registered brushed ids - idEdges.current.forEach((edgesR0RN, index) => { + informationEdges.current.idEdges.forEach((edgesR0RN, index) => { if (modifiedString == 0) { - if (idBrushed.current[0].length == 0 && idBrushed.current[index + 1].length == 0) { - select(`.edge_region0_to_region${index + 1}`) + //if (idBrushed.current[0].length == 0 && idBrushed.current[index + 1].length == 0) { + + if ( + idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length === 0 && + idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data.length === 0 + ) { + select(`.edge_region0_to_region_${index + 1}`) .selectAll('path') .style('stroke-opacity', 0); } else { - const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_'); + //const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_'); + + const edgesVisible = filterArray( + idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [], + idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data ?? [], + edgesR0RN, + '_fromto_', + ); if (edgesVisible.length != 0) { const edgesVisibleSlash = edgesVisible.map((element) => element.replace(/\//g, '_')); @@ -161,25 +241,34 @@ export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstr }); const edgesVisibleSlashDot: string = edgesVisibleSlashNotNum.map((edgeClass) => `.${edgeClass}`).join(','); - - select(`.edge_region0_to_region${index + 1}`) + select(`.edge_region0_to_region_${index + 1}`) .selectAll('path') .style('stroke-opacity', 0); selectAll(edgesVisibleSlashDot).style('stroke-opacity', 1); } else { - select(`.edge_region0_to_region${index + 1}`) + select(`.edge_region0_to_region_${index + 1}`) .selectAll('path') .style('stroke-opacity', 0); } } } else if (modifiedString == index + 1) { - if (idBrushed.current[0].length == 0 && idBrushed.current[index + 1].length == 0) { - select(`.edge_region0_to_region${index + 1}`) + //if (idBrushed.current[0].length == 0 && idBrushed.current[index + 1].length == 0) { + + if ( + idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length === 0 && + idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data.length === 0 + ) { + select(`.edge_region0_to_region_${index + 1}`) .selectAll('path') .style('stroke-opacity', 0); } else { - const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_'); - + //const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_'); + const edgesVisible = filterArray( + idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [], + idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data ?? [], + edgesR0RN, + '_fromto_', + ); if (edgesVisible.length != 0) { const edgesVisibleSlash = edgesVisible.map((element) => element.replace(/\//g, '_')); const edgesVisibleSlashNotNum = edgesVisibleSlash.map((item) => { @@ -192,12 +281,12 @@ export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstr const edgesVisibleSlashDot: string = edgesVisibleSlashNotNum.map((edgeClass) => `.${edgeClass}`).join(','); - select(`.edge_region0_to_region${index + 1}`) + select(`.edge_region0_to_region_${index + 1}`) .selectAll('path') .style('stroke-opacity', 0); selectAll(edgesVisibleSlashDot).style('stroke-opacity', 1); } else { - select(`.edge_region0_to_region${index + 1}`) + select(`.edge_region0_to_region_${index + 1}`) .selectAll('path') .style('stroke-opacity', 0); } @@ -207,67 +296,391 @@ export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstr }; }, []); - const handleResultJitter = (data: DataPoint[]) => { - setComputedData((prevData) => { - if (prevData.region1.length === 0) { - return { - ...prevData, - region1: data, - }; - } else { - return { - ...prevData, - region2: data, - }; - } - }); + const handleResultJitter = (data: DataPoint[], idScatterplot: number) => { + //console.log('DONE JITTER ', idScatterplot); + if (idScatterplot == IDScatterplotR0.current) { + setComputedData((prevData) => ({ + ...prevData, + region1: data, + })); + } else { + setComputedData((prevData) => ({ + ...prevData, + region2: data, + })); + } }; useEffect(() => { - if (divRef.current) { - setDivSize({ width: divRef.current.getBoundingClientRect().width, height: divRef.current.getBoundingClientRect().height }); + function handleResize() { + if (divRef.current) { + setDivSize({ width: divRef.current.getBoundingClientRect().width, height: divRef.current.getBoundingClientRect().height }); + } + } + + window.addEventListener('resize', handleResize); + if (divRef.current) new ResizeObserver(handleResize).observe(divRef.current); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + useEffect(() => { + if (idBrushed.current.length != 0) { + if (idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length != 0) { + handleBrushUpdate(idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [], 'region_0'); + } } - }, [divRef]); + }, [stateEdges]); + // manages the addition of edges elements useEffect(() => { + //console.log('computedData', computedData.region1.length > 0 && computedData.region2.length > 0, computedData, newScatterplotIs.current); if (computedData.region1.length > 0 && computedData.region2.length > 0) { - let colorEdges: string; + if (newScatterplotIs.current === 'add' || !stateEdges.edgePlotted[IDEdgeUpdating.current]) { + // !FIXME !stateEdges.edgePlotted[IDEdgeUpdating.current] was added to solve bug, but now only last scatter shows edges + + //console.log('ADD EDGES '); + const temporalConfigVisualEdges: VisualEdgesConfig = { + ...configVisualEdges.current, + height: configVisualRegion.height * appState.scatterplots.length, + offsetY: configVisualRegion.height * (appState.scatterplots.length - 2), + }; + + const visualConfigRef = { current: temporalConfigVisualEdges } as React.MutableRefObject<VisualEdgesConfig>; + + const newEdgePlot: KeyedEdgesLayerProps = { + key: appState.scatterplots.length.toString(), + dataConnections: arrayConnections.current[indexNewEdge.current - 1], + visualConfig: visualConfigRef, + visualScatterplot: configVisualRegion, + data1: computedData.region1, + data2: computedData.region2, + nameEdges: informationEdges.current.nameEdges[indexNewEdge.current - 1], + }; + + //console.log('newEdgePlot', newEdgePlot); + setStateEdges({ + edgePlotted: [...stateEdges.edgePlotted, newEdgePlot], + }); + + // update edges + } else if (IDScatterplotUpdating.current === IDScatterplotR0.current) { + //console.log('UPDATE EDGES of R0 '); + const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted); + + const modifiedStateEdges = stateEdgesTemporal.map((obj, index) => { + const data1 = (stateEdgesTemporal[index].data1 = computedData.region1); + const dataConnections = arrayConnections.current[index]; + + const temporalConfigVisualEdges: VisualEdgesConfig = { + ...configVisualEdges.current, + height: configVisualRegion.height * (index + 2), + offsetY: configVisualRegion.height * index, + }; + + const visualConfigRef = { current: temporalConfigVisualEdges } as React.MutableRefObject<VisualEdgesConfig>; + + return { + ...obj, + visualConfig: visualConfigRef, + data1, + dataConnections, + }; + }); + + setStateEdges({ + edgePlotted: modifiedStateEdges, + }); + } else if (stateEdges.edgePlotted[IDEdgeUpdating.current]) { + //console.log('UPDATE EDGES NO R0'); + + //console.log('informationEdges.current.nameEdges ', informationEdges.current.nameEdges); + + const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted); + + //console.log('IDEdgeUpdating', IDEdgeUpdating.current, stateEdges.edgePlotted, stateEdgesTemporal[IDEdgeUpdating.current]); + stateEdgesTemporal[IDEdgeUpdating.current].data1 = computedData.region1; // necessary ? + stateEdgesTemporal[IDEdgeUpdating.current].data2 = computedData.region2; + stateEdgesTemporal[IDEdgeUpdating.current].dataConnections = arrayConnections.current[IDEdgeUpdating.current]; + setStateEdges({ + edgePlotted: stateEdgesTemporal, + }); + } else { + console.error('Error: Edge not found', IDScatterplotUpdating.current, stateEdges.edgePlotted); + } + } + }, [computedData.region2, computedData.region1]); + + function createNewScatterplot(newPanel: DataFromPanel) { + //console.log('createNewScatterplot:', newPanel, 'IDScatterplotR0: ', IDScatterplotR0.current); + if (idScatterplotsFromConfig.current.length === 0 && !IDScatterplotR0.current) { + IDScatterplotR0.current = newPanel.id; + //console.log('newPanel ', newPanel); + } - if (isColorCircleFix) { - colorEdges = config.edges.stroke; + idScatterplotsFromConfig.current.push(newPanel.id); + newScatterplotIs.current = 'add'; + return handleNewDataRegion(newPanel.data, newPanel.id); + } + + function removeScatterplot(deletedPanel?: DataFromPanel) { + if (deletedPanel) { + // checks if deleted one is R0 + if (deletedPanel.id === IDScatterplotR0.current) { + // !FIXME: dont allow delete of R0 + /* + setComputedData((prevState) => ({ + ...prevState, + region1: [], + })); + */ } else { - colorEdges = config.edges.stroke; + // checks if deleted one is RX + + // Delete scatterplot and associated variables + idScatterplotsFromConfig.current = idScatterplotsFromConfig.current.filter((item) => item !== deletedPanel.id); + setAppState((prevState) => ({ + scatterplots: prevState.scatterplots.filter((scatterplot) => scatterplot.key !== deletedPanel.id), + dataRegions: prevState.dataRegions.filter((_, index) => prevState.scatterplots[index].key !== deletedPanel.id), + })); + + idBrushed.current = idBrushed.current.filter((item) => item.idScatterplot !== deletedPanel.id); + + // Delete edges + const removeIndexEdge = informationEdges.current.nameEdges.findIndex( + (name) => name === `edge_region0_to_region_${deletedPanel.id}`, + ); + const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted); + const stateEdgesTemporalFiltered = stateEdgesTemporal.filter((_, index) => index !== removeIndexEdge); + + if (removeIndexEdge != -1) { + // check if it need it + IDScatterplot2IDEdge.current = IDScatterplot2IDEdge.current.filter((_, index) => index !== removeIndexEdge); + + informationEdges.current.idEdges = informationEdges.current.idEdges.filter((_, index) => index !== removeIndexEdge); + informationEdges.current.nameEdges = informationEdges.current.nameEdges.filter((_, index) => index !== removeIndexEdge); + arrayConnections.current = arrayConnections.current.filter((_, index) => index !== removeIndexEdge); + + const modifiedStateEdges = stateEdgesTemporalFiltered.map((obj, index) => { + const temporalConfigVisualEdges: VisualEdgesConfig = { + ...configVisualEdges.current, + height: configVisualRegion.height * (index + 2), + offsetY: configVisualRegion.height * index, + }; + + const visualConfigRef = { current: temporalConfigVisualEdges } as React.MutableRefObject<VisualEdgesConfig>; + + return { + ...obj, + visualConfig: visualConfigRef, + }; + }); + + setStateEdges({ + edgePlotted: modifiedStateEdges, + }); + } + + indexNewEdge.current--; + } + } + } + + function updateScatterplot(updatedPanel?: DataFromPanel) { + if (updatedPanel) { + //console.log('updatedPanel', updatedPanel, appState.scatterplots); + + const idScatterplotUpdate = appState.scatterplots.findIndex((item) => item.key === updatedPanel.id); + newScatterplotIs.current = 'update'; + if (idScatterplotUpdate !== -1) { + //handleBrushClear(`region_${indexOnState}`); + + if (!updatedPanel.data.entitySelected) return; + //console.log('UPDATE: appState ', idScatterplotUpdate); + + const regionUserSelection: UserSelection = { + name: appState.dataRegions[idScatterplotUpdate].name, + nodeName: updatedPanel.data.entitySelected, + attributeAsRegion: updatedPanel.data.attributeSelected, + attributeAsRegionSelection: updatedPanel.data.attributeValueSelected, + placement: { + xAxis: updatedPanel.data.xAxisSelected, + yAxis: updatedPanel.data.yAxisSelected, + colorNodes: appState.dataRegions[idScatterplotUpdate].colorNodes, + colorNodesStroke: appState.dataRegions[idScatterplotUpdate].colorNodesStroke, + colorFillBrush: config.brush.fillClr, + colorStrokeBrush: config.brush.strokeClr, + }, + }; + + const regionDataUser: RegionData = getRegionData(augmentedNodes, regionUserSelection); + + let xScaleShared: any; + let yScaleShared: any; - if (appState.scatterPlots.length < numColorsCategorical) { - colorEdges = visualizationColors.GPCat.colors[14][appState.scatterPlots.length + 1]; + if (!regionDataUser.xAxisName && !regionDataUser.yAxisName) { + xScaleShared = noDataRange; + yScaleShared = noDataRange; + } else if (regionDataUser.xAxisName && regionDataUser.yAxisName) { + if (typeof regionDataUser.xData[0] != 'number') { + const xExtent = getUniqueValues(regionDataUser.xData); + xScaleShared = xExtent; + } else { + const xExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]); + xScaleShared = xExtent; + } + + if (typeof regionDataUser.yData[0] != 'number') { + const yExtent = getUniqueValues(regionDataUser.yData); + yScaleShared = yExtent; + } else { + const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]); + yScaleShared = yExtent; + } + } else if (regionDataUser.xAxisName) { + if (typeof regionDataUser.xData[0] != 'number') { + const xExtent = getUniqueValues(regionDataUser.xData); + xScaleShared = xExtent; + + yScaleShared = noDataRange; + } else { + const xExtent: string[] | number[] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]); + xScaleShared = xExtent; + + yScaleShared = noDataRange; + } + } else if (regionDataUser.yAxisName) { + if (typeof regionDataUser.yData[0] != 'number') { + const yExtent = getUniqueValues(regionDataUser.yData); + yScaleShared = yExtent; + + xScaleShared = noDataRange; + } else { + const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]); + + yScaleShared = yExtent; + + xScaleShared = noDataRange; + } + } + + const updatedScatterplot = { + key: appState.scatterplots[idScatterplotUpdate].key, + data: regionDataUser, + visualConfig: configVisualRegion, + xScaleRange: xScaleShared, + yScaleRange: yScaleShared, + onBrushUpdate: handleBrushUpdate, + onResultJitter: handleResultJitter, + onBrushClear: handleBrushClear, + }; + + // modify scatterplot + setAppState((prevState) => { + const scatterplots = [...prevState.scatterplots]; + scatterplots[idScatterplotUpdate] = updatedScatterplot; + return { ...prevState, scatterplots }; + }); + + // modify edge layer + const arrayIDs = regionDataUser.idData.map((item) => item); + IDScatterplotUpdating.current = idScatterplotUpdate; + //console.log('IDScatterplotUpdating', IDScatterplotUpdating.current, IDScatterplotR0.current, idScatterplotUpdate); + if (idScatterplotUpdate === IDScatterplotR0.current) { + const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted); + + stateEdgesTemporal.forEach((obj, index) => { + const IDsConnectedScatterplot = obj.data2.map((value) => value.id); + const [connectedD, connectedD2] = findConnectionsNodes( + arrayIDs, + IDsConnectedScatterplot, + graphologyGraph, + regionDataUser.label, + true, + ); + + informationEdges.current.idEdges[index] = connectedD2; + arrayConnections.current[index] = connectedD; + }); } else { - colorEdges = visualizationColors.GPCat.colors[14][appState.scatterPlots.length + 1 - numColorsCategorical]; + // get indices to modify + const IDEdgeBasedOnIDscatterplot = IDScatterplot2IDEdge.current[idScatterplotUpdate - 1]; + IDEdgeUpdating.current = IDEdgeBasedOnIDscatterplot; + + const indexEdgeInvolved = informationEdges.current.nameEdges.findIndex( + (name) => name === `edge_region0_to_region_${idScatterplotUpdate}`, + ); + //console.log('edge involved: ', IDEdgeBasedOnIDscatterplot, indexEdgeInvolved); + // modify data structures + const [connectedD, connectedD2] = findConnectionsNodes( + arrayIDs, + appState.dataRegions[IDScatterplotR0.current].idData, + graphologyGraph, + regionDataUser.label, + ); + + informationEdges.current.idEdges[IDEdgeBasedOnIDscatterplot] = connectedD2; + arrayConnections.current[IDEdgeBasedOnIDscatterplot] = connectedD; } + + // + } else { + console.log('Error: Scatterplot not found', idScatterplotUpdate, updatedPanel.id, appState.scatterplots); } + } + } - configVisualEdges.current = { - ...configVisualEdges.current, - stroke: colorEdges, - }; + // manages when the settingsPanel changes + useEffect(() => { + if (isMounted.current && configVisualRegion.width > 0 && !isEqual(settings.dataPanels, prevDataPanelsRef.current)) { + const prevDataPanels = prevDataPanelsRef.current; + const currentDataPanels = settings.dataPanels; + + if (currentDataPanels.length > prevDataPanels.length) { + // Element added + //console.log('ADD SCATTERPLOT'); + const newPanel = currentDataPanels.filter((panel: DataFromPanel) => !prevDataPanels.some((prevPanel) => prevPanel.id === panel.id)); + const newDataRegions = []; + const newScatterplots = []; + for (const panel of newPanel) { + const { dataRegions, scatterplot } = createNewScatterplot(panel); + newDataRegions.push(dataRegions); + newScatterplots.push(scatterplot); + } + setAppState({ + scatterplots: [...appState.scatterplots, ...newScatterplots], + dataRegions: [...appState.dataRegions, ...newDataRegions], + }); + } else if (currentDataPanels.length < prevDataPanels.length) { + // Element deleted + const deletedPanel = prevDataPanels.find( + (prevPanel) => !currentDataPanels.some((panel: DataFromPanel) => panel.id === prevPanel.id), + ); - const newEdgePlot: KeyedEdgesLayerProps = { - key: appState.scatterPlots.length.toString(), - dataConnections: arrayConnections[appState.scatterPlots.length - 2], - visualConfig: configVisualEdges, - data1: computedData.region1, - data2: computedData.region2, - nameEdges: nameEdges.current[appState.dataRegions.length - 2], - width: 0, - nameRegions: [appState.dataRegions[0].attributeSelected, appState.dataRegions[appState.dataRegions.length - 1].attributeSelected], - }; + //console.log('DELETE SCATTERPLOT: ', deletedPanel); - setStateEdges({ - edgePlotted: [...stateEdges.edgePlotted, newEdgePlot], - }); + removeScatterplot(deletedPanel); + } else { + // Element updated + const updatedPanel = currentDataPanels.find((panel: DataFromPanel) => { + const prevPanel = prevDataPanels.find((prevPanel) => prevPanel.id === panel.id); + return prevPanel && !isEqual(prevPanel.data, panel.data); + }); + //console.log('UPDATE SCATTERPLOT: ', updatedPanel); + if (updatedPanel !== undefined) { + updateScatterplot(updatedPanel); + } + } + prevDataPanelsRef.current = currentDataPanels; + } else { + isMounted.current = true; } - }, [computedData.region2]); + }, [settings.dataPanels, configVisualRegion]); + //}, [settings.dataPanels]); - const handleUpdateData = (data: DataConfig) => { + const handleNewDataRegion = (data: DataPanelConfig, idNew: number) => { let colorCircle: string; let strokeCircle: string; @@ -275,23 +688,23 @@ export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstr colorCircle = config.circle.fillClr; strokeCircle = config.circle.strokeClr; } else { - if (appState.scatterPlots.length < numColorsCategorical) { - colorCircle = visualizationColors.GPCat.colors[14][appState.scatterPlots.length + 1]; + if (idNew < numColorsCategorical) { + colorCircle = nodeColorHex(idNew + 1); } else { - colorCircle = visualizationColors.GPCat.colors[14][appState.scatterPlots.length + 1 - numColorsCategorical]; + colorCircle = nodeColorHex(idNew + 1 - numColorsCategorical); } strokeCircle = config.circle.strokeClr; } const regionUserSelection: UserSelection = { - name: `region${appState.scatterPlots.length}`, - nodeName: data.entityVertical, - attributeAsRegion: data.attributeEntity, + name: `region_${idNew}`, + nodeName: data.entitySelected, + attributeAsRegion: data.attributeSelected, attributeAsRegionSelection: data.attributeValueSelected, placement: { - xAxis: data.orderNameXAxis, - yAxis: data.orderNameYAxis, + xAxis: data.xAxisSelected, + yAxis: data.yAxisSelected, colorNodes: colorCircle, colorNodesStroke: strokeCircle, colorFillBrush: config.brush.fillClr, @@ -304,7 +717,10 @@ export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstr let xScaleShared: any; let yScaleShared: any; - if (regionDataUser.xData.length != 0 && regionDataUser.yData.length != 0) { + if (!regionDataUser.xAxisName && !regionDataUser.yAxisName) { + xScaleShared = noDataRange; + yScaleShared = noDataRange; + } else if (regionDataUser.xAxisName && regionDataUser.yAxisName) { if (typeof regionDataUser.xData[0] != 'number') { const xExtent = getUniqueValues(regionDataUser.xData); xScaleShared = xExtent; @@ -312,7 +728,6 @@ export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstr const xExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]); xScaleShared = xExtent; } - if (typeof regionDataUser.yData[0] != 'number') { const yExtent = getUniqueValues(regionDataUser.yData); yScaleShared = yExtent; @@ -320,100 +735,92 @@ export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstr const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]); yScaleShared = yExtent; } - } else if (regionDataUser.xData.length != 0) { + } else if (regionDataUser.xAxisName) { if (typeof regionDataUser.xData[0] != 'number') { const xExtent = getUniqueValues(regionDataUser.xData); xScaleShared = xExtent; - - yScaleShared = [-1, 1]; + yScaleShared = noDataRange; } else { const xExtent: string[] | number[] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]); xScaleShared = xExtent; - - yScaleShared = [-1, 1]; + yScaleShared = noDataRange; } - } else if (regionDataUser.yData.length != 0) { + } else if (regionDataUser.yAxisName) { if (typeof regionDataUser.yData[0] != 'number') { const yExtent = getUniqueValues(regionDataUser.yData); yScaleShared = yExtent; - - xScaleShared = [-1, 1]; + xScaleShared = noDataRange; } else { const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]); - yScaleShared = yExtent; - - xScaleShared = [-1, 1]; + xScaleShared = noDataRange; } } const arrayIDs = regionDataUser.idData.map((item) => item); - const newScatterPlot: KeyedScatterPlotProps = { - key: appState.scatterPlots.length, + + // update scatterplot state + + const newScatterplot: KeyedScatterplotProps = { + key: idNew, data: regionDataUser, visualConfig: configVisualRegion, xScaleRange: xScaleShared, yScaleRange: yScaleShared, - width: 0, onBrushUpdate: handleBrushUpdate, onResultJitter: handleResultJitter, onBrushClear: handleBrushClear, }; - setAppState({ - ...appState, - scatterPlots: [...appState.scatterPlots, newScatterPlot], - dataRegions: [...appState.dataRegions, regionDataUser], - }); - - idBrushed.current[appState.scatterPlots.length] = []; + idBrushed.current.push({ idScatterplot: idNew, data: [] }); - if (appState.scatterPlots.length >= 2) { - configVisualEdges.current = { - ...configVisualEdges.current, - height: configVisualEdges.current.height + configVisualRegion.height * (appState.scatterPlots.length - 1), - offsetY: configVisualRegion.height * (appState.scatterPlots.length - 1), - }; - } - - if (appState.scatterPlots.length == 0) { + if (IDScatterplotR0.current === idNew) { + arrayIDScatterplotR0.current = arrayIDs; } else { if (graphologyGraph) { + if (IDScatterplot2IDEdge.current) { + IDScatterplot2IDEdge.current[idNew - 1] = indexNewEdge.current; + } const [connectedD, connectedD2] = findConnectionsNodes( arrayIDs, - appState.dataRegions[0].idData, + arrayIDScatterplotR0.current, graphologyGraph, regionDataUser.label, ); - idEdges.current.push(connectedD2); - - pushElement(connectedD); - - nameEdges.current.push(`edge_region0_to_${regionUserSelection.name}`); + informationEdges.current.nameEdges.push(`edge_region0_to_${regionUserSelection.name}`); + //console.log('informationEdges ', informationEdges.current.nameEdges); + informationEdges.current.idEdges.push(connectedD2); + arrayConnections.current.push(connectedD); + indexNewEdge.current++; } } + + return { + scatterplot: newScatterplot, + dataRegions: regionDataUser, + }; }; return ( - <div className="w-full font-inter overflow-x-hidden overflow-y-hidden"> - <div className="w-full flex flex-row justify-center"> - <ConfigPanel data={augmentedNodes} onUpdateData={handleUpdateData} /> - </div> + <div className="w-full font-inter overflow-x-hidden "> <div className="w-full relative" ref={divRef}> - {divSize.width > 0 && ( + {configVisualRegion.width > 0 && appState.scatterplots.some((s) => s.visualConfig.width > 0) && ( <> <div className="w-full regionContainer z-0"> - {appState.scatterPlots.map((scatterPlot) => ( - <ScatterPlot {...scatterPlot} width={divSize.width} /> + {appState.scatterplots.map((scatterplot) => ( + <Scatterplot {...scatterplot} /> ))} </div> <div className="pointer-events-none absolute top-0 w-full flex flex-row justify-center"> - {stateEdges.edgePlotted.map((edgePlot, index) => ( - <div key={index} className="absolute"> - <EdgesLayer {...edgePlot} width={divSize.width} /> - </div> - ))} + {stateEdges.edgePlotted.map( + (edegePlot, index) => + edegePlot.dataConnections && ( + <div key={index} className="absolute"> + <EdgesLayer {...edegePlot} /> + </div> + ), + )} </div> </> )} @@ -422,10 +829,101 @@ export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstr ); }; -const SemSubstrSettings = ({ settings, updateSettings }: VisualizationSettingsPropTypes<SemSubstrProps>) => { +const SemSubstrSettings = ({ settings, updateSettings, graphMetadata }: VisualizationSettingsPropTypes<SemSubstrProps>) => { + useEffect(() => { + // setup default scatterplots + + if (settings.dataPanels.length === 0 && graphMetadata.nodes.labels.length > 0) { + const panels = graphMetadata.nodes.labels.map((nodeName, index) => { + return { + id: index, + data: { + entitySelected: nodeName, + attributeSelected: undefined, + attributeValueSelected: undefined, + xAxisSelected: undefined, + yAxisSelected: undefined, + }, + settingsOpen: true, + }; + }); + + updateDataPanels([...settings.dataPanels, ...panels]); + } + }, [graphMetadata]); + + const updateDataPanels = (updatedData: DataFromPanel[]) => { + const newConfiguration = { + ...settings, + dataPanels: updatedData, + }; + + if (!isEqual(settings, newConfiguration)) { + updateSettings(newConfiguration); + } else { + } + }; + + const addPanel = (data?: DataPanelConfig) => { + const nextId = settings.dataPanels.length === 0 ? 0 : settings.dataPanels[settings.dataPanels.length - 1].id + 1; + + if (!data) { + const firstNodeName = graphMetadata.nodes.labels?.[0]; + data = { + entitySelected: firstNodeName, + attributeSelected: undefined, + attributeValueSelected: undefined, + xAxisSelected: undefined, + yAxisSelected: undefined, + }; + } + + const newPanelData = { + id: nextId, + data: data, + settingsOpen: true, + }; + updateDataPanels([...settings.dataPanels, newPanelData]); + }; + + const getDataFromPanel = (data: DataPanelConfig, id: number) => { + const updatedData = settings.dataPanels.map((panel) => (panel.id === id ? { ...panel, data } : panel)); + updateDataPanels(updatedData); + return updatedData; + }; + + const handleDeletePanel = (id: number) => { + const updatedPanelData = settings.dataPanels.filter((panel) => panel.id !== id); + updateDataPanels(updatedPanelData); + }; + return ( <SettingsContainer> - <Input type="boolean" label="Show color" value={settings.showColor} onChange={(val) => updateSettings({ showColor: val })} /> + <div className=""> + <div className="flex justify-between items-center px-3 py-1"> + <span className="text-xs font-semibold">Substrates</span> + <Button variantType="secondary" variant="ghost" size="sm" iconComponent={<Add />} onClick={() => addPanel()} /> + </div> + {settings.dataPanels.map((panel, index) => ( + <SemSubsConfigPanel + dataFromPanel={panel} + key={panel.id} + colorNode={nodeColorHex(panel.id + 1)} + onCollapse={(isOpen: boolean) => { + const updatedData = settings.dataPanels.map((panelData) => + panelData.id === panel.id ? { ...panelData, settingsOpen: isOpen } : panelData, + ); + //updateDataPanels(updatedData); + }} + graphMetaData={graphMetadata} + onUpdateData={(data) => { + getDataFromPanel({ ...panel.data, ...data }, panel.id); + }} + isFirstPanel={index === 0} + onDelete={() => handleDeletePanel(panel.id)} + /> + ))} + </div> </SettingsContainer> ); }; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/utils.ts b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/utils.ts index 3c224437d292546b0384e53eeb46135d08835c93..f622beab65176c707f8c5dd047ec20b5e7f6a28c 100644 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/utils.ts +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/utils.ts @@ -1,7 +1,10 @@ import { visualizationColors } from 'config'; import { MultiGraph } from 'graphology'; -import { AugmentedEdgeAttributes, GraphData, VisualRegionConfig } from './components/types'; -import { GraphQueryResult, Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; +import { GraphData } from './components/types'; +import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; + +export const noDataRange = [-1, 1]; +export const noSelection = 'None'; const buildGraphology = (data: GraphData): MultiGraph => { const graph = new MultiGraph(); @@ -34,7 +37,8 @@ const buildGraphology = (data: GraphData): MultiGraph => { }; const numColorsCategorical = visualizationColors.GPCat.colors[14].length; -export const isColorCircleFix = true; +export const isColorCircleFix = false; + let config: any = {}; if (isColorCircleFix) { config = { @@ -43,7 +47,7 @@ if (isColorCircleFix) { strokeClr: 'hsl(var(--clr-sec--600))', }, edges: { - stroke: 'bg-primary-600', + stroke: 'bg-primary-800', strokeWidth: '2', strokeOpacity: '0.7', }, @@ -60,7 +64,7 @@ if (isColorCircleFix) { strokeClr: 'hsl(var(--clr-sec--500))', }, edges: { - stroke: 'NO_USED', + stroke: 'hsl(var(--clr-sec--300))', strokeWidth: '2', strokeOpacity: '0.7', }, @@ -73,24 +77,4 @@ if (isColorCircleFix) { const marginAxis = 0.2; -const configVisualRegion: VisualRegionConfig = { - marginPercentage: { top: 0.14, right: 0.15, bottom: 0.2, left: 0.15 }, - margin: { top: 0.0, right: 0.0, bottom: 0.0, left: 0.0 }, - width: 600, - widthPercentage: 0.4, - height: 300, - widthMargin: 0.0, - heightMargin: 0.0, -}; - -configVisualRegion.margin = { - top: configVisualRegion.marginPercentage.top * configVisualRegion.height, - right: configVisualRegion.marginPercentage.right * configVisualRegion.width, - bottom: configVisualRegion.marginPercentage.bottom * configVisualRegion.height, - left: configVisualRegion.marginPercentage.left * configVisualRegion.width, -}; - -configVisualRegion.widthMargin = configVisualRegion.width - configVisualRegion.margin.right - configVisualRegion.margin.left; -configVisualRegion.heightMargin = configVisualRegion.height - configVisualRegion.margin.top - configVisualRegion.margin.bottom; - -export { buildGraphology, numColorsCategorical, config, configVisualRegion, marginAxis }; +export { buildGraphology, numColorsCategorical, config, marginAxis };