Increase cloud node thickness in graph view (#2418)

* Unified node shapes rendering templates.

* Addressed @foot's comments (fix shadow thickness across all shapes).

* Made getClipPathDefinition slightly more readable.
This commit is contained in:
Filip Barl
2017-04-06 14:44:52 +02:00
committed by GitHub
parent a404d82a91
commit 82e373777a
15 changed files with 141 additions and 255 deletions

View File

@@ -1,39 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import {
getMetricValue,
getMetricColor,
getClipPathDefinition,
renderMetricValue,
} from '../utils/metric-utils';
import {
NODE_SHAPE_HIGHLIGHT_RADIUS,
NODE_SHAPE_BORDER_RADIUS,
NODE_SHAPE_SHADOW_RADIUS,
} from '../constants/styles';
export default function NodeShapeCircle({ id, highlighted, color, metric }) {
const { height, hasMetric, formattedValue } = getMetricValue(metric);
const metricStyle = { fill: getMetricColor(metric) };
const className = classNames('shape', 'shape-circle', { metrics: hasMetric });
const clipId = `mask-${id}`;
return (
<g className={className}>
{hasMetric && getClipPathDefinition(clipId, height)}
{highlighted && <circle className="highlighted" r={NODE_SHAPE_HIGHLIGHT_RADIUS} />}
<circle className="border" stroke={color} r={NODE_SHAPE_BORDER_RADIUS} />
<circle className="shadow" r={NODE_SHAPE_SHADOW_RADIUS} />
{hasMetric && <circle
className="metric-fill"
clipPath={`url(#${clipId})`}
style={metricStyle}
r={NODE_SHAPE_SHADOW_RADIUS}
/>}
{renderMetricValue(formattedValue, highlighted && hasMetric)}
</g>
);
}

View File

@@ -1,28 +0,0 @@
import React from 'react';
import {
NODE_SHAPE_HIGHLIGHT_RADIUS,
NODE_SHAPE_BORDER_RADIUS,
NODE_SHAPE_SHADOW_RADIUS,
NODE_SHAPE_DOT_RADIUS,
NODE_BASE_SIZE,
} from '../constants/styles';
// This path is already normalized so no dynamic rescaling is needed.
const CLOUD_PATH = 'M-125 23.333Q-125 44.036-110.352 58.685-95.703 73.333-75 73.333H66.667Q90.755 '
+ '73.333 107.878 56.211 125 39.089 125 15 125-2.188 115.755-16.445 106.51-30.703 91.406-37.734q'
+ '0.26-3.646 0.261-5.599 0-27.604-19.532-47.136-19.531-19.531-47.135-19.531-20.573 0-37.305 '
+ '11.458-16.732 11.458-24.414 29.948-9.115-8.073-21.614-8.073-13.802 0-23.568 9.766-9.766 9.766-'
+ '9.766 23.568 0 9.766 5.339 17.968-16.797 3.906-27.735 17.513-10.938 13.607-10.937 31.185z';
export default function NodeShapeCloud({ highlighted, color }) {
const pathProps = r => ({ d: CLOUD_PATH, transform: `scale(${r / NODE_BASE_SIZE})` });
return (
<g className="shape shape-cloud">
{highlighted && <path className="highlighted" {...pathProps(NODE_SHAPE_HIGHLIGHT_RADIUS)} />}
<path className="border" stroke={color} {...pathProps(NODE_SHAPE_BORDER_RADIUS)} />
<path className="shadow" {...pathProps(NODE_SHAPE_SHADOW_RADIUS)} />
<circle className="node" r={NODE_SHAPE_DOT_RADIUS} />
</g>
);
}

View File

@@ -1,41 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import { nodeShapePolygon } from '../utils/node-shape-utils';
import {
getMetricValue,
getMetricColor,
getClipPathDefinition,
renderMetricValue,
} from '../utils/metric-utils';
import {
NODE_SHAPE_HIGHLIGHT_RADIUS,
NODE_SHAPE_BORDER_RADIUS,
NODE_SHAPE_SHADOW_RADIUS,
} from '../constants/styles';
export default function NodeShapeHeptagon({ id, highlighted, color, metric }) {
const { height, hasMetric, formattedValue } = getMetricValue(metric);
const metricStyle = { fill: getMetricColor(metric) };
const className = classNames('shape', 'shape-heptagon', { metrics: hasMetric });
const pathProps = r => ({ d: nodeShapePolygon(r, 7) });
const clipId = `mask-${id}`;
return (
<g className={className}>
{hasMetric && getClipPathDefinition(clipId, height)}
{highlighted && <path className="highlighted" {...pathProps(NODE_SHAPE_HIGHLIGHT_RADIUS)} />}
<path className="border" stroke={color} {...pathProps(NODE_SHAPE_BORDER_RADIUS)} />
<path className="shadow" {...pathProps(NODE_SHAPE_SHADOW_RADIUS)} />
{hasMetric && <path
className="metric-fill"
clipPath={`url(#${clipId})`}
style={metricStyle}
{...pathProps(NODE_SHAPE_SHADOW_RADIUS)}
/>}
{renderMetricValue(formattedValue, highlighted && hasMetric)}
</g>
);
}

View File

@@ -1,41 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import { nodeShapePolygon } from '../utils/node-shape-utils';
import {
getMetricValue,
getMetricColor,
getClipPathDefinition,
renderMetricValue,
} from '../utils/metric-utils';
import {
NODE_SHAPE_HIGHLIGHT_RADIUS,
NODE_SHAPE_BORDER_RADIUS,
NODE_SHAPE_SHADOW_RADIUS,
} from '../constants/styles';
export default function NodeShapeHexagon({ id, highlighted, color, metric }) {
const { height, hasMetric, formattedValue } = getMetricValue(metric);
const metricStyle = { fill: getMetricColor(metric) };
const className = classNames('shape', 'shape-hexagon', { metrics: hasMetric });
const pathProps = r => ({ d: nodeShapePolygon(r, 6) });
const clipId = `mask-${id}`;
return (
<g className={className}>
{hasMetric && getClipPathDefinition(clipId, height)}
{highlighted && <path className="highlighted" {...pathProps(NODE_SHAPE_HIGHLIGHT_RADIUS)} />}
<path className="border" stroke={color} {...pathProps(NODE_SHAPE_BORDER_RADIUS)} />
<path className="shadow" {...pathProps(NODE_SHAPE_SHADOW_RADIUS)} />
{hasMetric && <path
className="metric-fill"
clipPath={`url(#${clipId})`}
style={metricStyle}
{...pathProps(NODE_SHAPE_SHADOW_RADIUS)}
/>}
{renderMetricValue(formattedValue, highlighted && hasMetric)}
</g>
);
}

View File

@@ -1,11 +0,0 @@
import React from 'react';
import NodeShapeSquare from './node-shape-square';
// TODO how to express a cmp in terms of another cmp? (Rather than a sub-cmp as here).
// HOC!
export default function NodeShapeRoundedSquare(props) {
return (
<NodeShapeSquare {...props} rx="0.4" ry="0.4" />
);
}

View File

@@ -1,47 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import {
getMetricValue,
getMetricColor,
getClipPathDefinition,
renderMetricValue,
} from '../utils/metric-utils';
import {
NODE_SHAPE_HIGHLIGHT_RADIUS,
NODE_SHAPE_BORDER_RADIUS,
NODE_SHAPE_SHADOW_RADIUS,
} from '../constants/styles';
export default function NodeShapeSquare({ id, highlighted, color, rx = 0, ry = 0, metric }) {
const { height, hasMetric, formattedValue } = getMetricValue(metric);
const metricStyle = { fill: getMetricColor(metric) };
const className = classNames('shape', 'shape-square', { metrics: hasMetric });
const rectProps = (scale, borderRadiusAdjustmentFactor = 1) => ({
width: scale * 2,
height: scale * 2,
rx: scale * rx * borderRadiusAdjustmentFactor,
ry: scale * ry * borderRadiusAdjustmentFactor,
x: -scale,
y: -scale
});
const clipId = `mask-${id}`;
return (
<g className={className}>
{hasMetric && getClipPathDefinition(clipId, height)}
{highlighted && <rect className="highlighted" {...rectProps(NODE_SHAPE_HIGHLIGHT_RADIUS)} />}
<rect className="border" stroke={color} {...rectProps(NODE_SHAPE_BORDER_RADIUS)} />
<rect className="shadow" {...rectProps(NODE_SHAPE_SHADOW_RADIUS, 0.85)} />
{hasMetric && <rect
className="metric-fill"
clipPath={`url(#${clipId})`}
style={metricStyle}
{...rectProps(NODE_SHAPE_SHADOW_RADIUS, 0.85)}
/>}
{renderMetricValue(formattedValue, highlighted && hasMetric)}
</g>
);
}

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { NODE_BASE_SIZE } from '../constants/styles';
export default function NodeShapeStack(props) {
const shift = props.contrastMode ? 0.15 : 0.1;
const highlightScale = [1, 1 + shift];

View File

@@ -0,0 +1,71 @@
import React from 'react';
import classNames from 'classnames';
import { NODE_BASE_SIZE } from '../constants/styles';
import {
getMetricValue,
getMetricColor,
getClipPathDefinition,
} from '../utils/metric-utils';
import {
pathElement,
circleElement,
rectangleElement,
cloudShapeProps,
circleShapeProps,
squareShapeProps,
hexagonShapeProps,
heptagonShapeProps,
} from '../utils/node-shape-utils';
function NodeShape(shapeType, shapeElement, shapeProps, { id, highlighted, color, metric }) {
const { height, hasMetric, formattedValue } = getMetricValue(metric);
const className = classNames('shape', `shape-${shapeType}`, { metrics: hasMetric });
const metricStyle = { fill: getMetricColor(metric) };
const clipId = `metric-clip-${id}`;
return (
<g className={className}>
{highlighted && shapeElement({
className: 'highlight',
transform: `scale(${NODE_BASE_SIZE * 0.7})`,
...shapeProps,
})}
{shapeElement({
className: 'background',
transform: `scale(${NODE_BASE_SIZE * 0.48})`,
...shapeProps,
})}
{hasMetric && getClipPathDefinition(clipId, height, 0.48)}
{hasMetric && shapeElement({
className: 'metric-fill',
transform: `scale(${NODE_BASE_SIZE * 0.48})`,
clipPath: `url(#${clipId})`,
style: metricStyle,
...shapeProps,
})}
{shapeElement({
className: 'shadow',
transform: `scale(${NODE_BASE_SIZE * 0.49})`,
...shapeProps,
})}
{shapeElement({
className: 'border',
transform: `scale(${NODE_BASE_SIZE * 0.5})`,
stroke: color,
...shapeProps,
})}
{hasMetric && highlighted ?
<text>{formattedValue}</text> :
<circle className="node" r={NODE_BASE_SIZE * 0.1} />
}
</g>
);
}
export const NodeShapeCloud = props => NodeShape('cloud', pathElement, cloudShapeProps, props);
export const NodeShapeCircle = props => NodeShape('circle', circleElement, circleShapeProps, props);
export const NodeShapeHexagon = props => NodeShape('hexagon', pathElement, hexagonShapeProps, props);
export const NodeShapeHeptagon = props => NodeShape('heptagon', pathElement, heptagonShapeProps, props);
export const NodeShapeSquare = props => NodeShape('square', rectangleElement, squareShapeProps, props);

View File

@@ -9,13 +9,15 @@ import MatchedText from '../components/matched-text';
import MatchedResults from '../components/matched-results';
import { NODE_BASE_SIZE } from '../constants/styles';
import NodeShapeCircle from './node-shape-circle';
import NodeShapeStack from './node-shape-stack';
import NodeShapeRoundedSquare from './node-shape-rounded-square';
import NodeShapeHexagon from './node-shape-hexagon';
import NodeShapeHeptagon from './node-shape-heptagon';
import NodeShapeCloud from './node-shape-cloud';
import NodeNetworksOverlay from './node-networks-overlay';
import {
NodeShapeCloud,
NodeShapeCircle,
NodeShapeSquare,
NodeShapeHexagon,
NodeShapeHeptagon,
} from './node-shapes';
const labelWidth = 1.2 * NODE_BASE_SIZE;
@@ -23,8 +25,8 @@ const nodeShapes = {
circle: NodeShapeCircle,
hexagon: NodeShapeHexagon,
heptagon: NodeShapeHeptagon,
square: NodeShapeRoundedSquare,
cloud: NodeShapeCloud
square: NodeShapeSquare,
cloud: NodeShapeCloud,
};
function stackedShape(Shape) {

View File

@@ -17,10 +17,11 @@ export const RESOURCES_LABEL_MIN_SIZE = 50;
export const RESOURCES_LABEL_PADDING = 10;
// Node shapes
export const NODE_SHAPE_HIGHLIGHT_RADIUS = 70;
export const NODE_SHAPE_BORDER_RADIUS = 50;
export const NODE_SHAPE_SHADOW_RADIUS = 45;
export const NODE_SHAPE_DOT_RADIUS = 10;
export const UNIT_CLOUD_PATH = 'M-1.25 0.233Q-1.25 0.44-1.104 0.587-0.957 0.733-0.75 0.733H0.667Q'
+ '0.908 0.733 1.079 0.562 1.25 0.391 1.25 0.15 1.25-0.022 1.158-0.164 1.065-0.307 0.914-0.377q'
+ '0.003-0.036 0.003-0.056 0-0.276-0.196-0.472-0.195-0.195-0.471-0.195-0.206 0-0.373 0.115-0.167'
+ ' 0.115-0.244 0.299-0.091-0.081-0.216-0.081-0.138 0-0.236 0.098-0.098 0.098-0.098 0.236 0 0.098'
+ ' 0.054 0.179-0.168 0.039-0.278 0.175-0.109 0.136-0.109 0.312z';
// NOTE: This value represents the node unit radius (in pixels). Since zooming is
// controlled at the top level now, this renormalization would be obsolete (i.e.
// value 1 could be used instead), if it wasn't for the following factors:

View File

@@ -2,24 +2,20 @@ import { includes } from 'lodash';
import { scaleLog } from 'd3-scale';
import React from 'react';
import { NODE_BASE_SIZE, NODE_SHAPE_DOT_RADIUS } from '../constants/styles';
import { formatMetricSvg } from './string-utils';
import { colors } from './color-utils';
export function getClipPathDefinition(clipId, height) {
export function getClipPathDefinition(clipId, height, radius) {
const barHeight = 1 - (2 * height); // in the interval [-1, 1]
return (
<defs>
<clipPath id={clipId} transform={`scale(${NODE_BASE_SIZE})`}>
<rect width={1} height={1} x={-0.5} y={0.5 - height} />
<clipPath id={clipId} transform={`scale(${2 * radius})`}>
<rect width={2} height={2} x={-1} y={barHeight} />
</clipPath>
</defs>
);
}
export function renderMetricValue(value, condition) {
return condition ? <text>{value}</text> : <circle className="node" r={NODE_SHAPE_DOT_RADIUS} />;
}
//
// loadScale(1) == 0.5; E.g. a nicely balanced system :).
const loadScale = scaleLog().domain([0.01, 100]).range([0, 1]);

View File

@@ -1,12 +1,27 @@
import { line, curveCardinalClosed } from 'd3-shape';
import React from 'react';
import range from 'lodash/range';
import { line, curveCardinalClosed } from 'd3-shape';
const shapeSpline = line().curve(curveCardinalClosed.tension(0.65));
import { UNIT_CLOUD_PATH } from '../constants/styles';
export function nodeShapePolygon(radius, n) {
export const pathElement = React.createFactory('path');
export const circleElement = React.createFactory('circle');
export const rectangleElement = React.createFactory('rect');
function curvedUnitPolygonPath(n) {
const curve = curveCardinalClosed.tension(0.65);
const spline = line().curve(curve);
const innerAngle = (2 * Math.PI) / n;
return shapeSpline(range(0, n).map(k => [
radius * Math.sin(k * innerAngle),
-radius * Math.cos(k * innerAngle)
return spline(range(0, n).map(k => [
Math.sin(k * innerAngle),
-Math.cos(k * innerAngle),
]));
}
export const squareShapeProps = { width: 1.8, height: 1.8, rx: 0.4, ry: 0.4, x: -0.9, y: -0.9 };
export const heptagonShapeProps = { d: curvedUnitPolygonPath(7) };
export const hexagonShapeProps = { d: curvedUnitPolygonPath(6) };
export const cloudShapeProps = { d: UNIT_CLOUD_PATH };
export const circleShapeProps = { r: 1 };

View File

@@ -424,7 +424,7 @@
}
}
.stack .shape .highlighted {
.stack .shape .highlight {
display: none;
}
@@ -432,7 +432,7 @@
.border { display: none; }
.shadow { display: none; }
.node { display: none; }
.highlighted { display: inline; }
.highlight { display: inline; }
}
.stack .shape .metric-fill {
@@ -443,16 +443,17 @@
transform: scale(1);
cursor: pointer;
.border {
stroke-width: $node-border-stroke-width;
fill: $background-color;
transition: stroke-opacity 0.333s $base-ease, fill 0.333s $base-ease;
stroke-opacity: 1;
.highlight {
fill: $weave-blue;
fill-opacity: $node-highlight-fill-opacity;
stroke: $weave-blue;
stroke-width: $node-highlight-stroke-width;
stroke-opacity: $node-highlight-stroke-opacity;
}
&.metrics .border {
.background {
stroke: none;
fill: $background-lighter-color;
stroke-opacity: 0.3;
}
.metric-fill {
@@ -461,9 +462,21 @@
fill-opacity: 0.7;
}
.border {
fill: none;
stroke-opacity: 1;
stroke-width: $node-border-stroke-width;
transition: stroke-opacity 0.333s $base-ease, fill 0.333s $base-ease;
}
&.metrics .border {
stroke-opacity: 0.3;
}
.shadow {
stroke: none;
fill: $background-lighter-color;
fill: none;
stroke: $background-color;
stroke-width: $node-shadow-stroke-width;
}
.node {
@@ -478,14 +491,6 @@
dominant-baseline: middle;
text-anchor: middle;
}
.highlighted {
fill: $weave-blue;
fill-opacity: $node-highlight-fill-opacity;
stroke: $weave-blue;
stroke-width: $node-highlight-stroke-width;
stroke-opacity: $node-highlight-stroke-opacity;
}
}
.stack .shape .border {

View File

@@ -14,8 +14,9 @@ $edge-color: black;
$node-highlight-fill-opacity: 0.3;
$node-highlight-stroke-opacity: 0.5;
$node-highlight-stroke-width: 8;
$node-border-stroke-width: 10;
$node-highlight-stroke-width: 0.16;
$node-border-stroke-width: 0.2;
$node-shadow-stroke-width: 0.25;
$node-pseudo-opacity: 1;
$edge-highlight-opacity: 0.3;
$edge-opacity-blurred: 0;

View File

@@ -32,8 +32,9 @@ $terminal-header-height: 44px;
$node-highlight-fill-opacity: 0.1;
$node-highlight-stroke-opacity: 0.4;
$node-highlight-stroke-width: 2;
$node-border-stroke-width: 6;
$node-highlight-stroke-width: 0.04;
$node-border-stroke-width: 0.12;
$node-shadow-stroke-width: 0.18;
$node-pseudo-opacity: 0.8;
$node-text-scale: 2;
$edge-highlight-opacity: 0.1;