Skip to content

Commit

Permalink
Refactor selector object structure
Browse files Browse the repository at this point in the history
- The benchmark for selector matching shows a 1.59x improvement over v3.2.
- Represent the match types as an integer enum.  Represent queries with minimal trees.  Direct lookups are used when doing a match of a selector on an element, rather than checking all expressions in order.
- Add more selector tests.
- Update docs for newly enabled edge selectors with subjects, e.g. `$node -> node`.
- Fixes `modules` tests.
- Add JSDoc comments.

Ref #2145, #2165
  • Loading branch information
maxkfranz committed Aug 7, 2018
1 parent e750a47 commit a4ad745
Show file tree
Hide file tree
Showing 15 changed files with 837 additions and 640 deletions.
24 changes: 24 additions & 0 deletions benchmark/selector-filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
var Suite = require('./suite');

var eles;

var suite = new Suite('eles.filter(selector)', {
setup: function( cytoscape ){
var cy = cytoscape({ elements: require('./graphs/gal') });

eles = cy.nodes();

return cy;
}
});

suite
.add( function( cy ) {
// n.b.
// - use a selector that matches all nodes so we really compare the selector matching rather than letting the matches exit early
// - only create one selector : compare matching perf, not creation perf
eles.filter('node:unselected:grabbable[gal80Rexp > 0][SUID > 0][Stress >= 5][AverageShortestPathLength > 0]');
})
;

module.exports = suite;
2 changes: 1 addition & 1 deletion benchmark/single/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// set this to run just a single suite via `gulp benchmark-single`
// (useful when working on a specific function)
var suite = require('../karger-stein');
var suite = require('../selector-filter');

