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
Commits on Source (11)
Showing
with 306 additions and 212 deletions
......@@ -6,7 +6,6 @@ import {
useQuerybuilderGraph,
useQuerybuilderSettings,
useSessionCache,
useQuerybuilderUnionTypes,
} from '@graphpolaris/shared/lib/data-access';
import { addError, setCurrentTheme } from '@graphpolaris/shared/lib/data-access/store/configSlice';
import { resetGraphQueryResults, queryingBackend } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
......@@ -39,7 +38,6 @@ export function App(props: App) {
const dispatch = useAppDispatch();
const queryBuilderSettings = useQuerybuilderSettings();
const [monitoringOpen, setMonitoringOpen] = useState<boolean>(false);
const unionTypes = useQuerybuilderUnionTypes();
const runQuery = () => {
if (session?.currentSaveState && query) {
......@@ -47,7 +45,7 @@ export function App(props: App) {
dispatch(resetGraphQueryResults());
} else {
dispatch(queryingBackend());
wsQueryRequest(Query2BackendQuery(session.currentSaveState, query, queryBuilderSettings, ml, unionTypes));
wsQueryRequest(Query2BackendQuery(session.currentSaveState, query, queryBuilderSettings, ml, queryBuilderSettings.unionTypes));
}
}
};
......
......@@ -64,9 +64,9 @@ export const TableUI = <T extends Record<string, any>>({ data, fieldConfigs, dro
return (
<div className="mt-2 w-full overflow-x-auto">
<table className="min-w-full bg-white border border-gray-300 rounded-md">
<table className="min-w-full bg-light border border-secondary-300 rounded-md">
<thead>
<tr className="bg-gray-100 border-b">
<tr className="bg-secondary-100 border-b">
{fieldConfigs.map((field) => (
<th key={field.key.toString()} className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
{field.label}
......@@ -79,7 +79,7 @@ export const TableUI = <T extends Record<string, any>>({ data, fieldConfigs, dro
{data.map((item, index) => (
<tr key={index} className="border-b rounded-md">
{fieldConfigs.map((field) => (
<td key={field.key.toString()} className="px-6 py-4 text-sm text-gray-700">
<td key={field.key.toString()} className="px-6 py-4 text-sm text-light-700">
{editingIndex === index ? (
field.type === 'dropdown' ? (
<Input
......@@ -103,7 +103,7 @@ export const TableUI = <T extends Record<string, any>>({ data, fieldConfigs, dro
)}
</td>
))}
<td className="px-6 py-4 text-sm text-gray-700">
<td className="px-6 py-4 text-sm text-secondary-700">
<div className="flex space-x-2">
{editingIndex === index ? (
<>
......
......@@ -14,14 +14,12 @@ import {
wsSchemaSubscription,
useQuerybuilderAttributesShown,
wsSchemaStatsRequest,
useQuerybuilderUnionTypes,
} from '@graphpolaris/shared/lib/data-access';
import { Broker, wsQuerySubscription, wsQueryTranslationSubscription } from '@graphpolaris/shared/lib/data-access/broker';
import { addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice';
import { allMLTypes, LinkPredictionInstance, setMLResult } from '@graphpolaris/shared/lib/data-access/store/mlSlice';
import {
QueryBuilderText,
queryUnionTypes,
setQueryText,
setQuerybuilderNodes,
} from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
......@@ -70,7 +68,6 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
const mlHash = useMLEnabledHash();
const visState = useVisualization();
const queryBuilderSettings = useQuerybuilderSettings();
const unionTypes = useQuerybuilderUnionTypes();
function loadSaveState(saveStateID: string | undefined, saveStates: Record<string, SaveStateI>) {
if (saveStateID && saveStates && saveStateID in saveStates) {
......@@ -291,7 +288,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
// body: { type: 'query_builder_state', status: '', value: queryBuilder },
// });
}
}, [queryHash, mlHash, queryBuilderSettings, unionTypes]);
}, [queryHash, mlHash, queryBuilderSettings, queryBuilderSettings.unionTypes]);
return <div className="hide"></div>;
};
import { useAppDispatch, useAuthCache } from '../store';
import { authenticated, changeRoom, UserAuthenticationHeader } from '../store/authSlice';
import { addInfo, addError } from '../store/configSlice';
const domain = import.meta.env.BACKEND_URL;
const userURI = import.meta.env.BACKEND_USER;
......@@ -16,15 +17,15 @@ export const useAuthentication = () => {
const handleError = (err: any) => {
console.error(err);
dispatch(addError('Failed to copy: ' + (err.message || 'Unknown error occurred')));
};
const login = () => {
fetch(`${domain}${userURI}/headers`, fetchSettings)
.then((res) => {
.then((res) =>
res
.json()
.then((res: UserAuthenticationHeader) => {
console.log(res, 'headers');
dispatch(
authenticated({
username: res.username,
......@@ -36,24 +37,47 @@ export const useAuthentication = () => {
}),
);
})
.catch(handleError);
})
.catch(handleError);
};
const newShareRoom = () => {
fetch(`${domain}${userURI}/share`, { ...fetchSettings, method: 'POST' })
.then((res) =>
res
.json()
.then((res: { Roomid: string; Sessionid: string }) => {
// TODO: send to backend current state and make redux accordingly
dispatch(changeRoom(res.Roomid));
})
.catch(handleError),
)
.catch(handleError);
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Failed to copy:', err);
return false;
}
};
const newShareRoom = async () => {
try {
// TODO: Implement share room functionality when backend is ready
// fetch(`${domain}${userURI}/share`, { ...fetchSettings, method: 'POST' })
// .then((res) =>
// res
// .json()
// .then((res: { Roomid: string; Sessionid: string }) => {
// dispatch(changeRoom(res.Roomid));
// })
// .catch(handleError),
// )
// .catch(handleError);
const shareUrl = window.location.href;
const copied = await copyToClipboard(shareUrl);
if (copied) {
dispatch(addInfo('Link copied to clipboard!'));
} else {
throw new Error('Failed to copy to clipboard');
}
} catch (error: any) {
handleError(error);
}
};
return { login, newShareRoom };
};
};
\ No newline at end of file
......@@ -20,7 +20,6 @@ import {
queryBuilderState,
selectQuerybuilderGraph,
selectQuerybuilderHash,
queryUnionTypes,
QueryUnionType
} from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
import { activeSaveState, activeSaveStateAuthorization, SessionCacheI, sessionCacheState } from './sessionSlice';
......@@ -69,7 +68,6 @@ export const useQuerybuilderHash: () => string = () => useAppSelector(selectQuer
export const useQuerybuilderSettings: () => QueryBuilderSettings = () => useAppSelector(queryBuilderSettingsState);
export const useQuerybuilder: () => QueryBuilderState = () => useAppSelector(queryBuilderState);
export const useQuerybuilderAttributesShown: () => QueryGraphEdgeHandle[] = () => useAppSelector(queryBuilderAttributesShown);
export const useQuerybuilderUnionTypes: () => {[nodeId: string]: QueryUnionType} = () => useAppSelector(queryUnionTypes);
// Overall Configuration of the app
export const useConfig: () => ConfigStateI = () => useAppSelector(configState);
......
......@@ -13,6 +13,7 @@ export type QueryBuilderSettings = {
depth: { min: number; max: number };
layout: AllLayoutAlgorithms | 'manual';
autocompleteRelation: boolean;
unionTypes: { [nodeId: string]: QueryUnionType };
};
export type QueryBuilderText = {
......@@ -33,7 +34,6 @@ export type QueryBuilderState = {
settings: QueryBuilderSettings;
queryTranslation: QueryBuilderText;
attributesBeingShown: QueryGraphEdgeHandle[];
unionTypes: { [nodeId: string]: QueryUnionType };
};
export type SchemaState = {
......@@ -49,13 +49,13 @@ export const initialState: QueryBuilderState = {
depth: { min: 1, max: 1 },
layout: 'manual',
autocompleteRelation: true,
unionTypes: {},
},
queryTranslation: {
queryId: '',
result: '',
},
attributesBeingShown: [],
unionTypes: {},
// schemaLayout: 'Graphology_noverlap',
} as QueryBuilderState;
......@@ -100,7 +100,10 @@ export const querybuilderSlice = createSlice({
}
},
setQueryUnionType: (state: QueryBuilderState, action: PayloadAction<{ nodeId: string; unionType: QueryUnionType }>) => {
state.unionTypes[action.payload.nodeId] = action.payload.unionType;
if (state.settings.unionTypes == null) {
state.settings.unionTypes = {};
}
state.settings.unionTypes[action.payload.nodeId] = action.payload.unionType;
},
},
});
......@@ -169,6 +172,4 @@ export const {
setQueryUnionType,
} = querybuilderSlice.actions;
export const queryBuilderAttributesShown = (state: RootState) => state.querybuilder.attributesBeingShown;
export const queryUnionTypes = (state: RootState) => state.querybuilder.unionTypes;
export const queryBuilderAttributesShown = (state: RootState) => state.querybuilder.attributesBeingShown;
\ No newline at end of file
......@@ -5,8 +5,8 @@ import { isEqual } from 'lodash-es';
export type VisStateSettings = VisualizationSettingsType[];
export type VisState = {
activeVisualizationIndex: number; // uses underscore_case to match data model from backend
openVisualizationArray: VisStateSettings; // uses underscore_case to match data model from backend
activeVisualizationIndex: number;
openVisualizationArray: VisStateSettings;
};
export const initialState: VisState = {
......@@ -19,48 +19,58 @@ export const visualizationSlice = createSlice({
initialState,
reducers: {
removeVisualization: (state, action: PayloadAction<number | undefined>) => {
let index = action.payload || state.activeVisualizationIndex;
state.openVisualizationArray.splice(index, 1);
if (state.activeVisualizationIndex === index) {
if (state.openVisualizationArray.length === 0) state.activeVisualizationIndex = -1;
else {
state.activeVisualizationIndex = Math.max(state.openVisualizationArray.length - 1, 0);
const index = action.payload ?? state.activeVisualizationIndex;
if (index >= 0 && index < state.openVisualizationArray.length) {
state.openVisualizationArray.splice(index, 1);
if (state.openVisualizationArray.length === 0) {
state.activeVisualizationIndex = -1;
} else if (state.activeVisualizationIndex >= state.openVisualizationArray.length) {
state.activeVisualizationIndex = state.openVisualizationArray.length - 1;
} else if (state.activeVisualizationIndex === index) {
state.activeVisualizationIndex = index < state.openVisualizationArray.length ? index : index - 1;
} else if (state.activeVisualizationIndex > index) {
state.activeVisualizationIndex--;
}
}
},
setActiveVisualization: (state, action: PayloadAction<number>) => {
state.activeVisualizationIndex = action.payload;
if (action.payload >= -1 && action.payload < state.openVisualizationArray.length) {
state.activeVisualizationIndex = action.payload;
}
},
setVisualizationState: (state, action: PayloadAction<VisState>) => {
if (action.payload.activeVisualizationIndex !== undefined && !isEqual(action.payload, state)) {
state.openVisualizationArray = action.payload.openVisualizationArray || [];
state.activeVisualizationIndex = action.payload.activeVisualizationIndex;
state.activeVisualizationIndex = Math.min(
action.payload.activeVisualizationIndex,
state.openVisualizationArray.length - 1
);
}
},
updateVisualization: (state, action: PayloadAction<{ id: number; settings: VisualizationSettingsType }>) => {
const { id, settings } = action.payload;
state.openVisualizationArray[id] = settings;
if (id >= 0 && id < state.openVisualizationArray.length) {
state.openVisualizationArray[id] = settings;
}
},
addVisualization: (state, action: PayloadAction<VisualizationSettingsType>) => {
state.openVisualizationArray.push(action.payload);
state.activeVisualizationIndex = state.openVisualizationArray.length - 1;
},
updateActiveVisualization: (state, action: PayloadAction<VisualizationSettingsType>) => {
if (state.activeVisualizationIndex === -1) {
return;
if (state.activeVisualizationIndex >= 0) {
state.openVisualizationArray[state.activeVisualizationIndex] = action.payload;
}
state.openVisualizationArray[state.activeVisualizationIndex] = action.payload;
},
updateActiveVisualizationAttributes: (state, action: PayloadAction<Record<string, any>>) => {
if (state.activeVisualizationIndex === -1) {
return;
if (state.activeVisualizationIndex >= 0) {
state.openVisualizationArray[state.activeVisualizationIndex] = {
...state.openVisualizationArray[state.activeVisualizationIndex],
...action.payload,
};
}
state.openVisualizationArray[state.activeVisualizationIndex] = {
...state.openVisualizationArray[state.activeVisualizationIndex],
...action.payload,
};
},
reorderVisState: (
state,
......@@ -70,14 +80,30 @@ export const visualizationSlice = createSlice({
}>,
) => {
const { id, newPosition } = action.payload;
const settingsCopy = [...state.openVisualizationArray];
settingsCopy.splice(newPosition, 0, settingsCopy.splice(id, 1)[0]);
state.openVisualizationArray = settingsCopy;
if (
id >= 0 &&
id < state.openVisualizationArray.length &&
newPosition >= 0 &&
newPosition < state.openVisualizationArray.length
) {
const settingsCopy = [...state.openVisualizationArray];
const [movedItem] = settingsCopy.splice(id, 1);
settingsCopy.splice(newPosition, 0, movedItem);
state.openVisualizationArray = settingsCopy;
if (state.activeVisualizationIndex === id) {
state.activeVisualizationIndex = newPosition;
} else if (state.activeVisualizationIndex === newPosition) {
state.activeVisualizationIndex = id;
if (state.activeVisualizationIndex === id) {
state.activeVisualizationIndex = newPosition;
} else if (
state.activeVisualizationIndex > id &&
state.activeVisualizationIndex <= newPosition
) {
state.activeVisualizationIndex--;
} else if (
state.activeVisualizationIndex < id &&
state.activeVisualizationIndex >= newPosition
) {
state.activeVisualizationIndex++;
}
}
},
},
......@@ -100,4 +126,4 @@ export const visualizationActive = (state: RootState) =>
state.visualize.activeVisualizationIndex !== -1
? state.visualize.openVisualizationArray?.[state.visualize.activeVisualizationIndex]
: undefined;
export default visualizationSlice.reducer;
export default visualizationSlice.reducer;
\ No newline at end of file
......@@ -7,7 +7,7 @@ import {
useQuerybuilderAttributesShown,
useQuerybuilderGraph,
useQuerybuilderHash,
useQuerybuilderUnionTypes,
useQuerybuilderSettings
} from '../..';
import { isEqual } from 'lodash-es';
import {
......@@ -31,6 +31,8 @@ export const ContextMenu = (props: {
const dispatch = useAppDispatch();
const graph = useQuerybuilderGraph();
const qbHash = useQuerybuilderHash();
const settings = useQuerybuilderSettings();
const unionType = settings.unionTypes == null || props.node == null ? QueryUnionType.AND : settings.unionTypes[props.node.id];
const graphologyGraph = useMemo(() => toQuerybuilderGraphology(graph), [graph, qbHash]);
......@@ -69,7 +71,6 @@ export const ContextMenu = (props: {
dispatch(attributeShownToggle(attribute.handleData));
}
const unionType = useQuerybuilderUnionTypes();
function setUnionType(unionType: QueryUnionType) {
if (!props.node) return;
dispatch(setQueryUnionType({ nodeId: props.node.id, unionType: unionType }));
......@@ -125,12 +126,12 @@ export const ContextMenu = (props: {
<DropdownItem
value="AND"
onClick={(_) => setUnionType(QueryUnionType.AND)}
selected={props.node ? unionType[props.node.id] != QueryUnionType.OR : false} // Also selected when null
selected={props.node ? unionType != QueryUnionType.OR : false} // Also selected when null
/>,
<DropdownItem
value="OR"
onClick={(_) => setUnionType(QueryUnionType.OR)}
selected={props.node ? unionType[props.node.id] == QueryUnionType.OR : false}
selected={props.node ? unionType == QueryUnionType.OR : false}
/>,
]}
/>
......
......@@ -262,7 +262,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => {
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger>
<PopoverTrigger disabled={!saveStateAuthorization.query.W}>
<Tooltip>
<TooltipTrigger>
<Button variantType="secondary" variant="ghost" size="xs" iconComponent="icon-[ic--baseline-search]" />
......
......@@ -2,7 +2,7 @@ import {
useQuerybuilderAttributesShown,
useQuerybuilderGraph,
useQuerybuilderHash,
useQuerybuilderUnionTypes,
useQuerybuilderSettings,
} from '@graphpolaris/shared/lib/data-access';
import {
setQuerybuilderGraphology,
......@@ -38,9 +38,10 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
() => graph.edges.filter((edge) => edge.source === node.id && !!edge?.attributes?.sourceHandleData.attributeType),
[graph],
);
const settings = useQuerybuilderSettings();
const uniqueAttributes = useMemo(() => uniqBy(data.attributes, (attr) => attr.handleData.attributeName), [data.attributes]);
const unionType = useQuerybuilderUnionTypes()[node.id];
const unionType = settings.unionTypes == null ? QueryUnionType.AND : settings.unionTypes[node.id];
return (
<div className="w-fit h-fit nowheel" ref={ref} id="asd">
......
......@@ -4,13 +4,13 @@ import {
useAppDispatch,
useQuerybuilderGraph,
useQuerybuilderSettings,
useQuerybuilderUnionTypes,
} from '@graphpolaris/shared/lib/data-access';
import { addWarning } from '@graphpolaris/shared/lib/data-access/store/configSlice';
import {
setQuerybuilderGraphology,
toQuerybuilderGraphology,
attributeShownToggle,
QueryUnionType
} from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
import { PillAttributes } from '../../pillattributes/PillAttributes';
import {
......@@ -91,7 +91,7 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => {
dispatch(attributeShownToggle(attribute.handleData));
}
const unionType = useQuerybuilderUnionTypes()[node.id];
const unionType = settings.unionTypes == null ? QueryUnionType.AND : settings.unionTypes[node.id];
const attributesBeingShown = useQuerybuilderAttributesShown();
function isAttributeAdded(attribute: NodeAttribute): boolean {
......
......@@ -64,6 +64,9 @@ export function schemaGraphology2Reactflow(
export function createReactFlowNodes(graph: Graph): Array<Node> {
const nodeElements: Array<Node> = [];
graph.forEachNode((node: string, attributes: Attributes): void => {
if (!Array.isArray(attributes.attributes)) {
attributes.attributes = Object.values(attributes.attributes);
}
const newNode: Node = {
id: node,
data: {
......@@ -74,7 +77,6 @@ export function createReactFlowNodes(graph: Graph): Array<Node> {
};
nodeElements.push(newNode);
});
return nodeElements;
}
......
......@@ -136,7 +136,7 @@ export const VisualizationPanel = ({ fullSize }: { fullSize: () => void }) => {
</div>
)}
</div>
<VisualizationTabBar fullSize={fullSize} exportImage={exportImage} />
<VisualizationTabBar fullSize={fullSize} exportImage={exportImage} handleSelect={handleSelect} />
</div>
);
};
......@@ -7,7 +7,7 @@ import { useActiveSaveState, useActiveSaveStateAuthorization, useAppDispatch, us
import { addVisualization, removeVisualization, reorderVisState, setActiveVisualization } from '../../data-access/store/visualizationSlice';
import { Visualizations } from './VisualizationPanel';
export default function VisualizationTabBar(props: { fullSize: () => void; exportImage: () => void }) {
export default function VisualizationTabBar(props: { fullSize: () => void; exportImage: () => void; handleSelect: () => void }) {
const { activeVisualizationIndex, openVisualizationArray } = useVisualization();
const saveStateAuthorization = useActiveSaveStateAuthorization();
const [open, setOpen] = useState(false);
......@@ -34,6 +34,7 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor
const onDelete = (id: number) => {
dispatch(removeVisualization(id));
props.handleSelect();
};
useEffect(() => {
......
......@@ -56,15 +56,6 @@ export function VisualizationSettings({}: Props) {
<div className="text-sm px-4 py-2 flex flex-col gap-1">
<div className="flex justify-between items-center">
<span className="font-bold">Visualization</span>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-delete]"
onClick={() => {
if (activeVisualization) removeVisualization();
}}
/>
</div>
<Input
type="text"
......
......@@ -2,7 +2,7 @@ import { Edge, GraphQueryResult, Node, useML } from '@graphpolaris/shared/lib/da
import { dataColors, visualizationColors } from 'config';
import { Viewport } from 'pixi-viewport';
import { Application, ColorSource, Container, FederatedPointerEvent, Graphics, IPointData, Point, Text } from 'pixi.js';
import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react';
import { useEffect, useRef, useState, useMemo, useImperativeHandle, forwardRef } from 'react';
import { LinkType, NodeType } from '../types';
import { NLPopup } from './MatrixPopup';
......@@ -63,10 +63,11 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
const globalConfig = useConfig();
useEffect(() => {
if (props.graph) {
setup();
if (props.graph && internalRef.current && imperative.current) {
if (isSetup.current === false) setup();
else update();
}
}, [globalConfig.theme]);
}, [props.graph, globalConfig.theme]);
let columnOrder: string[] = [];
let rowOrder: string[] = [];
......@@ -82,6 +83,20 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
const isSetup = useRef(false);
const ml = useML();
const app = useMemo(
() =>
new Application({
backgroundAlpha: 0,
antialias: true,
autoDensity: true,
eventMode: 'auto',
resolution: window.devicePixelRatio || 2,
view: canvas.current as HTMLCanvasElement,
}),
[canvas.current],
);
useEffect(() => {
if (typeof refExternal === 'function') {
refExternal(internalRef.current);
......@@ -135,13 +150,11 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
},
}));
let app: Application;
function resize() {
const width = internalRef?.current?.clientWidth || 1000;
const height = internalRef?.current?.clientHeight || 1000;
app.renderer.resize(width, height);
app?.renderer.resize(width, height);
if (viewport.current) {
viewport.current.screenWidth = width;
viewport.current.worldWidth = width;
......@@ -152,22 +165,23 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
if (props.graph) {
setup();
}
app.render();
app?.render();
}
useEffect(() => {
if (props.graph && internalRef.current && internalRef.current.children.length > 0) {
if (!isSetup.current) setup();
else update();
return () => {
app.destroy();
}
}, [props.graph]);
}, []);
useEffect(() => {
if (props.graph && internalRef.current && internalRef.current.children.length > 0) {
setup();
}
}, [props.settings]);
if (!internalRef.current) return;
const resizeObserver = new ResizeObserver(() => {
resize();
});
resizeObserver.observe(internalRef.current);
return () => resizeObserver.disconnect();
}, []);
// TODO implement search results
// useEffect(() => {
......@@ -306,22 +320,6 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
const setup = () => {
if (!props.graph) throw Error('Graph is undefined; setup not possible');
if (app == null) {
app = new Application({
backgroundAlpha: 0,
antialias: true,
autoDensity: true,
eventMode: 'auto',
resolution: window.devicePixelRatio || 1,
view: canvas.current as HTMLCanvasElement,
});
const resizeObserver = new ResizeObserver(() => {
resize();
});
resizeObserver.observe(internalRef.current as HTMLDivElement);
}
if (svg.current != null) {
select(svg.current).selectAll('*').remove();
}
......@@ -409,8 +407,9 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
viewport.current.drag().pinch().wheel().animate({}).decelerate({ friction: 0.75 });
app.ticker.add(tick);
update();
isSetup.current = true;
update();
};
let scaleColumns: ScaleBand<string>;
......
......@@ -774,10 +774,7 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
return () => {
nodeMap.current.clear();
linkLabelMap.current.clear();
linkGfx.clear();
nodeLayer.removeChildren();
linkLabelLayer.removeChildren();
nodeLabelLayer.removeChildren();
app.destroy();
const layout = layoutAlgorithm.current as GraphologyForceAtlas2Webworker;
if (layout?.cleanup != null) layout.cleanup();
......
......@@ -147,7 +147,6 @@ export const Table = ({ data, itemsPerPage, showBarPlot, showAttributes, selecte
data: [],
numElements: 0,
};
if (
firstRowData.type[dataColumn] === 'string' ||
firstRowData.type[dataColumn] === 'date' ||
......@@ -201,18 +200,31 @@ export const Table = ({ data, itemsPerPage, showBarPlot, showAttributes, selecte
// number: float and int
} else if (firstRowData.type[dataColumn] === 'int' || firstRowData.type[dataColumn] === 'float') {
categoryCounts = data.map((obj) => ({
category: 'placeholder', // add something
count: obj.attribute[dataColumn] as number, // fill values of data
}));
const isArrayOfFloats =
Array.isArray(data[0].attribute[dataColumn]) && data[0].attribute[dataColumn].every((val) => typeof val === 'number');
newData2Render.numElements = categoryCounts.length;
newData2Render.data = categoryCounts;
newData2Render.showBarPlot = true;
if (!isArrayOfFloats) {
categoryCounts = data.map((obj) => ({
category: 'placeholder', // add something
count: obj.attribute[dataColumn] as number, // fill values of data
}));
newData2Render.numElements = categoryCounts.length;
newData2Render.data = categoryCounts;
newData2Render.showBarPlot = true;
} else {
const groupedData = group(data, (d) => (d.attribute[dataColumn] as any)?.[0]);
categoryCounts = Array.from(groupedData, ([category, items]) => ({
category: category as string,
count: items.length,
}));
newData2Render.numElements = categoryCounts.length;
newData2Render.showBarPlot = false;
}
} else {
// there is also array type, when considering labels
const groupedData = group(data, (d) => (d.attribute[dataColumn] as any)?.[0]);
categoryCounts = Array.from(groupedData, ([category, items]) => ({
category: category as string,
count: items.length,
......@@ -224,7 +236,6 @@ export const Table = ({ data, itemsPerPage, showBarPlot, showAttributes, selecte
return newData2Render;
});
const _data2RenderSorted = _data2Render.sort((a, b) => a.name.localeCompare(b.name));
setData2Render(_data2RenderSorted);
}, [currentPage, data, sortedData, selectedEntity, maxBarsCount, showAttributes]);
......
import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '@graphpolaris/shared/lib/components/accordion';
import { Button } from '@graphpolaris/shared/lib/components/buttons';
import { Input } from '@graphpolaris/shared/lib/components/inputs';
import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
import { EntityPill, RelationPill } from '@graphpolaris/shared/lib/components/pills/Pill';
import { useSearchResultData } from '@graphpolaris/shared/lib/data-access';
import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config';
import html2canvas from 'html2canvas';
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { VISComponentType, VisualizationPropTypes, VisualizationSettingsPropTypes } from '../../common';
import { AugmentedNodeAttributes, Table } from './components/Table';
......@@ -19,7 +19,7 @@ export type TableProps = {
showBarplot: boolean;
itemsPerPage: number;
displayAttributes: string[];
displayEntity: string;
selectedEntity: string;
maxBarsCount: number;
};
......@@ -29,24 +29,14 @@ const settings: TableProps = {
itemsPerPage: 10,
showBarplot: true,
displayAttributes: [],
displayEntity: '',
selectedEntity: '',
maxBarsCount: 10,
};
export const TableVis = forwardRef<TableVisHandle, VisualizationPropTypes<TableProps>>(
({ data, schema, settings, updateSettings, graphMetadata }, refExternal) => {
({ data, schema, settings, graphMetadata }, refExternal) => {
const searchResults = useSearchResultData();
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (graphMetadata != undefined && settings.displayEntity === '') {
if (!graphMetadata.nodes.labels.includes(settings.displayEntity)) {
updateSettings({
displayEntity: graphMetadata.nodes.labels[0],
displayAttributes: Object.keys(graphMetadata.nodes.types[graphMetadata.nodes.labels[0]].attributes),
});
}
}
}, [graphMetadata, data, settings]);
const displayAttributesSorted = useMemo<string[]>(() => {
if (settings.displayAttributes.length != 0) {
......@@ -56,12 +46,17 @@ export const TableVis = forwardRef<TableVisHandle, VisualizationPropTypes<TableP
}, [settings.displayAttributes]);
const attributesArray = useMemo<AugmentedNodeAttributes[]>(() => {
//const similiarityThreshold = 0.9;
let displayAttributesSorted: string[];
displayAttributesSorted = [...settings.displayAttributes].sort((a, b) => a.localeCompare(b));
const dataNodes = (searchResults?.nodes?.length ?? 0) === 0 ? data.nodes : searchResults.nodes;
const nodesLabels = graphMetadata.nodes.labels;
let dataNodes = [];
if (nodesLabels.includes(settings.selectedEntity)) {
dataNodes = (searchResults?.nodes?.length ?? 0) === 0 ? data.nodes : searchResults.nodes;
} else {
dataNodes = data.edges;
}
return (
dataNodes
......@@ -74,7 +69,7 @@ export const TableVis = forwardRef<TableVisHandle, VisualizationPropTypes<TableP
const idParts = node._id.split('/');
labelNode = idParts[0];
}
return labelNode === settings.displayEntity;
return labelNode === settings.selectedEntity;
})
///.filter((obj) => obj.similarity === undefined || obj.similarity >= similiarityThreshold)
.map((node) => {
......@@ -111,7 +106,7 @@ export const TableVis = forwardRef<TableVisHandle, VisualizationPropTypes<TableP
}
})
);
}, [data.nodes, settings.displayEntity, settings.displayAttributes, searchResults]);
}, [data.nodes, settings.selectedEntity, settings.displayAttributes, searchResults]);
const exportImageInternal = () => {
if (ref.current) {
......@@ -140,7 +135,7 @@ export const TableVis = forwardRef<TableVisHandle, VisualizationPropTypes<TableP
itemsPerPage={settings.itemsPerPage}
showBarPlot={settings.showBarplot}
showAttributes={displayAttributesSorted}
selectedEntity={settings.displayEntity}
selectedEntity={settings.selectedEntity}
maxBarsCount={settings.maxBarsCount}
/>
)}
......@@ -150,27 +145,37 @@ export const TableVis = forwardRef<TableVisHandle, VisualizationPropTypes<TableP
);
const TableSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<TableProps>) => {
const [attributes, setAttributes] = useState<string[]>([]);
useEffect(() => {
if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0 && settings.displayEntity === '') {
updateSettings({ displayEntity: graphMetadata.nodes.labels[0] });
if (settings.selectedEntity === '' && graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) {
const firstEntity = graphMetadata.nodes.labels[0];
const attributesFirstEntity = Object.keys(graphMetadata.nodes.types[firstEntity].attributes);
updateSettings({ selectedEntity: graphMetadata.nodes.labels[0], displayAttributes: attributesFirstEntity });
setAttributes(attributesFirstEntity);
}
}, [graphMetadata]);
const selectedNodeAttributes = useMemo(() => {
if (settings.displayEntity) {
const nodeType = graphMetadata.nodes.types[settings.displayEntity];
if (nodeType && nodeType.attributes) {
return Object.keys(nodeType.attributes).sort((a, b) => a.localeCompare(b));
useEffect(() => {
if (
graphMetadata &&
graphMetadata.nodes &&
graphMetadata.nodes.labels.length > 0 &&
settings.selectedEntity != '' &&
settings.displayAttributes.length != 0
) {
const nodesLabels = graphMetadata.nodes.labels;
let attributesFirstEntity = [];
if (nodesLabels.includes(settings.selectedEntity)) {
attributesFirstEntity = Object.keys(graphMetadata.nodes.types[settings.selectedEntity].attributes);
} else {
attributesFirstEntity = Object.keys(graphMetadata.edges.types[settings.selectedEntity].attributes);
}
}
return [];
}, [settings.displayEntity, graphMetadata]);
setAttributes(attributesFirstEntity);
useEffect(() => {
if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0 && settings.displayAttributes.length === 0) {
updateSettings({ displayAttributes: selectedNodeAttributes });
updateSettings({ displayAttributes: attributesFirstEntity });
}
}, [selectedNodeAttributes, graphMetadata]);
}, [settings.selectedEntity, graphMetadata]);
return (
<SettingsContainer>
......@@ -178,18 +183,29 @@ const TableSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
<Input
className="w-full text-justify justify-center"
type="dropdown"
value={settings.displayEntity}
options={graphMetadata.nodes.labels}
onChange={(val) => updateSettings({ displayEntity: val as string })}
value={settings.selectedEntity}
options={[...graphMetadata.nodes.labels, ...graphMetadata.edges.labels]}
onChange={(val) => updateSettings({ selectedEntity: val as string })}
overrideRender={
<EntityPill
title={
<div className="flex flex-row justify-between items-center cursor-pointer">
<span>{settings.displayEntity || ''}</span>
<Button variantType="secondary" variant="ghost" size="2xs" iconComponent="icon-[ic--baseline-arrow-drop-down]" />
</div>
}
/>
graphMetadata.nodes.labels.includes(settings.selectedEntity) ? (
<EntityPill
title={
<div className="flex flex-row justify-between items-center cursor-pointer">
<span>{settings.selectedEntity || ''}</span>
<Button variantType="secondary" variant="ghost" size="2xs" iconComponent="icon-[ic--baseline-arrow-drop-down]" />
</div>
}
/>
) : (
<RelationPill
title={
<div className="flex flex-row justify-between items-center cursor-pointer">
<span>{settings.selectedEntity || ''}</span>
<Button variantType="secondary" variant="ghost" size="2xs" iconComponent="icon-[ic--baseline-arrow-drop-down]" />
</div>
}
/>
)
}
></Input>
<div className="my-2">
......@@ -226,7 +242,7 @@ const TableSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
<Input
type="checkbox"
value={settings.displayAttributes}
options={selectedNodeAttributes}
options={attributes}
onChange={(val: string[] | string) => {
const updatedVal = Array.isArray(val) ? val : [val];
updateSettings({ displayAttributes: updatedVal });
......
......@@ -4,27 +4,31 @@ import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/confi
import html2canvas from 'html2canvas';
import { CustomChartPlotly, plotTypeOptions } from './components/CustomChartPlotly';
import { Input } from '@graphpolaris/shared/lib/components/inputs';
import { DropdownTextAndIcon } from '@graphpolaris/shared/lib/components/selectors/textAndIcon';
import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
import { Button } from '@graphpolaris/shared/lib/components/buttons';
export interface Vis1DProps {
plotType: (typeof plotTypeOptions)[number];
title: string;
attribute?: string;
nodeLabel: string;
plotType: (typeof plotTypeOptions)[number]; // plotly plot type
title: string; // title of the plot
nodeLabel: string; // node label to plot
xAxisLabel?: string;
yAxisLabel?: string;
showAxis: boolean;
}
const defaultSettings: Vis1DProps = {
plotType: 'bar',
title: '',
attribute: '',
nodeLabel: '',
xAxisLabel: '',
yAxisLabel: '',
showAxis: true,
};
export interface Vis1DVisHandle {
exportImageInternal: () => void;
}
const Vis1D = forwardRef<Vis1DVisHandle, VisualizationPropTypes<Vis1DProps>>(({ data, settings }, refExternal) => {
const internalRef = useRef<HTMLDivElement>(null);
......@@ -63,48 +67,52 @@ const Vis1D = forwardRef<Vis1DVisHandle, VisualizationPropTypes<Vis1DProps>>(({
},
}));
const attributeValues = useMemo(() => {
if (!settings.nodeLabel || !settings.attribute) {
const getAttributeValues = (attributeKey: string | number | undefined) => {
if (!settings.nodeLabel || !attributeKey) {
return [];
}
return data.nodes
.filter((item) => item.label === settings.nodeLabel && item.attributes && settings.attribute! in item.attributes)
.map((item) => item.attributes[settings.attribute!] as string | number);
}, [data, settings.nodeLabel, settings.attribute]);
.filter((item) => item.label === settings.nodeLabel && item.attributes && attributeKey in item.attributes)
.map((item) => item.attributes[attributeKey]);
};
const xAxisData = useMemo(() => getAttributeValues(settings.xAxisLabel), [data, settings.nodeLabel, settings.xAxisLabel]);
const yAxisData = useMemo(() => getAttributeValues(settings.yAxisLabel), [data, settings.nodeLabel, settings.yAxisLabel]);
return (
<div className="h-full w-full flex items-center justify-center overflow-hidden" ref={internalRef}>
<CustomChartPlotly data={attributeValues as string[] | number[]} plotType={settings.plotType} title={settings.title} />
<CustomChartPlotly
xAxisData={xAxisData as string[] | number[]}
yAxisData={yAxisData as string[] | number[]}
plotType={settings.plotType}
title={settings.title}
showAxis={settings.showAxis}
xAxisLabel={settings.xAxisLabel}
yAxisLabel={settings.yAxisLabel}
/>
</div>
);
});
const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<Vis1DProps>) => {
const mutablePlotTypes = [...plotTypeOptions];
const [attributeOptions, setAttributeOptions] = useState<{ name: string; type: string }[]>([]);
const [selectedOption, setSelectedOption] = useState<{ name: string; type: string } | null>(null);
const [attributeOptions, setAttributeOptions] = useState<string[]>([]);
const handleChange = (option: { name: string; type: string }) => {
setSelectedOption(option);
updateSettings({ attribute: option.name });
};
useEffect(() => {
if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) {
if (settings.nodeLabel === '' && graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) {
const nodeLabelTemp = graphMetadata.nodes.labels[0];
updateSettings({ nodeLabel: nodeLabelTemp });
}
}, [graphMetadata]);
}, [settings.nodeLabel, graphMetadata]);
useEffect(() => {
if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0 && settings.nodeLabel != '') {
const newAttributeOptions = Object.entries(graphMetadata.nodes.types[settings.nodeLabel].attributes).map(([key, value]) => ({
name: key,
type: value.attributeType,
}));
updateSettings({ attribute: newAttributeOptions[0].name });
const newAttributeOptions = Object.keys(graphMetadata.nodes.types[settings.nodeLabel].attributes);
if (settings.xAxisLabel === '') {
updateSettings({ xAxisLabel: newAttributeOptions[0] });
}
// initialize the selected option for creating the dropdown and plots
setSelectedOption(newAttributeOptions[0]);
setAttributeOptions(newAttributeOptions);
}
}, [graphMetadata, settings.nodeLabel]);
......@@ -129,14 +137,13 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
/>
}
/>
<div className="mb-2">
<Input type="text" label="Title" value={settings.title} onChange={(value) => updateSettings({ title: value as string })} />
</div>
<div className="mb-2">
<Input
type="dropdown"
label="Type Chart"
label="Chart"
value={settings.plotType}
options={mutablePlotTypes}
onChange={(value: string | number) => {
......@@ -145,7 +152,31 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
/>
</div>
<div className="mb-2">
<DropdownTextAndIcon value={selectedOption} onChange={handleChange} options={attributeOptions} />
<Input
type="dropdown"
label="X-axis:"
value={settings.xAxisLabel}
options={attributeOptions}
onChange={(value) => {
updateSettings({ xAxisLabel: value as string });
}}
/>
</div>
{(settings.plotType === 'scatter' || settings.plotType === 'line') && (
<div className="mb-2">
<Input
type="dropdown"
label="Y-axis:"
value={settings.yAxisLabel}
options={attributeOptions}
onChange={(value) => {
updateSettings({ yAxisLabel: value as string });
}}
/>
</div>
)}
<div className="mb-2">
<Input type="boolean" label="Show axis" value={settings.showAxis} onChange={(val) => updateSettings({ showAxis: val })} />
</div>
</div>
</SettingsContainer>
......