diff --git a/src/components/main-space/new-icicle/icicles-container.tsx b/src/components/main-space/new-icicle/icicles-container.tsx
index 845705812..b8055bdc5 100644
--- a/src/components/main-space/new-icicle/icicles-container.tsx
+++ b/src/components/main-space/new-icicle/icicles-container.tsx
@@ -1,8 +1,13 @@
-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 (
);
};
diff --git a/src/components/main-space/new-icicle/icicles-elements.ts b/src/components/main-space/new-icicle/icicles-elements.ts
new file mode 100644
index 000000000..a329a3d05
--- /dev/null
+++ b/src/components/main-space/new-icicle/icicles-elements.ts
@@ -0,0 +1,89 @@
+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[ROOT_FF_ID],
+ (element) =>
+ element?.children.map((childId) => filesAndFolders[childId]) ?? []
+ )
+ .sum((d) => d?.file_size ?? 0);
+ return d3
+ .partition()
+ .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)}`);
+};
diff --git a/src/components/main-space/new-icicle/icicles-utils.ts b/src/components/main-space/new-icicle/icicles-utils.ts
new file mode 100644
index 000000000..1fe3f4912
--- /dev/null
+++ b/src/components/main-space/new-icicle/icicles-utils.ts
@@ -0,0 +1,79 @@
+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 = {};
+
+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);
+};
diff --git a/src/components/main-space/new-icicle/icicles.tsx b/src/components/main-space/new-icicle/icicles.tsx
index 5cc3039da..171683f1b 100644
--- a/src/components/main-space/new-icicle/icicles.tsx
+++ b/src/components/main-space/new-icicle/icicles.tsx
@@ -1,158 +1,112 @@
-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 = ({
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[ROOT_FF_ID],
- (element) =>
- element?.children.map((childId) => filesAndFolders[childId]) ?? []
- )
- .sum((d) => d?.file_size ?? 0);
-
- return d3
- .partition()
- .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 = {};
-
- 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 ;
+ useEffect(() => {
+ getAllRects(iciclesRef)
+ .on("click", handleLockedElement)
+ .on("mouseover", handleSetCurrentHoveredElementId)
+ .on("mouseout", handleResetCurrentHoveredElementId);
+ }, [currentLockedElementId, iciclesRef]);
+
+ return (
+
+ );
};
export default Icicles;
diff --git a/src/css/index.scss b/src/css/index.scss
index 850acc5e0..3afc697e0 100644
--- a/src/css/index.scss
+++ b/src/css/index.scss
@@ -14,3 +14,8 @@ body {
h4 {
font-family: "Quicksand", sans-serif;
}
+
+#icicles {
+ width: 100%;
+ height: 80%;
+}