File size: 25,714 Bytes
bc20498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
import * as util from '../util';
import * as is from '../is';
import Promise from '../promise';

const styfn = {};

// keys for style blocks, e.g. ttfftt
const TRUE = 't';
const FALSE = 'f';

// (potentially expensive calculation)
// apply the style to the element based on
// - its bypass
// - what selectors match it
styfn.apply = function( eles ){
  let self = this;
  let _p = self._private;
  let cy = _p.cy;
  let updatedEles = cy.collection();

  for( let ie = 0; ie < eles.length; ie++ ){
    let ele = eles[ ie ];
    let cxtMeta = self.getContextMeta( ele );

    if( cxtMeta.empty ){
      continue;
    }

    let cxtStyle = self.getContextStyle( cxtMeta );
    let app = self.applyContextStyle( cxtMeta, cxtStyle, ele );

    if( ele._private.appliedInitStyle ){
      self.updateTransitions( ele, app.diffProps );
    } else {
      ele._private.appliedInitStyle = true;
    }

    let hintsDiff = self.updateStyleHints( ele );

    if( hintsDiff ){
      updatedEles.push( ele );
    }

  } // for elements

  return updatedEles;
};

styfn.getPropertiesDiff = function( oldCxtKey, newCxtKey ){
  let self = this;
  let cache = self._private.propDiffs = self._private.propDiffs || {};
  let dualCxtKey = oldCxtKey + '-' + newCxtKey;
  let cachedVal = cache[ dualCxtKey ];

  if( cachedVal ){
    return cachedVal;
  }

  let diffProps = [];
  let addedProp = {};

  for( let i = 0; i < self.length; i++ ){
    let cxt = self[ i ];
    let oldHasCxt = oldCxtKey[ i ] === TRUE;
    let newHasCxt = newCxtKey[ i ] === TRUE;
    let cxtHasDiffed = oldHasCxt !== newHasCxt;
    let cxtHasMappedProps = cxt.mappedProperties.length > 0;

    if( cxtHasDiffed || ( newHasCxt && cxtHasMappedProps )){
      let props;

      if( cxtHasDiffed && cxtHasMappedProps ){
        props = cxt.properties; // suffices b/c mappedProperties is a subset of properties
      } else if( cxtHasDiffed ){
        props = cxt.properties; // need to check them all
      } else if( cxtHasMappedProps ){
        props = cxt.mappedProperties; // only need to check mapped
      }

      for( let j = 0; j < props.length; j++ ){
        let prop = props[ j ];
        let name = prop.name;

        // if a later context overrides this property, then the fact that this context has switched/diffed doesn't matter
        // (semi expensive check since it makes this function O(n^2) on context length, but worth it since overall result
        // is cached)
        let laterCxtOverrides = false;
        for( let k = i + 1; k < self.length; k++ ){
          let laterCxt = self[ k ];
          let hasLaterCxt = newCxtKey[ k ] === TRUE;

          if( !hasLaterCxt ){ continue; } // can't override unless the context is active

          laterCxtOverrides = laterCxt.properties[ prop.name ] != null;

          if( laterCxtOverrides ){ break; } // exit early as long as one later context overrides
        }

        if( !addedProp[ name ] && !laterCxtOverrides ){
          addedProp[ name ] = true;
          diffProps.push( name );
        }
      } // for props
    } // if

  } // for contexts

  cache[ dualCxtKey ] = diffProps;
  return diffProps;
};

styfn.getContextMeta = function( ele ){
  let self = this;
  let cxtKey = '';
  let diffProps;
  let prevKey = ele._private.styleCxtKey || '';

  // get the cxt key
  for( let i = 0; i < self.length; i++ ){
    let context = self[ i ];
    let contextSelectorMatches = context.selector && context.selector.matches( ele ); // NB: context.selector may be null for 'core'

    if( contextSelectorMatches ){
      cxtKey += TRUE;
    } else {
      cxtKey += FALSE;
    }
  } // for context

  diffProps = self.getPropertiesDiff( prevKey, cxtKey );

  ele._private.styleCxtKey = cxtKey;

  return {
    key: cxtKey,
    diffPropNames: diffProps,
    empty: diffProps.length === 0
  };
};

