Merge pull request #60 from weaveworks/layout-tuning

Cleanup menu layout and remove unused styles
This commit is contained in:
David
2015-05-20 10:09:25 +02:00
8 changed files with 9 additions and 568 deletions

View File

@@ -1,302 +0,0 @@
var d3 = require('d3');
var _ = require('lodash');
var EventEmitter = require('events').EventEmitter;
var Node = require('./node');
function determineAllEdges(nodes, allNodes) {
var edges = [],
edgeIds = {},
nodeIds = _.pluck(nodes, 'id');
_.each(nodes, function(node) {
var nodeId = node.id;
_.each(node.adjacency, function(adjacent) {
var edge = [nodeId, adjacent],
edgeId = edge.join('-');
if (!edgeIds[edgeId] && _.contains(nodeIds, nodeId) && _.contains(nodeIds, adjacent)) {
edges.push({
id: edgeId,
value: 5,
source: allNodes[edge[0]],
target: allNodes[edge[1]]
});
edgeIds[edgeId] = true;
}
});
});
return edges;
}
function getAdjacentNodes(nodes, rootNodeIds) {
var rootNodes = _.compact(_.map(rootNodeIds, function(nodeId) {
return nodes[nodeId];
}));
var allAdjacentNodeIds = _.union(_.flatten(_.map(rootNodes, function(node) {
return node.adjacency;
})), rootNodeIds);
return _.compact(_.map(allAdjacentNodeIds, function(nodeId) {
return nodes[nodeId];
}));
}
function getAdjacentEdges(nodes, root) {
return _.map(root.adjacency, function(nodeId) {
var edge = [root.id, nodeId],
edgeId = edge.join('-');
return {
id: edgeId,
value: 10,
source: nodes[edge[0]],
target: nodes[edge[1]]
};
});
}
function id(d) {
return d.id;
}
function degree(d) {
return d.adjacency ? d.adjacency.length : 1;
}
function getChildren(node, allNodes) {
return _.map(node.adjacency, function(nodeId) {
return allNodes[nodeId];
});
}
function dblclick(d) {
d.fixed = false;
}
function nodeExplorer() {
var color = d3.scale.category20();
var pie = d3.layout.pie()
.value(degree);
var radius = d3.scale.sqrt()
.range([1, 8]);
var drag = d3.behavior.drag();
var dispatcher = new EventEmitter();
var nodeLocations = {};
var width, height;
function circleRadius() {
return width / 4;
}
function radialLayout(centerNode, nodes, radius) {
var slices = pie(_.sortBy(_.filter(nodes, function(node) {
return !node.layedout && _.contains(centerNode.adjacency, node.id);
}), 'id'));
_.each(slices, function(slice) {
var previousXY = nodeLocations[slice.data.id];
slice.data.x = centerNode.x + circleRadius() * Math.sin((slice.startAngle + slice.endAngle) / 2);
slice.data.y = centerNode.y + circleRadius() * Math.cos((slice.startAngle + slice.endAngle) / 2);
slice.data.layedout = true;
// nodeLocations[slice.data.id] = [slice.data.forceX, slice.data.forceY];
});
}
function chart(selection) {
selection.each(function(data) {
var allNodes = data.nodes;
var root = allNodes[data.root];
var expandedNodeIds = data.expandedNodes;
var centerX = width / 2;
var centerY = height / 2;
var nodes = getAdjacentNodes(allNodes, expandedNodeIds);
if (root) {
var previousXY = nodeLocations[root.id];
root.x = previousXY ? previousXY[0] : centerX;
root.y = previousXY ? previousXY[1] : centerY;
root.layedout = true;
_.each(nodes, function(node) {
if (node.id !== root.id) {
node.layedout = false;
}
});
_.each(expandedNodeIds, function(nodeId, i) {
var centerNode = allNodes[nodeId];
if (centerNode) {
radialLayout(centerNode, nodes, i ? circleRadius() / 4 : circleRadius());
}
});
if (nodes.length == 0) {
nodes.push(root);
}
}
var edges = determineAllEdges(nodes, allNodes);
// Select the svg element, if it exists.
var svg = d3.select(this).selectAll("svg").data([data]);
// Otherwise, create the skeletal chart.
var gEnter = svg.enter().append("svg")
.attr('width', "100%")
.attr('height', "100%");
gEnter.append('g')
.classed('links', true);
gEnter.append('g')
.classed('nodes', true);
var link = svg.select('.links').selectAll(".link")
.data(edges, id);
link.exit()
.remove()
link.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.select('.nodes').selectAll(".node")
.data(nodes, id);
node.exit()
.remove();
gEnter = node.enter().append("g")
.attr("class", "node")
.on("dblclick", dblclick)
.on('click', function(d) {
if (d3.event.defaultPrevented) {
return; // click suppressed
}
dispatcher.emit('node.click', d);
})
.call(drag);
gEnter.append("circle")
.attr('class', 'outer')
.style("fill", function(d) {
return color(d.label_major);
})
.attr("r", function(d) {
return radius(degree(d)) + 4;
});
gEnter.append("circle")
.style("fill", function(d) {
return color(d.label_major);
})
.attr("r", function(d) {
return radius(degree(d));
});
gEnter.append("text")
.attr('class', 'label-major')
.attr("dy", "-.25em")
.text(function(d) { return d.label_major; });
gEnter.append("text")
.attr('class', 'label-minor')
.attr("dy", ".75em")
.text(function(d) { return d.label_minor; });
if (!root) {
return;
}
node
.classed('node-root', function(d) {
return d.id === root.id;
})
.classed('node-expanded', function(d) {
return _.contains(expandedNodeIds, d.id);
})
.classed('node-leaf', function(d) {
return _.size(d.adjacency) === 0;
});
function updatePositions() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
node.selectAll('text')
.attr("dx", function(d) { return d.x > centerX ? radius(degree(d)) + 6 : - radius(degree(d)) - 6})
.attr("text-anchor", function(d) { return d.x > centerX ? "start" : "end"; });
}
var density = Math.sqrt(nodes.length / (width * height));
drag.on('drag', function(d) {
d.x = d3.event.x;
d.y = d3.event.y;
updatePositions();
});
updatePositions();
});
}
chart.create = function(el, state) {
d3.select(el)
.datum(state)
.call(chart);
return chart;
};
chart.update = _.throttle(function(el, state) {
d3.select(el)
.datum(state)
.call(chart);
return chart;
}, 500);
chart.on = function(event, callback) {
dispatcher.on(event, callback);
};
chart.teardown = function() {
force.on('tick', null);
};
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
return chart;
}
module.exports = nodeExplorer;

