Adds node-shapes to the canvas

- circle
- rect (w/ radius)
- fluffy cloud shape
- hexagons
- stacks
This commit is contained in:
Simon Howe
2016-01-14 19:56:22 +01:00
parent d2bf204181
commit a34d9c97b8
9 changed files with 219 additions and 22 deletions

View File

@@ -0,0 +1,14 @@
import React from 'react';
export default function NodeShapeCircle({highlighted, size, color}) {
return (
<g className="shape">
{highlighted &&
<circle r={size * 0.7} className="highlighted"></circle>}
<circle r={size * 0.5} className="border" stroke={color}></circle>
<circle r={size * 0.45} className="shadow"></circle>
<circle r={Math.max(2, size * 0.125)} className="node"></circle>
</g>
);
}

View File

@@ -0,0 +1,41 @@
import React from 'react';
import d3 from 'd3';
const CLOUD_PATH = 'M 1920,384 Q 1920,225 1807.5,112.5 1695,0 1536,0 H 448 Q 263,0 131.5,131.5 0,263 0,448 0,580 71,689.5 142,799 258,853 q -2,28 -2,43 0,212 150,362 150,150 362,150 158,0 286.5,-88 128.5,-88 187.5,-230 70,62 166,62 106,0 181,-75 75,-75 75,-181 0,-75 -41,-138 129,-30 213,-134.5 84,-104.5 84,-239.5 z';
function toPoint(stringPair) {
return stringPair.split(',').map(p => parseFloat(p, 10));
}
function getExtents(svgPath) {
const points = svgPath.split(' ').filter(s => s.length > 1).map(toPoint);
return [d3.extent(points, p => p[0]), d3.extent(points, p => p[1])];
}
export default function NodeShapeCloud({highlighted, size, color}) {
const [[minx, maxx], [miny, maxy]] = getExtents(CLOUD_PATH);
const width = (maxx - minx);
const height = (maxy - miny);
const cx = width / 2;
const cy = height / 2;
const pathSize = (width + height) / 2;
const baseScale = (size * 2) / pathSize;
const pathProps = (v) => {
return {
d: CLOUD_PATH,
transform: `scale(-${v * baseScale}) translate(-${cx},-${cy})`,
style: {strokeWidth: 1 / baseScale}
};
};
return (
<g className="shape">
{highlighted &&
<path className="highlighted" {...pathProps(0.7)} />}
<path className="border" stroke={color} {...pathProps(0.5)} />
<path className="shadow" {...pathProps(0.45)} />
<circle className="node" r={Math.max(2, (size * 0.125))} />
</g>
);
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import d3 from 'd3';
const line = d3.svg.line()
.interpolate('cardinal-closed')
.tension(0.25);
function getWidth(h) {
return (Math.sqrt(3) / 2) * h;
}
function getPoints(h) {
const w = getWidth(h);
const points = [
[w * 0.5, 0],
[w, 0.25 * h],
[w, 0.75 * h],
[w * 0.5, h],
[0, 0.75 * h],
[0, 0.25 * h]
];
return line(points);
}
export default function NodeShapeHex({highlighted, size, color}) {
const pathProps = (v) => {
return {
d: getPoints(size * v * 2),
transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})`
};
};
return (
<g className="shape">
{highlighted &&
<path className="highlighted" {...pathProps(0.7)} />}
<path className="border" stroke={color} {...pathProps(0.5)} />
<path className="shadow" {...pathProps(0.45)} />
<circle className="node" r={Math.max(2, (size * 0.125))} />
</g>
);
}

View File

@@ -0,0 +1,10 @@
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).
export default function NodeShapeRoundedSquare(props) {
return (
<NodeShapeSquare {...props} rx="0.4" ry="0.4" />
);
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
export default function NodeShapeSquare({highlighted, size, color, rx = 0, ry = 0}) {
const rectProps = (v) => {
return {
width: v * size * 2,
height: v * size * 2,
rx: v * size * rx,
ry: v * size * ry,
transform: `translate(-${size * v}, -${size * v})`
};
};
return (
<g className="shape">
{highlighted &&
<rect className="highlighted" {...rectProps(0.7)} />}
<rect className="border" stroke={color} {...rectProps(0.5)} />
<rect className="shadow" {...rectProps(0.45)} />
<circle className="node" r={Math.max(2, (size * 0.125))} />
</g>
);
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import _ from 'lodash';
export default function NodeShapeCircleStack(props) {
const propsNoHighlight = _.clone(props);
const Shape = props.shape;
delete propsNoHighlight.highlighted;
return (
<g className="stack">
<g transform="translate(0, 4)">
<Shape {...propsNoHighlight} />
</g>
<Shape {...props} />
<g transform="translate(0, -4)">
<Shape {...propsNoHighlight} />
</g>
</g>
);
}

View File

@@ -5,6 +5,42 @@ import { Motion, spring } from 'react-motion';
import { clickNode, enterNode, leaveNode } from '../actions/app-actions';
import { getNodeColor } from '../utils/color-utils';
import NodeShapeCircle from './node-shape-circle';
import NodeShapeStack from './node-shape-stack';
import NodeShapeRoundedSquare from './node-shape-rounded-square';
import NodeShapeHex from './node-shape-hex';
import NodeShapeCloud from './node-shape-cloud';
function stackedShape(Shape) {
const factory = React.createFactory(NodeShapeStack);
return function(props) {
return factory(Object.assign({}, props, {shape: Shape}));
};
}
const nodeShapes = {
'hosts': NodeShapeCircle,
'containers': NodeShapeHex,
'containers-by-hostname': stackedShape(NodeShapeHex),
'containers-by-image': stackedShape(NodeShapeHex),
'applications': NodeShapeRoundedSquare,
'applications-by-name': stackedShape(NodeShapeRoundedSquare)
};
function isTheInternet(id) {
return id === 'theinternet';
}
function getNodeShape({id, pseudo, topologyId}) {
if (isTheInternet(id)) {
return NodeShapeCloud;
} else if (pseudo) {
return NodeShapeCircle;
}
return nodeShapes[topologyId];
}
export default class Node extends React.Component {
constructor(props, context) {
super(props, context);
@@ -52,8 +88,11 @@ export default class Node extends React.Component {
if (this.props.pseudo) {
classNames.push('pseudo');
}
const classes = classNames.join(' ');
const NodeShapeType = getNodeShape(this.props);
return (
<Motion style={{
x: spring(this.props.dx, animConfig),
@@ -69,10 +108,10 @@ export default class Node extends React.Component {
return (
<g className={classes} transform={transform} id={props.id}
onClick={onMouseClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{props.highlighted && <circle r={nodeScale(0.7 * interpolated.f)} className="highlighted"></circle>}
<circle r={nodeScale(0.5 * interpolated.f)} className="border" stroke={color}></circle>
<circle r={nodeScale(0.45 * interpolated.f)} className="shadow"></circle>
<circle r={Math.max(2, nodeScale(0.125 * interpolated.f))} className="node"></circle>
<NodeShapeType
size={nodeScale(interpolated.f)}
color={color}
{...props} />
<text className="node-label" textAnchor="middle" style={{fontSize: interpolated.labelFontSize}}
x="0" y={interpolated.labelOffsetY + nodeScale(0.5 * interpolated.f)}>
{label}

View File

@@ -142,6 +142,7 @@ export default class NodesChart extends React.Component {
blurred={node.get('blurred')}
focused={node.get('focused')}
highlighted={node.get('highlighted')}
topologyId={this.props.topologyId}
onClick={onNodeClick}
key={node.get('id')}
id={node.get('id')}

View File

@@ -303,6 +303,10 @@ h2 {
}
}
g.stack g.shape .border {
stroke-width: 2px;
}
g.node {
cursor: pointer;
transition: opacity .5s ease-in-out;
@@ -357,28 +361,29 @@ h2 {
}
circle.border {
stroke-width: @node-border-stroke-width;
fill: none;
}
.shape {
.border {
stroke-width: @node-border-stroke-width;
fill: @background-color;
}
circle.shadow {
stroke: none;
fill: @background-lighter-color;
}
.shadow {
stroke: none;
fill: @background-lighter-color;
}
circle.node {
fill: @text-color;
}
.node {
fill: @text-color;
}
circle.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;
.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;
}
}
}
.details {