// gets a computed ele style object based on matched contexts
styfn.getContextStyle = function( cxtMeta ){
  let cxtKey = cxtMeta.key;
  let self = this;
  let cxtStyles = this._private.contextStyles = this._private.contextStyles || {};

  // if already computed style, returned cached copy
  if( cxtStyles[ cxtKey ] ){ return cxtStyles[ cxtKey ]; }

  let style = {
    _private: {
      key: cxtKey
    }
  };

  for( let i = 0; i < self.length; i++ ){
    let cxt = self[ i ];
    let hasCxt = cxtKey[ i ] === TRUE;

    if( !hasCxt ){ continue; }

    for( let j = 0; j < cxt.properties.length; j++ ){
      let prop = cxt.properties[ j ];

      style[ prop.name ] = prop;
    }
  }

  cxtStyles[ cxtKey ] = style;
  return style;
};

styfn.applyContextStyle = function( cxtMeta, cxtStyle, ele ){
  let self = this;
  let diffProps = cxtMeta.diffPropNames;
  let retDiffProps = {};
  let types = self.types;

  for( let i = 0; i < diffProps.length; i++ ){
    let diffPropName = diffProps[ i ];
    let cxtProp = cxtStyle[ diffPropName ];
    let eleProp = ele.pstyle( diffPropName );

    if( !cxtProp ){ // no context prop means delete
      if( !eleProp ){
        continue; // no existing prop means nothing needs to be removed
        // nb affects initial application on mapped values like control-point-distances
      } else if( eleProp.bypass ){
        cxtProp = { name: diffPropName, deleteBypassed: true };
      } else {
        cxtProp = { name: diffPropName, delete: true };
      }
    }

    // save cycles when the context prop doesn't need to be applied
    if( eleProp === cxtProp ){ continue; }

    // save cycles when a mapped context prop doesn't need to be applied
    if(
      cxtProp.mapped === types.fn // context prop is function mapper
      && eleProp != null // some props can be null even by default (e.g. a prop that overrides another one)
      && eleProp.mapping != null // ele prop is a concrete value from from a mapper
      && eleProp.mapping.value === cxtProp.value // the current prop on the ele is a flat prop value for the function mapper
    ){ // NB don't write to cxtProp, as it's shared among eles (stored in stylesheet)
      let mapping = eleProp.mapping; // can write to mapping, as it's a per-ele copy
      let fnValue = mapping.fnValue = cxtProp.value( ele ); // temporarily cache the value in case of a miss

      if( fnValue === mapping.prevFnValue ){ continue; }
    }

    let retDiffProp = retDiffProps[ diffPropName ] = {
      prev: eleProp
    };

    self.applyParsedProperty( ele, cxtProp );

    retDiffProp.next = ele.pstyle( diffPropName );

    if( retDiffProp.next && retDiffProp.next.bypass ){
      retDiffProp.next = retDiffProp.next.bypassed;
    }
  }

  return {
    diffProps: retDiffProps
  };
};

