Skip to content

Commit

Permalink
Merge pull request #1105 from weaveworks/metrics-on-canvas
Browse files Browse the repository at this point in the history
Metrics on canvas
  • Loading branch information
foot committed Apr 4, 2016
2 parents 7643d7a + 4ec9750 commit c9b323f
Show file tree
Hide file tree
Showing 24 changed files with 890 additions and 188 deletions.
37 changes: 37 additions & 0 deletions client/app/scripts/actions/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import debug from 'debug';
import AppDispatcher from '../dispatcher/app-dispatcher';
import ActionTypes from '../constants/action-types';
import { saveGraph } from '../utils/file-utils';
import { modulo } from '../utils/math-utils';
import { updateRoute } from '../utils/router-utils';
import { bufferDeltaUpdate, resumeUpdate,
resetUpdateBuffer } from '../utils/update-buffer-utils';
Expand All @@ -12,6 +13,41 @@ import AppStore from '../stores/app-store';

const log = debug('scope:app-actions');

export function selectMetric(metricId) {
AppDispatcher.dispatch({
type: ActionTypes.SELECT_METRIC,
metricId
});
}

export function pinNextMetric(delta) {
const metrics = AppStore.getAvailableCanvasMetrics().map(m => m.get('id'));
const currentIndex = metrics.indexOf(AppStore.getSelectedMetric());
const nextIndex = modulo(currentIndex + delta, metrics.count());
const nextMetric = metrics.get(nextIndex);

AppDispatcher.dispatch({
type: ActionTypes.PIN_METRIC,
metricId: nextMetric,
});
updateRoute();
}

export function pinMetric(metricId) {
AppDispatcher.dispatch({
type: ActionTypes.PIN_METRIC,
metricId,
});
updateRoute();
}

export function unpinMetric() {
AppDispatcher.dispatch({
type: ActionTypes.UNPIN_METRIC,
});
updateRoute();
}

