blob: 719503af710e3c726c5fe9e402d23df4cbcd595a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, {
forwardRef, useEffect, useImperativeHandle, useState,
} from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import {
edgeLabelColors, edgeLabelSizes, nodeLabelColors, nodeLabelSizes,
} from '../../../features/cypher/CypherUtil';
import CypherResultCytoscapeChart from '../../cytoscape/CypherResultCytoscapeChart';
import CypherResultCytoscapeLegend from '../../cytoscape/CypherResultCytoscapeLegend';
import CypherResultCytoscapeFooter from '../../cytoscape/CypherResultCytoscapeFooter';
import CypherResultTab from '../../cytoscape/CypherResultTab';
const CypherResultCytoscape = forwardRef((props, ref) => {
const [footerData, setFooterData] = useState({});
const [legendData, setLegendData] = useState({ edgeLegend: {}, nodeLegend: {} });
const [elements, setElements] = useState({ edges: [], nodes: [] });
const [isReloading, setIsReloading] = useState(false);
const [maxDataOfGraph] = useState(props.maxDataOfGraph);
const dispatch = useDispatch();
const [selectedCaption, setSelectedCaption] = useState(null);
const [captions, setCaptions] = useState([]);
const [cytoscapeObject, setCytoscapeObject] = useState(null);
const [cytoscapeLayout, setCytoscapeLayout] = useState('coseBilkent');
useEffect(() => {
if (props.data.legend !== undefined && Object.keys(props.data.legend.nodeLegend).length > 0) {
if (Object.keys(legendData.edgeLegend).length === 0
&& Object.keys(legendData.nodeLegend).length === 0) {
setIsReloading(false);
}
setLegendData(props.data.legend);
setElements(props.data.elements);
}
}, [
setIsReloading,
elements.edges.length,
elements.nodes.length,
legendData.edgeLegend,
legendData.nodeLegend,
props.data,
]);
useEffect(() => {
props.setChartLegend(props.data.legend);
}, []);
const getCaptionsFromCytoscapeObject = (elementType, label) => {
const elementsObject = cytoscapeObject.elements(`${elementType}[label = "${label}"]`).jsons();
let extendedSet = new Set([]);
elementsObject.forEach((ele) => {
extendedSet = new Set([...extendedSet, ...Object.keys(ele.data.properties)]);
});
return extendedSet;
};
const getFooterData = (event) => {
if (event.type === 'labels') {
setCaptions(['gid', 'label'].concat(Array.from(getCaptionsFromCytoscapeObject(event.data.type, event.data.label))));
if (event.data.type === 'node') {
setSelectedCaption(legendData.nodeLegend[event.data.label].caption);
} else {
setSelectedCaption(legendData.edgeLegend[event.data.label].caption);
}
}
setFooterData(event);
};
const addLegendData = (addedLegendData) => {
setIsReloading(false);
setLegendData(addedLegendData);
props.setChartLegend(addedLegendData);
};
const truncateLabel = (label) => {
const maxLenEnglish = 8;
const maxLenKorean = 6;
const koreanCharRegex = /[\u3131-\uD79D]/ugi;
const maxLength = koreanCharRegex.test(label) ? maxLenKorean : maxLenEnglish;
return label.length > maxLength ? `${label.substring(0, maxLength)}...` : label;
};
const changeColorOnCytoscapeElements = (elementType, originalLabel, color) => {
const label = truncateLabel(originalLabel);
const colorObject = Array.isArray(color) ? {
color: color[0],
borderColor: color[1],
fontColor: color[2],
} : color;
if (elementType === 'node') {
cytoscapeObject.nodes(`[label = "${label}"]`).data('backgroundColor', colorObject.color)
.data('borderColor', colorObject.borderColor).data('fontColor', colorObject.fontColor);
} else if (elementType === 'edge') {
cytoscapeObject.edges(`[label = "${label}"]`).data('backgroundColor', colorObject.color)
.data('fontColor', colorObject.fontColor).data('fontColor', '#2A2C34');
}
};
const colorChange = (elementType, label, color) => {
const footerObj = footerData.data;
footerObj.backgroundColor = color.color;
footerObj.fontColor = color.fontColor;
setIsReloading(false);
setFooterData(Object.assign(footerData, { data: footerObj }));
if (elementType === 'node') {
const nodeLegendObj = legendData.nodeLegend;
if (Object.prototype.hasOwnProperty.call(nodeLegendObj, label)) {
nodeLegendObj[label].color = color.color;
nodeLegendObj[label].borderColor = color.borderColor;
nodeLegendObj[label].fontColor = color.fontColor;
}
setLegendData(Object.assign(legendData, { nodeLegend: nodeLegendObj }));
changeColorOnCytoscapeElements(elementType, label, color);
} else if (elementType === 'edge') {
const edgeLegendObj = legendData.edgeLegend;
if (Object.prototype.hasOwnProperty.call(edgeLegendObj, label)) {
edgeLegendObj[label].color = color.color;
edgeLegendObj[label].borderColor = color.borderColor;
edgeLegendObj[label].fontColor = color.fontColor;
}
setLegendData(Object.assign(legendData, { edgeLegend: edgeLegendObj }));
changeColorOnCytoscapeElements(elementType, label, color);
}
dispatch(() => props.setLabels(elementType, label, {
borderColor: color.borderColor,
color: color.color,
fontColor: color.fontColor,
}));
};
const changeSizeOnCytoscapeElements = (elementType, label, size) => {
const changedData = cytoscapeObject.elements(`${elementType}[label = "${label}"]`).data('size', size);
if (size > 6) {
changedData.style('text-background-opacity', 0);
} else {
changedData.style('text-background-opacity', 1);
}
};
const sizeChange = (elementType, label, size) => {
const footerObj = footerData.data;
footerObj.size = size;
setFooterData({ ...footerData, data: footerObj });
setIsReloading(false);
changeSizeOnCytoscapeElements(elementType, label, size);
if (elementType === 'node') {
const nodeLegendObj = legendData.nodeLegend;
if (Object.prototype.hasOwnProperty.call(nodeLegendObj, label)) {
nodeLegendObj[label].size = size;
}
setLegendData({ ...legendData, nodeLegend: nodeLegendObj });
} else if (elementType === 'edge') {
const edgeLegendObj = legendData.edgeLegend;
if (Object.prototype.hasOwnProperty.call(edgeLegendObj, label)) {
edgeLegendObj[label].size = size;
}
setLegendData({ ...legendData, edgeLegend: edgeLegendObj });
}
dispatch(() => props.setLabels(elementType, label, { size }));
};
const changeCaptionOnCytoscapeElements = (elementType, label, caption) => {
if (caption === null) {
cytoscapeObject.elements(`${elementType}[label = "${label}"]`).style('label', '');
} else {
cytoscapeObject.elements(`${elementType}[label = "${label}"]`).style('label', (ele) => {
let displayValue = '< NULL >';
if (caption === 'gid') {
const idValue = ele.data('id');
if (idValue !== null && idValue !== undefined) {
displayValue = `[ ${idValue} ]`;
}
} else if (caption === 'label') {
const labelValue = ele.data('label');
if (labelValue !== null && labelValue !== undefined) {
displayValue = `[ :${labelValue} ]`;
}
} else if (ele !== null && ele !== undefined) {
const anonValue = ele.data('properties')[caption];
if (anonValue !== null && anonValue !== undefined) {
displayValue = anonValue;
}
}
return displayValue;
});
}
};
const applyEdgeThicknessCytoscapeElements = (thickness) => {
const edgeSizes = [1, 6, 11, 16, 21];
if (thickness !== null) {
const range = thickness.max - thickness.min;
const edgeSizeByRate = (rate) => {
let size = edgeSizes[0];
size = (rate >= 0) ? edgeSizes[0] : size;
size = (rate >= 20) ? edgeSizes[1] : size;
size = (rate >= 40) ? edgeSizes[2] : size;
size = (rate >= 60) ? edgeSizes[3] : size;
size = (rate >= 80) ? edgeSizes[4] : size;
size = (rate >= 100) ? edgeSizes[4] : size;
return size;
};
if (cytoscapeObject) {
cytoscapeObject.elements().forEach((e) => {
const ele = e;
if (ele.group() === 'edges') {
if (ele.data().label === thickness.edge && ele.data().properties[thickness.property]) {
const tempValue = ele.data().properties[thickness.property];
const propertyValue = (Number.isNaN(Number(tempValue)))
? Number(tempValue.replace(',', ''))
: Number(tempValue);
const propertyRate = ((propertyValue - thickness.min) / range) * 100;
ele.style('width', edgeSizeByRate(propertyRate).toString());
}
}
});
}
} else if (cytoscapeObject) {
cytoscapeObject.elements().forEach((e) => {
const ele = e;
if (ele.group() === 'edges') {
ele.style('width', '');
}
});
}
};
const applyFilterOnCytoscapeElements = (filters) => {
const gFilteredClassName = 'g-filtered';
cytoscapeObject.elements(`.${gFilteredClassName}`).style('opacity', '1.0').removeClass(gFilteredClassName);
let notFilteredNodeLength = 0;
const notFilteredNodes = [];
const filterLength = filters.length;
let nullFilterCount = 0;
for (let i = 0; i < filterLength; i += 1) {
const { keyword } = filters[i];
if (keyword === null || keyword === '') {
nullFilterCount += 1;
}
}
if (nullFilterCount === 1 && filterLength === 1) {
// if null filter size is 1 and filter length is 1 -> not filtering.
return;
}
cytoscapeObject.nodes().filter((ele) => {
let notIncluded = true;
const currentLabel = ele.data('label');
for (let i = 0; i < filterLength; i += 1) {
const { keyword } = filters[i];
const { label, property } = filters[i].property;
if (currentLabel === label) {
const propertyValue = ele.data('properties')[property];
if (keyword === null || keyword === '') {
notIncluded = false;
} else if (propertyValue === undefined || propertyValue === null) {
notIncluded = true;
} else if (propertyValue.toString().includes(keyword)) {
notIncluded = false;
}
}
if (!notIncluded) {
break;
}
}
if (notIncluded) {
notFilteredNodeLength += 1;
notFilteredNodes.push(ele);
}
return notIncluded;
}).addClass(gFilteredClassName);
// Step2. Edge Highlight from not filtered nodes.
const targetAndSourceNodeList = [];
for (let nodeIndex = 0; nodeIndex < notFilteredNodeLength; nodeIndex += 1) {
const currentNode = notFilteredNodes[nodeIndex];
const edges = currentNode.connectedEdges();
const edgesSize = edges.length;
for (let edgeIndex = 0; edgeIndex < edgesSize; edgeIndex += 1) {
const currentEdge = edges[edgeIndex];
const edgeTargetNode = currentEdge.target();
const edgeSourceNode = currentEdge.source();
const connectedWithHighlightNode = currentEdge.connectedNodes().not(`.${gFilteredClassName}`).filter((ele) => ele !== currentNode);
if (connectedWithHighlightNode.length === 0) {
currentEdge.addClass(gFilteredClassName);
} else {
targetAndSourceNodeList.push(edgeTargetNode);
targetAndSourceNodeList.push(edgeSourceNode);
}
}
}
// Step3 . Edge Highlighting target And source filtered remove
targetAndSourceNodeList.forEach((node) => { node.removeClass(gFilteredClassName); });
cytoscapeObject.elements(`.${gFilteredClassName}`).style('opacity', '0.1');
};
const resetFilterOnCytoscapeElements = () => {
const gFilteredClassName = 'g-filtered';
if (cytoscapeObject) {
cytoscapeObject.elements(`.${gFilteredClassName}`).style('opacity', '1.0').removeClass(gFilteredClassName);
}
};
const captionChange = (elementType, label, caption) => {
changeCaptionOnCytoscapeElements(elementType, label, caption);
setSelectedCaption(caption);
if (elementType === 'node') {
const nodeLegendObj = legendData.nodeLegend;
if (Object.prototype.hasOwnProperty.call(nodeLegendObj, label)) {
nodeLegendObj[label].caption = caption;
}
setLegendData({ ...legendData, nodeLegend: nodeLegendObj });
} else if (elementType === 'edge') {
const edgeLegendObj = legendData.edgeLegend;
if (Object.prototype.hasOwnProperty.call(edgeLegendObj, label)) {
edgeLegendObj[label].caption = caption;
}
setLegendData({ ...legendData, edgeLegend: edgeLegendObj });
}
dispatch(() => props.setLabels(elementType, label, { caption }));
};
useImperativeHandle(ref, () => ({
getCy() {
return cytoscapeObject;
},
getLabels() {
return Object.keys(props.data.legend.nodeLegend);
},
getEdges() {
return Object.keys(props.data.legend.edgeLegend);
},
getCaptionsFromCytoscapeObject,
applyFilterOnCytoscapeElements,
resetFilterOnCytoscapeElements,
applyEdgeThicknessCytoscapeElements,
}));
return (
<div className="chart-frame-area">
<div className="contianer-frame-tab">
<CypherResultCytoscapeLegend
onLabelClick={getFooterData}
isReloading={isReloading}
legendData={legendData}
/>
<CypherResultTab refKey={props.refKey} setIsTable={props.setIsTable} currentTab="graph" />
</div>
<CypherResultCytoscapeChart
onElementsMouseover={getFooterData}
legendData={legendData}
elements={elements}
setCytoscapeObject={setCytoscapeObject}
cytoscapeObject={cytoscapeObject}
cytoscapeLayout={cytoscapeLayout}
addLegendData={addLegendData}
maxDataOfGraph={maxDataOfGraph}
graph={props.graph}
onAddSubmit={props.onAddSubmit}
onRemoveSubmit={props.onRemoveSubmit}
openModal={props.openModal}
addGraphHistory={props.addGraphHistory}
addElementHistory={props.addElementHistory}
/>
<CypherResultCytoscapeFooter
captions={captions}
colorChange={colorChange}
sizeChange={sizeChange}
captionChange={captionChange}
setCytoscapeLayout={setCytoscapeLayout}
cytoscapeLayout={cytoscapeLayout}
selectedCaption={selectedCaption}
footerData={footerData}
nodeLabelSizes={nodeLabelSizes}
edgeLabelSizes={edgeLabelSizes}
edgeLabelColors={edgeLabelColors}
nodeLabelColors={nodeLabelColors}
/>
</div>
);
});
CypherResultCytoscape.propTypes = {
maxDataOfGraph: PropTypes.number.isRequired,
data: PropTypes.shape({
label: PropTypes.string,
type: PropTypes.string,
legend: PropTypes.shape({
// eslint-disable-next-line react/forbid-prop-types
nodeLegend: PropTypes.any,
// eslint-disable-next-line react/forbid-prop-types
edgeLegend: PropTypes.any,
}).isRequired,
elements: PropTypes.shape({
// eslint-disable-next-line react/forbid-prop-types
edges: PropTypes.any,
// eslint-disable-next-line react/forbid-prop-types
nodes: PropTypes.any,
}).isRequired,
}).isRequired,
setLabels: PropTypes.func.isRequired,
refKey: PropTypes.string.isRequired,
setChartLegend: PropTypes.func.isRequired,
graph: PropTypes.string.isRequired,
onAddSubmit: PropTypes.func.isRequired,
onRemoveSubmit: PropTypes.func.isRequired,
openModal: PropTypes.func.isRequired,
addGraphHistory: PropTypes.func.isRequired,
addElementHistory: PropTypes.func.isRequired,
setIsTable: PropTypes.func.isRequired,
};
export default CypherResultCytoscape;