diff --git a/packages/frontend/src/components/graph/Graph.tsx b/packages/frontend/src/components/graph/Graph.tsx index 7ade370f..f967c6dd 100644 --- a/packages/frontend/src/components/graph/Graph.tsx +++ b/packages/frontend/src/components/graph/Graph.tsx @@ -6,6 +6,7 @@ import { QuizGitGraphCommit } from "../../types/quiz"; import fillColor from "./fillColor"; import { InitialDataProps, parsingMultipleParents } from "./parsing"; +import renderTooltip from "./renderTooltip"; const { grey500 } = color.$scale; @@ -24,7 +25,6 @@ function renderD3(svgRef: RefObject, data: InitialDataProps[]) { const svg = d3.select(svgRef.current); if (!parsedData.length) { - svg.select("#text").selectAll("*").remove(); svg.select("#link").selectAll("*").remove(); svg.select("#node").selectAll("*").remove(); return; @@ -45,24 +45,6 @@ function renderD3(svgRef: RefObject, data: InitialDataProps[]) { const treeData = treeLayout(rootNode); fillColor(treeData); - // Add text next to each node - svg - .select("#text") - .selectAll("text") - .data(treeData.descendants()) - .join( - (enter) => enter.append("text").style("opacity", 0), - (update) => update, - (exit) => - exit.transition().duration(DURATION).style("opacity", 0).remove(), - ) - .text((d) => d.data.message) - .attr("x", (d) => d.x + 20) - .attr("y", (d) => d.y + 5) - .transition() - .duration(DURATION) - .style("opacity", 1); - additionalLinks.forEach(({ id, parentId }) => { const sourceNode = treeData.descendants().filter((d) => d.id === id)[0]; const targetNode = treeData @@ -106,6 +88,12 @@ function renderD3(svgRef: RefObject, data: InitialDataProps[]) { (exit) => exit.transition().duration(DURATION).style("opacity", 0).remove(), ) + .on("mouseover", (event, d) => { + const existingTooltip = svg.select("#tooltip"); + if (existingTooltip.empty()) { + renderTooltip(svg, d); + } + }) .attr("cx", (d) => d.x) .attr("cy", (d) => d.y) .attr("r", 13) @@ -114,10 +102,20 @@ function renderD3(svgRef: RefObject, data: InitialDataProps[]) { .transition() .duration(DURATION) .style("opacity", 1) + .style("cursor", "pointer") .attr( "fill", (d: d3.HierarchyPointNode) => d.data?.color ?? "", ); + + svg.select("#node").on("mouseout", (event) => { + const hoverTooltip = + event.relatedTarget.nodeName === "rect" || + event.relatedTarget.nodeName === "tspan"; + if (!hoverTooltip) { + svg.select("#tooltip").remove(); + } + }); } interface GraphProps { @@ -138,7 +136,6 @@ export function Graph({ data, className = "" }: GraphProps) { - diff --git a/packages/frontend/src/components/graph/renderTooltip.ts b/packages/frontend/src/components/graph/renderTooltip.ts new file mode 100644 index 00000000..077880d0 --- /dev/null +++ b/packages/frontend/src/components/graph/renderTooltip.ts @@ -0,0 +1,116 @@ +import * as d3 from "d3"; + +import color from "../../design-system/tokens/color"; + +import { InitialDataProps } from "./parsing"; + +interface TooltipProps { + text: string; + value: string; + id: string; +} + +export default function renderTooltip( + svg: d3.Selection, + d: d3.HierarchyPointNode, +) { + const tooltipStyle = { + width: 220, + height: 40, + borderRadius: 5, + position: `translate(${d.x - 110},${d.y - 60})`, + }; + + const transparentRectPosition = "translate(95, 40)"; + + const tooltipDataFormat = [ + { + text: "Commit Hash: ", + value: d.data.id.slice(0, 7), + id: "text", + }, + { + text: "Commit Message: ", + value: d.data.message, + id: "value", + }, + ]; + + // hover 시 보이는 툴팁 + const tooltip = svg + .select("#node") + .append("g") + .attr("id", "tooltip") + .attr("tabindex", 0) + .attr("transform", tooltipStyle.position); + + tooltip + .append("rect") + .attr("width", tooltipStyle.width) + .attr("height", tooltipStyle.height) + .attr("rx", tooltipStyle.borderRadius) + .attr("ry", tooltipStyle.borderRadius) + .attr("fill", color.$semantic.bgDefault) + .attr("stroke", color.$scale.grey300); + + // 투명한 네모를 위에 위치시키기 + tooltip + .append("rect") + .attr("width", 30) + .attr("height", 30) + // 요기 바꿔보심 돼여 + .attr("fill", "transparent") + .style("cursor", "pointer") + .attr("transform", transparentRectPosition); + + tooltip + .append("text") + .attr("x", 0) + .attr("y", 16.5) + .selectAll("tspan") + .data(tooltipDataFormat) + .enter() + .append("tspan") + .attr("x", 6) + .attr("dy", (_: TooltipProps, index: number) => index * 15) // Adjust the line height as needed + .text((tooltipData: TooltipProps) => tooltipData.text) + .attr("fill", color.$scale.grey600) + .append("tspan") + .attr("fill", color.$scale.grey700) + .text((tooltipData: TooltipProps) => tooltipData.value) + .attr("id", (tooltipData: TooltipProps) => tooltipData.id) + .each(() => { + const currentValueNode: d3.Selection< + SVGTSpanElement | null, + unknown, + HTMLElement, + undefined + > = d3.select("#value"); + + if (!currentValueNode.empty() && currentValueNode.node()) { + const tspanMaxWidth = 110; + const originalText = currentValueNode.text(); + if (currentValueNode.node()!.getComputedTextLength() < tspanMaxWidth) { + return; + } + + let start = 0; + let end = tspanMaxWidth; + let mid; + while (start < end) { + mid = Math.floor((start + end) / 2); + const truncatedText = `${originalText.slice(0, mid)}...`; + currentValueNode.text(truncatedText); // Set text here for width calculation + const textWidth = + currentValueNode?.node()?.getComputedTextLength() || 0; + if (textWidth > tspanMaxWidth) { + end = mid; + } else { + start = mid + 1; + } + } + const truncatedText = `${originalText.slice(0, start - 1)}...`; + currentValueNode.text(truncatedText); + } + }); +} diff --git a/packages/frontend/src/pages/_app.page.tsx b/packages/frontend/src/pages/_app.page.tsx index 8f5f381a..a1217412 100644 --- a/packages/frontend/src/pages/_app.page.tsx +++ b/packages/frontend/src/pages/_app.page.tsx @@ -44,7 +44,6 @@ export default function App({ Component, pageProps }: AppProps) { - {/* eslint-disable-next-line react/jsx-props-no-spreading */} diff --git a/packages/frontend/src/pages/_document.page.tsx b/packages/frontend/src/pages/_document.page.tsx index 8c1e29bd..e0fe9641 100644 --- a/packages/frontend/src/pages/_document.page.tsx +++ b/packages/frontend/src/pages/_document.page.tsx @@ -2,7 +2,7 @@ import { Head, Html, Main, NextScript } from "next/document"; export default function Document() { return ( - +