View File

@@ -4,7 +4,6 @@ var React = require('react');
var _ = require('lodash');
var Logo = require('./logo');
var SearchBar = require('./search-bar.js');
var AppStore = require('../stores/app-store');
var Groupings = require('./groupings.js');
var Status = require('./status.js');
@@ -14,7 +13,6 @@ var WebapiUtils = require('../utils/web-api-utils');
var AppActions = require('../actions/app-actions');
var Details = require('./details');
var Nodes = require('./nodes');
var ViewOptions = require('./view-options');
var RouterUtils = require('../utils/router-utils');

View File

@@ -1,89 +0,0 @@
/** @jsx React.DOM */
var React = require('react');
var _ = require('lodash');
var Stats = require('./stats.js');
var TopologyActions = require('../actions/topology-actions');
var SearchBar = React.createClass({
getDefaultProps: function() {
return {
initialFilterText: ''
};
},
getInitialState: function() {
return {
filterAdjacent: false,
filterText: this.props.filterText
};
},
handleChange: function() {
TopologyActions.inputFilterText(this.state.filterText);
},
onVolatileChange: function(event) {
this.setState({
filterText: event.target.value
});
this.scheduleChange();
},
scheduleChange: _.debounce(function() {
this.handleChange();
}, 300),
onFilterAjacent: function(event) {
this.setState({
filterAdjacent: event.target.checked
});
TopologyActions.checkFilterAdjacent(event.target.checked);
},
componentWillMount: function() {
this.setState({
filterText: this.props.filterText
});
},
componentDidMount: function() {
this.refs.filterTextInput.getDOMNode().focus();
},
render: function() {
return (
<div className="row" id="search-bar">
<div className="form-group col-md-9">
<div className="form-control-wrapper">
<input className="form-control input-lg" type="text"
id="filterTextInput"
placeholder="Filter for nodes"
value={this.state.filterText}
ref="filterTextInput"
onChange={this.onVolatileChange} />
<span className="material-input"></span>
</div>
</div>
<div className="form-group col-md-3">
<div className="checkbox">
<label>
<input type="checkbox"
ref="filterAdjacent"
value={this.state.filterAdjacent}
onChange={this.onFilterAjacent} />
<span className="check"></span>
Include connected
</label>
</div>
</div>
</div>
);
}
});
module.exports = SearchBar;

View File

@@ -1,18 +0,0 @@
/** @jsx React.DOM */
var React = require('react');
var StatValue = React.createClass({
render: function() {
return (
<div className="stat-value">
<span className="value">{this.props.value}</span>
<span className="stat-label">{this.props.label}</span>
</div>
);
}
});
module.exports = StatValue;

View File