styfn.updateStyleHints = function(ele){
  let _p = ele._private;
  let self = this;
  let propNames = self.propertyGroupNames;
  let propGrKeys = self.propertyGroupKeys;
  let propHash = ( ele, propNames, seedKey ) => self.getPropertiesHash( ele, propNames, seedKey );
  let oldStyleKey = _p.styleKey;

  if( ele.removed() ){ return false; }

  let isNode = _p.group === 'nodes';

  // get the style key hashes per prop group
  // but lazily -- only use non-default prop values to reduce the number of hashes
  //

  let overriddenStyles = ele._private.style;

  propNames = Object.keys( overriddenStyles );

  for( let i = 0; i < propGrKeys.length; i++ ){
    let grKey = propGrKeys[i];

    _p.styleKeys[ grKey ] = [ util.DEFAULT_HASH_SEED, util.DEFAULT_HASH_SEED_ALT ];
  }

  let updateGrKey1 = (val, grKey) => _p.styleKeys[ grKey ][0] = util.hashInt( val, _p.styleKeys[ grKey ][0] );
  let updateGrKey2 = (val, grKey) => _p.styleKeys[ grKey ][1] = util.hashIntAlt( val, _p.styleKeys[ grKey ][1] );

  let updateGrKey = (val, grKey) => {
    updateGrKey1(val, grKey);
    updateGrKey2(val, grKey);
  };

  let updateGrKeyWStr = (strVal, grKey) => {
    for( let j = 0; j < strVal.length; j++ ){
      let ch = strVal.charCodeAt(j);

      updateGrKey1(ch, grKey);
      updateGrKey2(ch, grKey);
    }
  };

  // - hashing works on 32 bit ints b/c we use bitwise ops
  // - small numbers get cut off (e.g. 0.123 is seen as 0 by the hashing function)
  // - raise up small numbers so more significant digits are seen by hashing
  // - make small numbers larger than a normal value to avoid collisions
  // - works in practice and it's relatively cheap
  let N = 2000000000;
  let cleanNum = val => (-128 < val && val < 128) && Math.floor(val) !== val ? N - ((val * 1024) | 0) : val;

  for( let i = 0; i < propNames.length; i++ ){
    let name = propNames[i];
    let parsedProp = overriddenStyles[ name ];

    if( parsedProp == null ){ continue; }

    let propInfo = this.properties[name];
    let type = propInfo.type;
    let grKey = propInfo.groupKey;
    let normalizedNumberVal;

    if( propInfo.hashOverride != null ){
      normalizedNumberVal = propInfo.hashOverride(ele, parsedProp);
    } else if( parsedProp.pfValue != null ){
      normalizedNumberVal = parsedProp.pfValue;
    }

    // might not be a number if it allows enums
    let numberVal = propInfo.enums == null ? parsedProp.value : null;
    let haveNormNum = normalizedNumberVal != null;
    let haveUnitedNum = numberVal != null;
    let haveNum = haveNormNum || haveUnitedNum;
    let units = parsedProp.units;

    // numbers are cheaper to hash than strings
    // 1 hash op vs n hash ops (for length n string)
    if( type.number && haveNum && !type.multiple ){
      let v = haveNormNum ? normalizedNumberVal : numberVal;

      updateGrKey(cleanNum(v), grKey);

      if( !haveNormNum && units != null ){
        updateGrKeyWStr(units, grKey);
      }
    } else {
      updateGrKeyWStr(parsedProp.strValue, grKey);
    }
  }

  // overall style key
  //

  let hash = [ util.DEFAULT_HASH_SEED, util.DEFAULT_HASH_SEED_ALT ];

  for( let i = 0; i < propGrKeys.length; i++ ){
    let grKey = propGrKeys[i];
    let grHash = _p.styleKeys[ grKey ];

    hash[0] = util.hashInt( grHash[0], hash[0] );
    hash[1] = util.hashIntAlt( grHash[1], hash[1] );
  }

  _p.styleKey = util.combineHashes(hash[0], hash[1]);

  // label dims
  //

  let sk = _p.styleKeys;
  
  _p.labelDimsKey = util.combineHashesArray(sk.labelDimensions);

  let labelKeys = propHash( ele, ['label'], sk.labelDimensions );
  
  _p.labelKey = util.combineHashesArray(labelKeys);
  _p.labelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, labelKeys));

  if( !isNode ){
    let sourceLabelKeys = propHash( ele, ['source-label'], sk.labelDimensions );
    _p.sourceLabelKey = util.combineHashesArray(sourceLabelKeys);
    _p.sourceLabelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, sourceLabelKeys));

    let targetLabelKeys = propHash( ele, ['target-label'], sk.labelDimensions );
    _p.targetLabelKey = util.combineHashesArray(targetLabelKeys);
    _p.targetLabelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, targetLabelKeys));
  }

  // node
  //

  if( isNode ){
    let { nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie } = _p.styleKeys;

    let nodeKeys = [ nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie ].filter(k => k != null).reduce(util.hashArrays, [
      util.DEFAULT_HASH_SEED,
      util.DEFAULT_HASH_SEED_ALT
    ]);
    _p.nodeKey = util.combineHashesArray(nodeKeys);
    
    _p.hasPie = pie != null && pie[0] !== util.DEFAULT_HASH_SEED && pie[1] !== util.DEFAULT_HASH_SEED_ALT;
  }

  return oldStyleKey !== _p.styleKey;
};

styfn.clearStyleHints = function(ele){
  let _p = ele._private;

  _p.styleCxtKey = '';
  _p.styleKeys = {};
  _p.styleKey = null;
  _p.labelKey = null;
  _p.labelStyleKey = null;
  _p.sourceLabelKey = null;
  _p.sourceLabelStyleKey = null;
  _p.targetLabelKey = null;
  _p.targetLabelStyleKey = null;
  _p.nodeKey = null;
  _p.hasPie = null;
};

// apply a property to the style (for internal use)
// returns whether application was successful
//
// now, this function flattens the property, and here's how:
//
// for parsedProp:{ bypass: true, deleteBypass: true }
// no property is generated, instead the bypass property in the
// element's style is replaced by what's pointed to by the `bypassed`
// field in the bypass property (i.e. restoring the property the
// bypass was overriding)
//
// for parsedProp:{ mapped: truthy }
// the generated flattenedProp:{ mapping: prop }
//
// for parsedProp:{ bypass: true }
// the generated flattenedProp:{ bypassed: parsedProp }
styfn.applyParsedProperty = function( ele, parsedProp ){
  let self = this;
  let prop = parsedProp;
  let style = ele._private.style;
  let flatProp;
  let types = self.types;
  let type = self.properties[ prop.name ].type;
  let propIsBypass = prop.bypass;
  let origProp = style[ prop.name ];
  let origPropIsBypass = origProp && origProp.bypass;
  let _p = ele._private;
  let flatPropMapping = 'mapping';

  let getVal = p => {
    if( p == null ){
      return null;
    } else if( p.pfValue != null ){
      return p.pfValue;
    } else {
      return p.value;
    }
  };

  let checkTriggers = () => {
    let fromVal = getVal(origProp);
    let toVal = getVal(prop);

    self.checkTriggers( ele, prop.name, fromVal, toVal );
  };

  // edge sanity checks to prevent the client from making serious mistakes
  if(
    parsedProp.name === 'curve-style'
    && ele.isEdge()
    && (
      ( // loops must be bundled beziers
        parsedProp.value !== 'bezier'
        && ele.isLoop()
      ) || ( // edges connected to compound nodes can not be haystacks
        parsedProp.value === 'haystack'
        && ( ele.source().isParent() || ele.target().isParent() )
      )
    )
  ){
    prop = parsedProp = this.parse( parsedProp.name, 'bezier', propIsBypass );
  }

  if( prop.delete ){ // delete the property and use the default value on falsey value
    style[ prop.name ] = undefined;

    checkTriggers();

    return true;
  }

  if( prop.deleteBypassed ){ // delete the property that the
    if( !origProp ){
      checkTriggers();

      return true; // can't delete if no prop

    } else if( origProp.bypass ){ // delete bypassed
      origProp.bypassed = undefined;

      checkTriggers();

      return true;

    } else {
      return false; // we're unsuccessful deleting the bypassed
    }
  }

  // check if we need to delete the current bypass
  if( prop.deleteBypass ){ // then this property is just here to indicate we need to delete
    if( !origProp ){
      checkTriggers();

      return true; // property is already not defined

    } else if( origProp.bypass ){ // then replace the bypass property with the original
      // because the bypassed property was already applied (and therefore parsed), we can just replace it (no reapplying necessary)
      style[ prop.name ] = origProp.bypassed;

      checkTriggers();

      return true;

    } else {
      return false; // we're unsuccessful deleting the bypass
    }
  }

  let printMappingErr = function(){
    util.warn( 'Do not assign mappings to elements without corresponding data (i.e. ele `' + ele.id() + '` has no mapping for property `' + prop.name + '` with data field `' + prop.field + '`); try a `[' + prop.field + ']` selector to limit scope to elements with `' + prop.field + '` defined' );
  };

  // put the property in the style objects
  switch( prop.mapped ){ // flatten the property if mapped
  case types.mapData: {
    // flatten the field (e.g. data.foo.bar)
    let fields = prop.field.split( '.' );
    let fieldVal = _p.data;

    for( let i = 0; i < fields.length && fieldVal; i++ ){
      let field = fields[ i ];
      fieldVal = fieldVal[ field ];
    }

    if( fieldVal == null ){
      printMappingErr();
      return false;
    }

    let percent;
    if( !is.number( fieldVal ) ){ // then don't apply and fall back on the existing style
      util.warn('Do not use continuous mappers without specifying numeric data (i.e. `' + prop.field + ': ' + fieldVal + '` for `' + ele.id() + '` is non-numeric)');
      return false;
    } else {
      let fieldWidth = prop.fieldMax - prop.fieldMin;

      if( fieldWidth === 0 ){ // safety check -- not strictly necessary as no props of zero range should be passed here
        percent = 0;
      } else {
        percent = (fieldVal - prop.fieldMin) / fieldWidth;
      }
    }

    // make sure to bound percent value
    if( percent < 0 ){
      percent = 0;
    } else if( percent > 1 ){
      percent = 1;
    }

    if( type.color ){
      let r1 = prop.valueMin[0];
      let r2 = prop.valueMax[0];
      let g1 = prop.valueMin[1];
      let g2 = prop.valueMax[1];
      let b1 = prop.valueMin[2];
      let b2 = prop.valueMax[2];
      let a1 = prop.valueMin[3] == null ? 1 : prop.valueMin[3];
      let a2 = prop.valueMax[3] == null ? 1 : prop.valueMax[3];

      let clr = [
        Math.round( r1 + (r2 - r1) * percent ),
        Math.round( g1 + (g2 - g1) * percent ),
        Math.round( b1 + (b2 - b1) * percent ),
        Math.round( a1 + (a2 - a1) * percent )
      ];

      flatProp = { // colours are simple, so just create the flat property instead of expensive string parsing
        bypass: prop.bypass, // we're a bypass if the mapping property is a bypass
        name: prop.name,
        value: clr,
        strValue: 'rgb(' + clr[0] + ', ' + clr[1] + ', ' + clr[2] + ')'
      };

    } else if( type.number ){
      let calcValue = prop.valueMin + (prop.valueMax - prop.valueMin) * percent;
      flatProp = this.parse( prop.name, calcValue, prop.bypass, flatPropMapping );

    } else {
      return false; // can only map to colours and numbers
    }

    if( !flatProp ){ // if we can't flatten the property, then don't apply the property and fall back on the existing style
      printMappingErr();
      return false;
    }

    flatProp.mapping = prop; // keep a reference to the mapping
    prop = flatProp; // the flattened (mapped) property is the one we want

    break;
  }

  // direct mapping
  case types.data: {
    // flatten the field (e.g. data.foo.bar)
    let fields = prop.field.split( '.' );
    let fieldVal = _p.data;

    for( let i = 0; i < fields.length && fieldVal; i++ ){
      let field = fields[ i ];
      fieldVal = fieldVal[ field ];
    }

    if( fieldVal != null ){
      flatProp = this.parse( prop.name, fieldVal, prop.bypass, flatPropMapping );
    }

    if( !flatProp ){ // if we can't flatten the property, then don't apply and fall back on the existing style
      printMappingErr();
      return false;
    }

    flatProp.mapping = prop; // keep a reference to the mapping
    prop = flatProp; // the flattened (mapped) property is the one we want

    break;
  }

  case types.fn: {
    let fn = prop.value;
    let fnRetVal = prop.fnValue != null ? prop.fnValue : fn( ele ); // check for cached value before calling function

    prop.prevFnValue = fnRetVal;

    if( fnRetVal == null ){
      util.warn('Custom function mappers may not return null (i.e. `' + prop.name + '` for ele `' + ele.id() + '` is null)');
      return false;
    }

    flatProp = this.parse( prop.name, fnRetVal, prop.bypass, flatPropMapping );

    if( !flatProp ){
      util.warn('Custom function mappers may not return invalid values for the property type (i.e. `' + prop.name + '` for ele `' + ele.id() + '` is invalid)');
      return false;
    }

    flatProp.mapping = util.copy( prop ); // keep a reference to the mapping
    prop = flatProp; // the flattened (mapped) property is the one we want

    break;
  }

  case undefined:
    break; // just set the property

  default:
    return false; // not a valid mapping
  }

  // if the property is a bypass property, then link the resultant property to the original one
  if( propIsBypass ){
    if( origPropIsBypass ){ // then this bypass overrides the existing one
      prop.bypassed = origProp.bypassed; // steal bypassed prop from old bypass
    } else { // then link the orig prop to the new bypass
      prop.bypassed = origProp;
    }

    style[ prop.name ] = prop; // and set

  } else { // prop is not bypass
    if( origPropIsBypass ){ // then keep the orig prop (since it's a bypass) and link to the new prop
      origProp.bypassed = prop;
    } else { // then just replace the old prop with the new one
      style[ prop.name ] = prop;
    }
  }

  checkTriggers();

  return true;
};

styfn.cleanElements = function( eles, keepBypasses ){
  for( let i = 0; i < eles.length; i++ ){
    let ele = eles[i];

    this.clearStyleHints(ele);

    ele.dirtyCompoundBoundsCache();
    ele.dirtyBoundingBoxCache();

    if( !keepBypasses ){
      ele._private.style = {};
    } else {
      let style = ele._private.style;
      let propNames = Object.keys(style);

      for( let j = 0; j < propNames.length; j++ ){
        let propName = propNames[j];
        let eleProp = style[ propName ];

        if( eleProp != null ){
          if( eleProp.bypass ){
            eleProp.bypassed = null;
          } else {
            style[ propName ] = null;
          }
        }
      }
    }
  }
};

// updates the visual style for all elements (useful for manual style modification after init)
styfn.update = function(){
  let cy = this._private.cy;
  let eles = cy.mutableElements();

  eles.updateStyle();
};

// diffProps : { name => { prev, next } }
styfn.updateTransitions = function( ele, diffProps ){
  let self = this;
  let _p = ele._private;
  let props = ele.pstyle( 'transition-property' ).value;
  let duration = ele.pstyle( 'transition-duration' ).pfValue;
  let delay = ele.pstyle( 'transition-delay' ).pfValue;

  if( props.length > 0 && duration > 0 ){

    let style = {};

    // build up the style to animate towards
    let anyPrev = false;
    for( let i = 0; i < props.length; i++ ){
      let prop = props[ i ];
      let styProp = ele.pstyle( prop );
      let diffProp = diffProps[ prop ];

      if( !diffProp ){ continue; }

      let prevProp = diffProp.prev;
      let fromProp = prevProp;
      let toProp = diffProp.next != null ? diffProp.next : styProp;
      let diff = false;
      let initVal;
      let initDt = 0.000001; // delta time % value for initVal (allows animating out of init zero opacity)

      if( !fromProp ){ continue; }

      // consider px values
      if( is.number( fromProp.pfValue ) && is.number( toProp.pfValue ) ){
        diff = toProp.pfValue - fromProp.pfValue; // nonzero is truthy
        initVal = fromProp.pfValue + initDt * diff;

      // consider numerical values
      } else if( is.number( fromProp.value ) && is.number( toProp.value ) ){
        diff = toProp.value - fromProp.value; // nonzero is truthy
        initVal = fromProp.value + initDt * diff;

      // consider colour values
      } else if( is.array( fromProp.value ) && is.array( toProp.value ) ){
        diff = fromProp.value[0] !== toProp.value[0]
          || fromProp.value[1] !== toProp.value[1]
          || fromProp.value[2] !== toProp.value[2]
        ;

        initVal = fromProp.strValue;
      }

      // the previous value is good for an animation only if it's different
      if( diff ){
        style[ prop ] = toProp.strValue; // to val
        this.applyBypass( ele, prop, initVal ); // from val
        anyPrev = true;
      }

    } // end if props allow ani

    // can't transition if there's nothing previous to transition from
    if( !anyPrev ){ return; }

    _p.transitioning = true;

    ( new Promise(function( resolve ){
      if( delay > 0 ){
        ele.delayAnimation( delay ).play().promise().then( resolve );
      } else {
        resolve();
      }
    }) ).then(function(){
      return ele.animation( {
        style: style,
        duration: duration,
        easing: ele.pstyle( 'transition-timing-function' ).value,
        queue: false
      } ).play().promise();
    }).then(function(){
      // if( !isBypass ){
        self.removeBypasses( ele, props );
        ele.emitAndNotify('style');
      // }

      _p.transitioning = false;
    });

  } else if( _p.transitioning ){
    this.removeBypasses( ele, props );
    ele.emitAndNotify('style');

    _p.transitioning = false;
  }
};

