From a2aa426f1d4b6c1408d4b83761b09935dad3102d Mon Sep 17 00:00:00 2001 From: Nikita Rokotyan Date: Fri, 5 Jan 2024 10:55:36 -0800 Subject: [PATCH] Component | Graph | Links: Supporting longer link labels; Clean up #321 --- .../components/graph/modules/link/helper.ts | 1 - .../components/graph/modules/link/index.ts | 121 +++++++++--------- .../components/graph/modules/link/style.ts | 36 +++--- .../components/graph/modules/node/style.ts | 9 +- packages/ts/src/components/graph/types.ts | 2 + 5 files changed, 87 insertions(+), 82 deletions(-) diff --git a/packages/ts/src/components/graph/modules/link/helper.ts b/packages/ts/src/components/graph/modules/link/helper.ts index cdafa3c2b..0a756603c 100644 --- a/packages/ts/src/components/graph/modules/link/helper.ts +++ b/packages/ts/src/components/graph/modules/link/helper.ts @@ -17,7 +17,6 @@ import { getX, getY } from '../node/helper' export const getPolylineData = (d: { x1: number; x2: number; y1: number; y2: number}): string => `${d.x1},${d.y1} ${(d.x1 + d.x2) / 2},${(d.y1 + d.y2) / 2} ${d.x2},${d.y2}` -export const LINK_LABEL_RADIUS = 8 export const LINK_MARKER_WIDTH = 9 export const LINK_MARKER_HEIGHT = 7 diff --git a/packages/ts/src/components/graph/modules/link/index.ts b/packages/ts/src/components/graph/modules/link/index.ts index 927778a45..cff2a47e3 100644 --- a/packages/ts/src/components/graph/modules/link/index.ts +++ b/packages/ts/src/components/graph/modules/link/index.ts @@ -1,10 +1,13 @@ import { select, Selection } from 'd3-selection' import { range } from 'd3-array' import { Transition } from 'd3-transition' +import toPx from 'to-px' // Utils import { throttle, getValue, getNumber, getBoolean } from 'utils/data' import { smartTransition } from 'utils/d3' +import { getCSSVariableValueInPixels } from 'utils/misc' +import { estimateStringPixelLength } from 'utils/text' // Types import { GraphInputLink, GraphInputNode } from 'types/graph' @@ -23,7 +26,6 @@ import { getLinkBandWidth, getLinkColor, getLinkLabelTextColor, - LINK_LABEL_RADIUS, getLinkArrowStyle, LINK_MARKER_WIDTH, } from './helper' @@ -33,6 +35,7 @@ import { ZoomLevel } from '../zoom-levels' import * as generalSelectors from '../../style' import * as linkSelectors from './style' + export function createLinks ( selection: Selection, SVGGElement, unknown> ): void { @@ -57,8 +60,14 @@ export function createLinks .append('circle') .attr('class', linkSelectors.flowCircle) - selection.append('g') - .attr('class', linkSelectors.labelGroups) + const linkLabelGroup = selection.append('g') + .attr('class', linkSelectors.linkLabelGroup) + + linkLabelGroup.append('rect') + .attr('class', linkSelectors.linkLabelBackground) + + linkLabelGroup.append('text') + .attr('class', linkSelectors.linkLabelContent) } export function updateSelectedLinks ( @@ -113,6 +122,8 @@ export function updateLinks const flowGroup = linkGroup.select(`.${linkSelectors.flowGroup}`) const linkColor = getLinkColor(d, config) const linkShiftTransform = getLinkShiftTransform(d, config.linkNeighborSpacing) + const linkLabelDatum = getValue, GraphCircleLabel>(d, linkLabel, d._indexGlobal) + const linkLabelText = linkLabelDatum ? linkLabelDatum.text.toString() : undefined const x1 = getX(d.source) const y1 = getY(d.source) @@ -154,7 +165,7 @@ export function updateLinks const linkPathElement = linkSupport.node() const pathLength = linkPathElement.getTotalLength() if (linkArrowStyle) { - const arrowPos = pathLength * 0.5 + const arrowPos = pathLength * (linkLabelText ? 0.65 : 0.5) const p1 = linkPathElement.getPointAtLength(arrowPos) const p2 = linkPathElement.getPointAtLength(arrowPos + 1) // A point very close to p1 @@ -183,62 +194,54 @@ export function updateLinks .style('opacity', scale < ZoomLevel.Level2 ? 0 : 1) // Labels - const labelGroups = linkGroup.selectAll(`.${linkSelectors.labelGroups}`) - const labelDatum = getValue, GraphCircleLabel>(d, linkLabel, d._indexGlobal) - const markerWidth = linkArrowStyle ? LINK_MARKER_WIDTH * 2 : 0 - const labelShift = getBoolean(d, linkLabelShiftFromCenter, d._indexGlobal) ? -markerWidth + 4 : 0 - const labelPos = linkPathElement.getPointAtLength(pathLength / 2 + labelShift) - const labelTranslate = `translate(${labelPos.x}, ${labelPos.y})` - - const labels = labelGroups - .selectAll>(`.${linkSelectors.labelGroup}`) - .data(labelDatum && labelDatum.text ? [labelDatum] : []) - - // Enter - const labelsEnter = labels.enter().append('g') - .attr('class', linkSelectors.labelGroup) - .attr('transform', labelTranslate) - .style('opacity', 0) - - labelsEnter.append('circle') - .attr('class', linkSelectors.labelCircle) - .attr('r', 0) - - labelsEnter.append('text') - .attr('class', linkSelectors.labelContent) - - // Update - const labelsUpdate = labels.merge(labelsEnter) - - smartTransition(labelsUpdate.select(`.${linkSelectors.labelCircle}`), duration) - .attr('r', label => label.radius ?? LINK_LABEL_RADIUS) - .style('fill', label => label.color) - - labelsUpdate.select(`.${linkSelectors.labelContent}`) - .text(label => label.text) - .attr('dy', '0.1em') - .style('fill', label => label.textColor ?? getLinkLabelTextColor(label)) - .style('font-size', label => { - if (label.fontSize) return label.fontSize - const radius = label.radius ?? LINK_LABEL_RADIUS - return `${radius / Math.pow(label.text.toString().length, 0.4)}px` - }) - - smartTransition(labelsUpdate, duration) - .attr('transform', labelTranslate) - .style('cursor', label => label.cursor) - .style('opacity', 1) - - // Exit - const labelsExit = labels.exit() - smartTransition(labelsExit.select(`.${linkSelectors.labelCircle}`), duration) - .attr('r', 0) - - smartTransition(labelsExit, duration) - .style('opacity', 0) - .remove() + const linkLabelGroup = linkGroup.select(`.${linkSelectors.linkLabelGroup}`) + + if (linkLabelText) { + const linkMarkerWidth = linkArrowStyle ? LINK_MARKER_WIDTH * 2 : 0 + const linkLabelShift = getBoolean(d, linkLabelShiftFromCenter, d._indexGlobal) ? -linkMarkerWidth + 4 : 0 + const linkLabelPos = linkPathElement.getPointAtLength(pathLength / 2 + linkLabelShift) + const linkLabelTranslate = `translate(${linkLabelPos.x}, ${linkLabelPos.y})` + const linkLabelBackground = linkLabelGroup.select(`.${linkSelectors.linkLabelBackground}`) + const linkLabelContent = linkLabelGroup.select(`.${linkSelectors.linkLabelContent}`) + + // If the label was hidden or didn't have text before, we need to set the initial position + if (!linkLabelContent.text() || linkLabelContent.attr('hidden')) { + linkLabelGroup.attr('transform', linkLabelTranslate) + } + + linkLabelGroup.attr('hidden', null) + .style('cursor', linkLabelDatum.cursor) + + smartTransition(linkLabelGroup, duration) + .attr('transform', linkLabelTranslate) + .style('opacity', 1) + + linkLabelContent + .text(linkLabelText) + .attr('dy', '0.1em') + .style('font-size', linkLabelDatum.fontSize) + .style('fill', linkLabelDatum.textColor ?? getLinkLabelTextColor(linkLabelDatum)) + + const shouldBeRenderedAsCircle = linkLabelText.length <= 2 + const linkLabelPaddingVertical = 4 + const linkLabelPaddingHorizontal = shouldBeRenderedAsCircle ? linkLabelPaddingVertical : 8 + const linkLabelFontSize = toPx(linkLabelDatum.fontSize) ?? getCSSVariableValueInPixels('var(--vis-graph-link-label-font-size)', linkLabelContent.node()) + const linkLabelWidthPx = estimateStringPixelLength(linkLabelText, linkLabelFontSize) + const linkLabelBackgroundBorderRadius = linkLabelDatum.radius ?? (shouldBeRenderedAsCircle ? linkLabelFontSize : 4) + const linkLabelBackgroundWidth = (shouldBeRenderedAsCircle ? linkLabelFontSize : linkLabelWidthPx) + linkLabelBackground + .attr('x', -linkLabelBackgroundWidth / 2 - linkLabelPaddingHorizontal) + .attr('y', -linkLabelFontSize / 2 - linkLabelPaddingVertical) + .attr('width', linkLabelBackgroundWidth + linkLabelPaddingHorizontal * 2) + .attr('height', linkLabelFontSize + linkLabelPaddingVertical * 2) + .attr('rx', linkLabelBackgroundBorderRadius) + .style('fill', linkLabelDatum.color) + } else { + linkLabelGroup.attr('hidden', true) + } }) + // Pointer Events if (duration > 0) { selection.attr('pointer-events', 'none') const t = smartTransition(selection, duration) as Transition, SVGGElement, GraphLink> @@ -279,7 +282,7 @@ export function animateLinkFlow(`.${linkSelectors.link}`).node() + const linkPathElement = linkGroup.select(`.${linkSelectors.linkSupport}`).node() const pathLength = linkPathElement.getTotalLength() if (!getBoolean(d, linkFlow, d._indexGlobal)) return diff --git a/packages/ts/src/components/graph/modules/link/style.ts b/packages/ts/src/components/graph/modules/link/style.ts index 031dce229..1b886e2b5 100644 --- a/packages/ts/src/components/graph/modules/link/style.ts +++ b/packages/ts/src/components/graph/modules/link/style.ts @@ -11,9 +11,9 @@ export const variables = injectGlobal` --vis-graph-link-greyout-opacity: 0.3; --vis-graph-link-dashed-stroke-dasharray: 6 6; - --vis-graph-link-label-stroke-color: #fff; - --vis-graph-link-label-fill-color: #e6e9f3; - --vis-graph-link-label-text-color-dark: #494b56; + --vis-graph-link-label-font-size: 9pt; + --vis-graph-link-label-background: #e6e9f3; + --vis-graph-link-label-text-color-dark: #18181B; --vis-graph-link-label-text-color-bright: #fff; --vis-graph-link-label-text-color: var(--vis-graph-link-label-text-color-dark); @@ -21,16 +21,17 @@ export const variables = injectGlobal` --vis-graph-link-support-stroke-width: 10px; --vis-dark-graph-link-stroke-color: #494b56; - --vis-dark-graph-link-label-stroke-color: #222; - --vis-dark-graph-link-label-fill-color: var(--vis-color-grey); - --vis-dark-graph-link-label-text-color: var(--vis-graph-link-label-text-color-bright) + --vis-dark-graph-link-label-background: #3f3f45; + --vis-dark-graph-link-label-text-color: var(--vis-graph-link-label-text-color-bright); + + --vis-graph-link-dominant-baseline: middle; } body.theme-dark ${`.${links}`} { --vis-graph-link-stroke-color: var(--vis-dark-graph-link-stroke-color); --vis-graph-link-label-stroke-color: var(--vis-dark-graph-link-label-stroke-color); --vis-graph-link-label-text-color: var(--vis-dark-graph-link-label-text-color); - --vis-graph-link-label-fill-color: var(--vis-dark-graph-link-label-fill-color); + --vis-graph-link-label-background: var(--vis-dark-graph-link-label-background); } ` @@ -105,27 +106,24 @@ export const flowCircle = css` fill: var(--vis-graph-link-stroke-color); ` -export const labelGroups = css` - label: label-groups; -` - -export const labelGroup = css` +export const linkLabelGroup = css` label: label-group; pointer-events: all; ` -export const labelCircle = css` - label: label-circle; +export const linkLabelBackground = css` + label: label-background; - fill: var(--vis-graph-link-label-fill-color); - stroke: var(--vis-graph-link-label-stroke-color); + fill: var(--vis-graph-link-label-background); ` -export const labelContent = css` +export const linkLabelContent = css` label: label-content; - font-family: var(--vis-graph-icon-font-family), var(--vis-font-family); + font-size: var(--vis-graph-link-label-font-size); + font-family: var(--vis-font-family); fill: var(--vis-graph-link-label-text-color); text-anchor: middle; - dominant-baseline: middle; + dominant-baseline: var(--vis-graph-link-dominant-baseline); + user-select: none; ` diff --git a/packages/ts/src/components/graph/modules/node/style.ts b/packages/ts/src/components/graph/modules/node/style.ts index 6867ba690..5a7cac839 100644 --- a/packages/ts/src/components/graph/modules/node/style.ts +++ b/packages/ts/src/components/graph/modules/node/style.ts @@ -65,6 +65,9 @@ export const variables = injectGlobal` --vis-dark-graph-node-greyout-color: #494b56; --vis-dark-graph-node-icon-greyout-color: var(--vis-color-grey); --vis-dark-graph-node-side-label-background-greyout-color: #494B56; + + /* Misc */ + --vis-graph-node-dominant-baseline: middle; } body.theme-dark ${`.${nodes}`} { @@ -105,7 +108,7 @@ export const nodeIcon = css` label: icon; font-family: var(--vis-graph-icon-font-family), var(--vis-font-family); - dominant-baseline: middle; + dominant-baseline: var(--vis-graph-node-dominant-baseline); text-anchor: middle; pointer-events: none; transition: .4s all; @@ -116,7 +119,7 @@ export const nodeBottomIcon = css` label: node-bottom-icon; font-family: var(--vis-graph-icon-font-family), var(--vis-font-family); font-size: var(--vis-graph-node-bottom-icon-font-size); - dominant-baseline: middle; + dominant-baseline: var(--vis-graph-node-dominant-baseline); text-anchor: middle; pointer-events: none; transition: .4s fill; @@ -181,7 +184,7 @@ export const sideLabel = css` label: side-label; font-family: var(--vis-graph-icon-font-family), var(--vis-font-family); - dominant-baseline: middle; + dominant-baseline: var(--vis-graph-node-dominant-baseline); text-anchor: middle; font-size: 16px; fill: var(--vis-graph-node-side-label-fill-color-bright); diff --git a/packages/ts/src/components/graph/types.ts b/packages/ts/src/components/graph/types.ts index 6d37dc780..0132b6d20 100644 --- a/packages/ts/src/components/graph/types.ts +++ b/packages/ts/src/components/graph/types.ts @@ -64,6 +64,8 @@ export type GraphCircleLabel = { radius?: number; } +export type GraphLinkLabel = GraphCircleLabel + export enum GraphLinkStyle { Dashed = 'dashed', Solid = 'solid',