@@ -1,30 +0,0 @@
/** @jsx React.DOM */
var React = require('react');
var _ = require('lodash');
var StatValue = require('./stat-value.js');
var Stats = React.createClass({
render: function() {
var nodeCount = _.size(this.props.nodes),
edgeCount = _.reduce(this.props.nodes, function(result, node) {
return result + _.size(node.adjacency);
}, 0);
return (
<div id="stats">
<div className="col-xs-6">
<StatValue value={nodeCount} label="Nodes" />
</div>
<div className="col-xs-6">
<StatValue value={edgeCount} label="Connections" />
</div>
</div>
);
}
});
module.exports = Stats;

View File

@@ -1,93 +0,0 @@
/** @jsx React.DOM */
var React = require('react');
var _ = require('lodash');
var AppActions = require('../actions/app-actions');
var ViewOptions = React.createClass({
getInitialState: function() {
return {
menuVisible: false
};
},
onClick: function(ev) {
ev.preventDefault();
AppActions.clickTopologyMode(ev.currentTarget.rel);
},
onMouseOver: function(ev) {
this.handleMouseDebounced(true);
},
onMouseOut: function(ev) {
this.handleMouseDebounced(false);
},
componentWillMount: function() {
this.handleMouseDebounced = _.debounce(function(isOver) {
this.setState({
menuVisible: isOver
});
}, 200);
},
getActiveViewMode: function() {
var className = this.props.active === 'class' ? "glyphicon glyphicon-th-large" : "glyphicon glyphicon-th";
return (
<div className="nav-preview" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<div rel={this.props.active} onClick={this.props.onClick}>
<div className="nav-icon">
<span className={className}></span>
</div>
<div className="nav-label">
View Options
</div>
</div>
</div>
);
},
render: function() {
var activeMode = this.props.active,
activeOptions = this.getActiveViewMode(),
baseClass = "",
individualClass = activeMode == 'individual' ? baseClass + ' active' : baseClass,
classClass = activeMode == 'class' ? baseClass + ' active' : baseClass,
navClassName = "nav navbar-nav";
return (
<div className="navbar-view-options">
{activeOptions}
{this.state.menuVisible && <ul className={navClassName} onMouseOut={this.onMouseOut} onMouseOver={this.onMouseOver}>
<li className={individualClass}>
<a href="#" className="row" rel="individual" onClick={this.onClick}>
<div className="col-xs-5 nav-item-preview">
<span className="glyphicon glyphicon-th"></span>
</div>
<div className="col-xs-7 nav-item-label">
Standard View
</div>
</a>
</li>
<li className={classClass}>
<a href="#" className="row" rel="class" onClick={this.onClick}>
<div className="col-xs-5 nav-item-preview">
<span className="glyphicon glyphicon-th-large"></span>
</div>
<div className="col-xs-7 nav-item-label">
Group View
</div>
</a>
</li>
</ul>}
</div>
);
}
});
module.exports = ViewOptions;

View File

@@ -52,18 +52,20 @@ body {
}
.logo {
margin: -8px 64px 0 64px;
margin: -8px 0 0 64px;
height: 64px;
width: 250px;
float: left;
}
.topologies {
.topologies,
.groupings {
float: left;
position: relative;
margin-top: 7px;
margin-left: 128px;
margin-left: 48px;
}
.topologies {
&-icon {
font-size: 12px;
color: @text-secondary-color;
@@ -79,7 +81,7 @@ body {
&-label {
color: @text-secondary-color;
font-size: 16px;
font-size: 15px;
text-transform: uppercase;
}
@@ -92,13 +94,8 @@ body {
}
.groupings {
float: left;
position: relative;
margin-top: 7px;
margin-left: 128px;
&-item {
font-size: 16px;
font-size: 15px;
margin: 8px 12px 6px 0;
cursor: pointer;
display: inline-block;
@@ -112,7 +109,6 @@ body {
.status {
float: right;
position: relative;
margin-top: 14px;
margin-right: 64px;
@@ -126,27 +122,6 @@ body {
&-label {
margin-left: 0.5em;
}
}
#stats {
.stat-value {
text-align: center;
padding-top: 15px;
}
.value {
font-size: 36px;
display: block;
}
.stat-label {
position: relative;
top: 4px;
color: @text-secondary-color;
text-transform: uppercase;
font-weight: normal;
}
}
#nodes {

View File

@@ -62,7 +62,7 @@ gulp.task('images', function () {
gulp.task('fonts', function () {
return gulp.src('node_modules/font-awesome/fonts/*')
.pipe($.filter('**/*.{eot,svg,ttf,woff}'))
.pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}'))
.pipe($.flatten())
.pipe($.if(isDev, gulp.dest('.tmp/fonts')))
.pipe($.if(isProd, gulp.dest('dist/fonts')))