suite.run({ async: true });
3 changes: 2 additions & 1 deletion documentation/md/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ You can get much better performance out of Cytoscape.js by tuning your options,
* Background and outlines increase the expense of rendering labels.
* **Simplify edge style** : Drawing edges can be expensive.
* Set your edges `curve-style` to `haystack` in your stylesheet. Haystack edges are straight lines, which are much less expensive to render than `bezier` edges. This is the default edge style.
* Use solid edges. Dotted and dashed edges are much more expensive to draw, so you will get increased performance by not using them.
* Use solid edges. Dotted and dashed edges are much more expensive to draw, so you will get increased performance by not using them.
* Edge arrows are expensive to render, so consider not using them if they do not have any semantic meaning in your graph.
* Opaque edges with arrows are more than twice as fast as semitransparent edges with arrows.
* **Simplify node style** : Certain styles for nodes can be expensive.
* Background images are very expensive in certain cases. The most performant background images are non-repeating (`background-repeat: no-repeat`) and non-clipped (`background-clip: none`). For simple node shapes like squares or circles, you can use `background-fit` for scaling and preclip your images to simulate software clipping (e.g. with [Gulp](https://github.com/scalableminds/gulp-image-resize) so it's automated). In lieu of preclipping, you could make clever use of PNGs with transparent backgrounds.
* Node borders can be slightly expensive, so you can experiment with removing them to see if it makes a noticeable difference for your use case.
* **Avoid compound and edge selectors** : Compound selectors (e.g. `$node node`) and edge selectors (e.g. `$node -> node`) can be expensive because traversals are necessary.
* **Set a lower pixel ratio** : Because it is more expensive to render more pixels, you can set `pixelRatio` to `1` [in the initialisation options](#init-opts/pixelRatio) to increase performance for large graphs on high density displays. However, this makes the rendering less crisp.
* **Compound nodes** : [Compound nodes](#notation/compound-nodes) make style calculations and rendering more expensive. If your graph does not require compound nodes, you can improve performance by not using compound parent nodes.
* **Hide edges during interactivity** : Set `hideEdgesOnViewport` to `true` in your [initialisation options](#core/initialisation). This can make interactivity less expensive for very large graphs by hiding edges during pan, mouse wheel zoom, pinch-to-zoom, and node drag actions. This option makes a difference on only very, very large graphs.
Expand Down
43 changes: 23 additions & 20 deletions documentation/md/selectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,52 +51,52 @@ Matches element with the matching ID (e.g. `#foo` is the same as `[id = 'foo']`)

## Data

**`[name]`**
**`[name]`**
Matches elements if they have the specified data attribute defined, i.e. not `undefined` (e.g. `[foo]` for an attribute named "foo"). Here, `null` is considered a defined value.

**`[^name]`**
**`[^name]`**
Matches elements if the specified data attribute is not defined, i.e. `undefined` (e.g `[^foo]`). Here, `null` is considered a defined value.

**`[?name]`**
**`[?name]`**
Matches elements if the specified data attribute is a [truthy](http://javascriptweblog.wordpress.com/2011/02/07/truth-equality-and-javascript/) value (e.g. `[?foo]`).

**`[!name]`**
**`[!name]`**
Matches elements if the specified data attribute is a [falsey](http://javascriptweblog.wordpress.com/2011/02/07/truth-equality-and-javascript/) value (e.g. `[!foo]`).

**`[name = value]`**
**`[name = value]`**
Matches elements if their data attribute matches a specified value (e.g. `[foo = 'bar']` or `[num = 2]`).

**`[name != value]`**
**`[name != value]`**
Matches elements if their data attribute doesn't match a specified value (e.g. `[foo != 'bar']` or `[num != 2]`).

**`[name > value]`**
**`[name > value]`**
Matches elements if their data attribute is greater than a specified value (e.g. `[foo > 'bar']` or `[num > 2]`).

**`[name >= value]`**
**`[name >= value]`**
Matches elements if their data attribute is greater than or equal to a specified value (e.g. `[foo >= 'bar']` or `[num >= 2]`).

**`[name < value]`**
**`[name < value]`**
Matches elements if their data attribute is less than a specified value (e.g. `[foo < 'bar']` or `[num < 2]`).

**`[name <= value]`**
**`[name <= value]`**
Matches elements if their data attribute is less than or equal to a specified value (e.g. `[foo <= 'bar']` or `[num <= 2]`).

**`[name *= value]`**
**`[name *= value]`**
Matches elements if their data attribute contains the specified value as a substring (e.g. `[foo *= 'bar']`).

**`[name ^= value]`**
**`[name ^= value]`**
Matches elements if their data attribute starts with the specified value (e.g. `[foo ^= 'bar']`).

**`[name $= value]`**
**`[name $= value]`**
Matches elements if their data attribute ends with the specified value (e.g. `[foo $= 'bar']`).

**`@` (data attribute operator modifier)**
**`@` (data attribute operator modifier)**
Prepended to an operator so that is case insensitive (e.g. `[foo @$= 'ar']`, `[foo @>= 'a']`, `[foo @= 'bar']`)

**`!` (data attribute operator modifier)**
**`!` (data attribute operator modifier)**
Prepended to an operator so that it is negated (e.g. `[foo !$= 'ar']`, `[foo !>= 'a']`)

**`[[]]` (metadata brackets)**
**`[[]]` (metadata brackets)**
Use double square brackets in place of square ones to match against metadata instead of data (e.g. `[[degree > 2]]` matches elements of degree greater than 2). The properties that are supported include `degree`, `indegree`, and `outdegree`.

## Edges
Expand All @@ -107,16 +107,19 @@ Matches edges for which the source and target subselectors match (e.g. `.src ->
**`<->` (undirected edge selector)**
Matches edges for which the connected node subselectors match (e.g. `.foo <-> .bar`)

**`$` (subject selector)**
Sets the subject of the selector (e.g. `$node -> node` to select the source nodes instead of the edges).

## Compound nodes

**`>` (child selector)**
**`>` (child selector)**
Matches direct children of the parent node (e.g. `node > node`).

**<code>&nbsp;</code> (descendant selector)**
**<code>&nbsp;</code> (descendant selector)**
Matches descendants of the parent node (e.g. `node node`).

**`$` (subject selector)**
Sets the subject of the selector (e.g. `$node > node` to select the parent nodes instead of the children). A subject selector may not be used with an edge selector, because the edge ought to be the subject.
**`$` (subject selector)**
Sets the subject of the selector (e.g. `$node > node` to select the parent nodes instead of the children).


## State
Expand Down
92 changes: 92 additions & 0 deletions src/selector/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as is from '../is';

export const valCmp = (fieldVal, operator, value) => {
let matches;
let isFieldStr = is.string( fieldVal );
let isFieldNum = is.number( fieldVal );
let isValStr = is.string(value);
let fieldStr, valStr;
let caseInsensitive = false;
let notExpr = false;
let isIneqCmp = false;

if( operator.indexOf( '!' ) >= 0 ){
operator = operator.replace( '!', '' );
notExpr = true;
}

if( operator.indexOf( '@' ) >= 0 ){
operator = operator.replace( '@', '' );
caseInsensitive = true;
}

if( isFieldStr || isValStr || caseInsensitive ){
fieldStr = !isFieldStr && !isFieldNum ? '' : '' + fieldVal;
valStr = '' + value;
}

// if we're doing a case insensitive comparison, then we're using a STRING comparison
// even if we're comparing numbers
if( caseInsensitive ){
fieldVal = fieldStr = fieldStr.toLowerCase();
value = valStr = valStr.toLowerCase();
}

switch( operator ){
case '*=':
matches = fieldStr.indexOf( valStr ) >= 0;
break;
case '$=':
matches = fieldStr.indexOf( valStr, fieldStr.length - valStr.length ) >= 0;
break;
case '^=':
matches = fieldStr.indexOf( valStr ) === 0;
break;
case '=':
matches = fieldVal === value;
break;
case '>':
isIneqCmp = true;
matches = fieldVal > value;
break;
case '>=':
isIneqCmp = true;
matches = fieldVal >= value;
break;
case '<':
isIneqCmp = true;
matches = fieldVal < value;
break;
case '<=':
isIneqCmp = true;
matches = fieldVal <= value;
break;
default:
matches = false;
break;
}

// apply the not op, but null vals for inequalities should always stay non-matching
if( notExpr && ( fieldVal != null || !isIneqCmp ) ){
matches = !matches;
}

return matches;
};

export const boolCmp = (fieldVal, operator) => {
switch( operator ){
case '?':
return fieldVal ? true : false;
case '!':
return fieldVal ? false : true;
case '^':
return fieldVal === undefined;
}
};

export const existCmp = (fieldVal) => fieldVal !== undefined;

export const data = (ele, field) => ele.data(field);

export const meta = (ele, field) => ele[field]();
Loading

0 comments on commit a4ad745

Please sign in to comment.