diff --git a/package-lock.json b/package-lock.json index d0cccfb..d3e6e61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "byte-size": "^8.1.1", "chroma-js": "^2.4.2", "classnames": "^2.5.1", + "color": "^4.2.3", "copy-to-clipboard": "^3.3.3", "file-saver": "^2.0.5", "graphology": "^0.25.4", @@ -83,6 +84,7 @@ "vitest": "^1.2.2" }, "devDependencies": { + "@types/color": "^3.0.6", "@types/react-linkify": "^1.0.4", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", @@ -1724,6 +1726,30 @@ "classnames": "*" } }, + "node_modules/@types/color": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.6.tgz", + "integrity": "sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A==", + "dev": true, + "dependencies": { + "@types/color-convert": "*" + } + }, + "node_modules/@types/color-convert": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.3.tgz", + "integrity": "sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==", + "dev": true, + "dependencies": { + "@types/color-name": "*" + } + }, + "node_modules/@types/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-87W6MJCKZYDhLAx/J1ikW8niMvmGRyY+rpUxWpL1cO7F8Uu5CHuQoFv+R0/L5pgNdW4jTyda42kv60uwVIPjLw==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2787,6 +2813,18 @@ "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==" }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2800,6 +2838,31 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -6671,6 +6734,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index 673b01a..c80a668 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "byte-size": "^8.1.1", "chroma-js": "^2.4.2", "classnames": "^2.5.1", + "color": "^4.2.3", "copy-to-clipboard": "^3.3.3", "file-saver": "^2.0.5", "graphology": "^0.25.4", @@ -101,6 +102,7 @@ "sigma": "3.0.0-beta.5" }, "devDependencies": { + "@types/color": "^3.0.6", "@types/react-linkify": "^1.0.4", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx index 0ef6053..30959ab 100644 --- a/src/components/ColorPicker.tsx +++ b/src/components/ColorPicker.tsx @@ -2,6 +2,7 @@ import { FC, useRef } from "react"; import { SketchPicker } from "react-color"; import { AiOutlineCheck, AiOutlineClose } from "react-icons/ai"; +import { hexToRgba, rgbaToHex } from "../utils/colors"; import Tooltip, { TooltipAPI } from "./Tooltip"; const ColorPicker: FC< @@ -14,13 +15,13 @@ const ColorPicker: FC< return ( -
onChange(color.hex)} + color={color ? hexToRgba(color) : undefined} + onChange={(color) => onChange(rgbaToHex(color.rgb))} styles={{ default: { picker: { diff --git a/src/components/GraphAppearance/index.tsx b/src/components/GraphAppearance/index.tsx index 2153397..134262b 100644 --- a/src/components/GraphAppearance/index.tsx +++ b/src/components/GraphAppearance/index.tsx @@ -1,10 +1,11 @@ -import { FC, useState } from "react"; +import { FC, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useAppearance, useAppearanceActions } from "../../core/context/dataContexts"; import { ItemType } from "../../core/types"; -import { Toggle } from "../Toggle"; -import { EdgeIcon, NodeIcon } from "../common-icons"; +import ColorPicker from "../ColorPicker"; +import { ToggleBar } from "../Toggle"; +import { EdgeIcon, GraphIcon, NodeIcon } from "../common-icons"; import { ColorItem } from "./color/ColorItem"; import { LabelItem } from "./label/LabelItem"; import { LabelSizeItem } from "./label/LabelSizeItem"; @@ -12,30 +13,46 @@ import { SizeItem } from "./size/SizeItem"; export const GraphAppearance: FC = () => { const { t } = useTranslation(); - const [showEdges, setShowEdges] = useState(false); + const [selected, setSelected] = useState("nodes"); + + const tabs = useMemo(() => { + return [ + { + value: "nodes", + label: ( + <> + {t("graph.model.nodes")} + + ), + }, + { + value: "edges", + label: ( + <> + {t("graph.model.edges")} + + ), + }, + { + value: "graph", + label: ( + <> + {t("graph.model.graph")} + + ), + }, + ]; + }, [t]); return ( <> + setSelected(e)} options={tabs} /> +
- - {t("graph.model.nodes")} - - } - rightLabel={ - <> - {t("graph.model.edges")} - - } - /> + {selected === "nodes" && } + {selected === "edges" && } + {selected === "graph" && }
- -
- - {showEdges ? : } ); }; @@ -73,3 +90,20 @@ const GraphItemAppearance: FC<{ itemType: ItemType }> = ({ itemType }) => { ); }; + +const GraphGraphAppearance: FC = () => { + const { t } = useTranslation(); + const { backgroundColor } = useAppearance(); + const { setBackgroundColorAppearance } = useAppearanceActions(); + + return ( +
+

{t("appearance.graph.background.title")}

+ +
+ + setBackgroundColorAppearance(v)} /> +
+
+ ); +}; diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx index 71ae0a0..47e2bfb 100644 --- a/src/components/Toggle.tsx +++ b/src/components/Toggle.tsx @@ -1,5 +1,5 @@ import cx from "classnames"; -import { FC } from "react"; +import { FC, Fragment } from "react"; /** * This toggle button displays and controls a boolean value, with two "left" and @@ -48,3 +48,30 @@ export const Toggle: FC<{
); }; + +export function ToggleBar(props: { + value: T; + options: Array<{ label: JSX.Element | string; value: T }>; + onChange: (value: T) => void; + className?: string; + disabled?: boolean; +}) { + const { value, onChange, options, className, disabled } = props; + return ( + + ); +} diff --git a/src/core/appearance/index.ts b/src/core/appearance/index.ts index 9f945a4..bc0d5ce 100644 --- a/src/core/appearance/index.ts +++ b/src/core/appearance/index.ts @@ -2,7 +2,7 @@ import { ItemType } from "../types"; import { atom } from "../utils/atoms"; import { Producer, producerToAction } from "../utils/producers"; import { AppearanceState, BooleanAppearance, Color, Label, LabelSize, Size } from "./types"; -import { getEmptyAppearanceState, serializeAppearanceState } from "./utils"; +import { DEFAULT_BACKGROUND_COLOR, getEmptyAppearanceState, serializeAppearanceState } from "./utils"; const resetState: Producer = () => { return () => getEmptyAppearanceState(); @@ -16,6 +16,9 @@ const setSizeAppearance: Producer = (itemType return (state) => ({ ...state, [itemType === "nodes" ? "nodesSize" : "edgesSize"]: size }); }; +const setBackgroundColorAppearance: Producer = (color) => { + return (state) => ({ ...state, backgroundColor: color || DEFAULT_BACKGROUND_COLOR }); +}; const setColorAppearance: Producer = (itemType, color) => { return (state) => ({ ...state, [itemType === "nodes" ? "nodesColor" : "edgesColor"]: color }); }; @@ -39,6 +42,7 @@ export const appearanceActions = { setShowEdges: producerToAction(setShowEdges, appearanceAtom), setSizeAppearance: producerToAction(setSizeAppearance, appearanceAtom), setColorAppearance: producerToAction(setColorAppearance, appearanceAtom), + setBackgroundColorAppearance: producerToAction(setBackgroundColorAppearance, appearanceAtom), setLabelAppearance: producerToAction(setLabelAppearance, appearanceAtom), setLabelSizeAppearance: producerToAction(setLabelSizeAppearance, appearanceAtom), } as const; diff --git a/src/core/appearance/types.ts b/src/core/appearance/types.ts index 4b76dfc..d576445 100644 --- a/src/core/appearance/types.ts +++ b/src/core/appearance/types.ts @@ -84,6 +84,7 @@ export interface AppearanceState { showEdges: BooleanAppearance; nodesSize: Size; edgesSize: Size; + backgroundColor: string; nodesColor: Color; edgesColor: EdgeColor; nodesLabel: Label; diff --git a/src/core/appearance/utils.ts b/src/core/appearance/utils.ts index eade333..ddf4b83 100644 --- a/src/core/appearance/utils.ts +++ b/src/core/appearance/utils.ts @@ -32,6 +32,7 @@ export const DEFAULT_NODE_SIZE = 20; export const DEFAULT_EDGE_SIZE = 6; export const DEFAULT_NODE_LABEL_SIZE = 14; export const DEFAULT_EDGE_LABEL_SIZE = 14; +export const DEFAULT_BACKGROUND_COLOR = "#FFFFFFFF"; export function getEmptyAppearanceState(): AppearanceState { return { @@ -47,6 +48,7 @@ export function getEmptyAppearanceState(): AppearanceState { itemType: "edges", type: "data", }, + backgroundColor: DEFAULT_BACKGROUND_COLOR, nodesColor: { itemType: "nodes", type: "data", diff --git a/src/core/graph/index.ts b/src/core/graph/index.ts index 2c90d0d..9290ed8 100644 --- a/src/core/graph/index.ts +++ b/src/core/graph/index.ts @@ -1,5 +1,5 @@ import { Attributes } from "graphology-types"; -import { isNil, last, mapValues, omit, omitBy } from "lodash"; +import { isNil, isString, last, mapValues, omit, omitBy } from "lodash"; import { Coordinates } from "sigma/types"; import { appearanceAtom } from "../appearance"; @@ -263,6 +263,7 @@ graphDatasetAtom.bind((graphDataset, previousGraphDataset) => { ...initialState, ...omitBy(appearanceState, (appearanceElement) => { if ( + !isString(appearanceElement) && appearanceElement.field && ((appearanceElement.itemType === "edges" && !edgeFields.includes(appearanceElement.field)) || (appearanceElement.itemType === "nodes" && !nodeFields.includes(appearanceElement.field))) diff --git a/src/locales/dev.json b/src/locales/dev.json index c0799aa..585527f 100644 --- a/src/locales/dev.json +++ b/src/locales/dev.json @@ -113,6 +113,12 @@ "log": "log", "spline": "spline" }, + "graph": { + "background": { + "title":"Background", + "color": "Color" + } + }, "color": { "title": "Color", "set_color_from": "Set color from...", @@ -244,6 +250,7 @@ "directed": "Directed", "undirected": "Undirected", "mixed": "Mixed", + "graph":"graph", "nodes_one": "node", "nodes_zero": "node", "nodes": "nodes", diff --git a/src/locales/en.json b/src/locales/en.json index e0dd8b8..e71d134 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -112,6 +112,12 @@ "log": "log", "spline": "spline" }, + "graph": { + "background": { + "title":"Background", + "color": "Color" + } + }, "color": { "title": "Color", "set_color_from": "Set color from...", @@ -240,6 +246,7 @@ "directed": "Directed", "undirected": "Undirected", "mixed": "Mixed", + "graph":"graph", "nodes_one": "node", "nodes_zero": "node", "nodes": "nodes", diff --git a/src/locales/fr.json b/src/locales/fr.json index 06bd7a8..82de084 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -16,7 +16,7 @@ "error": { "title": "Erreur", "unknown": "Une erreur inconnue s'est produite.", - "message": "Désolé ! geph-lite est en constante amélioration mais des bugs nous échappe... Vous pouvez signaler cette erreur sur github en cliquant sur le bouton ci-dessous.", + "message": "Désolé ! gephi-lite est en constante amélioration mais des bugs nous échappe... Vous pouvez signaler cette erreur sur github en cliquant sur le bouton ci-dessous.", "report": "Signaler l'erreur", "not_found": { "title": "Page non trouvée", @@ -113,6 +113,12 @@ "log": "logarithme", "spline": "spline" }, + "graph": { + "background": { + "title":"Arrière-plan", + "color": "Couleur" + } + }, "color": { "title": "Couleur", "set_color_from": "Définir la couleur à partir de...", @@ -240,6 +246,7 @@ "directed": "Orienté", "undirected": "Non orienté", "mixed": "Mixte", + "graph":"graph", "nodes_one": "nœud", "nodes_zero": "nœud", "nodes": "nœuds", diff --git a/src/utils/colors.ts b/src/utils/colors.ts index d4cd657..bf78e8e 100644 --- a/src/utils/colors.ts +++ b/src/utils/colors.ts @@ -1,5 +1,17 @@ import chroma from "chroma-js"; +import Color from "color"; import { memoize } from "lodash"; +import { RGBColor } from "react-color"; export const memoizedBrighten = memoize((color: string) => chroma.mix(color, "white", 0.75).hex()); export const memoizedDarken = memoize((color: string) => chroma.mix(color, "black", 0.75).hex()); + +export function hexToRgba(value: string): RGBColor { + const parsed = Color(value); + return { r: parsed.red(), g: parsed.green(), b: parsed.blue(), a: parsed.alpha() }; +} + +export function rgbaToHex(value: RGBColor): string { + const parsed = Color({ r: value.r, g: value.g, b: value.b }).alpha(value.a || 1); + return parsed.hexa(); +} diff --git a/src/views/graphPage/AppearancePanel.tsx b/src/views/graphPage/AppearancePanel.tsx index aa591f0..c2fbe30 100644 --- a/src/views/graphPage/AppearancePanel.tsx +++ b/src/views/graphPage/AppearancePanel.tsx @@ -14,8 +14,6 @@ export const AppearancePanel: FC = () => { -
- ); diff --git a/src/views/graphPage/GraphRendering.tsx b/src/views/graphPage/GraphRendering.tsx index cac75c1..2cb2a8c 100644 --- a/src/views/graphPage/GraphRendering.tsx +++ b/src/views/graphPage/GraphRendering.tsx @@ -8,7 +8,7 @@ import { FaRegDotCircle } from "react-icons/fa"; import { Settings } from "sigma/settings"; import GraphCaption from "../../components/GraphCaption"; -import { useSigmaAtom, useSigmaGraph, useSigmaState } from "../../core/context/dataContexts"; +import { useAppearance, useSigmaAtom, useSigmaGraph, useSigmaState } from "../../core/context/dataContexts"; import { resetCamera } from "../../core/sigma"; import NodeProgramBorder from "../../utils/bordered-node-program"; import { AppearanceController } from "./controllers/AppearanceController"; @@ -95,6 +95,7 @@ const GraphCaptionLayer: FC = () => { }; export const GraphRendering: FC = () => { + const { backgroundColor } = useAppearance(); const sigmaGraph = useSigmaGraph(); const { hoveredNode, hoveredEdge } = useSigmaState(); const [isReady, setIsReady] = useState(false); @@ -110,6 +111,7 @@ export const GraphRendering: FC = () => { !isReady && "visually-hidden", (hoveredNode || hoveredEdge) && "cursor-pointer", )} + style={{ backgroundColor }} graph={sigmaGraph} settings={ { diff --git a/src/views/graphPage/modals/save/ExportPNGModal.tsx b/src/views/graphPage/modals/save/ExportPNGModal.tsx index cf6260f..46449a7 100644 --- a/src/views/graphPage/modals/save/ExportPNGModal.tsx +++ b/src/views/graphPage/modals/save/ExportPNGModal.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import { FaSave, FaTimes } from "react-icons/fa"; import { Modal } from "../../../../components/modals"; -import { useSigmaAtom } from "../../../../core/context/dataContexts"; +import { useAppearance, useSigmaAtom } from "../../../../core/context/dataContexts"; import { ModalProps } from "../../../../core/modals/types"; import { useNotifications } from "../../../../core/notifications"; import { getGraphSnapshot } from "../../../../utils/sigma"; @@ -13,6 +13,7 @@ export const ExportPNGModal: FC> = ({ cancel }) => { const { t } = useTranslation(); const { notify } = useNotifications(); const sigma = useSigmaAtom(); + const { backgroundColor } = useAppearance(); const [data, setData] = useState<{ width: number; @@ -30,7 +31,7 @@ export const ExportPNGModal: FC> = ({ cancel }) => { const blob = await getGraphSnapshot(sigma.getGraph(), sigma.getSettings(), { width: data.width, height: data.height, - backgroundColor: "white", + backgroundColor, cameraState: data.preserve_camera ? sigma.getCamera().getState() : undefined, ratio: 1, }); @@ -42,7 +43,7 @@ export const ExportPNGModal: FC> = ({ cancel }) => { message: t("graph.export.png.success").toString(), }); cancel(); - }, [cancel, data.filename, data.height, data.preserve_camera, data.width, notify, sigma, t]); + }, [cancel, data.filename, data.height, data.preserve_camera, data.width, notify, sigma, t, backgroundColor]); return (