Browse Source

fix: Font styles and width and height of nodes (#2385)

* Multifont node and edge labels
* adjust documentation
* rework mod-font property inheritance
* Node max/min widths, min heights; Edge max widths
* Accumulate sizes correctly for unconstrained multipoint labels
* handle font.multi when groupOptions.font is not declared
* propagate label fonts on changes, not just creation
* Allow top-level constraint numbers
* accumulate width and empty lines appropriately
codeClimate
David Anderson 7 years ago
committed by Alexander Wunschik
parent
commit
8a9edbbf87
17 changed files with 1506 additions and 87 deletions
  1. +210
    -2
      docs/network/edges.html
  2. +236
    -2
      docs/network/nodes.html
  3. +115
    -0
      examples/network/labels/labelMultifont.html
  4. +118
    -0
      examples/network/nodeStyles/widthHeight.html
  5. +20
    -2
      lib/network/modules/EdgesHandler.js
  6. +21
    -3
      lib/network/modules/NodesHandler.js
  7. +7
    -4
      lib/network/modules/components/Edge.js
  8. +8
    -4
      lib/network/modules/components/Node.js
  9. +2
    -2
      lib/network/modules/components/nodes/shapes/Box.js
  10. +1
    -1
      lib/network/modules/components/nodes/shapes/Circle.js
  11. +2
    -2
      lib/network/modules/components/nodes/shapes/Database.js
  12. +2
    -2
      lib/network/modules/components/nodes/shapes/Icon.js
  13. +2
    -2
      lib/network/modules/components/nodes/shapes/Text.js
  14. +2
    -1
      lib/network/modules/components/nodes/util/NodeBase.js
  15. +657
    -60
      lib/network/modules/components/shared/Label.js
  16. +82
    -0
      lib/network/options.js
  17. +21
    -0
      lib/util.js

+ 210
- 2
docs/network/edges.html View File

@ -135,7 +135,37 @@ var options = {
background: 'none',
strokeWidth: 2, // px
strokeColor: '#ffffff',
align:'horizontal'
align: 'horizontal',
multi: false,
vadjust: 0,
bold: {
color: '#343434',
size: 14, // px
face: 'arial',
vadjust: 0,
mod: 'bold'
},
ital: {
color: '#343434',
size: 14, // px
face: 'arial',
vadjust: 0,
mod: 'italic',
},
boldital: {
color: '#343434',
size: 14, // px
face: 'arial',
vadjust: 0,
mod: 'bold italic'
},
mono: {
color: '#343434',
size: 15, // px
face: 'courier new',
vadjust: 2,
mod: ''
}
},
hidden: false,
hoverWidth: 1.5,
@ -178,8 +208,9 @@ var options = {
roundness: 0.5
},
title:undefined,
value: undefined,
width: 1,
value: undefined
widthConstraint: false
}
}
@ -390,6 +421,171 @@ network.setOptions(options);
the label will align itself according to the edge.
</td>
</tr>
<tr parent="font" class="hidden">
<td class="indent">font.vadjust</td>
<td>String</td>
<td><code>0</code></td>
<td>A font-specific correction to the vertical positioning of the base font in the label text. (Positive is down.)</td>
</tr>
<tr parent="font" class="hidden">
<td class="indent">font.multi</td>
<td>Boolean or String</td>
<td><code>false</code></td>
<td>If <code>false</code>, the label is treated as pure text drawn with the base font. If <code>true</code> or <code>'html'</code> the label may be multifonted, with bold, italic and code markup, interpreted as html. If the value is <code>'markdown'</code> or <code>'md'</code> the label may be multifonted, with bold, italic and code markup, interpreted as markdown.
The bold, italic, bold-italic and monospaced fonts may be set up under in the font.bold, font.ital, font.boldital and font.mono properties, respectively.</td>
</tr>
<tr parent="font" class='hidden toggle collapsible' onclick="toggleTable('optionTable','bold', this);">
<td><span parent="bold" class="right-caret"></span> font.bold</td>
<td>Object or String</td>
<td><code>false</code></td>
<td>This object defines the details of the bold font in the label. A shorthand is also supported in the form <code>'size face
color'</code> for example: <code>'14px arial red'</code>.
</td>
</tr>
<tr parent="bold" class="hidden">
<td class="indent">font.bold.color</td>
<td>String</td>
<td><code>'#343434'</code></td>
<td>Color of the bold font in the label text. Defaults to the base font's color.</td>
</tr>
<tr parent="bold" class="hidden">
<td class="indent">font.bold.size</td>
<td>Number</td>
<td><code>14</code></td>
<td>Size of the bold font in the label text. Defaults to the base font's size.</td>
</tr>
<tr parent="bold" class="hidden">
<td class="indent">font.bold.face</td>
<td>String</td>
<td><code>'arial'</code></td>
<td>Font face (or font family) of the bold font in the label text. Defaults to the base font's face.</td>
</tr>
<tr parent="bold" class="hidden">
<td class="indent">font.bold.mod</td>
<td>String</td>
<td><code>'bold'</code></td>
<td>A string added to the face and size when determining the bold font in the label text.</td>
</tr>
<tr parent="bold" class="hidden">
<td class="indent">font.bold.vadjust</td>
<td>String</td>
<td><code>0</code></td>
<td>A font-specific correction to the vertical positioning of the bold italic font in the label text. (Positive is down.) Defaults to the base font's valign.</td>
</tr>
<tr parent="font" class='hidden toggle collapsible' onclick="toggleTable('optionTable','ital', this);">
<td><span parent="ital" class="right-caret"></span> font.ital</td>
<td>Object or String</td>
<td><code>false</code></td>
<td>This object defines the details of the italic font in the label. A shorthand is also supported in the form <code>'size face
color'</code> for example: <code>'14px arial red'</code>.
</td>
</tr>
<tr parent="ital" class="hidden">
<td class="indent">font.ital.color</td>
<td>String</td>
<td><code>'#343434'</code></td>
<td>Color of the italic font in the label text. Defaults to the base font's color.</td>
</tr>
<tr parent="ital" class="hidden">
<td class="indent">font.ital.size</td>
<td>Number</td>
<td><code>14</code></td>
<td>Size of the italic font in the label text. Defaults to the base font's size.</td>
</tr>
<tr parent="ital" class="hidden">
<td class="indent">font.ital.face</td>
<td>String</td>
<td><code>'arial'</code></td>
<td>Font face (or font family) of the italic font in the label text. Defaults to the base font's face.</td>
</tr>
<tr parent="ital" class="hidden">
<td class="indent">font.ital.mod</td>
<td>String</td>
<td><code>'italic'</code></td>
<td>A string added to the face and size when determining the italic font in the label text.</td>
</tr>
<tr parent="ital" class="hidden">
<td class="indent">font.ital.vadjust</td>
<td>String</td>
<td><code>0</code></td>
<td>A font-specific correction to the vertical positioning of the italic font in the label text. (Positive is down.) Defaults to the base font's valign.</td>
</tr>
<tr parent="font" class='hidden toggle collapsible' onclick="toggleTable('optionTable','boldital', this);">
<td><span parent="boldital" class="right-caret"></span> font.boldital</td>
<td>Object or String</td>
<td><code>false</code></td>
<td>This object defines the details of the bold italic font in the label. A shorthand is also supported in the form <code>'size face
color'</code> for example: <code>'14px arial red'</code>.
</td>
</tr>
<tr parent="boldital" class="hidden">
<td class="indent">font.boldital.color</td>
<td>String</td>
<td><code>'#343434'</code></td>
<td>Color of the bold italic font in the label text. Defaults to the base font's color.</td>
</tr>
<tr parent="boldital" class="hidden">
<td class="indent">font.boldital.size</td>
<td>Number</td>
<td><code>14</code></td>
<td>Size of the bold italic font in the label text. Defaults to the base font's size.</td>
</tr>
<tr parent="boldital" class="hidden">
<td class="indent">font.boldital.face</td>
<td>String</td>
<td><code>'arial'</code></td>
<td>Font face (or font family) of the bold italic font in the label text. Defaults to the base font's face.</td>
</tr>
<tr parent="boldital" class="hidden">
<td class="indent">font.boldital.mod</td>
<td>String</td>
<td><code>'bold'</code></td>
<td>A string added to the face and size when determining the bold italic font in the label text.</td>
</tr>
<tr parent="boldital" class="hidden">
<td class="indent">font.boldital.vadjust</td>
<td>String</td>
<td><code>0</code></td>
<td>A font-specific correction to the vertical positioning of the bold italic font in the label text. (Positive is down.) Defaults to the base font's valign.</td>
</tr>
<tr parent="font" class='hidden toggle collapsible' onclick="toggleTable('optionTable','mono', this);">
<td><span parent="mono" class="right-caret"></span> font.mono</td>
<td>Object or String</td>
<td><code>false</code></td>
<td>This object defines the details of the monospaced font in the label. A shorthand is also supported in the form <code>'size face
color'</code> for example: <code>'15px courier red'</code>.
</td>
</tr>
<tr parent="mono" class="hidden">
<td class="indent">font.mono.color</td>
<td>String</td>
<td><code>'#343434'</code></td>
<td>Color of the monospaced font in the label text. Defaults to the base font's color.</td>
</tr>
<tr parent="mono" class="hidden">
<td class="indent">font.mono.size</td>
<td>Number</td>
<td><code>15</code></td>
<td>Size of the monospaced font in the label text. Defaults to the base font's size.</td>
</tr>
<tr parent="mono" class="hidden">
<td class="indent">font.mono.face</td>
<td>String</td>
<td><code>'courier new'</code></td>
<td>Font face (or font family) of the monospaced font in the label text.</td>
</tr>
<tr parent="mono" class="hidden">
<td class="indent">font.mono.mod</td>
<td>String</td>
<td><code>''</code></td>
<td>A string added to the face and size when determining the monospaced font in the label text.</td>
</tr>
<tr parent="mono" class="hidden">
<td class="indent">font.mono.vadjust</td>
<td>String</td>
<td><code>2</code></td>
<td>A font-specific correction to the vertical positioning of the monospaced font in the label text. (Positive is down.) Defaults to the base font's valign.</td>
</tr>
<tr>
<td>from</td>
<td>Number or String</td>
@ -691,6 +887,18 @@ var options: {
<td><code>1</code></td>
<td>The width of the edge. If value is set, this is not used.</td>
</tr>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','widthConstraint', this);">
<td><span parent="widthConstraint" class="right-caret"></span> widthConstraint</td>
<td>Number, Boolean or Object</td>
<td><code>false</code></td>
<td>If false, no widthConstraint is applied. If a number is specified, the maximum width of the edge's label is set to the value. The edge's label's lines will be broken on spaces to stay below the maximum.</td>
</tr>
<tr parent="widthConstraint" class="hidden">
<td class="indent">widthConstraint.maximum</td>
<td>Number</td>
<td><code>undefined</code></td>
<td>If a number is specified, the maximum width of the edge's label is set to the value. The edge's label's lines will be broken on spaces to stay below the maximum.</td>
</tr>
</table>
</div>

+ 236
- 2
docs/network/nodes.html View File

@ -133,9 +133,40 @@ var options = {
background: 'none',
strokeWidth: 0, // px
strokeColor: '#ffffff',
align: 'center'
align: 'center',
multi: false,
vadjust: 0,
bold: {
color: '#343434',
size: 14, // px
face: 'arial',
vadjust: 0,
mod: 'bold'
},
ital: {
color: '#343434',
size: 14, // px
face: 'arial',
vadjust: 0,
mod: 'italic',
},
boldital: {
color: '#343434',
size: 14, // px
face: 'arial',
vadjust: 0,
mod: 'bold italic'
},
mono: {
color: '#343434',
size: 15, // px
face: 'courier new',
vadjust: 2,
mod: ''
}
},
group: undefined,
heightConstraint: false,
hidden: false,
icon: {
face: 'FontAwesome',
@ -187,6 +218,7 @@ var options = {
size: 25,
title: undefined,
value: undefined,
widthConstraint: false,
x: undefined,
y: undefined
}
@ -381,14 +413,198 @@ network.setOptions(options);
<td>This can be set to 'left' to make the label left-aligned. Otherwise,
defaults to 'center'.</td>
</tr>
<tr parent="font" class="hidden">
<td class="indent">font.vadjust</td>
<td>String</td>
<td><code>0</code></td>
<td>A font-specific correction to the vertical positioning of the base font in the label text. (Positive is down.)</td>
</tr>
<tr parent="font" class="hidden">
<td class="indent">font.multi</td>
<td>Boolean or String</td>
<td><code>false</code></td>
<td>If <code>false</code>, the label is treated as pure text drawn with the base font. If <code>true</code> or <code>'html'</code> the label may be multifonted, with bold, italic and code markup, interpreted as html. If the value is <code>'markdown'</code> or <code>'md'</code> the label may be multifonted, with bold, italic and code markup, interpreted as markdown.
The bold, italic, bold-italic and monospaced fonts may be set up under in the font.bold, font.ital, font.boldital and font.mono properties, respectively.</td>
</tr>
<tr parent="font" class='hidden toggle collapsible' onclick="toggleTable('optionTable','bold', this);">
<td><span parent="bold" class="right-caret"></span> font.bold</td>
<td>Object or String</td>
<td><code>false</code></td>
<td>This object defines the details of the bold font in the label. A shorthand is also supported in the form <code>'size face
color'</code> for example: <code>'14px arial red'</code>.
</td>
</tr>
<tr parent="bold" class="hidden">
<td class="indent">font.bold.color</td>
<td>String</td>
<td><code>'#343434'</code></td>
<td>Color of the bold font in the label text. Defaults to the base font's color.</td>
</tr>
<tr parent="bold" class="hidden">
<td class="indent">font.bold.size</td>
<td>Number</td>
<td><code>14</code></td>
<td>Size of the bold font in the label text. Defaults to the base font's size.</td>
</tr>
<tr parent="bold" class="hidden">
<td class="indent">font.bold.face</td>
<td>String</td>
<td><code>'arial'</code></td>
<td>Font face (or font family) of the bold font in the label text. Defaults to the base font's face.</td>
</tr>
<tr parent="bold" class="hidden">
<td class="indent">font.bold.mod</td>
<td>String</td>
<td><code>'bold'</code></td>
<td>A string added to the face and size when determining the bold font in the label text.</td>
</tr>
<tr parent="bold" class="hidden">
<td class="indent">font.bold.vadjust</td>
<td>String</td>
<td><code>0</code></td>
<td>A font-specific correction to the vertical positioning of the bold italic font in the label text. (Positive is down.) Defaults to the base font's valign.</td>
</tr>
<tr parent="font" class='hidden toggle collapsible' onclick="toggleTable('optionTable','ital', this);">
<td><span parent="ital" class="right-caret"></span> font.ital</td>
<td>Object or String</td>
<td><code>false</code></td>
<td>This object defines the details of the italic font in the label. A shorthand is also supported in the form <code>'size face
color'</code> for example: <code>'14px arial red'</code>.
</td>
</tr>
<tr parent="ital" class="hidden">
<td class="indent">font.ital.color</td>
<td>String</td>
<td><code>'#343434'</code></td>
<td>Color of the italic font in the label text. Defaults to the base font's color.</td>
</tr>
<tr parent="ital" class="hidden">
<td class="indent">font.ital.size</td>
<td>Number</td>
<td><code>14</code></td>
<td>Size of the italic font in the label text. Defaults to the base font's size.</td>
</tr>
<tr parent="ital" class="hidden">
<td class="indent">font.ital.face</td>
<td>String</td>
<td><code>'arial'</code></td>
<td>Font face (or font family) of the italic font in the label text. Defaults to the base font's face.</td>
</tr>
<tr parent="ital" class="hidden">
<td class="indent">font.ital.mod</td>
<td>String</td>
<td><code>'italic'</code></td>
<td>A string added to the face and size when determining the italic font in the label text.</td>
</tr>
<tr parent="ital" class="hidden">
<td class="indent">font.ital.vadjust</td>
<td>String</td>
<td><code>0</code></td>
<td>A font-specific correction to the vertical positioning of the italic font in the label text. (Positive is down.) Defaults to the base font's valign.</td>
</tr>
<tr parent="font" class='hidden toggle collapsible' onclick="toggleTable('optionTable','boldital', this);">
<td><span parent="boldital" class="right-caret"></span> font.boldital</td>
<td>Object or String</td>
<td><code>false</code></td>
<td>This object defines the details of the bold italic font in the label. A shorthand is also supported in the form <code>'size face
color'</code> for example: <code>'14px arial red'</code>.
</td>
</tr>
<tr parent="boldital" class="hidden">
<td class="indent">font.boldital.color</td>
<td>String</td>
<td><code>'#343434'</code></td>
<td>Color of the bold italic font in the label text. Defaults to the base font's color.</td>
</tr>
<tr parent="boldital" class="hidden">
<td class="indent">font.boldital.size</td>
<td>Number</td>
<td><code>14</code></td>
<td>Size of the bold italic font in the label text. Defaults to the base font's size.</td>
</tr>
<tr parent="boldital" class="hidden">
<td class="indent">font.boldital.face</td>
<td>String</td>
<td><code>'arial'</code></td>
<td>Font face (or font family) of the bold italic font in the label text. Defaults to the base font's face.</td>
</tr>
<tr parent="boldital" class="hidden">
<td class="indent">font.boldital.mod</td>
<td>String</td>
<td><code>'bold'</code></td>
<td>A string added to the face and size when determining the bold italic font in the label text.</td>
</tr>
<tr parent="boldital" class="hidden">
<td class="indent">font.boldital.vadjust</td>
<td>String</td>
<td><code>0</code></td>
<td>A font-specific correction to the vertical positioning of the bold italic font in the label text. (Positive is down.) Defaults to the base font's valign.</td>
</tr>
<tr parent="font" class='hidden toggle collapsible' onclick="toggleTable('optionTable','mono', this);">
<td><span parent="mono" class="right-caret"></span> font.mono</td>
<td>Object or String</td>
<td><code>false</code></td>
<td>This object defines the details of the monospaced font in the label. A shorthand is also supported in the form <code>'size face
color'</code> for example: <code>'15px courier red'</code>.
</td>
</tr>
<tr parent="mono" class="hidden">
<td class="indent">font.mono.color</td>
<td>String</td>
<td><code>'#343434'</code></td>
<td>Color of the monospaced font in the label text. Defaults to the base font's color.</td>
</tr>
<tr parent="mono" class="hidden">
<td class="indent">font.mono.size</td>
<td>Number</td>
<td><code>15</code></td>
<td>Size of the monospaced font in the label text. Defaults to the base font's size.</td>
</tr>
<tr parent="mono" class="hidden">
<td class="indent">font.mono.face</td>
<td>String</td>
<td><code>'courier new'</code></td>
<td>Font face (or font family) of the monospaced font in the label text.</td>
</tr>
<tr parent="mono" class="hidden">
<td class="indent">font.mono.mod</td>
<td>String</td>
<td><code>''</code></td>
<td>A string added to the face and size when determining the monospaced font in the label text.</td>
</tr>
<tr parent="mono" class="hidden">
<td class="indent">font.mono.vadjust</td>
<td>String</td>
<td><code>2</code></td>
<td>A font-specific correction to the vertical positioning of the monospaced font in the label text. (Positive is down.) Defaults to the base font's valign.</td>
</tr>
<tr>
<td>group</td>
<td>String</td>
<td><code>undefined</code></td>
<td>When not <code>undefined</code>, the node will belong to the defined group. Styling information of
that group will apply to this node. Node specific styling overrides group styling.
that group will apply to this node. Node specific styling overrides group styling.
</td>
</tr>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','heightConstraint', this);">
<td><span parent="heightConstraint" class="right-caret"></span> heightConstraint</td>
<td>Number, Boolean or Object</td>
<td><code>false</code></td>
<td>If false, no heightConstraint is applied. If a number is specified, the value is used as the minimum height of the node. The node's height will be be set to the minimum if less than the value.</td>
</tr>
<tr parent="heightConstraint" class="hidden">
<td class="indent">heightConstraint.minimum</td>
<td>Number</td>
<td><code>undefined</code></td>
<td>If a number is specified, the value is used as the minimum height of the node. The node's height will be be set to the minimum if less than the value.</td>
</tr>
<tr parent="heightConstraint" class="hidden">
<td class="indent">heightConstraint.valign</td>
<td>String</td>
<td><code>'middle'</code></td>
<td>Valid values are <code>'top'</code>, <code>'middle'</code>, and <code>'bottom'</code>.
When specified, if the height of the label text is less than the minimum (including any top or bottom margins), it will be offset vertically to the designated position.</td>
</tr>
<tr>
<td>hidden</td>
<td>Boolean</td>
@ -731,6 +947,24 @@ mySize = minSize + diff * scale;
<td>When a value is set, the nodes will be scaled using the options in the scaling object defined above.
</td>
</tr>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','widthConstraint', this);">
<td><span parent="widthConstraint" class="right-caret"></span> widthConstraint</td>
<td>Number, Boolean or Object</td>
<td><code>false</code></td>
<td>If false, no widthConstraint is applied. If a number is specified, the minimum and maximum widths of the node are set to the value. The node's label's lines will be broken on spaces to stay below the maximum and the node's width will be set to the minimum if less than the value.</td>
</tr>
<tr parent="widthConstraint" class="hidden">
<td class="indent">widthConstraint.minimum</td>
<td>Number</td>
<td><code>undefined</code></td>
<td>If a number is specified, the minimum width of the node is set to the value. The node's width will be set to the minimum if less than the value.</td>
</tr>
<tr parent="widthConstraint" class="hidden">
<td class="indent">widthConstraint.maximum</td>
<td>Number</td>
<td><code>undefined</code></td>
<td>If a number is specified, the maximum width of the node is set to the value. The node's label's lines will be broken on spaces to stay below the maximum.</td>
</tr>
<tr>
<td>x</td>
<td>Number</td>

+ 115
- 0
examples/network/labels/labelMultifont.html View File

@ -0,0 +1,115 @@
<!doctype html>
<html>
<head>
<title>Network | Multifont Labels</title>
<script type="text/javascript" src="../../../dist/vis.js"></script>
<link href="../../../dist/vis-network.min.css" rel="stylesheet" type="text/css" />
<style type="text/css">
#mynetwork {
width: 600px;
height: 400px;
border: 1px solid lightgray;
}
code {
font-size: 15px;
}
p {
max-width: 600px;
}
.indented {
margin-left: 30px;
}
table {
border-collapse: collapse;
font-family: sans-serif;
}
table code {
background: #dddddd;
}
th, td {
border: 1px solid #aaaaaa;
text-align: center;
padding: 5px;
font-weight: normal;
}
</style>
<script src="../../googleAnalytics.js"></script>
</head>
<body>
<p>Node and edge labels may be marked up to be drawn with multiple fonts.</p>
<div id="mynetwork"></div>
<p>The value of the <code>font.multi</code> property may be set to <code>'html'</code>, <code>'markdown'</code> or a boolean.</p>
<table class="indented">
<tr><th colspan='4'>Embedded Font Markup</th></tr>
<tr><th rowspan=2>font mod</th><th colspan=3><code>font.multi</code> setting</th></tr>
<tr><th><code>'html'</code> or <code>true</code></th><th><code>'markdown'</code> or <code>'md'</code></th><th><code>false<code></th></tr>
<tr><th>bold</th><td><code>&lt;b&gt;</code> ... <code>&lt;/b></code></td><td><code>&nbsp;*</code> ... <code>*&nbsp;</code></td><td>n/a</td></tr>
<tr><th>italic</th><td><code>&lt;i&gt;</code> ... <code>&lt;/i></code></td><td><code>&nbsp;_</code> ... <code>_&nbsp;</code></td><td>n/a</td></tr>
<tr><th>mono-spaced</th><td><code>&lt;code&gt;</code> ... <code>&lt;/code&gt;</code></td><td><code>&nbsp;`</code> ... <code>`&nbsp;</code></td><td>n/a</td></tr>
</table>
<p>
The <code>html</code> and <code>markdown</code> rendering is limited: bolds may be embedded in italics, italics may be embedded in bolds, and mono-spaced may be embedded in bold or italic, but will not be altered by those font mods, nor will embedded bolds or italics be handled.
The only entities that will be observed in html are <code>&amp;lt;</code> and <code>&amp;amp;</code> and in <code>markdown</code> a backslash will escape the following character (including a backslash) from special processing.
Any font mod that is started in a label line will be implicitly terminated at the end of that line.
While this interpretation may not exactly match <i>official</i> rendering standards, it is a consistent compromise for drawing multifont strings in the non-multifont html canvas element underlying vis.
</p>
<p>This implies that four additional sets of font properties will be recognized in label processing.</p>
<p class="indented"><code>font.bold</code> designates the font used for rendering bold font mods.
<br/><code>font.ital</code> designates the font used for rendering italic font mods.
<br/><code>font.boldital</code> designates the font used for rendering bold-<b><i>and</i></b>-italic font mods.
<br/><code>font.mono</code> designates the font used for rendering monospaced font mods.</p>
<p>Any font mod without a matching font will be rendered using the normal <code>font</code> (or default) value.</p>
<p>The <code>font.multi</code> and extended font settings may be set in the network's <code>nodes</code> or <code>edges</code> properties, or on individual nodes and edges.
Node and edge label fonts are separate.</p>
<script type="text/javascript">
var nodes = [
{ id: 1, label: 'This is a\nsingle-font label', x: -120, y: -120 },
{ id: 2, font: { multi: true }, label: '<b>This</b> is a\n<i>default</i> <b><i>multi-</i>font</b> <code>label</code>', x: -40, y: -40 },
{ id: 3, font: { multi: 'html', size: 20 }, label: '<b>This</b> is an\n<i>html</i> <b><i>multi-</i>font</b> <code>label</code>', x: 40, y: 40 },
{ id: 4, font: { multi: 'md', face: 'georgia' }, label: '*This* is a\n_markdown_ *_multi-_ font* `label`', x: 120, y: 120},
];
var edges = [
{from: 1, to: 2, label: "single to default"},
{from: 2, to: 3, font: { multi: true }, label: "default to <b>html</b>" },
{from: 3, to: 4, font: { multi: "md" }, label: "*html* to _md_" }
];
var container = document.getElementById('mynetwork');
var data = {
nodes: nodes,
edges: edges
};
var options = {
edges: {
font: {
size: 12
}
},
nodes: {
shape: 'box',
font: {
bold: {
color: '#0077aa'
}
}
},
physics: {
enabled: false
}
};
var network = new vis.Network(container, data, options);
</script>
</body>
</html>

+ 118
- 0
examples/network/nodeStyles/widthHeight.html View File

@ -0,0 +1,118 @@
<!doctype html>
<html>
<head>
<title>Network | Label Width and Height Settings</title>
<script type="text/javascript" src="../../../dist/vis.js"></script>
<link href="../../../dist/vis-network.min.css" rel="stylesheet" type="text/css" />
<style type="text/css">
#mynetwork {
width: 600px;
height: 400px;
border: 1px solid lightgray;
}
code {
font-size: 14px;
background: #dddddd;
}
p {
max-width: 600px;
}
.indented {
margin-left: 30px;
}
.sep {
height: 1px;
width: 35%;
margin-left: 40px;
background-color: #dddddd;
}
</style>
<script src="../../googleAnalytics.js"></script>
</head>
<body>
<p>Nodes may be set to have fixed, minimum and maximum widths and minimum heights.
Nodes with minimum heights may also have a vertical alignment set.</p>
<p>Edges may be set to have maximum widths.</p>
<div id="mynetwork"></div>
<p>The <code>widthConstraint: value</code> option means a fixed width, the minimum and maximum width of the element are set to the value (respecting left and right margins). Lines exceeding the maximum width will be broken at space boundaries to fit.</p>
<p>The <code>widthConstraint: { minimum: value }</code> option sets the minimum width of the element to the value.</p>
<p>The <code>widthConstraint: { maximum: value }</code> option sets the maximum width of the element to the value (respecting left and right margins). Lines exceeding the maximum width will be broken at space boundaries to fit.</p>
<p>Minimum width line sizing is applied after maximum width line breaking, so counterintuitively, the minimum being greater than the maximum has a meaningful interpretation.</p>
<div class="sep"></div>
<p>The <code>heightConstraint: value</code> option sets the minimum height of the element to the value (respecting top and bottom margins).</p>
<p>The <code>heightConstraint: { minimum: value }</code> option also sets the minimum height of the element to the value (respecting top and bottom margins).</p>
<p>The <code>heightConstraint: { valign: value }</code> option (with value <code>'top'</code>, <code>'middle'</code>, or <code>'bottom'</code>, sets the alignment of the text in the element's label to the elements top, middle or bottom (respecting top and bottom margins) when it's height is less than the minimum. The middle value is the default.</p>
<div class="sep"></div>
<p>Node width and height constraints may both be applied together, of course.</p>
<p>The constraint options may be applied to elements individually, or at the whole-set level.
Whole-set node and edge constraints are exclusive.</p>
<script type="text/javascript">
var nodes = [
{ id: 100, label: 'This node has no fixed, minimum or maximum width or height', x: -50, y: -300 },
{ id: 210, widthConstraint: { minimum: 120 }, label: 'This node has a mimimum width', x: -400, y: -200 },
{ id: 211, widthConstraint: { minimum: 120 }, label: '...so does this', x: -500, y: -50 },
{ id: 220, widthConstraint: { maximum: 170 }, label: 'This node has a maximum width and breaks have been automatically inserted into the label', x: -150, y: -150 },
{ id: 221, widthConstraint: { maximum: 170 }, label: '...this too', x: -100, y: 0 },
{ id: 200, font: { multi: true }, widthConstraint: 150, label: '<b>This</b> node has a fixed width and breaks have been automatically inserted into the label', x: -300, y: 50 },
{ id: 201, widthConstraint: 150, label: '...this as well', x: -300, y: 200 },
{ id: 300, heightConstraint: { minimum: 70 }, label: 'This node\nhas a\nminimum\nheight', x: 100, y: -150 },
{ id: 301, heightConstraint: { minimum: 70 }, label: '...this one here too', x: 100, y: 0 },
{ id: 400, heightConstraint: { minimum: 100, valign: 'top' }, label: 'Minimum height\nvertical alignment\nmay be top', x: 300, y: -200 },
{ id: 401, heightConstraint: { minimum: 100, valign: 'middle' }, label: 'Minimum height\nvertical alignment\nmay be middle\n(default)', x: 300, y: 0 },
{ id: 402, heightConstraint: { minimum: 100, valign: 'bottom' }, label: 'Minimum height\nvertical alignment\nmay be bottom', x: 300, y: 200 }
];
var edges = [
{ from: 100, to: 210, label: "unconstrained to minimum width"},
{ from: 210, to: 211, label: "more minimum width"},
{ from: 100, to: 220, label: "unconstrained to maximum width"},
{ from: 220, to: 221, label: "more maximum width"},
{ from: 210, to: 200, label: "minimum width to fixed width"},
{ from: 220, to: 200, label: "maximum width to fixed width"},
{ from: 200, to: 201, label: "more fixed width"},
{ from: 100, to: 300, label: "unconstrained to minimum height"},
{ from: 300, to: 301, label: "more minimum height"},
{ from: 100, to: 400, label: "unconstrained to top valign"},
{ from: 400, to: 401, label: "top valign to middle valign"},
{ from: 401, to: 402, label: "middle valign to bottom valign"},
];
var container = document.getElementById('mynetwork');
var data = {
nodes: nodes,
edges: edges
};
var options = {
edges: {
font: {
size: 12
},
widthConstraint: {
maximum: 90
}
},
nodes: {
shape: 'box',
margin: 10
},
physics: {
enabled: false
}
};
var network = new vis.Network(container, data, options);
</script>
</body>
</html>

+ 20
- 2
lib/network/modules/EdgesHandler.js View File

@ -43,7 +43,24 @@ class EdgesHandler {
background: 'none',
strokeWidth: 2, // px
strokeColor: '#ffffff',
align:'horizontal'
align:'horizontal',
multi: false,
vadjust: 0,
bold: {
mod: 'bold'
},
boldital: {
mod: 'bold italic'
},
ital: {
mod: 'italic'
},
mono: {
mod: '',
size: 15, // px
face: 'courier new',
vadjust: 2
}
},
hidden: false,
hoverWidth: 1.5,
@ -155,6 +172,7 @@ class EdgesHandler {
}
setOptions(options) {
this.edgeOptions = options;
if (options !== undefined) {
// use the parser from the Edge class to fill in all shorthand notations
Edge.parseOptions(this.options, options);
@ -340,7 +358,7 @@ class EdgesHandler {
}
create(properties) {
return new Edge(properties, this.body, this.options)
return new Edge(properties, this.body, this.options, this.defaultOptions, this.edgeOptions)
}

+ 21
- 3
lib/network/modules/NodesHandler.js View File

@ -49,7 +49,24 @@ class NodesHandler {
background: 'none',
strokeWidth: 0, // px
strokeColor: '#ffffff',
align: 'center'
align: 'center',
vadjust: 0,
multi: false,
bold: {
mod: 'bold'
},
boldital: {
mod: 'bold italic'
},
ital: {
mod: 'italic'
},
mono: {
mod: '',
size: 15, // px
face: 'courier new',
vadjust: 2
}
},
group: undefined,
hidden: false,
@ -135,6 +152,7 @@ class NodesHandler {
}
setOptions(options) {
this.nodeOptions = options;
if (options !== undefined) {
Node.parseOptions(this.options, options);
@ -301,7 +319,7 @@ class NodesHandler {
* @param constructorClass
*/
create(properties, constructorClass = Node) {
return new constructorClass(properties, this.body, this.images, this.groups, this.options)
return new constructorClass(properties, this.body, this.images, this.groups, this.options, this.defaultOptions, this.nodeOptions)
}
@ -454,4 +472,4 @@ class NodesHandler {
}
}
export default NodesHandler;
export default NodesHandler;

+ 7
- 4
lib/network/modules/components/Edge.js View File

@ -22,12 +22,14 @@ import StraightEdge from './edges/StraightEdge'
* example for the color
*/
class Edge {
constructor(options, body, globalOptions) {
constructor(options, body, globalOptions, defaultOptions, edgeOptions) {
if (body === undefined) {
throw "No body provided";
}
this.options = util.bridgeObject(globalOptions);
this.globalOptions = globalOptions;
this.defaultOptions = defaultOptions;
this.edgeOptions = edgeOptions;
this.body = body;
// initialize variables
@ -50,7 +52,6 @@ class Edge {
this.connected = false;
this.labelModule = new Label(this.body, this.options, true /* It's an edge label */);
this.setOptions(options);
}
@ -76,7 +77,8 @@ class Edge {
// update label Module
this.updateLabelModule();
this.updateLabelModule(options);
this.labelModule.propagateFonts(this.edgeOptions, options, this.defaultOptions);
let dataChanged = this.updateEdgeType();
@ -199,11 +201,12 @@ class Edge {
/**
* update the options in the label module
*/
updateLabelModule() {
updateLabelModule(options) {
this.labelModule.setOptions(this.options, true);
if (this.labelModule.baseSize !== undefined) {
this.baseFontSize = this.labelModule.baseSize;
}
this.labelModule.constrain(this.edgeOptions, options, this.defaultOptions);
}
/**

+ 8
- 4
lib/network/modules/components/Node.js View File

@ -46,9 +46,11 @@ import {printStyle} from "../../../shared/Validator";
*
*/
class Node {
constructor(options, body, imagelist, grouplist, globalOptions) {
constructor(options, body, imagelist, grouplist, globalOptions, defaultOptions, nodeOptions) {
this.options = util.bridgeObject(globalOptions);
this.globalOptions = globalOptions;
this.defaultOptions = defaultOptions;
this.nodeOptions = nodeOptions;
this.body = body;
this.edges = []; // all edges connected to this node
@ -117,7 +119,7 @@ class Node {
// clear x and y positions
if (options.x !== undefined) {
if (options.x === null) {this.x = undefined; this.predefinedPosition = false;}
else {this.x = parseInt(options.x); this.predefinedPosition = true;}
else {this.x = parseInt(options.x); this.predefinedPosition = true;}
}
if (options.y !== undefined) {
if (options.y === null) {this.y = undefined; this.predefinedPosition = false;}
@ -147,8 +149,9 @@ class Node {
}
}
this.updateLabelModule();
this.updateLabelModule(options);
this.updateShape(currentShape);
this.labelModule.propagateFonts(this.nodeOptions, options, this.defaultOptions);
if (options.hidden !== undefined || options.physics !== undefined) {
return true;
@ -216,7 +219,7 @@ class Node {
}
}
updateLabelModule() {
updateLabelModule(options) {
if (this.options.label === undefined || this.options.label === null) {
this.options.label = '';
}
@ -224,6 +227,7 @@ class Node {
if (this.labelModule.baseSize !== undefined) {
this.baseFontSize = this.labelModule.baseSize;
}
this.labelModule.constrain(this.nodeOptions, options, this.defaultOptions);
}
updateShape(currentShape) {

+ 2
- 2
lib/network/modules/components/nodes/shapes/Box.js View File

@ -5,7 +5,7 @@ import NodeBase from '../util/NodeBase'
class Box extends NodeBase {
constructor (options, body, labelModule) {
super(options,body,labelModule);
this._setMargins()
this._setMargins(labelModule);
}
resize(ctx, selected) {
@ -81,4 +81,4 @@ class Box extends NodeBase {
}
}
export default Box;
export default Box;

+ 1
- 1
lib/network/modules/components/nodes/shapes/Circle.js View File

@ -5,7 +5,7 @@ import CircleImageBase from '../util/CircleImageBase'
class Circle extends CircleImageBase {
constructor(options, body, labelModule) {
super(options, body, labelModule)
this._setMargins();
this._setMargins(labelModule);
}
resize(ctx, selected) {

+ 2
- 2
lib/network/modules/components/nodes/shapes/Database.js View File

@ -5,7 +5,7 @@ import NodeBase from '../util/NodeBase'
class Database extends NodeBase {
constructor (options, body, labelModule) {
super(options, body, labelModule);
this._setMargins();
this._setMargins(labelModule);
}
resize(ctx, selected) {
@ -74,4 +74,4 @@ class Database extends NodeBase {
}
}
export default Database;
export default Database;

+ 2
- 2
lib/network/modules/components/nodes/shapes/Icon.js View File

@ -5,7 +5,7 @@ import NodeBase from '../util/NodeBase'
class Icon extends NodeBase {
constructor(options, body, labelModule) {
super(options, body, labelModule);
this._setMargins();
this._setMargins(labelModule);
}
resize(ctx) {
@ -80,4 +80,4 @@ class Icon extends NodeBase {
}
}
export default Icon;
export default Icon;

+ 2
- 2
lib/network/modules/components/nodes/shapes/Text.js View File

@ -5,7 +5,7 @@ import NodeBase from '../util/NodeBase'
class Text extends NodeBase {
constructor(options, body, labelModule) {
super(options, body, labelModule);
this._setMargins();
this._setMargins(labelModule);
}
resize(ctx, selected) {
@ -50,4 +50,4 @@ class Text extends NodeBase {
}
}
export default Text;
export default Text;

+ 2
- 1
lib/network/modules/components/nodes/util/NodeBase.js View File

@ -16,7 +16,7 @@ class NodeBase {
this.options = options;
}
_setMargins() {
_setMargins(labelModule) {
this.margin = {};
if (this.options.margin) {
if (typeof this.options.margin == 'object') {
@ -31,6 +31,7 @@ class NodeBase {
this.margin.left = this.options.margin;
}
}
labelModule.adjustSizes(this.margin)
}
_distanceToBorder(ctx,angle) {

+ 657
- 60
lib/network/modules/components/shared/Label.js View File

@ -1,7 +1,7 @@
let util = require('../../../../util');
class Label {
constructor(body,options,edgelabel = false) {
constructor(body, options, edgelabel = false) {
this.body = body;
this.pointToSelf = false;
@ -13,7 +13,7 @@ class Label {
}
setOptions(options, allowDeletion = false) {
this.nodeOptions = options;
this.elementOptions = options;
// We want to keep the font options seperated from the node options.
// The node options have to mirror the globals when they are not overruled.
@ -39,14 +39,195 @@ class Label {
static parseOptions(parentOptions, newOptions, allowDeletion = false) {
if (typeof newOptions.font === 'string') {
let newOptionsArray = newOptions.font.split(" ");
parentOptions.size = newOptionsArray[0].replace("px",'');
parentOptions.face = newOptionsArray[1];
parentOptions.color = newOptionsArray[2];
parentOptions.size = newOptionsArray[0].replace("px",'');
parentOptions.face = newOptionsArray[1];
parentOptions.color = newOptionsArray[2];
parentOptions.vadjust = 0;
}
else if (typeof newOptions.font === 'object') {
util.fillIfDefined(parentOptions, newOptions.font, allowDeletion);
}
parentOptions.size = Number(parentOptions.size);
parentOptions.size = Number(parentOptions.size);
parentOptions.vadjust = Number(parentOptions.vadjust);
}
// set the width and height constraints based on 'nearest' value
constrain(elementOptions, options, defaultOptions) {
this.fontOptions.constrainWidth = false;
this.fontOptions.maxWdt = -1;
this.fontOptions.minWdt = -1;
let pile = [options, elementOptions, defaultOptions];
let widthConstraint = util.topMost(pile, 'widthConstraint');
if (typeof widthConstraint === 'number') {
this.fontOptions.maxWdt = Number(widthConstraint);
this.fontOptions.minWdt = Number(widthConstraint);
} else if (typeof widthConstraint === 'object') {
let widthConstraintMaximum = util.topMost(pile, ['widthConstraint', 'maximum']);
if (typeof widthConstraintMaximum === 'number') {
this.fontOptions.maxWdt = Number(widthConstraintMaximum);
}
let widthConstraintMinimum = util.topMost(pile, ['widthConstraint', 'minimum'])
if (typeof widthConstraintMinimum === 'number') {
this.fontOptions.minWdt = Number(widthConstraintMinimum);
}
}
this.fontOptions.constrainHeight = false;
this.fontOptions.minHgt = -1;
this.fontOptions.valign = 'middle';
let heightConstraint = util.topMost(pile, 'heightConstraint');
if (typeof heightConstraint === 'number') {
this.fontOptions.minHgt = Number(heightConstraint);
} else if (typeof heightConstraint === 'object') {
let heightConstraintMinimum = util.topMost(pile, ['heightConstraint', 'minimum']);
if (typeof heightConstraintMinimum === 'number') {
this.fontOptions.minHgt = Number(heightConstraintMinimum);
}
let heightConstraintValign = util.topMost(pile, ['heightConstraint', 'valign']);
if (typeof heightConstraintValign === 'string') {
if ((heightConstraintValign === 'top')||(heightConstraintValign === 'bottom')) {
this.fontOptions.valign = heightConstraintValign;
}
}
}
}
// When margins are set in an element, adjust sizes is called to remove them
// from the width/height constraints. This must be done prior to label sizing.
adjustSizes(margins) {
let widthBias = (margins) ? (margins.right + margins.left) : 0;
if (this.fontOptions.constrainWidth) {
this.fontOptions.maxWdt -= widthBias;
this.fontOptions.minWdt -= widthBias;
}
let heightBias = (margins) ? (margins.top + margins.bottom) : 0;
if (this.fontOptions.constrainHeight) {
this.fontOptions.minHgt -= heightBias;
}
}
propagateFonts(options, groupOptions, defaultOptions) {
if (this.fontOptions.multi) {
let mods = [ 'bold', 'ital', 'boldital', 'mono' ];
for (const mod of mods) {
let optionsFontMod;
if (options.font) {
optionsFontMod = options.font[mod];
}
if (typeof optionsFontMod === 'string') {
let modOptionsArray = optionsFontMod.split(" ");
this.fontOptions[mod].size = modOptionsArray[0].replace("px","");
this.fontOptions[mod].face = modOptionsArray[1];
this.fontOptions[mod].color = modOptionsArray[2];
this.fontOptions[mod].vadjust = this.fontOptions.vadjust;
this.fontOptions[mod].mod = defaultOptions.font[mod].mod;
} else {
// We need to be crafty about loading the modded fonts. We want as
// much 'natural' versatility as we can get, so a simple global
// change propagates in an expected way, even if not stictly logical.
// face: We want to capture any direct settings and overrides, but
// fall back to the base font if there aren't any. We make a
// special exception for mono, since we probably don't want to
// sync to a the base font face.
//
// if the mod face is in the node's options, use it
// else if the mod face is in the global options, use it
// else if the face is in the global options, use it
// else use the base font's face.
if (optionsFontMod && optionsFontMod.hasOwnProperty('face')) {
this.fontOptions[mod].face = optionsFontMod.face;
} else if (groupOptions.font && groupOptions.font[mod] &&
groupOptions.font[mod].hasOwnProperty('face')) {
this.fontOptions[mod].face = groupOptions.font[mod].face;
} else if (mod === 'mono') {
this.fontOptions[mod].face = defaultOptions.font[mod].face;
} else if (groupOptions.font &&
groupOptions.font.hasOwnProperty('face')) {
this.fontOptions[mod].face = groupOptions.font.face;
} else {
this.fontOptions[mod].face = this.fontOptions.face;
}
// color: this is handled just like the face.
if (optionsFontMod && optionsFontMod.hasOwnProperty('color')) {
this.fontOptions[mod].color = optionsFontMod.color;
} else if (groupOptions.font && groupOptions.font[mod] &&
groupOptions.font[mod].hasOwnProperty('color')) {
this.fontOptions[mod].color = groupOptions.font[mod].color;
} else if (groupOptions.font &&
groupOptions.font.hasOwnProperty('color')) {
this.fontOptions[mod].color = groupOptions.font.color;
} else {
this.fontOptions[mod].color = this.fontOptions.color;
}
// mod: this is handled just like the face, except we never grab the
// base font's mod. We know they're in the defaultOptions, and unless
// we've been steered away from them, we use the default.
if (optionsFontMod && optionsFontMod.hasOwnProperty('mod')) {
this.fontOptions[mod].mod = optionsFontMod.mod;
} else if (groupOptions.font && groupOptions.font[mod] &&
groupOptions.font[mod].hasOwnProperty('mod')) {
this.fontOptions[mod].mod = groupOptions.font[mod].mod;
} else if (groupOptions.font &&
groupOptions.font.hasOwnProperty('mod')) {
this.fontOptions[mod].mod = groupOptions.font.mod;
} else {
this.fontOptions[mod].mod = defaultOptions.font[mod].mod;
}
// size: It's important that we size up defaults similarly if we're
// using default faces unless overriden. We want to preserve the
// ratios closely - but if faces have changed, all bets are off.
//
// if the mod size is in the node's options, use it
// else if the mod size is in the global options, use it
// else if the mod face is the same as the default and the base face
// is the same as the default, scale the mod size using the same
// ratio
// else if the size is in the global options, use it
// else use the base font's size.
if (optionsFontMod && optionsFontMod.hasOwnProperty('size')) {
this.fontOptions[mod].size = optionsFontMod.size;
} else if (groupOptions.font && groupOptions.font[mod] &&
groupOptions.font[mod].hasOwnProperty('size')) {
this.fontOptions[mod].size = groupOptions.font[mod].size;
} else if ((this.fontOptions[mod].face === defaultOptions.font[mod].face) &&
(this.fontOptions.face === defaultOptions.font.face)) {
let ratio = this.fontOptions.size / Number(defaultOptions.font.size);
this.fontOptions[mod].size = defaultOptions.font[mod].size * ratio;
} else if (groupOptions.font &&
groupOptions.font.hasOwnProperty('size')) {
this.fontOptions[mod].size = groupOptions.font.size;
} else {
this.fontOptions[mod].size = this.fontOptions.size;
}
// vadjust: this is handled just like the size.
if (optionsFontMod && optionsFontMod.hasOwnProperty('vadjust')) {
this.fontOptions[mod].vadjust = optionsFontMod.vadjust;
} else if (groupOptions.font &&
groupOptions.font[mod] && groupOptions.font[mod].hasOwnProperty('vadjust')) {
this.fontOptions[mod].vadjust = groupOptions.font[mod].vadjust;
} else if ((this.fontOptions[mod].face === defaultOptions.font[mod].face) &&
(this.fontOptions.face === defaultOptions.font.face)) {
let ratio = this.fontOptions.size / Number(defaultOptions.font.size);
this.fontOptions[mod].vadjust = defaultOptions.font[mod].vadjust * Math.round(ratio);
} else if (groupOptions.font &&
groupOptions.font.hasOwnProperty('vadjust')) {
this.fontOptions[mod].vadjust = groupOptions.font.vadjust;
} else {
this.fontOptions[mod].vadjust = this.fontOptions.vadjust;
}
}
this.fontOptions[mod].size = Number(this.fontOptions[mod].size);
this.fontOptions[mod].vadjust = Number(this.fontOptions[mod].vadjust);
}
}
}
@ -60,12 +241,12 @@ class Label {
*/
draw(ctx, x, y, selected, baseline = 'middle') {
// if no label, return
if (this.nodeOptions.label === undefined)
if (this.elementOptions.label === undefined)
return;
// check if we have to render the label
let viewFontSize = this.fontOptions.size * this.body.view.scale;
if (this.nodeOptions.label && viewFontSize < this.nodeOptions.scaling.label.drawThreshold - 1)
if (this.elementOptions.label && viewFontSize < this.elementOptions.scaling.label.drawThreshold - 1)
return;
// update the size cache if required
@ -121,39 +302,49 @@ class Label {
let fontSize = this.fontOptions.size;
let viewFontSize = fontSize * this.body.view.scale;
// this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel)
if (viewFontSize >= this.nodeOptions.scaling.label.maxVisible) {
fontSize = Number(this.nodeOptions.scaling.label.maxVisible) / this.body.view.scale;
if (viewFontSize >= this.elementOptions.scaling.label.maxVisible) {
fontSize = Number(this.elementOptions.scaling.label.maxVisible) / this.body.view.scale;
}
let yLine = this.size.yLine;
let [fontColor, strokeColor] = this._getColor(viewFontSize);
[x, yLine] = this._setAlignment(ctx, x, yLine, baseline);
// configure context for drawing the text
ctx.font = (selected && this.nodeOptions.labelHighlightBold ? 'bold ' : '') + fontSize + "px " + this.fontOptions.face;
ctx.fillStyle = fontColor;
// When the textAlign property is 'left', make label left-justified
if ((!this.isEdgeLabel) && this.fontOptions.align === 'left') {
ctx.textAlign = this.fontOptions.align;
x = x - 0.5 * this.size.width; // Shift label 1/2-distance to the left
} else {
ctx.textAlign = 'center';
}
// set the strokeWidth
if (this.fontOptions.strokeWidth > 0) {
ctx.lineWidth = this.fontOptions.strokeWidth;
ctx.strokeStyle = strokeColor;
ctx.lineJoin = 'round';
ctx.textAlign = 'left'
x = x - this.size.width / 2; // Shift label 1/2-distance to the left
if ((this.fontOptions.valign) && (this.size.height > this.size.labelHeight)) {
if (this.fontOptions.valign === 'top') {
yLine -= (this.size.height - this.size.labelHeight) / 2;
}
if (this.fontOptions.valign === 'bottom') {
yLine += (this.size.height - this.size.labelHeight) / 2;
}
}
// draw the text
for (let i = 0; i < this.lineCount; i++) {
if (this.fontOptions.strokeWidth > 0) {
ctx.strokeText(this.lines[i], x, yLine);
if (this.lines[i] && this.lines[i].blocks) {
let width = 0;
if (this.isEdgeLabel || this.fontOptions.align === 'center') {
width += (this.size.width - this.lines[i].width) / 2
} else if (this.fontOptions.align === 'right') {
width += (this.size.width - this.lines[i].width)
}
for (let j = 0; j < this.lines[i].blocks.length; j++) {
let block = this.lines[i].blocks[j];
ctx.font = block.font;
let [fontColor, strokeColor] = this._getColor(block.color, viewFontSize);
if (this.fontOptions.strokeWidth > 0) {
ctx.lineWidth = this.fontOptions.strokeWidth;
ctx.strokeStyle = strokeColor;
ctx.lineJoin = 'round';
ctx.strokeText(block.text, x + width, yLine + block.vadjust);
}
ctx.fillStyle = fontColor;
ctx.fillText(block.text, x + width, yLine + block.vadjust);
width += block.width;
}
yLine += this.lines[i].height;
}
ctx.fillText(this.lines[i], x, yLine);
yLine += fontSize;
}
}
@ -180,7 +371,6 @@ class Label {
else {
ctx.textBaseline = baseline;
}
return [x,yLine];
}
@ -192,11 +382,11 @@ class Label {
* @returns {*[]}
* @private
*/
_getColor(viewFontSize) {
let fontColor = this.fontOptions.color || '#000000';
_getColor(color, viewFontSize) {
let fontColor = color || '#000000';
let strokeColor = this.fontOptions.strokeColor || '#ffffff';
if (viewFontSize <= this.nodeOptions.scaling.label.drawThreshold) {
let opacity = Math.max(0, Math.min(1, 1 - (this.nodeOptions.scaling.label.drawThreshold - viewFontSize)));
if (viewFontSize <= this.elementOptions.scaling.label.drawThreshold) {
let opacity = Math.max(0, Math.min(1, 1 - (this.elementOptions.scaling.label.drawThreshold - viewFontSize)));
fontColor = util.overrideOpacity(fontColor, opacity);
strokeColor = util.overrideOpacity(strokeColor, opacity);
}
@ -211,12 +401,12 @@ class Label {
* @returns {{width: number, height: number}}
*/
getTextSize(ctx, selected = false) {
let size = {
width: this._processLabel(ctx,selected),
height: this.fontOptions.size * this.lineCount,
this._processLabel(ctx, selected);
return {
width: this.size.width,
height: this.size.height,
lineCount: this.lineCount
};
return size;
}
@ -230,9 +420,8 @@ class Label {
*/
calculateLabelSize(ctx, selected, x = 0, y = 0, baseline = 'middle') {
if (this.labelDirty === true) {
this.size.width = this._processLabel(ctx,selected);
this._processLabel(ctx, selected);
}
this.size.height = this.fontOptions.size * this.lineCount;
this.size.left = x - this.size.width * 0.5;
this.size.top = y - this.size.height * 0.5;
this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size;
@ -241,37 +430,445 @@ class Label {
this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers
this.size.yLine += 4; // distance from node
}
this.labelDirty = false;
}
/**
* normalize the markup system
*/
decodeMarkupSystem(markupSystem) {
let system = 'none';
if (markupSystem === 'markdown' || markupSystem === 'md') {
system = 'markdown';
} else if (markupSystem === true || markupSystem === 'html') {
system = 'html'
}
return system;
}
/**
* This calculates the width as well as explodes the label string and calculates the amount of lines.
* Explodes a piece of text into single-font blocks using a given markup
* @param text
* @param markupSystem
* @returns [{ text, mod }]
*/
splitBlocks(text, markupSystem) {
let system = this.decodeMarkupSystem(markupSystem);
if (system === 'none') {
return [{
text: text,
mod: 'normal'
}]
} else if (system === 'markdown') {
return this.splitMarkdownBlocks(text);
} else if (system === 'html') {
return this.splitHtmlBlocks(text);
}
}
splitMarkdownBlocks(text) {
let blocks = [];
let s = {
bold: false,
ital: false,
mono: false,
beginable: true,
spacing: false,
position: 0,
buffer: "",
modStack: []
};
s.mod = function() {
return (this.modStack.length === 0) ? 'normal' : this.modStack[0];
}
s.modName = function() {
if (this.modStack.length === 0)
return 'normal';
else if (this.modStack[0] === 'mono')
return 'mono';
else {
if (s.bold && s.ital) {
return 'boldital';
} else if (s.bold) {
return 'bold';
} else if (s.ital) {
return 'ital';
}
}
}
s.emitBlock = function(override = false) {
if (this.spacing) {
this.add(" ");
this.spacing = false;
}
if (this.buffer.length > 0) {
blocks.push({ text: this.buffer, mod: this.modName() });
this.buffer = "";
}
}
s.add = function(text) {
if (text === " ") {
s.spacing = true;
}
if (s.spacing) {
this.buffer += " ";
this.spacing = false;
}
if (text != " ") {
this.buffer += text;
}
}
while (s.position < text.length) {
let char = text.charAt(s.position);
if (/[ \t]/.test(char)) {
if (!s.mono) {
s.spacing = true;
} else {
s.add(char);
}
s.beginable = true
} else if (/\\/.test(char)) {
if (s.position < text.length+1) {
s.position++;
char = text.charAt(s.position);
if (/ \t/.test(char)) {
s.spacing = true;
} else {
s.add(char);
s.beginable = false;
}
}
} else if (!s.mono && !s.bold && (s.beginable || s.spacing) && /\*/.test(char)) {
s.emitBlock();
s.bold = true;
s.modStack.unshift("bold");
} else if (!s.mono && !s.ital && (s.beginable || s.spacing) && /\_/.test(char)) {
s.emitBlock();
s.ital = true;
s.modStack.unshift("ital");
} else if (!s.mono && (s.beginable || s.spacing) && /`/.test(char)) {
s.emitBlock();
s.mono = true;
s.modStack.unshift("mono");
} else if (!s.mono && (s.mod() === "bold") && /\*/.test(char)) {
if ((s.position === text.length-1) || /[.,_` \t\n]/.test(text.charAt(s.position+1))) {
s.emitBlock();
s.bold = false;
s.modStack.shift();
} else {
s.add(char);
}
} else if (!s.mono && (s.mod() === "ital") && /\_/.test(char)) {
if ((s.position === text.length-1) || /[.,*` \t\n]/.test(text.charAt(s.position+1))) {
s.emitBlock();
s.ital = false;
s.modStack.shift();
} else {
s.add(char);
}
} else if (s.mono && (s.mod() === "mono") && /`/.test(char)) {
if ((s.position === text.length-1) || (/[.,*_ \t\n]/.test(text.charAt(s.position+1)))) {
s.emitBlock();
s.mono = false;
s.modStack.shift();
} else {
s.add(char);
}
} else {
s.add(char);
s.beginable = false;
}
s.position++
}
s.emitBlock();
return blocks;
}
splitHtmlBlocks(text) {
let blocks = [];
let s = {
bold: false,
ital: false,
mono: false,
spacing: false,
position: 0,
buffer: "",
modStack: []
};
s.mod = function() {
return (this.modStack.length === 0) ? 'normal' : this.modStack[0];
}
s.modName = function() {
if (this.modStack.length === 0)
return 'normal';
else if (this.modStack[0] === 'mono')
return 'mono';
else {
if (s.bold && s.ital) {
return 'boldital';
} else if (s.bold) {
return 'bold';
} else if (s.ital) {
return 'ital';
}
}
}
s.emitBlock = function(override = false) {
if (this.spacing) {
this.add(" ");
this.spacing = false;
}
if (this.buffer.length > 0) {
blocks.push({ text: this.buffer, mod: this.modName() });
this.buffer = "";
}
}
s.add = function(text) {
if (text === " ") {
s.spacing = true;
}
if (s.spacing) {
this.buffer += " ";
this.spacing = false;
}
if (text != " ") {
this.buffer += text;
}
}
while (s.position < text.length) {
let char = text.charAt(s.position);
if (/[ \t]/.test(char)) {
if (!s.mono) {
s.spacing = true;
} else {
s.add(char);
}
} else if (/</.test(char)) {
if (!s.mono && !s.bold && /<b>/.test(text.substr(s.position,3))) {
s.emitBlock();
s.bold = true;
s.modStack.unshift("bold");
s.position += 2;
} else if (!s.mono && !s.ital && /<i>/.test(text.substr(s.position,3))) {
s.emitBlock();
s.ital = true;
s.modStack.unshift("ital");
s.position += 2;
} else if (!s.mono && /<code>/.test(text.substr(s.position,6))) {
s.emitBlock();
s.mono = true;
s.modStack.unshift("mono");
s.position += 5;
} else if (!s.mono && (s.mod() === 'bold') && /<\/b>/.test(text.substr(s.position,4))) {
s.emitBlock();
s.bold = false;
s.modStack.shift();
s.position += 3;
} else if (!s.mono && (s.mod() === 'ital') && /<\/i>/.test(text.substr(s.position,4))) {
s.emitBlock();
s.ital = false;
s.modStack.shift();
s.position += 3;
} else if ((s.mod() === 'mono') && /<\/code>/.test(text.substr(s.position,7))) {
s.emitBlock();
s.mono = false;
s.modStack.shift();
s.position += 6;
} else {
s.add(char);
}
} else if (/&/.test(char)) {
if (/&lt;/.test(text.substr(s.position,4))) {
s.add("<");
s.position += 3;
} else if (/&amp;/.test(text.substr(s.position,5))) {
s.add("&");
s.position += 4;
} else {
s.add("&");
}
} else {
s.add(char);
}
s.position++
}
s.emitBlock();
return blocks;
}
setFont(ctx, selected, mod) {
let height
let vadjust
let color
if (mod === 'normal') {
ctx.font = (selected && this.elementOptions.labelHighlightBold ? 'bold ' : '') +
this.fontOptions.size + "px " + this.fontOptions.face;
color = this.fontOptions.color;
height = this.fontOptions.size;
vadjust = this.fontOptions.vadjust;
} else {
ctx.font = this.fontOptions[mod].mod + " " +
this.fontOptions[mod].size + "px " + this.fontOptions[mod].face;
color = this.fontOptions[mod].color;
height = this.fontOptions[mod].size;
vadjust = this.fontOptions[mod].vadjust || 0;
}
return {
font: ctx.font.replace(/"/g, ""),
color: color,
height: height,
vadjust: vadjust
}
}
/**
* This explodes the label string into lines and sets the width, height and number of lines.
* @param ctx
* @param selected
* @returns {number}
* @private
*/
_processLabel(ctx,selected) {
_processLabel(ctx, selected) {
let width = 0;
let lines = [''];
let lineCount = 0;
if (this.nodeOptions.label !== undefined) {
lines = String(this.nodeOptions.label).split('\n');
lineCount = lines.length;
ctx.font = (selected && this.nodeOptions.labelHighlightBold ? 'bold ' : '') + this.fontOptions.size + "px " + this.fontOptions.face;
width = ctx.measureText(lines[0]).width;
for (let i = 1; i < lineCount; i++) {
let lineWidth = ctx.measureText(lines[i]).width;
width = lineWidth > width ? lineWidth : width;
let height = 0;
let nlLines = [];
let lines = [];
let k = 0;
lines.add = function(l, text, font, color, width, height, vadjust) {
if (this.length == l) {
this[l] = { width: 0, height: 0, blocks: [] };
}
this[l].blocks.push({ text, font, color, width, height, vadjust });
}
lines.accumulate = function(l, width, height) {
this[l].width += width;
this[l].height = height > this[l].height ? height : this[l].height;
}
lines.addAndAccumulate = function(l, text, font, color, width, height, vadjust) {
this.add(l, text, font, color, width, height, vadjust);
this.accumulate(l, width, height);
}
if (this.elementOptions.label !== undefined) {
let nlLines = String(this.elementOptions.label).split('\n');
let lineCount = nlLines.length;
if (this.elementOptions.font.multi) {
for (let i = 0; i < lineCount; i++) {
let blocks = this.splitBlocks(nlLines[i], this.elementOptions.font.multi);
let lineWidth = 0;
let lineHeight = 0;
if (blocks) {
if (blocks.length == 0) {
this.setFont(ctx, selected, "normal");
lines.addAndAccumulate(k, "", ctx.font, "#000000", 0, this.fontOptions.size, this.fontOptions.vadjust);
height += lines[k].height;
k++;
continue;
}
for (let j = 0; j < blocks.length; j++) {
if (this.fontOptions.maxWdt > 0) {
let metrics = this.setFont(ctx, selected, blocks[j].mod);
let words = blocks[j].text.split(" ");
let atStart = true
let text = "";
let measure;
let lastMeasure;
let w = 0;
while (w < words.length) {
let pre = atStart ? "" : " ";
lastMeasure = measure;
measure = ctx.measureText(text + pre + words[w]);
if (lineWidth + measure.width > this.fontOptions.maxWdt) {
lineHeight = (metrics.height > lineHeight) ? metrics.height : lineHeight;
lines.add(k, text, ctx.font, metrics.color, lastMeasure.width, metrics.height, metrics.vadjust);
lines.accumulate(k, lastMeasure.width, lineHeight);
text = "";
atStart = true;
lineWidth = 0;
width = lines[k].width > width ? lines[k].width : width;
height += lines[k].height;
k++;
} else {
text = text + pre + words[w];
if (w === words.length-1) {
lineHeight = (metrics.height > lineHeight) ? metrics.height : lineHeight;
lineWidth += measure.width;
lines.add(k, text, ctx.font, metrics.color, measure.width, metrics.height, metrics.vadjust);
lines.accumulate(k, measure.width, lineHeight);
if (j === blocks.length-1) {
width = lines[k].width > width ? lines[k].width : width;
height += lines[k].height;
k++;
}
}
w++;
atStart = false;
}
}
} else {
let metrics = this.setFont(ctx, selected, blocks[j].mod)
let measure = ctx.measureText(blocks[j].text);
lines.addAndAccumulate(k, blocks[j].text, ctx.font, metrics.color, measure.width, metrics.height, metrics.vadjust);
width = lines[k].width > width ? lines[k].width : width;
if (blocks.length-1 === j) {
height += lines[k].height;
k++;
}
}
}
}
}
} else {
for (let i = 0; i < lineCount; i++) {
ctx.font = (selected && this.elementOptions.labelHighlightBold ? 'bold ' : '') + this.fontOptions.size + "px " + this.fontOptions.face;
if (this.fontOptions.maxWdt > 0) {
let words = nlLines[i].split(" ");
let text = "";
let measure;
let lastMeasure;
let w = 0;
while (w < words.length) {
let pre = (text === "") ? "" : " ";
lastMeasure = measure;
measure = ctx.measureText(text + pre + words[w]);
if (measure.width > this.fontOptions.maxWdt) {
lines.addAndAccumulate(k, text, ctx.font, this.fontOptions.color, lastMeasure.width, this.fontOptions.size, this.fontOptions.vadjust)
width = lines[k].width > width ? lines[k].width : width;
height += lines[k].height;
text = "";
k++;
} else {
text = text + pre + words[w];
if (w === words.length-1) {
lines.addAndAccumulate(k, text, ctx.font, this.fontOptions.color, measure.width, this.fontOptions.size, this.fontOptions.vadjust)
width = lines[k].width > width ? lines[k].width : width;
height += lines[k].height;
k++;
}
w++;
}
}
} else {
let text = nlLines[i];
let measure = ctx.measureText(text);
lines.addAndAccumulate(k, text, ctx.font, this.fontOptions.color, measure.width, this.fontOptions.size, this.fontOptions.vadjust);
width = lines[k].width > width ? lines[k].width : width;
height += lines[k].height;
k++;
}
}
}
}
if ((this.fontOptions.minWdt > 0) && (width < this.fontOptions.minWdt)) {
width = this.fontOptions.minWdt;
}
this.size.labelHeight = height;
if ((this.fontOptions.minHgt > 0) && (height < this.fontOptions.minHgt)) {
height = this.fontOptions.minHgt;
}
this.lines = lines;
this.lineCount = lineCount;
return width;
this.lineCount = lines.length;
this.size.width = width;
this.size.height = height;
}
}
export default Label;
export default Label;

+ 82
- 0
lib/network/options.js View File

@ -47,6 +47,40 @@ let allOptions = {
strokeWidth: { number }, // px
strokeColor: { string },
align: { string: ['horizontal', 'top', 'middle', 'bottom'] },
vadjust: { number },
multi: { boolean, string },
bold: {
color: { string },
size: { number }, // px
face: { string },
mod: { string },
vadjust: { number },
__type__: { object, string }
},
boldital: {
color: { string },
size: { number }, // px
face: { string },
mod: { string },
vadjust: { number },
__type__: { object, string }
},
ital: {
color: { string },
size: { number }, // px
face: { string },
mod: { string },
vadjust: { number },
__type__: { object, string }
},
mono: {
color: { string },
size: { number }, // px
face: { string },
mod: { string },
vadjust: { number },
__type__: { object, string }
},
__type__: { object, string }
},
hidden: { boolean },
@ -88,6 +122,10 @@ let allOptions = {
},
title: { string, 'undefined': 'undefined' },
width: { number },
widthConstraint: {
maximum: { number },
__type__: { object, boolean, number }
},
value: { number, 'undefined': 'undefined' },
__type__: { object }
},
@ -181,9 +219,48 @@ let allOptions = {
background: { string },
strokeWidth: { number }, // px
strokeColor: { string },
vadjust: { number },
multi: { boolean, string },
bold: {
color: { string },
size: { number }, // px
face: { string },
mod: { string },
vadjust: { number },
__type__: { object, string }
},
boldital: {
color: { string },
size: { number }, // px
face: { string },
mod: { string },
vadjust: { number },
__type__: { object, string }
},
ital: {
color: { string },
size: { number }, // px
face: { string },
mod: { string },
vadjust: { number },
__type__: { object, string }
},
mono: {
color: { string },
size: { number }, // px
face: { string },
mod: { string },
vadjust: { number },
__type__: { object, string }
},
__type__: { object, string }
},
group: { string, number, 'undefined': 'undefined' },
heightConstraint: {
minimum: { number },
valign: { string },
__type__: { object, boolean, number }
},
hidden: { boolean },
icon: {
face: { string },
@ -240,6 +317,11 @@ let allOptions = {
size: { number },
title: { string, 'undefined': 'undefined' },
value: { number, 'undefined': 'undefined' },
widthConstraint: {
minimum: { number },
maximum: { number },
__type__: { object, boolean, number }
},
x: { number },
y: { number },
__type__: { object }

+ 21
- 0
lib/util.js View File

@ -1481,3 +1481,24 @@ exports.getScrollBarWidth = function () {
return (w1 - w2);
};
exports.topMost = function (pile, accessors) {
let candidate;
if (!Array.isArray(accessors)) {
accessors = [accessors];
}
for (const member of pile) {
candidate = member[accessors[0]];
for (let i = 1; i < accessors.length; i++){
if (candidate) {
candidate = candidate[accessors[i]]
} else {
continue;
}
}
if (candidate) {
break;
}
}
return candidate;
}

Loading…
Cancel
Save