export function changeTopologyOption(option, value, topologyId) {
AppDispatcher.dispatch({
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
Expand Down Expand Up @@ -243,6 +279,7 @@ export function receiveNodesDelta(delta) {
}
}


export function receiveTopologies(topologies) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_TOPOLOGIES,
Expand Down
29 changes: 17 additions & 12 deletions client/app/scripts/charts/node-shape-circle.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import React from 'react';
import classNames from 'classnames';
import {getMetricValue, getMetricColor, getClipPathDefinition} from '../utils/metric-utils.js';
import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js';

export default function NodeShapeCircle({onlyHighlight, highlighted, size, color}) {
const hightlightNode = <circle r={size * 0.7} className="highlighted" />;

if (onlyHighlight) {
return (
<g className="shape">
{highlighted && hightlightNode}
</g>
);
}
export default function NodeShapeCircle({id, highlighted, size, color, metric}) {
const clipId = `mask-${id}`;
const {height, hasMetric, formattedValue} = getMetricValue(metric, size);
const metricStyle = { fill: getMetricColor(metric) };
const className = classNames('shape', { metrics: hasMetric });
const fontSize = size * CANVAS_METRIC_FONT_SIZE;

return (
<g className="shape">
{highlighted && hightlightNode}
<g className={className}>
{hasMetric && getClipPathDefinition(clipId, size, height)}
{highlighted && <circle r={size * 0.7} className="highlighted" />}
<circle r={size * 0.5} className="border" stroke={color} />
<circle r={size * 0.45} className="shadow" />
<circle r={Math.max(2, size * 0.125)} className="node" />
{hasMetric && <circle r={size * 0.45} className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} />}
{highlighted && hasMetric ?
<text style={{fontSize}}>{formattedValue}</text> :
<circle className="node" r={Math.max(2, (size * 0.125))} />}
</g>
);
}
33 changes: 20 additions & 13 deletions client/app/scripts/charts/node-shape-heptagon.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import React from 'react';
import d3 from 'd3';
import classNames from 'classnames';
import {getMetricValue, getMetricColor, getClipPathDefinition} from '../utils/metric-utils.js';
import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js';


const line = d3.svg.line()
.interpolate('cardinal-closed')
.tension(0.25);


function polygon(r, sides) {
const a = (Math.PI * 2) / sides;
const points = [[r, 0]];
Expand All @@ -14,29 +19,31 @@ function polygon(r, sides) {
return points;
}

export default function NodeShapeHeptagon({onlyHighlight, highlighted, size, color}) {

export default function NodeShapeHeptagon({id, highlighted, size, color, metric}) {
const scaledSize = size * 1.0;
const pathProps = v => ({
d: line(polygon(scaledSize * v, 7)),
transform: 'rotate(90)'
});

const hightlightNode = <path className="highlighted" {...pathProps(0.7)} />;

if (onlyHighlight) {
return (
<g className="shape">
{highlighted && hightlightNode}
</g>
);
}
const clipId = `mask-${id}`;
const {height, hasMetric, formattedValue} = getMetricValue(metric, size);
const metricStyle = { fill: getMetricColor(metric) };
const className = classNames('shape', { metrics: hasMetric });
const fontSize = size * CANVAS_METRIC_FONT_SIZE;

return (
<g className="shape">
{highlighted && hightlightNode}
<g className={className}>
{hasMetric && getClipPathDefinition(clipId, size, height, size * 0.5 - height, -size * 0.5)}
{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, (scaledSize * 0.125))} />
{hasMetric && <path className="metric-fill" clipPath={`url(#${clipId})`}
style={metricStyle} {...pathProps(0.45)} />}
{highlighted && hasMetric ?
<text style={{fontSize}}>{formattedValue}</text> :
<circle className="node" r={Math.max(2, (size * 0.125))} />}
</g>
);
}
39 changes: 26 additions & 13 deletions client/app/scripts/charts/node-shape-hex.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import React from 'react';
import d3 from 'd3';
import classNames from 'classnames';
import {getMetricValue, getMetricColor, getClipPathDefinition} from '../utils/metric-utils.js';
import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js';


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 = [
Expand All @@ -24,28 +30,35 @@ function getPoints(h) {
}


export default function NodeShapeHex({onlyHighlight, highlighted, size, color}) {
export default function NodeShapeHex({id, highlighted, size, color, metric}) {
const pathProps = v => ({
d: getPoints(size * v * 2),
transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})`
});

const hightlightNode = <path className="highlighted" {...pathProps(0.7)} />;
const shadowSize = 0.45;
const upperHexBitHeight = -0.25 * size * shadowSize;

if (onlyHighlight) {
return (
<g className="shape">
{highlighted && hightlightNode}
</g>
);
}
const clipId = `mask-${id}`;
const {height, hasMetric, formattedValue} = getMetricValue(metric, size);
const metricStyle = { fill: getMetricColor(metric) };
const className = classNames('shape', { metrics: hasMetric });
const fontSize = size * CANVAS_METRIC_FONT_SIZE;

return (
<g className="shape">
{highlighted && hightlightNode}
<g className={className}>
{hasMetric && getClipPathDefinition(clipId, size, height, size - height +
upperHexBitHeight, 0)}
{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))} />
<path className="shadow" {...pathProps(shadowSize)} />
{hasMetric && <path className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} {...pathProps(shadowSize)} />}
{highlighted && hasMetric ?
<text style={{fontSize}}>
{formattedValue}
</text> :
<circle className="node" r={Math.max(2, (size * 0.125))} />}
</g>
);
}
1 change: 1 addition & 0 deletions client/app/scripts/charts/node-shape-rounded-square.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 (
Expand Down
52 changes: 31 additions & 21 deletions client/app/scripts/charts/node-shape-square.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
import React from 'react';
import classNames from 'classnames';
import {getMetricValue, getMetricColor, getClipPathDefinition} from '../utils/metric-utils.js';
import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js';

export default function NodeShapeSquare({onlyHighlight, highlighted, size, color, rx = 0, ry = 0}) {
const rectProps = v => ({
width: v * size * 2,
height: v * size * 2,
rx: v * size * rx,
ry: v * size * ry,
transform: `translate(-${size * v}, -${size * v})`
});

const hightlightNode = <rect className="highlighted" {...rectProps(0.7)} />;
export default function NodeShapeSquare({
id, highlighted, size, color, rx = 0, ry = 0, metric
}) {
const rectProps = (scale, radiusScale) => ({
width: scale * size * 2,
height: scale * size * 2,
rx: (radiusScale || scale) * size * rx,
ry: (radiusScale || scale) * size * ry,
x: -size * scale,
y: -size * scale
});

if (onlyHighlight) {
return (
<g className="shape">
{highlighted && hightlightNode}
</g>
);
}
const clipId = `mask-${id}`;
const {height, hasMetric, formattedValue} = getMetricValue(metric, size);
const metricStyle = { fill: getMetricColor(metric) };
const className = classNames('shape', { metrics: hasMetric });
const fontSize = size * CANVAS_METRIC_FONT_SIZE;

return (
<g className="shape">
{highlighted && hightlightNode}
<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 className={className}>
{hasMetric && getClipPathDefinition(clipId, size, height)}
{highlighted && <rect className="highlighted" {...rectProps(0.7)} />}
<rect className="border" stroke={color} {...rectProps(0.5, 0.5)} />
<rect className="shadow" {...rectProps(0.45, 0.39)} />
{hasMetric && <rect className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} {...rectProps(0.45, 0.39)} />}
{highlighted && hasMetric ?
<text style={{fontSize}}>
{formattedValue}
</text> :
<circle className="node" r={Math.max(2, (size * 0.125))} />}
</g>
);
}
23 changes: 7 additions & 16 deletions client/app/scripts/charts/node-shape-stack.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import React from 'react';
import _ from 'lodash';

import { isContrastMode } from '../utils/contrast-utils';

function dissoc(obj, key) {
const newObj = _.clone(obj);
delete newObj[key];
return newObj;
}

export default function NodeShapeStack(props) {
const propsNoHighlight = dissoc(props, 'highlighted');
const propsOnlyHighlight = Object.assign({}, props, {onlyHighlight: true});
const contrastMode = isContrastMode();

const Shape = props.shape;
const [dx, dy] = contrastMode ? [0, 8] : [0, 5];
const dsx = (props.size * 2 + (dx * 2)) / (props.size * 2);
Expand All @@ -22,16 +11,18 @@ export default function NodeShapeStack(props) {

return (
<g transform={`translate(${dx * -1}, ${dy * -2.5})`} className="stack">
<g transform={`scale(${hls})translate(${dx}, ${dy}) `}>
<Shape {...propsOnlyHighlight} />
<g transform={`scale(${hls})translate(${dx}, ${dy})`} className="onlyHighlight">
<Shape {...props} />
</g>
<g transform={`translate(${dx * 2}, ${dy * 2})`}>
<Shape {...propsNoHighlight} />
<Shape {...props} />
</g>
<g transform={`translate(${dx * 1}, ${dy * 1})`}>
<Shape {...propsNoHighlight} />
<Shape {...props} />
</g>
<g className="onlyMetrics">
<Shape {...props} />
</g>
<Shape {...propsNoHighlight} />
</g>
);
}
16 changes: 15 additions & 1 deletion client/app/scripts/charts/nodes-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,13 @@ export default class NodesChart extends React.Component {
edges: makeMap()
});
}
//
// FIXME add PureRenderMixin, Immutables, and move the following functions to render()
// _.assign(state, this.updateGraphState(nextProps, state));
if (nextProps.forceRelayout || nextProps.nodes !== this.props.nodes) {
_.assign(state, this.updateGraphState(nextProps, state));
}

if (this.props.selectedNodeId !== nextProps.selectedNodeId) {
_.assign(state, this.restoreLayout(state));
}
Expand Down Expand Up @@ -131,6 +134,13 @@ export default class NodesChart extends React.Component {
return 1;
};

// TODO: think about pulling this up into the store.
const metric = node => (
node.get('metrics') && node.get('metrics')
.filter(m => m.get('id') === this.props.selectedMetric)
.first()
);

return nodes
.toIndexedSeq()
.map(setHighlighted)
Expand All @@ -151,6 +161,7 @@ export default class NodesChart extends React.Component {
pseudo={node.get('pseudo')}
nodeCount={node.get('nodeCount')}
subLabel={node.get('subLabel')}
metric={metric(node)}
rank={node.get('rank')}
selectedNodeScale={selectedNodeScale}
nodeScale={nodeScale}
Expand Down Expand Up @@ -246,6 +257,7 @@ export default class NodesChart extends React.Component {
id,
label: node.get('label'),
pseudo: node.get('pseudo'),
metrics: node.get('metrics'),
subLabel: node.get('label_minor'),
nodeCount: node.get('node_count'),
rank: node.get('rank'),
Expand Down Expand Up @@ -423,7 +435,9 @@ export default class NodesChart extends React.Component {
if (!graph) {
return {maxNodesExceeded: true};
}
stateNodes = graph.nodes;
stateNodes = graph.nodes.mergeDeep(stateNodes.map(node => makeMap({
metrics: node.get('metrics')
})));
stateEdges = graph.edges;

// save coordinates for restore
Expand Down
Loading

0 comments on commit c9b323f

Please sign in to comment.