Cache pan/zoom per topology

* remembers canvas position/zoom per topology
* makes it easy to go back to the last position in a prev topology

Fixes #1238
This commit is contained in:
David Kaltschmidt
2016-04-11 18:51:39 +02:00
parent d237f52a90
commit cd12d867ec
4 changed files with 59 additions and 22 deletions

View File

@@ -23,6 +23,8 @@ const MARGINS = {
bottom: 0
};
const ZOOM_CACHE_FIELDS = ['scale', 'panTranslateX', 'panTranslateY'];
// make sure circular layouts a bit denser with 3-6 nodes
const radiusDensity = d3.scale.threshold()
.domain([3, 6]).range([2.5, 3.5, 3]);
@@ -43,7 +45,8 @@ export default class NodesChart extends React.Component {
panTranslateY: 0,
scale: 1,
selectedNodeScale: d3.scale.linear(),
hasZoomed: false
hasZoomed: false,
zoomCache: {}
};
}
@@ -58,13 +61,25 @@ export default class NodesChart extends React.Component {
// wipe node states when showing different topology
if (nextProps.topologyId !== this.props.topologyId) {
_.assign(state, {
// re-apply cached canvas zoom/pan to d3 behavior
const nextZoom = this.state.zoomCache[nextProps.topologyId];
if (nextZoom) {
this.zoom.scale(nextZoom.scale);
this.zoom.translate([nextZoom.panTranslateX, nextZoom.panTranslateY]);
}
// saving previous zoom state
const prevZoom = _.pick(this.state, ZOOM_CACHE_FIELDS);
const zoomCache = _.assign({}, this.state.zoomCache);
zoomCache[this.props.topologyId] = prevZoom;
// clear canvas and apply zoom state
_.assign(state, nextZoom, { zoomCache }, {
nodes: makeMap(),
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));

View File

@@ -3,7 +3,7 @@ import debug from 'debug';
import { fromJS, Map as makeMap, Set as ImmSet } from 'immutable';
import { EDGE_ID_SEPARATOR } from '../constants/naming';
import { updateNodeDegrees } from '../utils/topology-utils';
import { buildTopologyCacheId, updateNodeDegrees } from '../utils/topology-utils';
const log = debug('scope:nodes-layout');
@@ -25,17 +25,6 @@ function fromGraphNodeId(encodedId) {
return encodedId.replace('<DOT>', '.');
}
function buildCacheIdFromOptions(options) {
if (options) {
let id = options.topologyId;
if (options.topologyOptions) {
id += JSON.stringify(options.topologyOptions);
}
return id;
}
return '';
}
/**
* Layout engine runner
* After the layout engine run nodes and edges have x-y-coordinates. Engine is
@@ -348,7 +337,7 @@ function copyLayoutProperties(layout, nodeCache, edgeCache) {
*/
export function doLayout(immNodes, immEdges, opts) {
const options = opts || {};
const cacheId = buildCacheIdFromOptions(options);
const cacheId = buildTopologyCacheId(options.topologyId, options.topologyOptions);
// one engine and node and edge caches per topology, to keep renderings similar
if (!topologyCaches[cacheId]) {

View File

@@ -107,17 +107,27 @@ describe('TopologyUtils', () => {
expect(nodes.n3.degree).toEqual(0);
});
describe('buildTopologyCacheId', () => {
it('should generate a cache ID', () => {
const fun = TopologyUtils.buildTopologyCacheId;
expect(fun()).toEqual('');
expect(fun('test')).toEqual('test');
expect(fun(undefined, 'test')).toEqual('');
expect(fun('test', {a: 1})).toEqual('test{"a":1}');
});
});
describe('filterHiddenTopologies', () => {
it('should filter out empty topos that set hide_if_empty=true', () => {
const topos = [
{id: 'a', hide_if_empty: true, stats: {node_count: 0, filtered_nodes:0}},
{id: 'b', hide_if_empty: true, stats: {node_count: 1, filtered_nodes:0}},
{id: 'c', hide_if_empty: true, stats: {node_count: 0, filtered_nodes:1}},
{id: 'd', hide_if_empty: false, stats: {node_count: 0, filtered_nodes:0}}
{id: 'a', hide_if_empty: true, stats: {node_count: 0, filtered_nodes: 0}},
{id: 'b', hide_if_empty: true, stats: {node_count: 1, filtered_nodes: 0}},
{id: 'c', hide_if_empty: true, stats: {node_count: 0, filtered_nodes: 1}},
{id: 'd', hide_if_empty: false, stats: {node_count: 0, filtered_nodes: 0}}
];
const res = TopologyUtils.filterHiddenTopologies(topos);
expect(res.map(t => t.id)).toEqual(['b', 'c', 'd']);
});
})
});
});

View File

@@ -1,5 +1,28 @@
import _ from 'lodash';
/**
* Returns a cache ID based on the topologyId and optionsQuery
* @param {String} topologyId
* @param {object} topologyOptions (optional)
* @return {String}
*/
export function buildTopologyCacheId(topologyId, topologyOptions) {
let id = '';
if (topologyId) {
id = topologyId;
if (topologyOptions) {
id += JSON.stringify(topologyOptions);
}
}
return id;
}
/**
* Returns a topology object from the topology tree
* @param {List} subTree
* @param {String} topologyId
* @return {Map} topology if found
*/
export function findTopologyById(subTree, topologyId) {
let foundTopology;