diff --git a/src/components/experiment-tracking/parallel-coordinates/components/line-path.js b/src/components/experiment-tracking/parallel-coordinates/components/line-path.js deleted file mode 100644 index 2d5438df7f..0000000000 --- a/src/components/experiment-tracking/parallel-coordinates/components/line-path.js +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import { useD3 } from '../../../../utils/hooks/use-d3'; - -const grey400 = '#ABB2B7'; -const slate300 = '#1C2E3A'; - -export const LinePath = ({ - d, - fill, - id, - isHovered, - selected, - setHoveredId, - stroke, -}) => { - const setHighlight = (el, highlighted) => { - if (highlighted) { - el.style('stroke', grey400); - el.style('cursor', 'pointer'); - el.raise(); - } else { - el.style('stroke', slate300); - } - }; - - const ref = useD3((el) => { - if (!selected) { - el.on('mouseover', () => setHoveredId(id)); - - el.on('mouseout', () => setHoveredId(null)); - - if (isHovered) { - setHighlight(el, true); - } else { - setHighlight(el, false); - } - } - }); - - return ( - - ); -}; diff --git a/src/components/experiment-tracking/parallel-coordinates/parallel-coordinates.js b/src/components/experiment-tracking/parallel-coordinates/parallel-coordinates.js index df0c6516bc..ad042e90d8 100644 --- a/src/components/experiment-tracking/parallel-coordinates/parallel-coordinates.js +++ b/src/components/experiment-tracking/parallel-coordinates/parallel-coordinates.js @@ -3,6 +3,9 @@ import classnames from 'classnames'; import * as d3 from 'd3'; import { HoverStateContext } from '../utils/hover-state-context'; import { v4 as uuidv4 } from 'uuid'; +import { MetricsChartsTooltip, tooltipDefaultProps } from '../tooltip/tooltip'; +import { sidebarWidth } from '../../../config'; +import { formatTimestamp } from '../../../utils/date-utils'; import './parallel-coordinates.css'; @@ -12,6 +15,11 @@ const paddingLr = 80; const axisGapBuffer = 3; const selectedMarkerRotate = [45, 0, 0]; +const tooltipMaxWidth = 300; +const tooltipLeftGap = 90; +const tooltipRightGap = 60; +const tooltipTopGap = 150; + const selectedMarkerColors = ['#00E3FF', '#3BFF95', '#FFE300']; const yAxis = {}; @@ -21,6 +29,7 @@ export const ParallelCoordinates = ({ metricsData, selectedRuns }) => { const [hoveredAxisG, setHoveredAxisG] = useState(null); const [chartHeight, setChartHeight] = useState(0); const [chartWidth, setChartWidth] = useState(0); + const [showTooltip, setShowTooltip] = useState(tooltipDefaultProps); const { hoveredElementId, setHoveredElementId } = useContext(HoverStateContext); @@ -76,11 +85,81 @@ export const ParallelCoordinates = ({ metricsData, selectedRuns }) => { }; const handleMouseOverMetric = (e, key) => { + const runsCount = graph.find((each) => each[0] === key)[1].length; setHoveredAxisG(key); + + const rect = e.target.getBoundingClientRect(); + const y = rect.y - tooltipTopGap + rect.height / 2; + let x, direction; + + if (window.innerWidth - rect.x > tooltipMaxWidth) { + x = e.clientX - sidebarWidth.open - tooltipRightGap; + direction = 'right'; + } else { + x = + e.clientX - sidebarWidth.open - sidebarWidth.open / 2 - tooltipLeftGap; + direction = 'left'; + } + + setShowTooltip({ + content: { + label1: 'Metric name', + value1: key, + label2: 'Run count', + value2: runsCount, + }, + direction, + position: { x, y }, + visible: true, + }); }; const handleMouseOutMetric = () => { setHoveredAxisG(null); + setShowTooltip(tooltipDefaultProps); + }; + + const handleMouseOverLine = (e, key) => { + setHoveredElementId(key); + + if (e) { + const y = e.clientY - tooltipTopGap; + const parsedDate = new Date(formatTimestamp(key)); + let x, direction; + + if (window.innerWidth - e.clientX > tooltipMaxWidth) { + x = e.clientX - sidebarWidth.open - tooltipRightGap; + direction = 'right'; + } else { + x = + e.clientX - + sidebarWidth.open - + sidebarWidth.open / 2 - + tooltipLeftGap; + direction = 'left'; + } + + setShowTooltip({ + content: { + label1: 'Run name', + value1: key, + label2: 'Date', + value2: parsedDate.toLocaleDateString('default', { + day: 'numeric', + month: 'long', + year: 'numeric', + }), + }, + direction, + position: { x, y }, + visible: true, + }); + } + }; + + const handleMouseOutLine = () => { + setHoveredElementId(null); + setShowTooltip(tooltipDefaultProps); }; useEffect(() => { @@ -95,6 +174,13 @@ export const ParallelCoordinates = ({ metricsData, selectedRuns }) => { return (
+ + { d={linePath(value, i)} id={id} key={id} - onMouseLeave={() => setHoveredElementId(null)} + onMouseLeave={handleMouseOutLine} onMouseOver={(e) => { - setHoveredElementId(id); + handleMouseOverLine(e, id); d3.select(e.target).raise(); }} /> diff --git a/src/components/experiment-tracking/tooltip/tooltip.js b/src/components/experiment-tracking/tooltip/tooltip.js new file mode 100644 index 0000000000..361547c396 --- /dev/null +++ b/src/components/experiment-tracking/tooltip/tooltip.js @@ -0,0 +1,35 @@ +import React from 'react'; +import classnames from 'classnames'; + +import './tooltip.css'; + +export const tooltipDefaultProps = { + content: { label1: '', value1: '', label2: '', value2: '' }, + direction: 'right', + position: { x: -500, y: -500 }, + visible: false, +}; + +export const MetricsChartsTooltip = ({ + content = tooltipDefaultProps.content, + direction = tooltipDefaultProps.direction, + position = tooltipDefaultProps.position, + visible = tooltipDefaultProps.visible, +}) => { + return ( +
+ +

{`${content?.label1}:`}

+

{content?.value1}

+ +
+

{`${content?.label2}:`}

+

{content?.value2}

+
+ ); +}; diff --git a/src/components/experiment-tracking/tooltip/tooltip.scss b/src/components/experiment-tracking/tooltip/tooltip.scss new file mode 100644 index 0000000000..6d2c23eb91 --- /dev/null +++ b/src/components/experiment-tracking/tooltip/tooltip.scss @@ -0,0 +1,104 @@ +@use '../../../styles/variables' as colors; + +@mixin fade-in($waitTime) { + animation: wait #{$waitTime}, fade-in 800ms #{$waitTime}; +} + +@keyframes wait { + 0% { + opacity: 0; + } + + 100% { + opacity: 0; + } +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +$triangle-size: 10px; + +.tooltip { + background: white; + display: flex; + flex-direction: column; + opacity: 0; + padding: 10px 30px 10px 10px; + position: absolute; +} + +.tooltip--show { + @include fade-in('700ms'); + animation-fill-mode: forwards; +} + +.tooltip-arrow { + display: inline-block; + height: 0; + left: 1px; + + margin-right: 1.6em; + margin-top: -1.2em; + + position: absolute; + top: 22px; + white-space: nowrap; + width: 0; +} + +.tooltip-arrow--right { + &::before { + border-left: $triangle-size solid transparent; + border-top: $triangle-size solid var(--color-bg-alt); + + content: ''; + + height: 0; + left: -$triangle-size + 0.5; + position: absolute; + top: calc(50% - #{$triangle-size}); + width: 0; + } +} + +.tooltip-arrow--left { + &::before { + border-right: $triangle-size solid transparent; + border-top: $triangle-size solid var(--color-bg-alt); + + content: ''; + + height: 0; + position: absolute; + right: -225.5px; + top: calc(50% - #{$triangle-size}); + width: 0; + } +} + +.tooltip-label, +.tooltip-value { + font-size: 12px; + font-weight: 400; + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 180px; +} + +.tooltip-label { + color: #{colors.$black-500}; +} + +.tooltip-value { + color: #{colors.$black-900}; +} diff --git a/src/utils/date-utils.js b/src/utils/date-utils.js index 1464c9b551..25149c612c 100644 --- a/src/utils/date-utils.js +++ b/src/utils/date-utils.js @@ -2,7 +2,7 @@ import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; const _dayJs = dayjs.extend(relativeTime); -const formatTimestamp = (timestamp) => +export const formatTimestamp = (timestamp) => timestamp.replace('.', ':').replace('.', ':'); /**