Skip to content
This repository has been archived by the owner on Jul 29, 2019. It is now read-only.

Network: Fix handling of multi-fonts #3486

Merged
merged 35 commits into from
Sep 29, 2017

Conversation

wimrijnders
Copy link
Contributor

@wimrijnders wimrijnders commented Sep 26, 2017

Fixes #3408, #2677.

This PR serves to get the handling of the font options right. The fixing of the issue is more or less a side-effect. As a special note, with this PR the group options for fonts are finally handled correctly.

Guiding principles:

  • Option values should not be changed inline. They should remain as entered.
  • Priority order for options should be respected: node, group, default node, global default.

Changes

  • Got rid of Label#parseOptions(). This changes the option values, and we want to keep them as
    specified.

  • Adjusted usage of groups, at least for the font options in groups.

    Group font options were merged into the options of a given node instance, and the label
    handling requires these to be separate. In fact, it is questionable if the group options
    should be merged into the node instance options at all.

  • Unit tests for the setting of font options in all manners have been added.

  • Special Note: The added unit tests picked up an unrelated bug in util.deepExtend() and util.selectiveDeepExtend(); added an extra check on null there.

Further changes

  • Removed member edgeOptions in module EdgesHandler, it's just a reference to this.options
  • Removed member nodeOptions in module NodesHandler, it was useless for the same reason
  • Added/edited commenting extensively
  • Completely rewrote Label.propagateFonts(), additional notes :
    • Special handling in the old code of multi-font mono has been dropped; if users want to
      have a non-proportional font for mono, that's their problem.
    • Special handling of ratio has been dropped. This does not have any effect on the outcome. The value of ratio was always 1 in all the cases I have conceived to test.

Final Comment

If anything is to be concluded from this fix, it's that the option setup and handling within
Network is unnecessarily complicated. This urgently needs to be looked at.

In particular, the prototype chaining of options appears to be deficient. While the idea is
elegant in itself, the prototype order is not sufficient to handle all option sequences; in fact,
the code is rife with exceptions on it. Prototype chaining needs to be replaced with something
probably less elegant but better suited to the task.

This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly).
By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to
squash the bug.

The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object.

Changes:

- Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors.
- Added catch clause in `CanvasRendered._determinePixelRatio()`
- small fix: raised timeout for the network `worldCup2014` unit test
- Small fixes and cleanup in `util.js`
- Removed `util.protoExtend()`, not used anywhere
Copy link
Contributor

@yotamberk yotamberk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really minor changes.
All in all, really good job! Cleans up the code a whole lot!