styfn.checkTrigger = function( ele, name, fromValue, toValue, getTrigger, onTrigger ){
  let prop = this.properties[ name ];
  let triggerCheck = getTrigger( prop );

  if( triggerCheck != null && triggerCheck( fromValue, toValue ) ){
    onTrigger(prop);
  }
};

styfn.checkZOrderTrigger = function( ele, name, fromValue, toValue ){
  this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersZOrder, () => {
    this._private.cy.notify('zorder', ele);
  });
};

styfn.checkBoundsTrigger = function( ele, name, fromValue, toValue ){
  this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersBounds, prop => {
    ele.dirtyCompoundBoundsCache();
    ele.dirtyBoundingBoxCache();

    // if the prop change makes the bb of pll bezier edges invalid,
    // then dirty the pll edge bb cache as well
    if( // only for beziers -- so performance of other edges isn't affected
      prop.triggersBoundsOfParallelBeziers
      && ( name === 'curve-style' && (fromValue === 'bezier' || toValue === 'bezier') )
    ){
      ele.parallelEdges().forEach(pllEdge => {
        if( pllEdge.isBundledBezier() ){
          pllEdge.dirtyBoundingBoxCache();
        }
      });
    }

    if(
      prop.triggersBoundsOfConnectedEdges
      && ( name === 'display' && (fromValue === 'none' || toValue === 'none') )  
    ){
      ele.connectedEdges().forEach(edge => {
        edge.dirtyBoundingBoxCache();
      });
    }

  });
};

styfn.checkTriggers = function( ele, name, fromValue, toValue ){
  ele.dirtyStyleCache();

  this.checkZOrderTrigger( ele, name, fromValue, toValue );
  this.checkBoundsTrigger( ele, name, fromValue, toValue );
};

export default styfn;