diff --git a/client/src/layout/Graph.jsx b/client/src/layout/Graph.jsx
index d4b07977..882816ca 100644
--- a/client/src/layout/Graph.jsx
+++ b/client/src/layout/Graph.jsx
@@ -1,5 +1,5 @@
import '@react-sigma/core/lib/react-sigma.min.css';
-import { Container, Col, Row, Alert, Radio, RadioGroup } from '@dataesr/react-dsfr';
+import { Container, Col, Row, Alert, Button } from '@dataesr/react-dsfr';
import {
ControlsContainer,
FullScreenControl,
@@ -12,11 +12,11 @@ import {
import { UndirectedGraph } from 'graphology';
import { useState, useEffect } from 'react';
import NodeProgramBorder from '../styles/rendering/node.border';
+import EdgeProgramCurve from '../styles/rendering/edge.curve';
import NodePanel from './NodePanel';
import ClustersPanel from './ClustersPanel';
import { groupBy } from '../utils/graphUtils';
-import { DEFAULT_NODE_COLOR, COMMUNTIY_COLORS } from '../styles/colors';
-import iwanthue from 'iwanthue';
+import { DEFAULT_NODE_COLOR, getColormap, getPalette } from '../styles/colors';
function GraphEvents({ onNodeClick, onStageClick }) {
const registerEvents = useRegisterEvents();
@@ -42,8 +42,8 @@ const highlightGraph = (graph, selectedNode) => {
(node, attr) => ({
...attr,
highlighted: node === selectedNode.id,
- label: node === selectedNode.id || graph.neighbors(selectedNode.id).includes(node) ? attr.label : '',
- color: node === selectedNode.id || graph.neighbors(selectedNode.id).includes(node) ? attr.color : '#E2E2E2',
+ label: node === selectedNode.id || graph.neighbors(selectedNode.id).includes(node) ? attr.label : null,
+ color: node === selectedNode.id || graph.neighbors(selectedNode.id).includes(node) ? attr.color : DEFAULT_NODE_COLOR,
}),
{ attributes: ['highlighted', 'color'] },
);
@@ -62,6 +62,8 @@ export default function Graph({ data, selectedGraph }) {
const { publications, structures } = data;
const [selectedNode, setSelectedNode] = useState(null);
+ const [switchMode, enableSwitchMode] = useState(false);
+ console.log('switchMode', switchMode);
console.log('selectedGraph', selectedGraph);
const graph = UndirectedGraph.from(data.graph[selectedGraph]);
@@ -74,23 +76,15 @@ export default function Graph({ data, selectedGraph }) {
const communities = groupBy(data.graph[selectedGraph].nodes, ({ attributes }) => attributes.community);
// console.log('communities', communities);
- // With some options
- // const palette = iwanthue(Object.keys(communities).length, {
- // clustering: 'force-vector',
- // colorSpace: 'sensible',
- // seed: 42,
- // attempts: 5,
- // });
-
- // console.log('palette', palette);
-
// Update nodes color
+ const palette = (switchMode) ? getColormap() : getPalette(Object.keys(communities).length);
graph.updateEachNodeAttributes(
(node, attr) => ({
...attr,
- color: COMMUNTIY_COLORS?.[attr.community] || DEFAULT_NODE_COLOR,
+ color: ((switchMode) ? palette[palette.length - 1 + Math.max(Object.keys(attr?.years ?? {})) - 2023] : palette?.[attr.community]) || DEFAULT_NODE_COLOR,
+ communityColor: palette?.[attr.community] || DEFAULT_NODE_COLOR,
}),
- { attributes: ['color'] },
+ { attributes: ['color', 'communityColor'] },
);
return (
@@ -100,7 +94,8 @@ export default function Graph({ data, selectedGraph }) {
{
@@ -122,6 +117,18 @@ export default function Graph({ data, selectedGraph }) {
+
+
+
+ {`Items: ${graph.order} | Links: ${graph.size} | Clusters: ${Object.keys(communities).length}`}
+
+
+
+
+
+
diff --git a/client/src/layout/NodePanel.jsx b/client/src/layout/NodePanel.jsx
index 18dd140b..4ee32873 100644
--- a/client/src/layout/NodePanel.jsx
+++ b/client/src/layout/NodePanel.jsx
@@ -2,7 +2,6 @@ import '@react-sigma/core/lib/react-sigma.min.css';
import { Badge, BadgeGroup, Title, Accordion, AccordionItem, Container } from '@dataesr/react-dsfr';
import { GetColorName } from 'hex-color-to-color-name';
import { publicationsGetTopicsCount } from '../utils/publicationUtils';
-import { COMMUNTIY_COLORS } from '../styles/colors';
export default function NodePanel({ selectedNode, graph, publications }) {
if (!selectedNode || !graph.order) return null;
@@ -19,13 +18,13 @@ export default function NodePanel({ selectedNode, graph, publications }) {
publications[publicationId].year),
)}`}
/>
@@ -38,9 +37,9 @@ export default function NodePanel({ selectedNode, graph, publications }) {
))}
{(selectedNode.domains && (
-
- {Object.entries(selectedNode.domains).map(({ key, value }) => (
- {`${key}: ${value}`}
+
+ {Object.entries(selectedNode.domains).map((item) => (
+ {`${item[0]} (${item[1]})`}
))}
))}
diff --git a/client/src/styles/colors.js b/client/src/styles/colors.js
index f6737252..915f6d53 100644
--- a/client/src/styles/colors.js
+++ b/client/src/styles/colors.js
@@ -1,4 +1,7 @@
-export const DEFAULT_NODE_COLOR = '#7b7b7b';
+import iwanthue from 'iwanthue';
+import colormap from 'colormap';
+
+export const DEFAULT_NODE_COLOR = '#E2E2E2';
export const COMMUNTIY_COLORS = [
'#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#bcbd22', '#17becf', '#66c2a5',
'#fc8d62', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f', '#e5c494', '#aec7e8', '#ffbb78', '#98df8a', '#ff9896',
@@ -8,3 +11,18 @@ export const COMMUNTIY_COLORS = [
'#a55194', '#ce6dbd', '#de9ed6', '#3182bd', '#6baed6', '#9ecae1', '#c6dbef', '#e6550d', '#fd8d3c', '#fdae6b',
'#fdd0a2', '#31a354', '#74c476', '#a1d99b', '#c7e9c0', '#756bb1', '#9e9ac8', '#bcbddc', '#dadaeb',
];
+export const VIRIDIS_COLORS = ['#46327e', '#365c8d', '#277f8e', '#1fa187', '#4ac16d', '#a0da39'];
+
+export const getPalette = (size) => iwanthue(size, {
+ clustering: 'force-vector',
+ colorSpace: 'default',
+ quality: 100,
+ seed: 42,
+});
+
+export const getColormap = (size = 9) => colormap({
+ colormap: 'cool',
+ nshades: size,
+ format: 'hex',
+ alpha: 1,
+});
diff --git a/client/src/styles/rendering/edge.curve.frag.glsl b/client/src/styles/rendering/edge.curve.frag.glsl
new file mode 100644
index 00000000..656b4e5e
--- /dev/null
+++ b/client/src/styles/rendering/edge.curve.frag.glsl
@@ -0,0 +1,42 @@
+precision mediump float;
+
+varying vec4 v_color;
+varying float v_thickness;
+varying vec2 v_cpA;
+varying vec2 v_cpB;
+varying vec2 v_cpC;
+
+float det(vec2 a, vec2 b) {
+ return a.x * b.y - b.x * a.y;
+}
+
+vec2 get_distance_vector(vec2 b0, vec2 b1, vec2 b2) {
+ float a = det(b0, b2), b = 2.0 * det(b1, b0), d = 2.0 * det(b2, b1);
+ float f = b * d - a * a;
+ vec2 d21 = b2 - b1, d10 = b1 - b0, d20 = b2 - b0;
+ vec2 gf = 2.0 * (b * d21 + d * d10 + a * d20);
+ gf = vec2(gf.y, -gf.x);
+ vec2 pp = -f * gf / dot(gf, gf);
+ vec2 d0p = b0 - pp;
+ float ap = det(d0p, d20), bp = 2.0 * det(d10, d0p);
+ float t = clamp((ap + bp) / (2.0 * a + b + d), 0.0, 1.0);
+ return mix(mix(b0, b1, t), mix(b1, b2, t), t);
+}
+
+float distToQuadraticBezierCurve(vec2 p, vec2 b0, vec2 b1, vec2 b2) {
+ return length(get_distance_vector(b0 - p, b1 - p, b2 - p));
+}
+
+const float epsilon = 0.7;
+const vec4 transparent = vec4(0.0, 0.0, 0.0, 0.0);
+
+void main(void) {
+ float dist = distToQuadraticBezierCurve(gl_FragCoord.xy, v_cpA, v_cpB, v_cpC);
+
+ if (dist < v_thickness + epsilon) {
+ float inCurve = 1.0 - smoothstep(v_thickness - epsilon, v_thickness + epsilon, dist);
+ gl_FragColor = inCurve * vec4(v_color.rgb * v_color.a, v_color.a);
+ } else {
+ gl_FragColor = transparent;
+ }
+}
\ No newline at end of file
diff --git a/client/src/styles/rendering/edge.curve.js b/client/src/styles/rendering/edge.curve.js
new file mode 100644
index 00000000..f7164d19
--- /dev/null
+++ b/client/src/styles/rendering/edge.curve.js
@@ -0,0 +1,102 @@
+/* eslint-disable */
+import { floatColor, canUse32BitsIndices } from 'sigma/utils';
+import { AbstractEdgeProgram } from 'sigma/rendering/webgl/programs/common/edge';
+
+import vertexShaderSource from './edge.curve.vert.glsl?raw';
+import fragmentShaderSource from './edge.curve.frag.glsl?raw';
+
+const POINTS = 2,
+ ATTRIBUTES = 3;
+
+export default class EdgeFastProgram extends AbstractEdgeProgram {
+ positionLocation;
+ colorLocation;
+ matrixLocation;
+
+ constructor(gl) {
+ super(gl, vertexShaderSource, fragmentShaderSource, POINTS, ATTRIBUTES);
+
+ // Locations:
+ this.positionLocation = gl.getAttribLocation(this.program, "a_position");
+ this.colorLocation = gl.getAttribLocation(this.program, "a_color");
+
+ // Uniform locations:
+ const matrixLocation = gl.getUniformLocation(this.program, "u_matrix");
+ if (matrixLocation === null) throw new Error("EdgeFastProgram: error while getting matrixLocation");
+ this.matrixLocation = matrixLocation;
+
+ this.bind();
+ }
+
+ bind() {
+ const gl = this.gl;
+
+ // Bindings
+ gl.enableVertexAttribArray(this.positionLocation);
+ gl.enableVertexAttribArray(this.colorLocation);
+
+ gl.vertexAttribPointer(
+ this.positionLocation,
+ 2,
+ gl.FLOAT,
+ false,
+ this.attributes * Float32Array.BYTES_PER_ELEMENT,
+ 0,
+ );
+ gl.vertexAttribPointer(
+ this.colorLocation,
+ 4,
+ gl.UNSIGNED_BYTE,
+ true,
+ this.attributes * Float32Array.BYTES_PER_ELEMENT,
+ 8,
+ );
+ }
+
+ computeIndices() {
+ //nothing to do
+ }
+
+ process(sourceData, targetData, data, hidden, offset) {
+ const array = this.array;
+
+ let i = 0;
+ if (hidden) {
+ for (let l = i + POINTS * ATTRIBUTES; i < l; i++) array[i] = 0;
+ return;
+ }
+
+ const x1 = sourceData.x,
+ y1 = sourceData.y,
+ x2 = targetData.x,
+ y2 = targetData.y,
+ color = floatColor(data.color);
+
+ i = POINTS * ATTRIBUTES * offset;
+
+ // First point
+ array[i++] = x1;
+ array[i++] = y1;
+ array[i++] = color;
+
+ // Second point
+ array[i++] = x2;
+ array[i++] = y2;
+ array[i] = color;
+ }
+
+ render(params) {
+ if (this.hasNothingToRender()) return;
+
+ const gl = this.gl;
+ const program = this.program;
+
+ gl.useProgram(program);
+
+ gl.uniformMatrix3fv(this.matrixLocation, false, params.matrix);
+
+ gl.drawArrays(gl.LINES, 0, this.array.length / ATTRIBUTES);
+ }
+}
+
+
diff --git a/client/src/styles/rendering/edge.curve.vert.glsl b/client/src/styles/rendering/edge.curve.vert.glsl
new file mode 100644
index 00000000..e940341e
--- /dev/null
+++ b/client/src/styles/rendering/edge.curve.vert.glsl
@@ -0,0 +1,79 @@
+attribute vec4 a_color;
+attribute float a_direction;
+attribute float a_thickness;
+attribute vec2 a_source;
+attribute vec2 a_target;
+attribute float a_current;
+
+uniform mat3 u_matrix;
+uniform float u_sizeRatio;
+uniform float u_pixelRatio;
+uniform vec2 u_dimensions;
+
+varying vec4 v_color;
+varying float v_thickness;
+varying vec2 v_cpA;
+varying vec2 v_cpB;
+varying vec2 v_cpC;
+
+const float bias = 255.0 / 254.0;
+const float epsilon = 0.7;
+const float minThickness = 0.3;
+
+const float defaultCurveness = 0.25;
+
+vec2 clipspaceToViewport(vec2 pos, vec2 dimensions) {
+ return vec2(
+ (pos.x + 1.0) * dimensions.x / 2.0,
+ (pos.y + 1.0) * dimensions.y / 2.0
+ );
+}
+
+vec2 viewportToClipspace(vec2 pos, vec2 dimensions) {
+ return vec2(
+ pos.x / dimensions.x * 2.0 - 1.0,
+ pos.y / dimensions.y * 2.0 - 1.0
+ );
+}
+
+void main() {
+
+ const float curveness = defaultCurveness;
+
+ // Selecting the correct position
+ // Branchless "position = a_source if a_current == 1.0 else a_target"
+ vec2 position = a_source * max(0.0, a_current) + a_target * max(0.0, 1.0 - a_current);
+ position = (u_matrix * vec3(position, 1)).xy;
+
+ vec2 source = (u_matrix * vec3(a_source, 1)).xy;
+ vec2 target = (u_matrix * vec3(a_target, 1)).xy;
+
+ vec2 viewportPosition = clipspaceToViewport(position, u_dimensions);
+ vec2 viewportSource = clipspaceToViewport(source, u_dimensions);
+ vec2 viewportTarget = clipspaceToViewport(target, u_dimensions);
+
+ vec2 delta = viewportTarget.xy - viewportSource.xy;
+ float len = length(delta);
+ vec2 normal = vec2(-delta.y, delta.x) * a_direction;
+ vec2 unitNormal = normal / len;
+ float boundingBoxThickness = len * curveness;
+ float curveThickness = max(minThickness, a_thickness / 2.0 / u_sizeRatio * u_pixelRatio);
+
+ v_thickness = curveThickness;
+
+ v_cpA = viewportSource;
+ v_cpB = 0.5 * (viewportSource + viewportTarget) + unitNormal * a_direction * boundingBoxThickness;
+ v_cpC = viewportTarget;
+
+ vec2 viewportOffsetPosition = (
+ viewportPosition +
+ unitNormal * (boundingBoxThickness / 2.0 + curveThickness + epsilon) *
+ max(0.0, a_direction) // NOTE: cutting the bounding box in half to avoid overdraw
+ );
+
+ position = viewportToClipspace(viewportOffsetPosition, u_dimensions);
+ gl_Position = vec4(position, 0, 1);
+
+ v_color = a_color;
+ v_color.a *= bias;
+}
\ No newline at end of file
diff --git a/client/src/utils/graphUtils.js b/client/src/utils/graphUtils.js
index 746f8892..d77b5d7d 100644
--- a/client/src/utils/graphUtils.js
+++ b/client/src/utils/graphUtils.js
@@ -9,7 +9,7 @@ export const graphEncodeToJson = (data) => {
label: node.attributes?.label,
cluster: (node.attributes?.community ?? 0) + 1,
weights: { Weight: node.attributes?.weight, Degree: node.attributes?.degree },
- scores: { 'Degree/weight': (node.attributes?.degree ?? 0) / (node.attributes?.weight || 1) },
+ scores: { 'Last year': Math.max(Object.keys(node.attributes?.years ?? {})) ?? 0 },
});
});
diff --git a/package-lock.json b/package-lock.json
index 298f5053..4bac2d44 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"server"
],
"dependencies": {
+ "colormap": "^2.3.2",
"concurrently": "^8.0.1",
"d3": "^7.8.5",
"graphology": "^0.25.1",
@@ -2459,6 +2460,14 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
+ "node_modules/colormap": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/colormap/-/colormap-2.3.2.tgz",
+ "integrity": "sha512-jDOjaoEEmA9AgA11B/jCSAvYE95r3wRoAyTf3LEHGiUVlNHJaL1mRkf5AyLSpQBVGfTEPwGEqCIzL+kgr2WgNA==",
+ "dependencies": {
+ "lerp": "^1.0.3"
+ }
+ },
"node_modules/colorspace": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
@@ -5148,6 +5157,11 @@
"node": ">=0.10"
}
},
+ "node_modules/lerp": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/lerp/-/lerp-1.0.3.tgz",
+ "integrity": "sha512-70Rh4rCkJDvwWiTsyZ1HmJGvnyfFah4m6iTux29XmasRiZPDBpT9Cfa4ai73+uLZxnlKruUS62jj2lb11wURiA=="
+ },
"node_modules/leven": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-4.0.0.tgz",
diff --git a/package.json b/package.json
index 3808eba4..2d457530 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"server"
],
"dependencies": {
+ "colormap": "^2.3.2",
"concurrently": "^8.0.1",
"d3": "^7.8.5",
"graphology": "^0.25.1",