*/
get(groupname) {
get(groupname, create = true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally like more declarative variables. I think it should be: create => shouldCreate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

var Node = require("./components/Node").default;
var Label = require("./components/shared/Label").default;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean that there are two empty lines? I actually prefer this, for better visual separation. I do it between methods as well, and in other places where I think a bigger distinction between blocks is useful.

* @param {MultiName} multiName sub path for the mod-font
* @returns {ModOptions}
*/
var getFontOptions = (multiName) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you use var? Why not use const and let?
(for consistency)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick response on this one in passing:

I prefer var because chrome has issues dealing with const and let, it can't seem to display the values correctly inline (usually shows undefined even though it has a value). I find this bothersome to the point of forsaking them, even though let would be better.

If you have a solution for this, let me know.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it seems that you douse let in the rest of the file. How does that work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really try to use let. But if it gets in the way of debugging, I revert it to var.

Copy link
Contributor Author

@wimrijnders wimrijnders Sep 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh well, I can change this since I'm done with debugging.

(I hope)

Update: Did var => let for `Label.js.


var fontString = "";
if (values.mod !== undefined && values.mod !== "") { // safeguard for undefined - this happened
fontString += values.mod = " ";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you mean fontString += values.mod + " " ? It seems really weird to me if you meant to assign values.mod = " " and simultaneously add a space to fontString

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Good catch.

@@ -144,7 +142,9 @@ class NodesHandler {
throw 'Internal error: mass in defaultOptions of NodesHandler may not be zero or negative';
}

util.extend(this.options, this.defaultOptions);
//this.options = {};
//util.extend(this.options, this.defaultOptions); // Candidate for the Hall of Shame!
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why keep the dead code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh. For the epitaph, actually. Major stupidity was performed here. I do this often in my own code, so that I remember not to do that thing every time I encounter the comment.

But this is shared code, and keeping it in is just vanity. Hereby gone.

@@ -2,11 +2,24 @@ let util = require('../../../../util');
let ComponentUtil = require('./ComponentUtil').default;
let LabelSplitter = require('./LabelSplitter').default;

/**
* @typedef {'bold'|'ital'|'boldital'|'mono'|'normal'} MultiName
*
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A better name for this type might be FontStyle

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, although I do want to convey that this is the list of names of special fonts that can be used when multi-font is enabled.

Will you settle for MultiFontStyle?

*/

/**
* @typedef {{color:string, size:number, face:string, mod:string, vadjust:number}} ModOptions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest MultiFontOptions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

//
// Main body of the method
//

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this main body comment block can be dropped

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. The idea was to indicate where the actual function code began, after the definitions of the local function. But I just moved them away, so....

* Member fontOptions serves as an accumulator for the current font options.
* As such, it needs to be completely separated from the node options.
*
* @param {Object} options
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the ModOptions from above, or other options?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other options, whatever the user specified or otherwise the defaults.

if (!this.fontOptions.multi) return;
propagateFonts(pile) {
let fontPile = []; // sequence of font objects to consider, order important
let mods = ['bold', 'ital', 'boldital', 'mono'];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a good candidate for a top level constant, rather than re initialized every time propagateFonts() is called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

*
* @returns {object} object with all current own basic font properties
*/
let getBasicOptions = () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm seeing a lot of nested function calls and wondering if it is a micro optimization to bring them up a level, so that they're initialized with the class, rather than initialized with every call to the function that encapsulates them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. My long-term planning is to isolate this pile handling code in a separate class. This is the approach that I want to use to replace the prototype chaining in options. I thought it was too early to make a class of it now.

But fair enough, I can define them as private methods for the time being. Wrt. 'micro optimization', it's an easy task to raise them to class-level, and every teeny bit helps.


assert.equal(modBold(0).color, 'black'); // nodes default
assert.equal(modBold(1).color, 'black'); // more specific bold value overrides group value
assert.equal(modBold(2).color, 'black'); // idem
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idem?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these three changed, and the others aren't? I'm missing something here...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'Idem' : Same as previous comment in this column.

Copy link
Contributor Author

@wimrijnders wimrijnders Sep 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview:

  • The priority of the options is: node, group, default node, global default.
  • black is in 'default node'.
  • multi-font options, if defined, take precedence. The 'normal' font options are fall-backs for the multi-font options.

Specific:

  • Node 0 has no 'node' or 'group' options, hence uses 'default node'.
  • Nodes 1 has group2, which has a default font but no explicit bold font. The bold definition in 'default node' trumps this.
  • Node 2 has group1, the same logic as with Node 1 applies here.
  • Nodes 3 and 4 have bold fonts defined in the node instance. These trump everything.

Welcome to the wonderful world of multi-font options. Precisely this kind of thing is what made fixing the issue so hard. My head still hurts.

//
// Same initialization as previous with a color set for the default node font
//
data.nodes = new vis.DataSet(dataNodes); // Need to reset nodes, changed in previous
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really a reset is it?

It's a new dataset, but the since dataNodes is a mutable array of mutable objects, and not a function who's return value is a new array of objects, the nodes have not been reset.

Copy link
Contributor Author

@wimrijnders wimrijnders Sep 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but dataNodes is not changed anywhere, so the same original values are being entered in the DataSet instance, and DataSet copies values internally[*].

Crap, let me confirm that.
Update: Confirmed with added assertions, nodes dataset is reset to initial values. You had me worried for a moment.


[*] Code check: confirmed. true for non-object options, can't tell for sure for object options, but it does work as described.


done();
});

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great addition of tests! Well done!

I would suggest, however, refactoring these very large tests into a number of much smaller tests, in separate test classes where required, and making use of before and after to set up the state you need.

Copy link
Contributor Author

@wimrijnders wimrijnders Sep 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess; they're kind of lengthy, that's true. I'll have a think about how to carve this up.

Update: See commit; this is as far as I wish/dare to take it. I hope this is satisfactory for you.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent! The only other suggestion I'll make is to follow the mocha convention for pending, or write the entire test and use skip, as described here: https://mochajs.org

This is a nit pick, and I'm approving as-is, with or without my suggestion.

@wimrijnders
Copy link
Contributor Author

wimrijnders commented Sep 27, 2017

@mbroad review 👍, you're being sharp.

* List of special styles for multi-fonts
* @private
*/
const multiFontStyle = ['bold', 'ital', 'boldital', 'mono'];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normal is omitted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. this array is the list of 'special' fonts. Style 'normal' would be the catch-all default font, i.e. not special. It should not be in this particular list.


done();
});

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent! The only other suggestion I'll make is to follow the mocha convention for pending, or write the entire test and use skip, as described here: https://mochajs.org

This is a nit pick, and I'm approving as-is, with or without my suggestion.

@wimrijnders
Copy link
Contributor Author

pending and skip conventions are new to me. I suppose the intention would be to get rid of the TODO's at the top of the unit test file(s)?

OK, I think this is a minor thing. I will leave it as is, but thanks for pointing it out.

@yotamberk yotamberk merged commit 75e6263 into almende:develop Sep 29, 2017
@wimrijnders wimrijnders deleted the fix_modFonts branch September 29, 2017 17:23
@wimrijnders
Copy link
Contributor Author

@justinharrel, since your comment on widthConstraint appears to have been deleted, I take it that the wrapping is not an issue? Just checking.

wimrijnders added a commit to wimrijnders/vis that referenced this pull request Oct 1, 2017
This fixes an oversight in almende#3486. Unit tests added, not only for null labels, but for all weird label values I could thing of.
yotamberk pushed a commit that referenced this pull request Oct 1, 2017
* Network: Add extra check on null value during label handling

This fixes an oversight in #3486. Unit tests added, not only for null labels, but for all weird label values I could thing of.

* Enhanced unit tests, adjusted label check
@justinharrell
Copy link
Contributor

I accidentally commented on commit rather than PR, its still there. widthConstraint still does not seem to work.

Should I just make a new issue?

@wimrijnders
Copy link
Contributor Author

What another failing case? Sure, make a new issue. If you can give a test-case that would be welcome.

primozs pushed a commit to primozs/vis that referenced this pull request Jan 3, 2019
* The next fix on Travis unit test failure

This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly).
By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to
squash the bug.

The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object.

Changes:

- Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors.
- Added catch clause in `CanvasRendered._determinePixelRatio()`
- small fix: raised timeout for the network `worldCup2014` unit test

* Preliminary refactoring in utils.js

* Added unit tests for extend routines, commenting and small fixes

* More unit tests for extend routines

* - Completed unit tests for extend routines in
- Small fixes and cleanup in `util.js`
- Removed `util.protoExtend()`, not used anywhere

* Added unit tests for known font options

* Interim save before trying out another proto chain strategy

* Fixed problem in first example almende#3408

* Removed silly file that shouldn't be there

* Added unit test for multi-fonts

* Comment edits

* Verufy unit tests, small adjustments for groups

* Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP!

* Further tweaks to get unit tests working

* All unit tests passing

* Fixes due to linting

* Small edits

* Removed prototype handling from font pile

* Fixes during testing examples of almende#3408

* Added unit test for edge labels, small fixes

* Added unit tests for shorthand string fonts; some tests still failing

* All unit tests pass

* Removed Label.parseOptions()

* Completed shorthand font tests, code cleanup, fixed choosify for edges

* Addressed review comments

* Addressed review comments, cleanup
primozs pushed a commit to primozs/vis that referenced this pull request Jan 3, 2019
…#3511)

* Network: Add extra check on null value during label handling

This fixes an oversight in almende#3486. Unit tests added, not only for null labels, but for all weird label values I could thing of.

* Enhanced unit tests, adjusted label check
mojoaxel added a commit to visjs/vis-network that referenced this pull request Jul 17, 2019
via @macleodbroad-wf, @wimrijnders
(almende/vis#3228, almende/vis#3470, almende/vis#3486, almende/vis#3511, almende#3520) , almende#3518, almende#3575, almende#3565, almende#3603, almende#3646)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants