Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/frontend-v2
  • rijkheere/frontend-v-2-reordering-paoh
2 results
Show changes
Showing
with 1487 additions and 783 deletions
......@@ -68,11 +68,12 @@ export function hslStringToHex(hsl: string) {
*/
export const getRelatedLinks = (graph: GraphType, nodes: NodeType[], jaccardThreshold: number): LinkType[] => {
const relatedLinks: LinkType[] = [];
graph.links.forEach((link: LinkType) => {
Object.keys(graph.links).forEach((id) => {
const link = graph.links[id];
const { source, target } = link;
if (isLinkVisible(link, jaccardThreshold)) {
nodes.forEach((node: NodeType) => {
if (source == node || target == node || source == node._id || target == node._id) {
if (source == node._id || target == node._id || source == node._id || target == node._id) {
relatedLinks.push(link);
}
});
......
import React, { useEffect, useRef, useState } from 'react';
import { GraphType, LinkType, NodeType } from './types';
import { GraphType, LinkType, NodeType, NodeTypeD3 } from './types';
import { NLPixi } from './components/NLPixi';
import { parseQueryResult } from './components/query2NL';
import { useImmer } from 'use-immer';
......@@ -66,7 +66,7 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel
}
}, [data, ml]);
const onClickedNode = (event?: { node: NodeType; pos: IPointData }, ml?: ML) => {
const onClickedNode = (event?: { node: NodeTypeD3; pos: IPointData }, ml?: ML) => {
if (graph) {
if (!event?.node) {
handleSelect();
......@@ -74,11 +74,12 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel
}
const node = event.node;
handleSelect({ nodes: [node as Node] });
const nodeMeta = graph.nodes[node._id];
handleSelect({ nodes: [nodeMeta as Node] });
if (ml && ml.shortestPath.enabled) {
setGraph((draft) => {
let _node = draft?.nodes.find((n) => n._id === node._id);
let _node = draft?.nodes[node._id];
if (!_node) return draft;
if (!ml.shortestPath.srcNode) {
......@@ -105,6 +106,7 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel
}
};
if (!graph) return null;
return (
<NLPixi
graph={graph}
......@@ -177,7 +179,7 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza
type="dropdown"
label="Shape"
value={settings.shapes.shape}
options={[{circle: 'Circle'}, {rectangle: 'Square'}]}
options={[{ circle: 'Circle' }, { rectangle: 'Square' }]}
onChange={(val) => updateSettings({ shapes: { ...settings.shapes, shape: val as any } })}
/>
) : (
......
......@@ -9,16 +9,21 @@ import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResul
/** Types for the nodes and links in the node-link diagram. */
export type GraphType = {
nodes: NodeType[];
links: LinkType[];
nodes: Record<string, NodeType>; // _id -> node
links: Record<string, LinkType>; // _id -> link
// linkPrediction?: boolean;
// shortestPath?: boolean;
// communityDetection?: boolean;
// numberOfMlClusters?: number;
};
export type GraphTypeD3 = {
nodes: NodeTypeD3[];
links: LinkTypeD3[];
};
/** The interface for a node in the node-link diagram */
export interface NodeType extends d3.SimulationNodeDatum, Node {
export interface NodeType extends Node {
_id: string;
// Number to determine the color of the node
......@@ -41,28 +46,14 @@ export interface NodeType extends d3.SimulationNodeDatum, Node {
// The text that will be shown on top of the node if selected.
displayInfo?: string;
// Node’s current x-position.
x?: number;
// Node’s current y-position.
y?: number;
// Node’s current x-velocity
vx?: number;
// Node’s current y-velocity
vy?: number;
// Node’s fixed x-position (if position was fixed)
fx?: number | null;
// Node’s fixed y-position (if position was fixed)
fy?: number | null;
defaultX?: number;
defaultY?: number;
}
export type NodeTypeD3 = d3.SimulationNodeDatum & { _id: string };
/** The interface for a link in the node-link diagram */
export interface LinkType extends d3.SimulationLinkDatum<NodeType> {
export interface LinkType {
// The thickness of a line
id: string;
value: number;
......@@ -70,14 +61,18 @@ export interface LinkType extends d3.SimulationLinkDatum<NodeType> {
mlEdge: boolean;
color: number;
alpha?: number;
source: string;
target: string;
}
export type LinkTypeD3 = d3.SimulationLinkDatum<NodeTypeD3> & { _id: string };
/**collectionNode holds 1 entry per node kind (so for example a MockNode with name "parties" and all associated attributes,) */
export type TypeNode = {
name: string; //Collection name
attributes: string[]; //attributes. This includes all attributes found in the collection
type: number | undefined; //number that represents collection of node, for colorscheme
visualisations: Visualization[]; //The way to visualize attributes of this Node kind
visualizations: Visualization[]; //The way to visualize attributes of this Node kind
};
export type CommunityDetectionNode = {
......
......@@ -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 && (
......
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.
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;
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;
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;
......@@ -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;
......
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;
}
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>
);
};
export { SemSubsConfigPanel } from './SemSubsConfigPanel';
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',
},
};
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 };