Commit 0e9e9e81 authored by mehdilouraoui's avatar mehdilouraoui
Browse files

feat(icicles): handle events, state & refacto icicles

parent f0040d16
import React, { FC } from "react";
import { useSelector } from "react-redux";
import React, { FC, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getFilesAndFoldersFromStore } from "reducers/files-and-folders/files-and-folders-selectors";
import Icicles from "./icicles";
import { getFilesAndFoldersMetadataFromStore } from "reducers/files-and-folders-metadata/files-and-folders-metadata-selectors";
import {
setHoveredElementId,
setLockedElementId,
} from "reducers/workspace-metadata/workspace-metadata-actions";
import { getWorkspaceMetadataFromStore } from "reducers/workspace-metadata/workspace-metadata-selectors";
const IciclesContainer: FC = () => {
const filesAndFoldersMap = useSelector(getFilesAndFoldersFromStore);
......@@ -10,10 +15,40 @@ const IciclesContainer: FC = () => {
getFilesAndFoldersMetadataFromStore
);
const { lockedElementId } = useSelector(getWorkspaceMetadataFromStore);
const dispatch = useDispatch();
const setHoveredElement = useCallback(
(id) => dispatch(setHoveredElementId(id)),
[dispatch]
);
const resetHoveredElement = useCallback(
() => dispatch(setHoveredElementId("")),
[dispatch]
);
const setLockedElement = useCallback(
(id) => {
dispatch(setLockedElementId(id));
},
[dispatch]
);
const resetLockedElement = useCallback(
() => dispatch(setLockedElementId("")),
[dispatch]
);
return (
<Icicles
filesAndFolders={filesAndFoldersMap}
filesAndFoldersMetadata={filesAndFoldersMetadataMap}
setHoveredElement={setHoveredElement}
resetHoveredElement={resetHoveredElement}
setLockedElement={setLockedElement}
resetLockedElement={resetLockedElement}
lockedElementId={lockedElementId}
/>
);
};
......
import { ROOT_FF_ID } from "reducers/files-and-folders/files-and-folders-selectors";
import { FilesAndFolders } from "reducers/files-and-folders/files-and-folders-types";
import { fromFileName } from "./../../../util/color/color-util";
import {
getRectangleHeight,
isLabelVisible,
format,
VIEWBOX_WIDTH,
VIEWBOX_HEIGHT,
handleZoom,
} from "./icicles-utils";
import { FOLDER_COLOR } from "util/color/color-util";
import * as d3 from "d3";
export const createPartition = (filesAndFolders) => {
const root = d3
.hierarchy<FilesAndFolders>(
filesAndFolders[ROOT_FF_ID],
(element) =>
element?.children.map((childId) => filesAndFolders[childId]) ?? []
)
.sum((d) => d?.file_size ?? 0);
return d3
.partition<FilesAndFolders>()
.size([VIEWBOX_HEIGHT, ((root.height + 1) * VIEWBOX_WIDTH) / 10])(root);
};
export const createSvg = (ref) =>
d3
.select(ref.current)
.append("svg")
.attr("viewBox", [0, 0, VIEWBOX_WIDTH, VIEWBOX_HEIGHT].join(" "))
.style("font", "10px Quicksand");
export const createCell = (svg, root) => {
return svg
.selectAll("g")
.data(root.descendants())
.join("g")
.attr("transform", ({ x0, y0 }) => `translate(${y0},${x0})`);
};
export const createRect = (cell, elements) => {
return cell
.append("rect")
.attr("width", ({ y0, y1 }) => y1 - y0 - 1)
.attr("height", (d) => getRectangleHeight(d))
.attr("fill-opacity", 0.5)
.attr("fill", (d: any) => {
if (!d.depth) {
return "#ccc";
}
if (d.data.children.length > 0) {
return FOLDER_COLOR;
}
return fromFileName(d.data.name);
})
.style("cursor", "pointer")
.on("dblclick", (_, currentRect) => handleZoom(_, currentRect, elements));
};
export const createTitle = (cellElement) => {
const cell = cellElement
.append("text")
.style("user-select", "none")
.attr("pointer-events", "none")
.attr("x", 4)
.attr("y", 13)
.attr("fill-opacity", (d) => +isLabelVisible(d))
.append("tspan")
.text((d: any) => d.data.name);
cell.append("title").text(
(d: any) =>
`${d
.ancestors()
.map((d: any) => d.data.name)
.reverse()
.join("/")}\n${format(d.value)}`
);
return cell;
};
export const createSubtitle = (title) => {
return title
.append("tspan")
.attr("fill-opacity", (d: any) => (isLabelVisible(d) ? 0.7 : 0))
.text((d: any) => ` ${format(d.value)}`);
};
import * as d3 from "d3";
export const VIEWBOX_WIDTH = 1000;
export const VIEWBOX_HEIGHT = 300;
export const TRANSITION_DURATION = 750;
type Dimensions = {
x0: number;
y0: number;
x1: number;
y1: number;
};
export const getRectangleHeight = ({ x0, x1 }): number => {
return x1 - x0 - Math.min(1, (x1 - x0) / 2);
};
export const getCurrentRect = (icicles, currentElementId) => {
const rects = getAllRects(icicles);
return rects.filter(({ data: { id } }) => id === currentElementId.data.id);
};
export const getAllRects = (icicles) =>
d3.select(icicles.current).selectAll("rect");
export const getRectById = (icicles, rectId) => {
const rects = getAllRects(icicles);
return rects.filter(({ data: { id } }) => id === rectId);
};
export const format = d3.format(",d");
export const isLabelVisible = ({ x0, x1, y0, y1 }): boolean => {
return y1 <= VIEWBOX_WIDTH && y0 >= 0 && x1 - x0 > 16;
};
export const dimensionsTarget: Record<string, Dimensions> = {};
export const getDimensions = (dimensions) =>
dimensionsTarget[dimensions.data.id];
export const handleZoom = (_, currentRect, elements) => {
let { root, cell, rect, title, subtitle } = elements;
elements.focus =
elements.focus === currentRect
? (currentRect = currentRect.parent)
: currentRect;
root.each((dimensions) => {
dimensionsTarget[dimensions.data.id] = {
x0:
((dimensions.x0 - currentRect.x0) / (currentRect.x1 - currentRect.x0)) *
VIEWBOX_HEIGHT,
x1:
((dimensions.x1 - currentRect.x0) / (currentRect.x1 - currentRect.x0)) *
VIEWBOX_HEIGHT,
y0: dimensions.y0 - currentRect.y0,
y1: dimensions.y1 - currentRect.y0,
};
});
const transition = cell
.transition()
.duration(TRANSITION_DURATION)
.attr(
"transform",
(d: any) => `translate(${getDimensions(d).y0},${getDimensions(d).x0})`
);
rect
.transition(transition)
.attr("height", (d: any) => getRectangleHeight(getDimensions(d)));
title
.transition(transition)
.attr("fill-opacity", (d: any) => +isLabelVisible(getDimensions(d)));
subtitle
.transition(transition)
.attr("fill-opacity", (d: any) => +isLabelVisible(getDimensions(d)) * 0.7);
};
import React, { FC, useEffect, useRef } from "react";
import * as d3 from "d3";
import {
FilesAndFolders,
FilesAndFoldersMap,
} from "reducers/files-and-folders/files-and-folders-types";
import { ROOT_FF_ID } from "reducers/files-and-folders/files-and-folders-selectors";
import React, { FC, useEffect, useRef, useState } from "react";
import { FilesAndFoldersMap } from "reducers/files-and-folders/files-and-folders-types";
import { FilesAndFoldersMetadataMap } from "reducers/files-and-folders-metadata/files-and-folders-metadata-types";
import { FOLDER_COLOR, fromFileName } from "util/color/color-util";
type Dimensions = {
x0: number;
y0: number;
x1: number;
y1: number;
};
import { getAllRects, getCurrentRect, getRectById } from "./icicles-utils";
import {
createCell,
createRect,
createSvg,
createTitle,
createSubtitle,
createPartition,
} from "./icicles-elements";
type IciclesProps = {
filesAndFolders: FilesAndFoldersMap;
filesAndFoldersMetadata: FilesAndFoldersMetadataMap;
setHoveredElement: (id: string) => void;
resetHoveredElement: () => void;
setLockedElement: (id: string) => void;
resetLockedElement: () => void;
lockedElementId: string;
};
const viewboxWidth = 1000;
const viewboxHeight = 300;
const TRANSITION_DURATION = 750;
const Icicles: FC<IciclesProps> = ({
filesAndFolders,
filesAndFoldersMetadata,
setHoveredElement,
setLockedElement,
resetLockedElement,
lockedElementId,
}) => {
const [currentHoveredElementId, setCurrentHoveredElementId] = useState("");
const [currentLockedElementId, setCurrentLockedElementId] = useState("");
const iciclesRef = useRef(null);
const format = d3.format(",d");
const root = createPartition(filesAndFolders);
let focus = root;
const getRectangleHeight = ({ x0, x1 }): number => {
return x1 - x0 - Math.min(1, (x1 - x0) / 2);
const handleResetLockedElement = ({ target, currentTarget }) => {
if (target === currentTarget && currentLockedElementId.length) {
resetLockedElement();
setCurrentLockedElementId("");
getAllRects(iciclesRef).style("fill-opacity", 0.5);
}
};
const isLabelVisible = ({ x0, x1, y0, y1 }): boolean => {
return y1 <= viewboxWidth && y0 >= 0 && x1 - x0 > 16;
const handleLockedElement = (_, lockedElement) => {
if (lockedElement.data.id === currentLockedElementId) return;
setLockedElement(lockedElement.data.id);
setCurrentLockedElementId(lockedElement.data.id);
getCurrentRect(iciclesRef, lockedElement).style("fill-opacity", 1);
currentLockedElementId.length
? getRectById(iciclesRef, currentLockedElementId).style(
"fill-opacity",
0.5
)
: null;
};
const partition = () => {
const root = d3
.hierarchy<FilesAndFolders>(
filesAndFolders[ROOT_FF_ID],
(element) =>
element?.children.map((childId) => filesAndFolders[childId]) ?? []
)
.sum((d) => d?.file_size ?? 0);
return d3
.partition<FilesAndFolders>()
.size([viewboxHeight, ((root.height + 1) * viewboxWidth) / 10])(root);
const handleSetCurrentHoveredElementId = (_, hoveredElement) => {
if (hoveredElement.data.id === currentLockedElementId) return;
setCurrentHoveredElementId(hoveredElement.data.id);
getCurrentRect(iciclesRef, hoveredElement).style("fill-opacity", 0.75);
};
const root = partition();
const handleResetCurrentHoveredElementId = (_, hoveredElement) => {
if (hoveredElement.data.id === currentLockedElementId) return;
let focus = root;
setCurrentHoveredElementId("");
getCurrentRect(iciclesRef, hoveredElement).style("fill-opacity", 0.5);
};
useEffect(() => {
const onClicked = (event, p) => {
focus = focus === p ? (p = p.parent) : p;
const dimensionsTarget: Record<string, Dimensions> = {};
const getDimensions = (dimensions) =>
dimensionsTarget[dimensions.data.id];
root.each((dimensions) => {
dimensionsTarget[dimensions.data.id] = {
x0: ((dimensions.x0 - p.x0) / (p.x1 - p.x0)) * viewboxHeight,
x1: ((dimensions.x1 - p.x0) / (p.x1 - p.x0)) * viewboxHeight,
y0: dimensions.y0 - p.y0,
y1: dimensions.y1 - p.y0,
};
});
const transition = cell
.transition()
.duration(TRANSITION_DURATION)
.attr(
"transform",
(d: any) => `translate(${getDimensions(d).y0},${getDimensions(d).x0})`
);
rect
.transition(transition)
.attr("height", (d: any) => getRectangleHeight(getDimensions(d)));
text
.transition(transition)
.attr("fill-opacity", (d: any) => +isLabelVisible(getDimensions(d)));
tspan
.transition(transition)
.attr(
"fill-opacity",
(d: any) => +isLabelVisible(getDimensions(d)) * 0.7
);
};
const svg = d3
.select(iciclesRef.current)
.append("svg")
.attr("viewBox", [0, 0, viewboxWidth, viewboxHeight].join(" "))
.style("font", "10px Quicksand");
const cell = svg
.selectAll("g")
.data(root.descendants())
.join("g")
.attr("transform", ({ x0, y0 }) => `translate(${y0},${x0})`);
const rect = cell
.append("rect")
.attr("width", ({ y0, y1 }) => y1 - y0 - 1)
.attr("height", (d) => getRectangleHeight(d))
.attr("fill-opacity", 0.6)
.attr("fill", (d: any) => {
if (!d.depth) {
return "#ccc";
}
if (d.data.children.length > 0) {
return FOLDER_COLOR;
}
return fromFileName(d.data.name);
})
.style("cursor", "pointer")
.on("click", onClicked);
const text = cell
.append("text")
.style("user-select", "none")
.attr("pointer-events", "none")
.attr("x", 4)
.attr("y", 13)
.attr("fill-opacity", (d) => +isLabelVisible(d));
text.append("tspan").text((d: any) => d.data.name);
const tspan = text
.append("tspan")
.attr("fill-opacity", (d: any) => (isLabelVisible(d) ? 0.7 : 0))
.text((d: any) => ` ${format(d.value)}`);
cell.append("title").text(
(d: any) =>
`${d
.ancestors()
.map((d: any) => d.data.name)
.reverse()
.join("/")}\n${format(d.value)}`
);
if (!lockedElementId) {
setHoveredElement(currentHoveredElementId);
}
}, [lockedElementId, setHoveredElement, currentHoveredElementId]);
useEffect(() => {
const iciclesElements = {};
const svg = createSvg(iciclesRef);
const cell = createCell(svg, root);
const rect = createRect(cell, iciclesElements);
const title = createTitle(cell);
const subtitle = createSubtitle(title);
Object.assign(iciclesElements, {
root,
focus,
cell,
rect,
title,
subtitle,
});
}, []);
return <svg ref={iciclesRef} width="100%" height="80%" />;
useEffect(() => {
getAllRects(iciclesRef)
.on("click", handleLockedElement)
.on("mouseover", handleSetCurrentHoveredElementId)
.on("mouseout", handleResetCurrentHoveredElementId);
}, [currentLockedElementId, iciclesRef]);
return (
<svg ref={iciclesRef} id="icicles" onClick={handleResetLockedElement} />
);
};
export default Icicles;
......@@ -14,3 +14,8 @@ body {
h4 {
font-family: "Quicksand", sans-serif;
}
#icicles {
width: 100%;
height: 80%;
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment