\n' +
+ '\n' +
+ '\n' +
+ '\n' +
+ '\n' +
+ '\n',
+ jshint: {
+ files: [
+ "Gruntfile.js",
+ "apexplugin.json",
+ "src/d3-force.js"
+ ]
+ },
+ clean: ["docs", "dist/*.css", "dist/*.js"],
+ copy: {
+ dist1: {
+ src: "src/d3-force.js",
+ dest: "dist/d3-force-<%= pkg.version %>.js",
+ options: {
+ process: function(content, srcpath) {
+ return grunt.template.process("<%= banner %>") + "\n" +
+ content.replace(/x\.x\.x/g, grunt.template.process("<%= pkg.version %>"));
+ }
+ }
+ },
+ dist2: {
+ files: [{
+ src: "src/d3-force.css",
+ dest: "dist/d3-force-<%= pkg.version %>.css"
+ },
+ {
+ src: "src/example.html",
+ dest: "dist/example.html"
+ },
+ {
+ src: "src/LICENSE.txt",
+ dest: "LICENSE.txt"
+ }
+ ],
+ options: {
+ process: function(content) {
+ return content
+ .replace(/x\.x\.x/g, grunt.template.process("<%= pkg.version %>"))
+ .replace(/{{CURRENT-YEAR}}/g, grunt.template.process("<%= currentYear %>"))
+ .replace(/{{EXAMPLE-GRAPH}}/g, grunt.template.process("<%= exampleGraph %>"));
+ }
+ }
+ },
+ docs1: {
+ files: [{
+ src: "docs/tutorial-1-getting-started.html",
+ dest: "docs/tutorial-1-getting-started.html"
+ }],
+ options: {
+ process: function(content, srcpath) {
+ return content.replace(/{{EXAMPLE-GRAPH}}/g, grunt.template.process("<%= exampleGraph %>"))
+ .replace(/{{EXAMPLE-GRAPH-CODE}}/g, grunt.template.process("<%= exampleGraph %>").replace(/.css",
+ dest: "docs/lib/d3-force-<%= pkg.version %>.css"
+ },
+ {
+ src: "dist/d3-force-<%= pkg.version %>.min.js",
+ dest: "docs/lib/d3-force-<%= pkg.version %>.min.js"
+ }
+ ]
+ }
+ },
+ uglify: {
+ options: {
+ banner: "<%= banner %>"
+ },
+ dist: {
+ src: "dist/d3-force-<%= pkg.version %>.js",
+ dest: "dist/d3-force-<%= pkg.version %>.min.js"
+ },
+ },
+ jsdoc: {
+ docs: {
+ src: ["README.md", "src/*.js"],
+ options: {
+ destination: "docs",
+ tutorials: "src/tutorials",
+ template: "node_modules/minami",
+ configure: ".jsdoc.json"
+ }
+ }
+ },
+ watch: {
+ files: [
+ "Gruntfile.js",
+ "apexplugin.json",
+ "src/**"
+ ],
+ tasks: ["default"]
+ }
+ });
+ grunt.loadNpmTasks("grunt-contrib-jshint");
+ grunt.loadNpmTasks("grunt-contrib-copy");
+ grunt.loadNpmTasks("grunt-contrib-uglify");
+ grunt.loadNpmTasks("grunt-contrib-clean");
+ grunt.loadNpmTasks("grunt-contrib-watch");
+ grunt.loadNpmTasks("grunt-notify");
+ grunt.loadNpmTasks("grunt-jsdoc");
+ grunt.registerTask("default", ["jshint", "clean", "copy:dist1", "copy:dist2", "uglify", "jsdoc", "copy:docs1", "copy:docs2"]);
+};
diff --git a/main/scripts/d3-force-apex-plugin/LICENSE.txt b/main/scripts/d3-force-apex-plugin/LICENSE.txt
new file mode 100644
index 0000000..3b429a6
--- /dev/null
+++ b/main/scripts/d3-force-apex-plugin/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2019 Ottmar Gobrecht
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/main/scripts/d3-force-apex-plugin/README.md b/main/scripts/d3-force-apex-plugin/README.md
new file mode 100644
index 0000000..51279ae
--- /dev/null
+++ b/main/scripts/d3-force-apex-plugin/README.md
@@ -0,0 +1,267 @@
+[Latest version][zip] | [Docs & API Reference][docs] | [Online demo][demo] | [APEX Plugin demo][apexdemo]
+
+[zip]: https://github.com/ogobrecht/d3-force-apex-plugin/releases/latest
+[docs]: https://ogobrecht.github.io/d3-force-apex-plugin/
+[demo]: https://ogobrecht.github.io/d3-force-apex-plugin/tutorial-1-getting-started.html
+[apexdemo]: https://apex.oracle.com/pls/apex/f?p=18290
+
+
+# Oracle APEX Region Type Plugin: D3 Force Network Chart
+
+This is a D3 force implementation, playground and Oracle APEX plugin, which uses the
+[D3 visualization library](http://d3js.org/) to render a network layout. It has the following features:
+
+- Works with APEX versions >= 5.1.4 or standalone in every HTML page
+- Interactive customization wizard
+- Source data can be a XML string, JSON string or JavaScript Object (JSON)
+- Link directions are visible and self references are rendered in a nice way - have a look in the online demos
+- Node sizes are calculated between given min and max values depending on the SIZEVALUE attribute in your source data
+- Node colors are assigned depending on the given COLORVALUE attribute in your source data - if you provide a IMAGE attribute for a node, then the image is used instead of a fill color
+- Optional tooltips depending on the given INFOSTRING attribute in your source data
+- If you have a node attribute called LINK, you can define on which event the URL should be called - default is dblclick - try it out in the online demos by double clicking the node KING
+- Nodes can be pinned and the current positions can be saved and loaded to predefine a layout - optionally you can align the nodes to a grid when they are dragged around
+- Labels can be wrapped and placed after force end to prevent overlapping (optional, per default switched off)
+- With the lasso mode you can select nodes and implement a graphical multi select
+- The graph can be zoomed between the two configured min and max scale factors
+- There is a JavaScript API to interact with the graph ([API reference][docs]), also including 12 events (node click, node double click, node contextmenu, node mouse enter, node mouse leave, link click, lasso start, lasso end, force start, force end, render end, resize)
+- All 12 events are available in APEX - the plugin region can be AJAX refreshed and triggers then also apexbeforerefresh and apexafterrefresh
+
+## Requirements
+
+- APEX 5.1.4 or higher, if used as a plugin
+- A modern browser, which is able to work with SVG and CSS3 - for more informations see the [D3 Wiki](https://github.com/mbostock/d3/wiki#browser--platform-support)
+
+
+
+## Installation
+
+
+### APEX
+
+- Download the [latest version][zip]
+- Install the plugin by importing the sql file in the folder `apex-plugin`
+
+
+### Any HTML page
+
+- Download the [latest version][zip]
+- See `dist/example.html` and `docs/tutorial-1-getting-started.html`
+
+
+## Credits
+
+I would like to say THANK YOU to all the people who share their knowledge. Without this sharing I would not have been able to create this D3 implementation. Special thanks to Mike Bostock for his great library and to Carsten Czarski for mentoring me on Oracle APEX plugin development.
+
+
+## Roadmap
+
+### 4.0.0 (201x-xx-xx) in planning
+
+- Update to current D3 version (5.x.x): [link 1](https://github.com/d3/d3/blob/master/CHANGES.md#forces-d3-force), [link 2](https://github.com/d3/d3-force/blob/master/README.md)
+- Devide code base into modularized graph code and APEX plugin code in different repos to make clear, that the graph function can run in any HTML environment
+
+
+## Changelog
+
+This D3 force implementation uses [semantic versioning](http://semver.org).
+
+Please refer to the [documentation](https://ogobrecht.github.io/d3-force-apex-plugin/) for more informations on how to get started and an overview of all graph methods. Please use for all comments and discussions the [issues functionality on GitHub](https://github.com/ogobrecht/d3-force-apex-plugin/issues).
+
+### 3.1.0 (2019-06-02)
+
+ATTENTION: You need at least APEX 5.1.4 to be able to import the plugin in your APEX apps. If you need to support older APEX versions (at least 4.2) then download the plugin release 3.0.0.
+
+- New option forceTimeLimit ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.forceTimeLimit))
+- Nodes have now also a background color when an background image is defined (useful for images with transparency)
+- New Link attribute LABEL ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/tutorial-2-node-and-link-attributes.html)), which is rendered as a text along the link path and fires the link click event when clicked (the label is easier to click then the link itself - so we have here a usability improvement)
+- Two new helper methods to get the center of the graph (border box) or the SVG viewport:
+ - centerPositionGraph ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.centerPositionGraph))
+ - centerPositionViewport ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.centerPositionViewport))
+
+Thanks are going to github.com/Ignacius68 for the valuable feedback and all the beta testing.
+
+
+### 3.0.0 (2018-11-26)
+
+Because of breaking API changes we have a new major realease:
+
+- Overall improvements
+ - Better responsiveness by implementing a resize observer (native in Chrome since v64, polyfill for other browsers)
+ - Default true for the following options: `zoomToFitOnForceEnd` (was false in the past), `zoomToFitOnResize` (new option), `keepAspectRatioOnResize` (new option)
+ - When setting the option `useDomParentWidth` to true together with the previous mentioned defaults you can achieve a responsiveness like with images set to width 100% - see the [online demo][demo] and play around with it
+ - All zoom relevant API methods are no longer depending on the `zoomMode` - they work simply always
+ - The `zoomMode` sets only the ability for the end user to use zoom and pan
+- Fixed
+ - APEX plug-in - sample data is rendered before live data (#32) - thanks are going to github.com/Ignacius68 for finding this bug
+- New events
+ - Resize ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.onResizeFunction))
+- New options
+ - labelSplitCharacter ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.labelSplitCharacter))
+ - onResizeFunction ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.onResizeFunction))
+ - onResizeFunctionTimeout ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.onResizeFunctionTimeout))
+ - zoomToFitOnResize ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.zoomToFitOnResize))
+ - keepAspectRatioOnResize ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.keepAspectRatioOnResize))
+- Changed methods
+ - `zoom` has now a parameter `duration` ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.zoom))
+ - `transform` has now a parameter `duration` ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.transform))
+ - `useDomParentWidth` ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.useDomParentWidth)) no longer needs a render call to take into effect - it works now immediately; Please remove unneccesary render calls to save CPU and battery time
+- Deprecated methods for clean API
+ - `zoomSmooth` - can be replaced with the `zoom` method ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.zoom)), please provide a appropriate duration parameter (default is 1500 with zoomSmooth)
+
+Thanks are going to github.com/Ignacius68 for the idea for option `labelSplitCharacter` and all the beta testing.
+
+### 2.2.0 (2018-09-29)
+
+- New events
+ - Render end ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.onRenderEndFunction))
+ - Force start ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.onForceStartFunction))
+ - Force end ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.onForceEndFunction))
+- New graph methods
+ - nodes ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.nodes))
+ - links ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.links))
+ - selfLinks ([API reference](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.selfLinks))
+ - All three returning a D3 selection (array) for direct manipulation with D3 methods like `style` or `classed` - also see the [D3 docs](https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#operating-on-selections)
+
+Thanks are going to github.com/Ignacius68 for the inspiration.
+
+### 2.1.2 (2018-01-07)
+
+- Fixed again :-(
+ - APEX plugin - semi colon in region query no longer throws an error
+ - Was a copy paste bug... no comments please...
+
+### 2.1.1 (2018-01-06)
+
+- Fixed: Nodes stick on the top left corner in APEX 5.x under some circumstances
+- Improved docs: getting started section
+
+### 2.1.0 (2017-12-30)
+
+- New option `wrapLabels` with a configurable max width - thanks to Ekaterina & Andrey for the idea
+- New option `zoomToFitOnForceEnd` to fit the graph in the available space on force end (like the automatic label placement) - needs the zoomMode switched on to work properly
+- New API method `zoomToFit`, which is used by the option zoomToFitOnForceEnd - now you can do things like `example.width(800).height(600).zoomToFit()` :-)
+- APEX enhancements: the graph is listen to the event `apexwindowresized` and the click on the navigation control button in the universal theme - together with the option `useDomParentWidth` the graph is then always using the available width
+- Changed: Use JSDoc to generate documentation and API reference. Relocate documentation from own Wiki to GitHub pages
+- Reorganized repository structure
+- Fixed: Standalone version not loading after APEX 5.1 bugfix
+- Fixed: APEX plugin - semi colon in region query no longer throws an error
+
+### 2.0.3 (2016-12-13)
+
+- Fixed: #18 - APEX 5.1: jQuery reports syntax error and graph stops loading, if "Page Items to Submit" is not configured - thanks to github.com/KiralyCs to report this issue
+
+
+### 2.0.2 (2016-07-17)
+
+- Fixed: #12 - tooltips not showing correctly, if showLabels are set to false - thanks to github.com/pocelka to report this issue
+
+
+### 2.0.1 (2015-11-18)
+
+- Fixed: Fixed positions not working in initial data in v2.0.0 - thanks to github.com/rlashaw to report this issue
+- Move online demo and documentation to own wiki for better maintenance
+
+
+### 2.0.0 (2015-11-07)
+
+- New option `preventLabelOverlappingOnForceEnd`: If set to true the labels are aligned with a simulated annealing function to prevent overlapping when the graph is cooled down (correctly on the force end event and only on labels, who are not circular) - thanks to Philippe Duchateau to ask for such a feature and all the testing
+- New option `labelPlacementIterations`: The number of iterations for the preventLabelOverlappingOnForceEnd function - default is 250 - as higher the number, as higher the quality of the result - for details refer to the description of the [simulated annealing function](https://github.com/tinker10/D3-Labeler) from the author Evan Wang
+- New behaviour: the font size and weight of a label is aligned when you hovering a node with your mouse - this helps you to find the right label in graphs with many nodes
+- New possible value `dotted` for the links `STYLE` attribute: Now you have solid, dashed and dotted available for link styles
+- New link attribute `INFOSTRING`: Like for nodes - this is shown as a tooltip, if tooltips are switched on in the configuration and you hover the links; ATTENTION: links are very narrow, so this plays nice together with the zoomMode; thanks again to Philippe Duchateau for the ideas of this and the next feature :-)
+- New link attribute `COLOR`: This must be a HTML color code like `green` or `#00ff00` because of SVG standard 1.1 does not support the inheritance of colors to markers and the graph function hast to manage dynamic markers for the colors and therefore the color names are used as identifiers for the markers
+- New API method/option `transform`: behaves like a normal getter/setter (the zoom and zoomSmooth methods implements only setters) and can be used in the conf object to initialize the graph with different translate/scale factors than [0,0]/1 - works only, if the zoomMode is set to true - the current transform value(an object) is rendered in the customization wizard conf object text area like all other options when the current value is different then the default value `{"translate":[0,0],"scale":1}`
+- Fixed: With the option alignFixedNodesToGrid it was possible to place nodes direct on the graphs left or top border - now the nodes are placed to the gridSize value, if the current position is smaller or equal the half of the gridsize
+- Fixed: Provided fixed positions on startup not correctly set
+- Fixed: No node shown if there is only one record return (thanks to Kenny Wang for reporting this issue)
+- Code integration of the D3 lasso and labeler plugins - no more need to load the files for this plugins
+- Code replacement of the XML to JSON converter X2JS with an own one
+- Code refactoring against JSHint: This refactoring is also the reason for a new major version (API changed: renamed graph function, integration of libs, new XML parser)
+- Update to D3 v3.5.6
+
+
+### 1.4.1 (2015-08-05)
+
+- Fixed "Tooltip on wrong positions in complex layouts". This was also the case with APEX 5 and universal theme. Thanks to Philippe Duchateau for telling me about this problem.
+
+
+### 1.4.0 (2015-08-03)
+
+- New possible node attribute `COLORLABEL`: Since there is an option to render a legend, it makes no sense to render the color names as legend labels, if the colorScheme "direct" is used to directly deliver CSS color codes (thanks to Philippe Duchateau for telling me about the problems); With other color schemes it is ok, since the COLORVALUE information can be any string like department names or ids or city names or whatever; To not to break existing graphs, the COLORVALUE is used as the legend label, if the COLORLABEL is not given in the nodes attributes
+- New option `onLinkClickFunction`: You can register a function which is called when a link is clicked (thanks to Niels de Bruijn for requesting this feature); It is not so easy to click a link, because the links are so narrow - if this option is needed I recommend to switch on the zoom mode - with zoom and pan it feels more natural to click links
+- New option `setDomParentPaddingToZero`: Boolean. If true, the style `padding: 0px;` is added to the graphs DOM parent element; If false, this style is removed from the graphs DOM parent element
+- The customization wizard shows now in the configuration object only non-default options; This reduces the size of the configuration object and is more transparent
+- New API methods `options` and `optionsCustomizationWizard`: with this API methods you can get and set the whole configuration object with one call; `options` ouput includes all options, which are accessible via the API methods including the registered event functions (no APEX dynamic actions, only the functions under the report attributes); `optionsCustomizationWizard` output includes only the options, which are accessible via the customization wizard; With both methods you can set all options which are accessible via the API in one call
+- Restructuring the online API reference method overview
+
+
+### 1.3.0 (2015-06-07)
+
+- New option `showLoadingIndicatorOnAjaxCall`: if set to true, a loading indicator is shown when used as a APEX plugin during the AJAX calls; If you want to show the loading indicator in a standalone implementation you can show and hide the loading indicator directly with the API method `showLoadingIndicator` (SHOW: `example.showLoadingIndicator(true);` HIDE: `example.showLoadingIndicator(false);`)
+- Update to D3 v3.5.5
+
+
+### 1.2.1 (2015-06-02)
+
+- Fixed "Customize wizard jumps down when dragged on pages with hidden or fixed elements"
+
+
+### 1.2.0 (2015-05-31)
+
+- Refactor render function, so that the returned graph function is only one line of code and does not spoil the console when debug is set to true
+- New option `zoomMode` (thanks to Alexej Schneider to ask for this feature and for testing the new version and his valuable feedback): I tried this before and was not happy with the solution, because the pan were disturbing the nodes drag functionality - now it is working :-) ATTENTION: When zoomMode is set to true then the lassoMode is only working with the pressed alt or shift key KNOWN BUG: In iOS it is after the first zoom event no more possible, to drag a node - instead the whole graph is moved - this is, because iOS Safari provide a wrong event.target.tagName. Also a problem: your are not able to press the alt or shift key - if you want to use lasso and zoom together on a touch device, you have to provide a workaround; One possible way is to provide a button, which turns zoom mode on and off with the API zoomMode method - then the user has the choice between these two modes - not comfortable, but working
+- New option `minZoomFactor`: The minimum possible zoom factor
+- New option `maxZoomFactor`: The maximum possible zoom factor
+- New method `zoom`: Can be used to programatically zoom to a point in the graph with the three parameters centerX, centerY and viewportWidth; [read more...](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.zoom)
+- New method `zoomSmooth`: Does the same as the zoom method, but animated in a nice way: [read more...](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.zoomSmooth)
+- New method `nodeDataById`: Helper function to get the data of one node. Can be helpful for the two new zoom methods to programatically focus a single node
+- New option `showLegend`: renders a legend for all (distinct) COLORVALUE attribute values of the nodes
+- New option `showLabels`: Labels are not new - a label is rendered, when a node has a filled attribute LABEL - new is the possibility to switch on and off the labels globally
+- Hint in the customize wizard, that the configuration object has to be saved in the region attributes to save the configuration permanently (thanks to Renato Nobre to ask me a question about this topic)
+- Reorganize the options in the customize wizard thematically: node/link/graph related options
+
+
+### 1.1.0 (2015-04-19)
+
+- New option `lassoMode`: boolean - if set to true you can select nodes with a lasso
+- New events for lasso mode: `lassostart`, `lassoend` - if You register to this events, you get as data an object with all nodes, number of selected nodes and also a APEX compatible string of selected node IDs in the form of the multi select lists like `1234:567:890` - for details and examples see API reference
+- New option `alignFixedNodesToGrid`: boolean - if set to true nodes are aligned to the nearest grid position on the drag end event - works only, if pinMode is set to true (thanks to Carsten Czarski for showing me an use case for this option)
+- New option `gridSize`: numeric - default 50 - grid size for the new option `alignFixedNodesToGrid`
+- New possible node attribute `IMAGE`: URL to an image - if you provide this attribute in your source data (SQL query with the APEX plugin), the node is rendered with an background image instead of a fill color (idea by Andrew Weir, thank you for your response!) - attention: this is definitly slowing down your visualization - please do not complain about this fact ;-)
+- New possible node attributes `fixed`, `x`, `y` (all lower case, because of these are also internal attributes of the D3 force layout): With these attributes you are able to predefine a layout already in your data (SQL query)
+- New API method `moveFixedNodes(x,y)`: moves all fixed nodes in the provided direction - `exampleGraphVariable.moveFixedNodes(10,-5).resume();` adds 10 to x position and -5 to y position on all fixed nodes - ATTENTION if alignFixedNodesToGrid is set to true this can have unexpected behavior - you must then provide values greater then grid size halved to see any changes on your graph, otherwise the positions are falling back to the nearest (current) grid position
+- New API method `releaseFixedNodes`
+- New API method `resume`: with this method you can resume the graph force without a complete render cycle - e.g. you call the new method `releaseFixedNodes` and to take your changes into effect you can call then resume `exampleGraphVariable.releaseFixedNodes().resume();`
+- New API method `render`: with this method you can render the graph with a complete render cycle - when used standalone there is no difference between the start and the render method - when used as APEX plugin the start method try to fetch new data with the query provided in your region source and call then the render method - with the render method you are now able to rerender the graph in APEX without fetching new data `exampleGraphVariable.minNodeRadius(4).maxNodeRadius(20).render();`
+- API method positions: In the past this method was only used to predefine a layout before rendering the graph - now you can call this method also after rendering is complete and with calling the new method resume you can apply new positions at runtime without rerender the graph `exampleGraphVariable.positions([...]).resume();` (thanks to Mark Russellbrown to show me an unconventional use case for my force implementation and therefore force me to think about modification after rendering ;-)
+- New third keyword for the option `nodeLinkTarget` in the customize wizard: "domContainerID" - if you use this keyword, then each event on a node, that opens the link is using the DOM container ID of your graph for the link target - this means, all your links are opened in the same browser window/tab, but a second graph is using a different browser window/tab (thanks to Philippe Duchateau for the question regarding this option) - please have a look in the [API reference for more details](https://ogobrecht.github.io/d3-force-apex-plugin/module-API.html#.nodeLinkTarget)
+- Reducing the rendered DOM data by removing unnecessary id attributes on nodes, links and labels
+- Input data can now be also an object: you have the choice to deliver graph data in three formats (XML string, JSON string or JavaScript Object) - when used as APEX plugin the data is transferred as text - your query has to select a single clob result and this clob can also be a XML or JSON string - you have the choice depending on your database version and existing libraries
+- Fixed "Dragging a node triggers a click event"
+
+
+### 1.0.5 (2015-02-21)
+
+- Fixed "Links not correctly rendered in IE 9, 10, 11 when showLinkDirection is set to true" (found by Philippe Duchateau, thank you for your response!)
+
+
+### 1.0.4 (2015-02-15)
+
+- Fixed "APEX - unable to view datasets > 32k" (found by Andrew Weir, thank you for your response!)
+- Improved error handling: errors are shown as single nodes with error text as label
+- Empty nodes array does no longer break render function
+- Positions are rounded on export to save space for APEX parameter item
+
+
+### 1.0.3 (2015-01-30)
+
+- Fixed "APEX - AJAX refresh not working without setting items to submit in region source"
+- Correct links from customize wizard to online API documentation
+- Activate also debug mode, when customize wizard is started
+- Some small cosmetic changes
+
+
+### 1.0.2 (2015-01-30)
+
+- Fixed "Configuration - Boolean values are not correct initialized" (found by Carsten Czarski, thank you for your response!)
+- Fixed "APEX - Page items to submit not working on AJAX refresh" (found by Carsten Czarski, thank you for your response!)
diff --git a/main/scripts/d3-force-apex-plugin/apex-plugin/d3-force-apex-plugin-3.1.0.sql b/main/scripts/d3-force-apex-plugin/apex-plugin/d3-force-apex-plugin-3.1.0.sql
new file mode 100644
index 0000000..5445cc8
--- /dev/null
+++ b/main/scripts/d3-force-apex-plugin/apex-plugin/d3-force-apex-plugin-3.1.0.sql
@@ -0,0 +1,9880 @@
+prompt --application/set_environment
+set define off verify off feedback off
+whenever sqlerror exit sql.sqlcode rollback
+--------------------------------------------------------------------------------
+--
+-- ORACLE Application Express (APEX) export file
+--
+-- You should run the script connected to SQL*Plus as the Oracle user
+-- APEX_050100 or as the owner (parsing schema) of the application.
+--
+-- NOTE: Calls to apex_application_install override the defaults below.
+--
+--------------------------------------------------------------------------------
+begin
+wwv_flow_api.import_begin (
+ p_version_yyyy_mm_dd=>'2016.08.24'
+,p_release=>'5.1.4.00.08'
+,p_default_workspace_id=>1833743955796265
+,p_default_application_id=>1000
+,p_default_owner=>'OGOBRECHT'
+);
+end;
+/
+prompt --application/shared_components/plugins/region_type/net_gobrechts_d3_force
+begin
+wwv_flow_api.create_plugin(
+ p_id=>wwv_flow_api.id(130317839079452583603)
+,p_plugin_type=>'REGION TYPE'
+,p_name=>'NET.GOBRECHTS.D3.FORCE'
+,p_display_name=>'D3 - Force Layout'
+,p_supported_ui_types=>'DESKTOP:JQM_SMARTPHONE'
+,p_plsql_code=>wwv_flow_string.join(wwv_flow_t_varchar2(
+'FUNCTION d3_force__render( p_region IN apex_plugin.t_region',
+' , p_plugin IN apex_plugin.t_plugin',
+' , p_is_printer_friendly IN BOOLEAN )',
+' RETURN apex_plugin.t_region_render_result',
+'IS',
+' v_configuration_object apex_application_page_regions.attribute_02%TYPE := p_region.attribute_02;',
+' v_custom_styles apex_application_page_regions.attribute_03%TYPE := p_region.attribute_03;',
+' v_region_static_id VARCHAR2( 100 );',
+'BEGIN',
+' v_region_static_id := apex_plugin_util.escape( p_region.static_id, TRUE );',
+'',
+' apex_css.add_file( p_name => ''d3-force-''',
+' , p_directory => p_plugin.file_prefix',
+' , p_version => ''3.1.0'' );',
+'',
+' apex_javascript.add_library( p_name => ''ResizeObserver-''',
+' , p_directory => p_plugin.file_prefix',
+' , p_version => ''1.5.0''',
+' , p_check_to_add_minified => TRUE );',
+'',
+' apex_javascript.add_library( p_name => ''d3-''',
+' , p_directory => p_plugin.file_prefix',
+' , p_version => ''3.5.6''',
+' , p_check_to_add_minified => TRUE );',
+'',
+' apex_javascript.add_library( p_name => ''d3-force-''',
+' , p_directory => p_plugin.file_prefix',
+' , p_version => ''3.1.0''',
+' , p_check_to_add_minified => TRUE );',
+'',
+' HTP.p( CASE',
+' WHEN v_custom_styles IS NOT NULL THEN',
+' '''' || CHR( 10 )',
+' END',
+' || ''''',
+' || CHR( 10 ) );',
+'',
+' apex_javascript.add_onload_code( --> initialize chart function',
+' ''d3_force_''',
+' || v_region_static_id --> we need to use a global var - that is the reason to NOT use the var keyword',
+' || '' = netGobrechtsD3Force(''',
+' --> domContainerId:',
+' || apex_javascript.add_value( v_region_static_id, TRUE )',
+' --> options:',
+' || CASE',
+' WHEN v_configuration_object IS NOT NULL THEN',
+' v_configuration_object',
+' ELSE',
+' ''null''',
+' END',
+' || '', ''',
+' --> apexPluginId:',
+' || apex_javascript.add_value( apex_plugin.get_ajax_identifier',
+' , TRUE )',
+' --> apexPageItemsToSubmit:',
+' || apex_javascript.add_value( p_region.ajax_items_to_submit',
+' , FALSE )',
+' || '')''',
+' || CASE WHEN v( ''DEBUG'' ) = ''YES'' THEN ''.debug(true)'' END',
+' || CASE',
+' WHEN p_region.attribute_09 IS NOT NULL THEN',
+' ''.positions('' || p_region.attribute_09 || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_04 IS NOT NULL THEN',
+' ''.onNodeClickFunction(''',
+' || p_region.attribute_04',
+' || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_05 IS NOT NULL THEN',
+' ''.onNodeDblclickFunction(''',
+' || p_region.attribute_05',
+' || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_06 IS NOT NULL THEN',
+' ''.onNodeContextmenuFunction(''',
+' || p_region.attribute_06',
+' || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_12 IS NOT NULL THEN',
+' ''.onLinkClickFunction(''',
+' || p_region.attribute_12',
+' || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_07 IS NOT NULL THEN',
+' ''.onNodeMouseenterFunction(''',
+' || p_region.attribute_07',
+' || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_08 IS NOT NULL THEN',
+' ''.onNodeMouseleaveFunction(''',
+' || p_region.attribute_08',
+' || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_10 IS NOT NULL THEN',
+' ''.onLassoStartFunction(''',
+' || p_region.attribute_10',
+' || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_11 IS NOT NULL THEN',
+' ''.onLassoEndFunction('' ',
+' || p_region.attribute_11',
+' || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_13 IS NOT NULL THEN',
+' ''.onForceStartFunction('' ',
+' || p_region.attribute_13',
+' || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_14 IS NOT NULL THEN',
+' ''.onForceEndFunction('' ',
+' || p_region.attribute_14',
+' || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_15 IS NOT NULL THEN',
+' ''.onRenderEndFunction('' ',
+' || p_region.attribute_15',
+' || '')''',
+' END',
+' || CASE',
+' WHEN p_region.attribute_16 IS NOT NULL THEN',
+' ''.onResizeFunction('' ',
+' || p_region.attribute_16',
+' || '')''',
+' END',
+' --> start the visualization',
+' || ''.start();'' );',
+' RETURN NULL;',
+'END d3_force__render;',
+'',
+'FUNCTION d3_force__ajax( p_region IN apex_plugin.t_region, p_plugin IN apex_plugin.t_plugin )',
+' RETURN apex_plugin.t_region_ajax_result',
+'IS',
+' v_clob CLOB;',
+' v_binds DBMS_SQL.varchar2_table;',
+' v_cur INTEGER;',
+' v_ret INTEGER;',
+'BEGIN',
+' IF p_region.source IS NOT NULL THEN',
+' v_binds := wwv_flow_utilities.get_binds( p_region.source );',
+' v_cur := DBMS_SQL.open_cursor;',
+' DBMS_SQL.parse( c => v_cur, statement => REGEXP_REPLACE(p_region.source,'';\s*$'',''''), language_flag => DBMS_SQL.native );',
+'',
+' IF v_binds.COUNT > 0 THEN',
+' FOR i IN v_binds.FIRST .. v_binds.LAST LOOP',
+' DBMS_SQL.bind_variable( v_cur',
+' , v_binds( i )',
+' , APEX_UTIL.get_session_state( SUBSTR( v_binds( i ), 2 ) ) );',
+' END LOOP;',
+' END IF;',
+'',
+' DBMS_SQL.define_column( c => v_cur, position => 1, column => v_clob );',
+' v_ret := DBMS_SQL.execute( c => v_cur );',
+'',
+' WHILE DBMS_SQL.fetch_rows( v_cur ) > 0 LOOP',
+' DBMS_SQL.COLUMN_VALUE( v_cur, 1, v_clob );',
+' END LOOP;',
+'',
+' DBMS_SQL.close_cursor( v_cur );',
+'',
+' IF sys.DBMS_LOB.getlength( v_clob ) > 0 THEN',
+' DECLARE',
+' v_len PLS_INTEGER;',
+' v_pos PLS_INTEGER := 1;',
+' v_amo PLS_INTEGER := 4000;',
+' v_chu VARCHAR2( 32767 );',
+' BEGIN',
+' v_len := DBMS_LOB.getlength( v_clob );',
+'',
+' WHILE v_pos <= v_len LOOP',
+' v_amo := LEAST( v_amo, v_len - ( v_pos - 1 ) );',
+' v_chu := DBMS_LOB.SUBSTR( v_clob, v_amo, v_pos );',
+' v_pos := v_pos + v_amo;',
+' HTP.prn( v_chu );',
+' END LOOP;',
+' END;',
+' ELSE',
+' HTP.prn( ''query_returned_no_data'' ); --> prn prints without newline',
+' END IF;',
+' ELSE',
+' HTP.prn( ''no_query_defined'' );',
+' END IF;',
+'',
+' --> Free the temp LOB, if necessary',
+' BEGIN',
+' DBMS_LOB.freetemporary( v_clob );',
+' EXCEPTION',
+' WHEN OTHERS THEN',
+' NULL;',
+' END;',
+'',
+' RETURN NULL;',
+'EXCEPTION',
+' WHEN OTHERS THEN',
+' --> Close the cursor, if open',
+' BEGIN',
+' IF v_cur IS NOT NULL',
+' AND DBMS_SQL.is_open( v_cur ) THEN',
+' DBMS_SQL.close_cursor( v_cur );',
+' END IF;',
+' EXCEPTION',
+' WHEN OTHERS THEN',
+' NULL;',
+' END;',
+'',
+' apex_debug.MESSAGE( SQLERRM );',
+' --> Write error back to the Browser',
+' HTP.prn( SQLERRM );',
+' RETURN NULL;',
+'END d3_force__ajax;',
+''))
+,p_api_version=>1
+,p_render_function=>'d3_force__render'
+,p_ajax_function=>'d3_force__ajax'
+,p_standard_attributes=>'SOURCE_SQL:AJAX_ITEMS_TO_SUBMIT'
+,p_substitute_attributes=>true
+,p_subscribe_plugin_settings=>true
+,p_help_text=>wwv_flow_string.join(wwv_flow_t_varchar2(
+'
Copy in here your customizing object from the interactive configuration.
',
+'
You can start the interactive configuration by clicking the link "Customize me" in the rendered page region. The link is shown when the developer toolbar is visible or when the page is in debug mode.
You can define custom styles. If you want to have custom styles only for one specific chart on a page with multiple charts, then you have to prefix the CSS with your region static ID. Here an example:
You can also create an APEX dynamic action on the component event "Node Click [D3 - Force Layout]" on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
You can also create an APEX dynamic action on the component event "Node Double Click [D3 - Force Layout]" on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
You can also create an APEX dynamic action on the component event "Node Contextmenu [D3 - Force Layout]" on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
You can provide here a function for the node mouse enter event.
',
+'',
+'
In the first two parameters you get the event and the d3 node data, inside your function you have access to the DOM node with the this keyword.
',
+'',
+'
',
+'function(event, data){',
+' console.log("Node mouse enter - event:", event);',
+' console.log("Node mouse enter - data:", data);',
+' console.log("Node mouse enter - this:", this);',
+'}',
+'
',
+'',
+'
You can also create an APEX dynamic action on the component event "Node Mouse Enter [D3 - Force Layout]" on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
',
+'',
+'
',
+'console.log("Node mouse enter - event:", this.browserEvent);',
+'console.log("Node mouse enter - data:", this.data);',
+'
',
+' ',
+'
Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
You can also create an APEX dynamic action on the component event "Node Mouse Leave [D3 - Force Layout]" on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
Copy in here your node positions from the customize wizard.
',
+' ',
+'
You can start the customize wizard by clicking the link "Customize me" in the rendered page region. The link is shown when the developer toolbar is visible or when the page is in debug mode.
You can provide here a function for the lasso start event.
',
+'',
+'
In the first two parameters you get the event and the d3 lasso data, inside your function you have access to the DOM node with the this keyword. In case of the lasso this is refering the svg container element, because the lasso itself is not inter'
+||'esting.
You can also create an APEX dynamic action on the component event "Lasso Start [D3 - Force Layout]" on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
You can provide here a function for the lasso end event.
',
+'',
+'
In the first two parameters you get the event and the d3 lasso data, inside your function you have access to the DOM node with the this keyword. In case of the lasso this is refering the svg container element, because the lasso itself is not inter'
+||'esting.
',
+'',
+'
',
+'function(event, data){',
+' console.log("Lasso end - event:", event);',
+' console.log("Lasso end - data:", data);',
+' console.log("Lasso end - this:", this);',
+'}',
+'
',
+'',
+'
You can also create an APEX dynamic action on the component event "Lasso End [D3 - Force Layout]" on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
',
+'',
+'
',
+'console.log("Lasso end - event:", this.browserEvent);',
+'console.log("Lasso end - data:", this.data);',
+'
',
+' ',
+'
Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
You can also create an APEX dynamic action on the component event "Link Click [D3 - Force Layout]" on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
Your query should result one column single row containing XML or JSON. This can be reached by using serialized XML',
+'or JSON. It depends on your system environment - the important thing is: One column, one row of data and NO semicolon',
+'to terminate the query.
',
+'',
+'
If you provide no query here, you get sample data from the plugin to play around with ;-)
',
+'',
+'
Your result should look like this examples - in case of XML you can use the short form with attributes instead of',
+'elements. The data is converted in the right way afterwards in JavaScript - therefore the first character of your result',
+'is used to identify JSON or XML and must be "{" or "<". You can copy the following example query in your preferred SQL',
+'tool and play around - I think, you know the example data :-)
',
+'',
+'
',
+'{"data":',
+' {"nodes":[',
+' {"ID":"7839","LABEL":"KING","COLORVALUE":"10","SIZEVALUE":"5000"},',
+' {"ID":"7934","LABEL":"MILLER","COLORVALUE":"10","SIZEVALUE":"1300"}],',
+' "links":[',
+' {"FROMID":"7839","TOID":"7839","STYLE":"solid"},',
+' {"FROMID":"7934","TOID":"7782","STYLE":"dashed"}]',
+' }',
+'}',
+'',
+'Sorry, XML example two times, because of different rendering of pre area in APEX4 and APEX5 :-(',
+'',
+'',
+' ',
+' ',
+' ',
+' ',
+'',
+'',
+'<data>',
+' <nodes ID="7839" LABEL="KING" COLORVALUE="10" SIZEVALUE="5000"/>',
+' <nodes ID="7934" LABEL="MILLER" COLORVALUE="10" SIZEVALUE="1300"/>',
+' <links FROMID="7839" TOID="7839" STYLE="solid"/>',
+' <links FROMID="7934" TOID="7782" STYLE="dashed"/>',
+'</data>',
+'
',
+'',
+'
If you look in detail to this example query, you will see, that you have only to provide two simple queries - it is',
+'enough to change the column and table names. If you need more complex data for your graph, you are free to find other',
+'solutions - depending on your database version and existing libs you can use whatever you want and fit in a SQL query',
+'resulting a single clob containing XML or JSON.
',
+'',
+'
',
+'WITH',
+'nodes AS ( --> START YOUR NODES QUERY HERE',
+' SELECT XMLELEMENT( "nodes", xmlattributes(',
+' empno AS id',
+' , ename AS label',
+' , sal AS sizevalue',
+' , d.deptno AS colorvalue',
+' --, d.dname AS colorlabel -- optional, used for the graph legend',
+' --, ''http://...'' AS link -- optional',
+' --, ''some text'' AS infostring -- optional, rendered as tooltip',
+' --, ''false'' AS labelcircular -- optional, overwrites the global labelsCircular',
+' --, ''http://...'' AS image -- optional, background image for a node instead of a fill color',
+' --, ''true'' AS "fixed" -- optional | fixed, x and y are native D3 attributes',
+' --, 100 AS "x" -- optional | they must be lowercase',
+' --, 100 AS "y" -- optional | you can predefine a layout with these attributes',
+' ) ) AS xml_nodes',
+' FROM emp e join dept d on e.deptno = d.deptno --< STOP YOUR NODES QUERY HERE',
+'),',
+'links AS ( --> START YOUR LINKS QUERY HERE',
+' SELECT XMLELEMENT( "links", xmlattributes(',
+' empno AS fromid',
+' , NVL(mgr,empno) AS toid',
+' --, ''dashed'' AS style -- optional, can be solid (default), dotted or dashed',
+' --, ''red'' AS color -- optional, must be a HTML color code like green or #00ff00',
+' --, ''some text'' AS infostring -- optional, rendered as tooltip',
+' ) ) AS xml_links',
+' FROM emp --< STOP YOUR LINKS QUERY HERE',
+')',
+'SELECT XMLSERIALIZE( DOCUMENT( XMLELEMENT( "data",',
+' ( SELECT XMLAGG( xml_nodes ) FROM nodes ),',
+' ( SELECT XMLAGG( xml_links ) FROM links ) ) ) INDENT ) AS single_clob_result',
+' FROM DUAL',
+'
" +
+ (v.status.apexPluginId ?
+ "To save your options please copy this to your plugin region attributes. " +
+ "Only non-default options are shown.
" :
+ "Use this to initialize your graph. Only non-default options are shown.")
+ );
+ v.dom.customizeConfObject = gridCell.append("textarea")
+ .attr("tabindex", i + 5)
+ .attr("readonly", "readonly");
+ gridCell.append("span").html("
Current Positions ");
+ v.dom.customizePositions = gridCell.append("textarea")
+ .attr("tabindex", i + 6)
+ .attr("readonly", "readonly")
+ .text((v.status.forceRunning ? "Force started - wait for end event to show positions..." :
+ JSON.stringify(graph.positions())));
+ gridCell.append("span").html("
");
+ };
+
+ // helper function to wrap text - https://bl.ocks.org/mbostock/7555321
+ v.tools.wrapLabels = function(labels, width) {
+ labels.each(function(label, i) {
+ var text = d3.select(this);
+ if (i === 0) {
+ v.status.labelFontSize = parseInt(text.style("font-size"));
+ }
+ if (!this.hasAttribute("lines")) {
+ var tokens = text.text()
+ .split( (v.conf.labelSplitCharacter !== "none" ? v.conf.labelSplitCharacter : /\s+/) )
+ .reverse(),
+ token,
+ line = [],
+ lineNumber = 0,
+ lineHeight = v.status.labelFontSize * v.conf.wrappedLabelLineHeight,
+ x = text.attr("x"),
+ y = text.attr("y"),
+ dy = 0,
+ tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "px");
+
+ if (v.conf.labelSplitCharacter !== "none") {
+ while (token = tokens.pop()) { // jshint ignore:line
+ tspan = text.append("tspan")
+ .attr("x", x)
+ .attr("y", y)
+ .attr("dy", ++lineNumber * lineHeight + dy + "px")
+ .text(token);
+ }
+ }
+ else {
+ while (token = tokens.pop()) { // jshint ignore:line
+ line.push(token);
+ tspan.text(line.join(" "));
+ if (tspan.node().getComputedTextLength() > width) {
+ line.pop();
+ tspan.text(line.join(" "));
+ line = [token];
+ tspan = text.append("tspan")
+ .attr("x", x)
+ .attr("y", y)
+ .attr("dy", ++lineNumber * lineHeight + dy + "px")
+ .text(token);
+ }
+ }
+ }
+ //save number of lines
+ text.attr("lines", lineNumber + 1);
+ }
+ });
+ };
+
+ /*******************************************************************************************************************
+ * LIBRARIES
+ */
+
+ // D3 labeler plugin
+ /* Source Code: https://github.com/tinker10/D3-Labeler
+ The MIT License (MIT)
+
+ Copyright (c) 2013 Evan Wang
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+ v.lib.labelerPlugin = function() {
+ /* jshint -W106 */
+ var lab = [],
+ anc = [],
+ w = 1, // box width
+ h = 1, // box width
+ labeler = {};
+
+ var max_move = 5, //5.0,
+ max_angle = 0.5, //0.5,
+ acc = 0,
+ rej = 0;
+
+ // weights
+ var w_len = 0.2, // leader line length
+ w_inter = 1.0, // leader line intersection
+ w_lab2 = 30.0, // label-label overlap
+ w_lab_anc = 30.0, // label-anchor overlap
+ w_orient = 1.0; //3.0; // orientation bias
+
+ // booleans for user defined functions
+ var user_energy = false,
+ user_schedule = false;
+
+ var user_defined_energy,
+ user_defined_schedule;
+
+ var energy = function(index) {
+ /* jshint -W071 */
+ // energy function, tailored for label placement
+
+ var m = lab.length,
+ ener = 0,
+ dx = lab[index].x - anc[index].x,
+ dy = anc[index].y - lab[index].y,
+ dist = Math.sqrt(dx * dx + dy * dy),
+ overlap = true;
+
+ // penalty for length of leader line
+ if (dist > 0) {
+ ener += dist * w_len;
+ }
+
+ // label orientation bias
+ dx /= dist;
+ dy /= dist;
+ if (dx > 0 && dy > 0) {
+ ener += 0;
+ } else if (dx < 0 && dy > 0) {
+ ener += w_orient;
+ } else if (dx < 0 && dy < 0) {
+ ener += 2 * w_orient;
+ } else {
+ ener += 3 * w_orient;
+ }
+
+ var x21 = lab[index].x,
+ y21 = lab[index].y - lab[index].height + 2.0,
+ x22 = lab[index].x + lab[index].width,
+ y22 = lab[index].y + 2.0;
+ var x11, x12, y11, y12, x_overlap, y_overlap, overlap_area;
+
+ for (var i = 0; i < m; i++) {
+ if (i !== index) {
+
+ // penalty for intersection of leader lines
+ overlap = intersect(anc[index].x, lab[index].x, anc[i].x, lab[i].x,
+ anc[index].y, lab[index].y, anc[i].y, lab[i].y);
+ if (overlap) {
+ ener += w_inter;
+ }
+
+ // penalty for label-label overlap
+ x11 = lab[i].x;
+ y11 = lab[i].y - lab[i].height + 2.0;
+ x12 = lab[i].x + lab[i].width;
+ y12 = lab[i].y + 2.0;
+ x_overlap = Math.max(0, Math.min(x12, x22) - Math.max(x11, x21));
+ y_overlap = Math.max(0, Math.min(y12, y22) - Math.max(y11, y21));
+ overlap_area = x_overlap * y_overlap;
+ ener += (overlap_area * w_lab2);
+ }
+
+ // penalty for label-anchor overlap
+ x11 = anc[i].x - anc[i].r;
+ y11 = anc[i].y - anc[i].r;
+ x12 = anc[i].x + anc[i].r;
+ y12 = anc[i].y + anc[i].r;
+ x_overlap = Math.max(0, Math.min(x12, x22) - Math.max(x11, x21));
+ y_overlap = Math.max(0, Math.min(y12, y22) - Math.max(y11, y21));
+ overlap_area = x_overlap * y_overlap;
+ ener += (overlap_area * w_lab_anc);
+
+ }
+ return ener;
+ };
+
+ var mcmove = function(currT) {
+ // Monte Carlo translation move
+
+ // select a random label
+ var i = Math.floor(Math.random() * lab.length);
+
+ // save old coordinates
+ var x_old = lab[i].x;
+ var y_old = lab[i].y;
+
+ // old energy
+ var old_energy;
+ if (user_energy) {
+ old_energy = user_defined_energy(i, lab, anc);
+ } else {
+ old_energy = energy(i);
+ }
+
+ // random translation
+ lab[i].x += (Math.random() - 0.5) * max_move;
+ lab[i].y += (Math.random() - 0.5) * max_move;
+
+ // hard wall boundaries
+ if (lab[i].x > w) {
+ lab[i].x = x_old;
+ }
+ if (lab[i].x < 0) {
+ lab[i].x = x_old;
+ }
+ if (lab[i].y > h) {
+ lab[i].y = y_old;
+ }
+ if (lab[i].y < 0) {
+ lab[i].y = y_old;
+ }
+
+ // new energy
+ var new_energy;
+ if (user_energy) {
+ new_energy = user_defined_energy(i, lab, anc);
+ } else {
+ new_energy = energy(i);
+ }
+
+ // delta E
+ var delta_energy = new_energy - old_energy;
+
+ if (Math.random() < Math.exp(-delta_energy / currT)) {
+ acc += 1;
+ } else {
+ // move back to old coordinates
+ lab[i].x = x_old;
+ lab[i].y = y_old;
+ rej += 1;
+ }
+
+ };
+
+ var mcrotate = function(currT) {
+ /* jshint -W071 */
+ // Monte Carlo rotation move
+
+ // select a random label
+ var i = Math.floor(Math.random() * lab.length);
+
+ // save old coordinates
+ var x_old = lab[i].x;
+ var y_old = lab[i].y;
+
+ // old energy
+ var old_energy;
+ if (user_energy) {
+ old_energy = user_defined_energy(i, lab, anc);
+ } else {
+ old_energy = energy(i);
+ }
+
+ // random angle
+ var angle = (Math.random() - 0.5) * max_angle;
+
+ var s = Math.sin(angle);
+ var c = Math.cos(angle);
+
+ // translate label (relative to anchor at origin):
+ lab[i].x -= anc[i].x;
+ lab[i].y -= anc[i].y;
+
+ // rotate label
+ var x_new = lab[i].x * c - lab[i].y * s,
+ y_new = lab[i].x * s + lab[i].y * c;
+
+ // translate label back
+ lab[i].x = x_new + anc[i].x;
+ lab[i].y = y_new + anc[i].y;
+
+ // hard wall boundaries
+ if (lab[i].x > w) {
+ lab[i].x = x_old;
+ }
+ if (lab[i].x < 0) {
+ lab[i].x = x_old;
+ }
+ if (lab[i].y > h) {
+ lab[i].y = y_old;
+ }
+ if (lab[i].y < 0) {
+ lab[i].y = y_old;
+ }
+
+ // new energy
+ var new_energy;
+ if (user_energy) {
+ new_energy = user_defined_energy(i, lab, anc);
+ } else {
+ new_energy = energy(i);
+ }
+
+ // delta E
+ var delta_energy = new_energy - old_energy;
+
+ if (Math.random() < Math.exp(-delta_energy / currT)) {
+ acc += 1;
+ } else {
+ // move back to old coordinates
+ lab[i].x = x_old;
+ lab[i].y = y_old;
+ rej += 1;
+ }
+
+ };
+
+ var intersect = function(x1, x2, x3, x4, y1, y2, y3, y4) { // jshint ignore:line
+ // returns true if two lines intersect, else false
+ // from http://paulbourke.net/geometry/lineline2d/
+
+ var mua, mub;
+ var denom, numera, numerb;
+
+ denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
+ numera = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
+ numerb = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3);
+
+ /* Is the intersection along the the segments */
+ mua = numera / denom;
+ mub = numerb / denom;
+ return !(mua < 0 || mua > 1 || mub < 0 || mub > 1);
+
+ };
+
+ var cooling_schedule = function(currT, initialT, nsweeps) {
+ // linear cooling
+ return (currT - (initialT / nsweeps));
+ };
+
+ labeler.start = function(nsweeps) {
+ // main simulated annealing function
+ var m = lab.length,
+ currT = 1.0,
+ initialT = 1.0;
+
+ for (var i = 0; i < nsweeps; i++) {
+ for (var j = 0; j < m; j++) {
+ if (Math.random() < 0.5) {
+ mcmove(currT);
+ } else {
+ mcrotate(currT);
+ }
+ }
+ currT = cooling_schedule(currT, initialT, nsweeps);
+ }
+ };
+
+ labeler.width = function(x) {
+ // users insert graph width
+ if (!arguments.length) {
+ return w;
+ }
+ w = x;
+ return labeler;
+ };
+
+ labeler.height = function(x) {
+ // users insert graph height
+ if (!arguments.length) {
+ return h;
+ }
+ h = x;
+ return labeler;
+ };
+
+ labeler.label = function(x) {
+ // users insert label positions
+ if (!arguments.length) {
+ return lab;
+ }
+ lab = x;
+ return labeler;
+ };
+
+ labeler.anchor = function(x) {
+ // users insert anchor positions
+ if (!arguments.length) {
+ return anc;
+ }
+ anc = x;
+ return labeler;
+ };
+
+ labeler.alt_energy = function(x) {
+ // user defined energy
+ if (!arguments.length) {
+ return energy;
+ }
+ user_defined_energy = x;
+ user_energy = true;
+ return labeler;
+ };
+
+ labeler.alt_schedule = function(x) {
+ // user defined cooling_schedule
+ if (!arguments.length) {
+ return cooling_schedule;
+ }
+ user_defined_schedule = x;
+ user_schedule = true;
+ return labeler;
+ };
+
+ return labeler;
+ };
+
+ // D3 lasso plugin
+ /* Source Code: https://github.com/d3/d3-plugins/blob/master/lasso/lasso.js
+ Copyright (c) 2012-2014, Michael Bostock
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ * The name Michael Bostock may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+ v.lib.lassoPlugin = function() {
+ /* jshint -W040, -W106 */
+ var items = null,
+ closePathDistance = 75,
+ closePathSelect = true,
+ isPathClosed = false,
+ hoverSelect = true,
+ area = null,
+ pathContainer = null,
+ on = {
+ start: function() {},
+ draw: function() {},
+ end: function() {}
+ };
+
+ function lasso() {
+ var _this = d3.select(this[0][0]);
+ /* START MODIFICATION ------------------------------------------------------>
+ * Reuse lasso path group element, if possible. In my D3 force implementation
+ * I provide the possibility to enable or disable the lasso. After enabling
+ * the lasso I get always a new lasso element. I prefer to reuse the existing
+ * one.
+ * */
+ //
+ var g, dyn_path, close_path, complete_path, path, origin, last_known_point, path_length_start, drag;
+ pathContainer = pathContainer || _this; // if not set then defaults to _this
+ if (pathContainer.selectAll("g.lasso").size() === 0) {
+ g = pathContainer.append("g").attr("class", "lasso");
+ dyn_path = g.append("path").attr("class", "drawn");
+ close_path = g.append("path").attr("class", "loop_close");
+ complete_path = g.append("path").attr("class", "complete_path").attr("display", "none");
+ } else {
+ g = pathContainer.select("g.lasso");
+ dyn_path = g.select("path.drawn");
+ close_path = g.select("path.loop_close");
+ complete_path = g.select("path.complete_path");
+ }
+ /* <-------------------------------------------------------- END MODIFICATION */
+
+ function dragstart() {
+ // Reset blank lasso path
+ path = "";
+ dyn_path.attr("d", null);
+ close_path.attr("d", null);
+ // Set path length start
+ path_length_start = 0;
+ // Set every item to have a false selection and reset their center point and counters
+ items[0].forEach(function(d) {
+ d.hoverSelected = false;
+ d.loopSelected = false;
+ var cur_box = d.getBBox();
+ /* START MODIFICATION ------------------------------------------------------>
+ * Implement correct values after zoom and pan based on the following article:
+ * http://stackoverflow.com/questions/18554224/getting-screen-positions-of-d3-nodes-after-transform
+ * */
+ var ctm = d.getCTM();
+ d.lassoPoint = {
+ cx: Math.round((cur_box.x + cur_box.width / 2) * ctm.a + ctm.e),
+ cy: Math.round((cur_box.y + cur_box.height / 2) * ctm.d + ctm.f),
+ /* <-------------------------------------------------------- END MODIFICATION */
+ edges: {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ },
+ close_edges: {
+ left: 0,
+ right: 0
+ }
+ };
+ });
+
+ // if hover is on, add hover function
+ if (hoverSelect === true) {
+ items.on("mouseover.lasso", function() {
+ // if hovered, change lasso selection attribute to true
+ d3.select(this)[0][0].hoverSelected = true;
+ });
+ }
+
+ // Run user defined start function
+ on.start();
+ }
+
+ function dragmove() {
+ /* jshint -W071 */
+ var x = d3.mouse(this)[0],
+ y = d3.mouse(this)[1],
+ distance,
+ close_draw_path,
+ complete_path_d,
+ close_path_node,
+ close_path_length,
+ close_path_edges,
+ path_node,
+ path_length_end,
+ i,
+ last_pos,
+ prior_pos,
+ prior_pos_obj,
+ cur_pos,
+ cur_pos_obj,
+ calcLassoPointEdges = function(d) {
+ if (cur_pos_obj.x > d.lassoPoint.cx) {
+ d.lassoPoint.edges.right = d.lassoPoint.edges.right + 1;
+ }
+ if (cur_pos_obj.x < d.lassoPoint.cx) {
+ d.lassoPoint.edges.left = d.lassoPoint.edges.left + 1;
+ }
+ },
+ calcLassoPointCloseEdges = function(d) {
+ if (Math.round(cur_pos.y) !== Math.round(prior_pos.y) &&
+ Math.round(cur_pos.x) > d.lassoPoint.cx) {
+ d.lassoPoint.close_edges.right = 1;
+ }
+ if (Math.round(cur_pos.y) !== Math.round(prior_pos.y) &&
+ Math.round(cur_pos.x) < d.lassoPoint.cx) {
+ d.lassoPoint.close_edges.left = 1;
+ }
+ },
+ ckeckIfNodeYequalsCurrentPosY = function(d) {
+ return d.lassoPoint.cy === Math.round(cur_pos.y);
+ },
+ ckeckIfNodeYequalsCurrentPriorPosY = function(d) {
+ var a;
+ if (d.lassoPoint.cy === cur_pos_obj.y && d.lassoPoint.cy !== prior_pos_obj.y) {
+ last_known_point = {
+ x: prior_pos_obj.x,
+ y: prior_pos_obj.y
+ };
+ a = false;
+ } else if (d.lassoPoint.cy === cur_pos_obj.y && d.lassoPoint.cy === prior_pos_obj.y) {
+ a = false;
+ } else if (d.lassoPoint.cy === prior_pos_obj.y && d.lassoPoint.cy !== cur_pos_obj.y) {
+ a = sign(d.lassoPoint.cy - cur_pos_obj.y) !== sign(d.lassoPoint.cy - last_known_point.y);
+ } else {
+ last_known_point = {
+ x: prior_pos_obj.x,
+ y: prior_pos_obj.y
+ };
+ a = sign(d.lassoPoint.cy - cur_pos_obj.y) !== sign(d.lassoPoint.cy - prior_pos_obj.y);
+ }
+ return a;
+ };
+
+ // Initialize the path or add the latest point to it
+ if (path === "") {
+ path = path + "M " + x + " " + y;
+ origin = [x, y];
+ } else {
+ path = path + " L " + x + " " + y;
+ }
+
+ // Reset closed edges counter
+ items[0].forEach(function(d) {
+ d.lassoPoint.close_edges = {
+ left: 0,
+ right: 0
+ };
+ });
+
+ // Calculate the current distance from the lasso origin
+ distance = Math.sqrt(Math.pow(x - origin[0], 2) + Math.pow(y - origin[1], 2));
+
+ // Set the closed path line
+ close_draw_path = "M " + x + " " + y + " L " + origin[0] + " " + origin[1];
+
+ // Draw the lines
+ dyn_path.attr("d", path);
+
+ // If within the closed path distance parameter, show the closed path. otherwise, hide it
+ if (distance <= closePathDistance) {
+ close_path.attr("display", null);
+ } else {
+ close_path.attr("display", "none");
+ }
+
+ isPathClosed = distance <= closePathDistance;
+
+ // create complete path
+ complete_path_d = d3.select("path")[0][0].attributes.d.value + "Z";
+ complete_path.attr("d", complete_path_d);
+
+ // get path length
+ path_node = dyn_path.node();
+ path_length_end = path_node.getTotalLength();
+ last_pos = path_node.getPointAtLength(path_length_start - 1);
+
+ for (i = path_length_start; i <= path_length_end; i++) {
+ cur_pos = path_node.getPointAtLength(i);
+ cur_pos_obj = {
+ x: Math.round(cur_pos.x * 100) / 100,
+ y: Math.round(cur_pos.y * 100) / 100
+ };
+ prior_pos = path_node.getPointAtLength(i - 1);
+ prior_pos_obj = {
+ x: Math.round(prior_pos.x * 100) / 100,
+ y: Math.round(prior_pos.y * 100) / 100
+ };
+
+ items[0].filter(ckeckIfNodeYequalsCurrentPriorPosY).forEach(calcLassoPointEdges);
+ }
+
+ if (isPathClosed === true && closePathSelect === true) {
+ close_path.attr("d", close_draw_path);
+ close_path_node = close_path.node();
+ close_path_length = close_path_node.getTotalLength();
+ close_path_edges = {
+ left: 0,
+ right: 0
+ };
+ for (i = 0; i <= close_path_length; i++) {
+ cur_pos = close_path_node.getPointAtLength(i);
+ prior_pos = close_path_node.getPointAtLength(i - 1);
+ items[0].filter(ckeckIfNodeYequalsCurrentPosY).forEach(calcLassoPointCloseEdges);
+ }
+ items[0].forEach(function(a) {
+ if ((a.lassoPoint.edges.left + a.lassoPoint.close_edges.left) > 0 &&
+ (a.lassoPoint.edges.right + a.lassoPoint.close_edges.right) % 2 === 1) {
+ a.loopSelected = true;
+ } else {
+ a.loopSelected = false;
+ }
+ });
+ } else {
+ items[0].forEach(function(d) {
+ d.loopSelected = false;
+ });
+ }
+
+ // Tag possible items
+ d3.selectAll(items[0].filter(function(d) {
+ return (d.loopSelected && isPathClosed) || d.hoverSelected;
+ }))
+ .attr("d", function(d) {
+ d.possible = true;
+ return d.possible;
+ });
+
+ d3.selectAll(items[0].filter(function(d) {
+ return !((d.loopSelected && isPathClosed) || d.hoverSelected);
+ }))
+ .attr("d", function(d) {
+ d.possible = false;
+ return d.possible;
+ });
+
+ on.draw();
+
+ // Continue drawing path from where it left off
+ path_length_start = path_length_end + 1;
+ }
+
+ function dragend() {
+ // Remove mouseover tagging function
+ items.on("mouseover.lasso", null);
+
+ // Tag selected items
+ items.filter(function(d) {
+ return d.possible === true;
+ })
+ .attr("d", function(d) {
+ d.selected = true;
+ return d.selected;
+ });
+
+ items.filter(function(d) {
+ return d.possible === false;
+ })
+ .attr("d", function(d) {
+ d.selected = false;
+ return d.selected;
+ });
+
+ // Reset possible items
+ items.attr("d", function(d) {
+ d.possible = false;
+ return d.possible;
+ });
+
+ // Clear lasso
+ dyn_path.attr("d", null);
+ close_path.attr("d", null);
+
+ // Run user defined end function
+ on.end();
+
+ }
+ drag = d3.behavior.drag()
+ .on("dragstart", dragstart)
+ .on("drag", dragmove)
+ .on("dragend", dragend);
+ area.call(drag);
+ }
+
+ lasso.items = function(_) {
+
+ if (!arguments.length) {
+ return items;
+ }
+ items = _;
+ items[0].forEach(function(d) {
+ var item = d3.select(d);
+ if (typeof item.datum() === "undefined") {
+ item.datum({
+ possible: false,
+ selected: false
+ });
+ } else {
+ item.attr("d", function(e) {
+ e.possible = false;
+ e.selected = false;
+ return e;
+ });
+ }
+ });
+ return lasso;
+ };
+
+ lasso.closePathDistance = function(_) {
+ if (!arguments.length) {
+ return closePathDistance;
+ }
+ closePathDistance = _;
+ return lasso;
+ };
+
+ lasso.closePathSelect = function(_) {
+ if (!arguments.length) {
+ return closePathSelect;
+ }
+ closePathSelect = _ === true;
+ return lasso;
+ };
+
+ lasso.isPathClosed = function(_) {
+ if (!arguments.length) {
+ return isPathClosed;
+ }
+ isPathClosed = _ === true;
+ return lasso;
+ };
+
+ lasso.hoverSelect = function(_) {
+ if (!arguments.length) {
+ return hoverSelect;
+ }
+ hoverSelect = _ === true;
+ return lasso;
+ };
+
+ lasso.on = function(type, _) {
+ if (!arguments.length) {
+ return on;
+ }
+ if (arguments.length === 1) {
+ return on[type];
+ }
+ var types = ["start", "draw", "end"];
+ if (types.indexOf(type) > -1) {
+ on[type] = _;
+ }
+ return lasso;
+ };
+
+ lasso.area = function(_) {
+ if (!arguments.length) {
+ return area;
+ }
+ area = _;
+ return lasso;
+ };
+
+ /* START MODIFICATION ------------------------------------------------------>
+ * Allow different container for lasso path than area, where lasso can be started
+ * */
+ lasso.pathContainer = function(_) {
+ if (!arguments.length) {
+ return pathContainer;
+ }
+ pathContainer = d3.select(_[0][0]);
+ return lasso;
+ };
+ /* <-------------------------------------------------------- END MODIFICATION */
+
+ function sign(x) { // jshint ignore:line
+ return x ? x < 0 ? -1 : 1 : 0;
+ }
+
+ return lasso;
+ };
+
+ /*******************************************************************************************************************
+ * PUBLIC GRAPH FUNCTION AND API METHODS
+ */
+
+ // public start function: get data and start visualization
+ /**
+ * This method starts the graph. You can configure your graph with all the available methods, but without the `start` method your changes will NOT take into effect.
+ *
+ * You can pass new data (see {@tutorial included-sample-data}) to the `start` method. Data can be a XML string, JSON string or JavaScript object (JSON). If you use the APEX plugin, then the `start` method internally does the AJAX call to your Oracle database, but you can prevent this behavior by passing data to this method.
+ *
+ * This also means, that you can use data from a textarea or a report for the APEX plugin, to overwrite the existing data and you do not need to configure any query to run this plugin. If you do so and you do not pass data to the `start` method on the very first call, then the plugin provides sample data - it is the same data with the [APEX online demo](https://apex.oracle.com/pls/apex/f?p=18290:1) of this plugin, there is no query configured and you get therefore the sampledata :-)
+ * @see {@link module:API.render}
+ * @see {@link module:API.resume}
+ * @param {(string|Object)} [data=Sample data EMP table flavoured] - Can be a XML string, JSON string or JavaScript object (JSON)
+ * @returns {Object} The graph object for method chaining.
+ */
+ graph.start = function(data) {
+ var firstChar;
+ // try to use the input data - this means also, we can overwrite the data from APEX with raw data (textarea or
+ // whatever you like...)
+ if (data) {
+ graph.render(data);
+ }
+ // if we have no data, then we try to use the APEX context (if APEX plugin ID is set)
+ else if (v.status.apexPluginId) {
+ if (v.conf.showLoadingIndicatorOnAjaxCall) {
+ graph.showLoadingIndicator(true);
+ }
+ apex.server.plugin(
+ v.status.apexPluginId, {
+ p_debug: $v("pdebug"), //jshint ignore:line
+ pageItems: v.status.apexPageItemsToSubmit
+ }, {
+ success: function(dataString) {
+ // dataString starts NOT with "<" or "{", when there are no queries defined in APEX or
+ // when the queries returns empty data or when a error occurs on the APEX backend side
+ if (v.conf.showLoadingIndicatorOnAjaxCall) {
+ graph.showLoadingIndicator(false);
+ }
+ firstChar = dataString.trim().substr(0, 1);
+ if (firstChar === "<" || firstChar === "{") {
+ graph.render(dataString.trim());
+ } else if (dataString.trim().substr(0, 16) === "no_query_defined") {
+ // this will keep the old data or using the sample data, if no old data existing
+ v.tools.logError("No query defined.");
+ graph.render();
+ } else if (dataString.trim().substr(0, 22) === "query_returned_no_data") {
+ v.tools.logError("Query returned no data.");
+ graph.render({
+ "data": {
+ "nodes": [{
+ "ID": "1",
+ "LABEL": "ERROR: No data.",
+ "COLORVALUE": "1",
+ "SIZEVALUE": "1"
+ }],
+ "links": []
+ }
+ });
+ } else {
+ v.tools.logError(dataString);
+ graph.render({
+ "data": {
+ "nodes": [{
+ "ID": "1",
+ "LABEL": "ERROR: " + dataString + ".",
+ "COLORVALUE": "1",
+ "SIZEVALUE": "1"
+ }],
+ "links": []
+ }
+ });
+ }
+ },
+ error: function(xhr, status, errorThrown) {
+ v.tools.logError("AJAX call terminated with errors: " + errorThrown + ".");
+ graph.render({
+ "data": {
+ "nodes": [{
+ "ID": "1",
+ "LABEL": "AJAX call terminated with errors.",
+ "COLORVALUE": "1",
+ "SIZEVALUE": "1"
+ }],
+ "links": []
+ }
+ });
+ },
+ dataType: "text"
+ }
+ );
+ }
+ // if we have no raw data and no APEX context, then we start to render without data (the render function will
+ // then provide sample data)
+ else {
+ graph.render();
+ }
+ return graph;
+ };
+ /**
+ * The `render` method does the same as the `start` method - the only difference is, that the `render` method does not try to load data, if you use the APEX plugin. You can use this method after changing options which need a `render` cycle to take the changes into effect:
+ *
+ * example.minNodeRadius(4).maxNodeRadius(20).render();
+ * @see {@link module:API.start}
+ * @see {@link module:API.resume}
+ * @param {(string|Object)} [data=Sample data EMP table flavoured] - Can be a XML string, JSON string or JavaScript object (JSON)
+ * @returns {Object} The graph object for method chaining.
+ */
+ graph.render = function(data) {
+ /* jshint -W074, -W071 */
+ var message;
+ v.status.graphStarted = true;
+ v.status.graphRendering = true;
+
+ v.tools.triggerApexEvent(document.querySelector("#" + v.dom.containerId), "apexbeforerefresh");
+
+ // if we start the rendering the first time and there is no input data, then provide sample data
+ if (data) {
+ v.status.sampleData = false;
+ } else if (!data && !v.status.graphReady) {
+ v.tools.logError("Houston, we have a problem - we have to provide sample data.");
+ v.status.sampleData = true;
+ data = v.data.sampleData;
+ }
+
+ // if we have incoming data, than we do our transformations here, otherwise we use the existing data
+ if (data) {
+
+ if (v.status.graphReady) {
+ v.status.graphOldPositions = graph.positions();
+ }
+
+ // data is an object
+ if (data.constructor === Object) {
+ v.data.dataConverted = data;
+ if (v.conf.debug) {
+ v.tools.log("Data object:");
+ v.tools.log(v.data.dataConverted, true);
+ }
+ }
+ // data is a string
+ else if (data.constructor === String) {
+ // convert incoming data depending on type
+ if (data.trim().substr(0, 1) === "<") {
+ try {
+ v.data.dataConverted = v.tools.xmlToJson(v.tools.parseXml(data));
+ if (v.data.dataConverted === null) {
+ message = "Unable to convert XML string.";
+ v.tools.logError(message);
+ v.data.dataConverted = v.tools.getGraphDataWithMessage(message);
+ }
+ } catch (e) {
+ message = "Unable to convert XML string: " + e.message + ".";
+ v.tools.logError(message);
+ v.data.dataConverted = v.tools.getGraphDataWithMessage(message);
+ }
+ } else if (data.trim().substr(0, 1) === "{") {
+ try {
+ v.data.dataConverted = JSON.parse(data);
+ } catch (e) {
+ message = "Unable to parse JSON string: " + e.message + ".";
+ v.tools.logError(message);
+ v.data.dataConverted = v.tools.getGraphDataWithMessage(message);
+ }
+ } else {
+ message = "Your data string is not starting with \"<\" or \"{\" - parsing not possible.";
+ v.tools.logError(message);
+ v.data.dataConverted = v.tools.getGraphDataWithMessage(message);
+ }
+ if (v.conf.debug) {
+ v.tools.log("Data string:");
+ v.tools.log(data, true);
+ v.tools.log("Converted data object:");
+ v.tools.log(v.data.dataConverted, true);
+ }
+ }
+ // data has unknown format
+ else {
+ message = "Unable to parse your data - input data can be a XML string, " +
+ "JSON string or JavaScript object.";
+ v.tools.logError(message);
+ v.data.dataConverted = v.tools.getGraphDataWithMessage(message);
+ }
+
+ // create references to our new data
+ if (v.data.dataConverted !== null) {
+ if (v.data.dataConverted.hasOwnProperty("data") && v.data.dataConverted.data !== null) {
+ if (v.data.dataConverted.data.hasOwnProperty("nodes") && v.data.dataConverted.data.nodes !== null) {
+ v.data.nodes = v.data.dataConverted.data.nodes;
+ if (v.data.nodes.length === 0) {
+ message = "Your data contains an empty nodes array.";
+ v.tools.logError(message);
+ v.data.nodes = v.tools.getNodesDataWithMessage(message);
+ }
+ } else {
+ message = "Your data contains no nodes.";
+ v.tools.logError(message);
+ v.data.nodes = v.tools.getNodesDataWithMessage(message);
+ }
+ if (v.data.dataConverted.data.hasOwnProperty("links") && v.data.dataConverted.data.links !== null) {
+ v.data.links = v.data.dataConverted.data.links;
+ } else {
+ v.data.links = [];
+ }
+ } else {
+ message = "Missing root element named data.";
+ v.tools.logError(message);
+ v.data = v.tools.getGraphDataWithMessage(message);
+ }
+ } else {
+ message = "Unable to parse your data - please consult the API reference for possible data formats.";
+ v.tools.logError(message);
+ v.data = v.tools.getGraphDataWithMessage(message);
+ }
+
+ // switch links to point to node objects instead of id's (needed for force layout) and calculate attributes
+ v.data.idLookup = []; // helper array to lookup node objects by id's
+ v.data.nodes.forEach(function(n) {
+ n.SIZEVALUE = parseFloat(n.SIZEVALUE); // convert size to float value
+ n.LABELCIRCULAR = v.tools.parseBool(n.LABELCIRCULAR); // convert labelCircular to boolean
+ if (n.fixed) {
+ n.fixed = v.tools.parseBool(n.fixed);
+ } // convert fixed to boolean
+ if (n.x) {
+ n.x = parseFloat(n.x);
+ } // convert X position to float value
+ if (n.y) {
+ n.y = parseFloat(n.y);
+ } // convert Y position to float value
+ v.data.idLookup[n.ID] = n; // add object reference to lookup array
+ });
+ v.data.links.forEach(function(l) {
+ l.source = v.data.idLookup[l.FROMID]; // add attribute source as a node reference to the link
+ l.target = v.data.idLookup[l.TOID]; // add attribute target as a node reference to the link
+ });
+
+ // sort out links with invalid node references
+ v.data.links = v.data.links.filter(function(l) {
+ return typeof l.source !== "undefined" && typeof l.target !== "undefined";
+ });
+
+ // create helper array to lookup if nodes are neighbors
+ v.data.neighbors = v.data.links.map(function(l) {
+ return l.FROMID + ":" + l.TOID;
+ });
+
+ // calculate distinct node colors for the legend
+ v.data.distinctNodeColorValues = v.data.nodes
+ .map(function(n) {
+ return (n.COLORLABEL ? n.COLORLABEL : "") + ";" + n.COLORVALUE;
+ })
+ // http://stackoverflow.com/questions/1960473/unique-values-in-an-array
+ .filter(function(value, index, self) {
+ return self.indexOf(value) === index;
+ })
+ .sort(function(a, b) { // http://www.sitepoint.com/sophisticated-sorting-in-javascript/
+ var x = a.toLowerCase(),
+ y = b.toLowerCase();
+ return x < y ? 1 : x > y ? -1 : 0;
+ });
+
+ // calculate distinct link colors for the markers
+ v.data.distinctLinkColorValues = v.data.links
+ .map(function(l) {
+ return l.COLOR;
+ })
+ // http://stackoverflow.com/questions/28607451/removing-undefined-values-from-array
+ // http://stackoverflow.com/questions/1960473/unique-values-in-an-array
+ .filter(Boolean)
+ .filter(function(value, index, self) {
+ return self.indexOf(value) === index;
+ })
+ .sort(function(a, b) { // http://www.sitepoint.com/sophisticated-sorting-in-javascript/
+ var x = a.toLowerCase(),
+ y = b.toLowerCase();
+ return x < y ? 1 : x > y ? -1 : 0;
+ });
+
+ // apply user provided positions once (new data has priority)
+ if (v.conf.positions) {
+ if (v.conf.positions.constructor === Array) {
+ v.conf.positions.forEach(function(n) {
+ if (v.data.idLookup[n.ID] !== undefined) {
+ if (!v.data.idLookup[n.ID].fixed) {
+ v.data.idLookup[n.ID].fixed = n.fixed;
+ }
+ if (!v.data.idLookup[n.ID].x) {
+ v.data.idLookup[n.ID].x = v.data.idLookup[n.ID].px = n.x;
+ }
+ if (!v.data.idLookup[n.ID].y) {
+ v.data.idLookup[n.ID].y = v.data.idLookup[n.ID].py = n.y;
+ }
+ }
+ });
+ } else {
+ v.tools.logError("Unable to set node positions: positions method parameter must be an array of " +
+ "node positions");
+ }
+ }
+ // apply old positions (new data has priority - if graph was ready, than user provided positions are
+ // already present in old positions) - see also graph.positions method
+ else if (v.status.graphOldPositions) {
+ v.status.graphOldPositions.forEach(function(n) {
+ if (v.data.idLookup[n.ID] !== undefined) {
+ if (!v.data.idLookup[n.ID].fixed) {
+ v.data.idLookup[n.ID].fixed = n.fixed;
+ }
+ if (!v.data.idLookup[n.ID].x) {
+ v.data.idLookup[n.ID].x = v.data.idLookup[n.ID].px = n.x;
+ }
+ if (!v.data.idLookup[n.ID].y) {
+ v.data.idLookup[n.ID].y = v.data.idLookup[n.ID].py = n.y;
+ }
+ }
+ });
+ }
+ // clear positions
+ v.conf.positions = null;
+ v.status.graphOldPositions = null;
+
+ } //END: if (data)
+
+ // set color and radius function and calculate nodes radius
+ v.tools.setColorFunction();
+ v.tools.setRadiusFunction();
+ v.data.nodes.forEach(function(n) {
+ n.radius = v.tools.radius(n.SIZEVALUE);
+ });
+
+ // MARKERS
+ v.main.markers = v.dom.defs.selectAll("marker.custom")
+ .data(v.data.distinctLinkColorValues,
+ function(m) {
+ return m;
+ }); // distinctLinkColorValues is a simple array, we return the "whole" color value string
+ v.main.markers.enter().append("svg:marker")
+ .attr("id", function(m) {
+ return v.dom.containerId + "_" + m;
+ })
+ .attr("class", "custom")
+ .attr("stroke", "none")
+ .attr("fill", function(m) {
+ return m;
+ })
+ .attr("viewBox", "0 0 10 10")
+ .attr("refX", 10)
+ .attr("refY", 5)
+ .attr("markerWidth", 5)
+ .attr("markerHeight", 5)
+ .attr("orient", "auto")
+ .attr("markerUnits", "strokeWidth")
+ .append("svg:path")
+ .attr("d", "M0,0 L10,5 L0,10");
+ v.main.markers.exit().remove();
+
+ // LINKS
+ v.main.links = v.dom.graph.selectAll("line.link")
+ .data(v.data.links.filter(function(l) {
+ return l.FROMID !== l.TOID;
+ }),
+ function(l) {
+ return v.tools.getLinkId(l);
+ });
+ v.main.links.enter().append("svg:line")
+ .attr("class", "link")
+ .on("mouseenter", v.tools.onLinkMouseenter)
+ .on("mouseleave", v.tools.onLinkMouseleave)
+ .on("click", v.tools.onLinkClick);
+ v.main.links.exit().remove();
+ // update all
+ v.main.links
+ .style("marker-end", v.tools.getMarkerUrl)
+ .classed("dotted", function(l) {
+ return (l.STYLE === "dotted");
+ })
+ .classed("dashed", function(l) {
+ return (l.STYLE === "dashed");
+ })
+ .style("stroke", function(l) {
+ return (l.COLOR ? l.COLOR : null);
+ });
+
+ // SELFLINKS
+ v.main.selfLinks = v.dom.graph.selectAll("path.link")
+ .data(v.data.links.filter(function(l) {
+ return l.FROMID === l.TOID && v.conf.showSelfLinks;
+ }),
+ function(l) {
+ return v.tools.getLinkId(l);
+ });
+ v.main.selfLinks.enter().append("svg:path")
+ .attr("id", function(l) {
+ return v.tools.getPathId(l);
+ })
+ .attr("class", "link")
+ .on("mouseenter", v.tools.onLinkMouseenter)
+ .on("mouseleave", v.tools.onLinkMouseleave)
+ .on("click", v.tools.onLinkClick);
+ v.main.selfLinks.exit().remove();
+ // update all
+ v.main.selfLinks
+ .attr("d", function(l) {
+ return v.tools.getSelfLinkPath(l);
+ })
+ .style("marker-end", v.tools.getMarkerUrl)
+ .classed("dotted", function(l) {
+ return (l.STYLE === "dotted");
+ })
+ .classed("dashed", function(l) {
+ return (l.STYLE === "dashed");
+ })
+ .style("stroke", function(l) {
+ return (l.COLOR ? l.COLOR : null);
+ });
+
+ // PATTERN for nodes with image attribute set
+ v.main.patterns = v.dom.defs.selectAll("pattern")
+ .data(v.data.nodes.filter(function(n) {
+ return (n.IMAGE ? true : false);
+ }),
+ function(n) {
+ return n.ID;
+ });
+ var patterns_enter = v.main.patterns.enter().append("svg:pattern")
+ .attr("id", function(n) {
+ return v.tools.getPatternId(n);
+ });
+ patterns_enter.append("svg:rect");
+ patterns_enter.append("svg:image");
+ patterns_enter = "";
+ v.main.patterns.exit().remove();
+ // update all
+ v.main.patterns.each(function() {
+ d3.select(this) //pattern itself
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("height", function(n) {
+ return n.radius * 2;
+ })
+ .attr("width", function(n) {
+ return n.radius * 2;
+ });
+ d3.select(this.firstChild) //rect with background color (fill)
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("height", function(n) {
+ return n.radius * 2;
+ })
+ .attr("width", function(n) {
+ return n.radius * 2;
+ })
+ .attr("fill", function(n) {
+ return v.tools.color(n.COLORVALUE);
+ });
+ d3.select(this.lastChild) //image or SVG?
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("height", function(n) {
+ return n.radius * 2;
+ })
+ .attr("width", function(n) {
+ return n.radius * 2;
+ })
+ .attr("xlink:href", function(n) {
+ return n.IMAGE;
+ });
+ });
+
+ // NODES
+ v.main.nodes = v.dom.graph.selectAll("circle.node")
+ .data(v.data.nodes,
+ function(n) {
+ return n.ID;
+ });
+ v.main.nodes.enter().append("svg:circle")
+ .attr("class", "node")
+ .attr("cx", function(n) {
+ if (!n.fixed && !n.x) {
+ n.x = Math.floor((Math.random() * v.tools.getGraphWidth()) + 1);
+ return n.x;
+ }
+ })
+ .attr("cy", function(n) {
+ if (!n.fixed && !n.y) {
+ n.y = Math.floor((Math.random() * v.conf.height) + 1);
+ return n.y;
+ }
+ })
+ .on("mouseenter", v.tools.onNodeMouseenter)
+ .on("mouseleave", v.tools.onNodeMouseleave)
+ .on("click", v.tools.onNodeClick)
+ .on("dblclick", v.tools.onNodeDblclick)
+ .on("contextmenu", v.tools.onNodeContextmenu);
+ v.main.nodes.exit().remove();
+ // update all
+ v.main.nodes
+ .attr("r", function(n) {
+ return n.radius;
+ })
+ .attr("fill", function(n) {
+ return (n.IMAGE ? "url(#" + v.tools.getPatternId(n) + ")" : v.tools.color(n.COLORVALUE));
+ });
+
+
+ // LABELS
+
+ if (v.conf.showLabels) {
+
+ // paths for normal link labels (no self links)
+ v.main.linkLabelPaths = v.dom.defs.selectAll("path.linkLabel")
+ .data(v.data.links.filter(function(l) {
+ return l.LABEL && l.FROMID !== l.TOID;
+ }),
+ function(l) {
+ return v.tools.getLinkId(l);
+ });
+ v.main.linkLabelPaths.enter().append("svg:path")
+ .attr("id", function(l) {
+ return v.tools.getPathId(l);
+ })
+ .attr("class", "linkLabel");
+ v.main.linkLabelPaths.exit().remove();
+ // update all
+ v.main.linkLabelPaths.attr("d", function(l) {
+ return 'M ' + l.source.x + ' ' + l.source.y + ' L ' + l.target.x + ' ' + l.target.y;
+ });
+
+ // link labels
+ v.main.linkLabels = v.dom.graph.selectAll("text.linkLabel")
+ .data(v.data.links.filter(function(l) {
+ return l.LABEL;
+ }),
+ function(l) {
+ return v.tools.getLinkId(l);
+ });
+ v.main.linkLabels.enter().append("svg:text")
+ .attr("class", "linkLabel")
+ .attr("dx", function(l) {
+ if (l.FROMID !== l.TOID) {
+ return v.conf.linkDistance / 2;
+ }
+ else {
+ return v.conf.selfLinkDistance + l.source.radius;
+ }
+ })
+ .attr("dy","-1")
+ .on("mouseenter", v.tools.onLinkMouseenter)
+ .on("mouseleave", v.tools.onLinkMouseleave)
+ .on("click", v.tools.onLinkClick)
+ .append("svg:textPath")
+ .attr("xlink:href", function(l) {
+ return "#" + v.tools.getPathId(l);
+ });
+ v.main.linkLabels.exit().remove();
+ // update all
+ v.main.linkLabels.each(function(l) {
+ d3.select(this.firstChild)
+ .text(l.LABEL);
+ });
+
+ // normal node labels
+ v.main.labels = v.dom.graph.selectAll("text.label")
+ .data(v.data.nodes.filter(function(n) {
+ return !n.LABELCIRCULAR && !v.conf.labelsCircular;
+ }),
+ function(n) {
+ return n.ID;
+ });
+ v.main.labels.enter().append("svg:text")
+ .attr("class", "label");
+ v.main.labels.exit().remove();
+ // update all
+ v.main.labels.text(function(n) {
+ return n.LABEL;
+ });
+
+ // paths for circular node labels
+ v.main.labelPaths = v.dom.defs.selectAll("path.label")
+ .data(v.data.nodes.filter(function(n) {
+ return n.LABELCIRCULAR || v.conf.labelsCircular;
+ }),
+ function(n) {
+ return n.ID;
+ });
+ v.main.labelPaths.enter().append("svg:path")
+ .attr("id", function(n) {
+ return v.dom.containerId + "_textPath_" + n.ID;
+ })
+ .attr("class", "label");
+ v.main.labelPaths.exit().remove();
+ // update all
+ v.main.labelPaths.attr("d", function(n) {
+ return v.tools.getLabelPath(n);
+ });
+
+ // circular node labels
+ v.main.labelsCircular = v.dom.graph.selectAll("text.labelCircular")
+ .data(v.data.nodes.filter(function(n) {
+ return n.LABELCIRCULAR || v.conf.labelsCircular;
+ }),
+ function(n) {
+ return n.ID;
+ });
+ v.main.labelsCircular.enter().append("svg:text")
+ .attr("class", "labelCircular")
+ .append("svg:textPath")
+ .attr("xlink:href", function(n) {
+ return "#" + v.dom.containerId + "_textPath_" + n.ID;
+ });
+ v.main.labelsCircular.exit().remove();
+ // update all
+ v.main.labelsCircular.each(function(n) {
+ d3.select(this.firstChild).text(n.LABEL);
+ });
+
+
+ } else {
+ v.dom.defs.selectAll("path.label,path.linkLabel").remove();
+ v.dom.graph.selectAll("text.label,text.labelCircular,text.linkLabel").remove();
+ }
+
+ // calculate initial aspect ratio
+ if (!v.status.aspectRatio) {
+ v.status.aspectRatio = v.conf.width / v.conf.height;
+ }
+
+ // recreate the legend
+ v.tools.removeLegend();
+ if (v.conf.showLegend) {
+ v.tools.createLegend();
+ }
+ // set inital size values
+ v.tools.executeResize();
+
+ // initialize the graph (some options implicit initializes v.main.force, e.g. linkDistance, charge, ...)
+ graph
+ .debug(v.conf.debug)
+ .showBorder(v.conf.showBorder)
+ .setDomParentPaddingToZero(v.conf.setDomParentPaddingToZero)
+ .useDomParentWidth(v.conf.useDomParentWidth)
+ .alignFixedNodesToGrid(v.conf.alignFixedNodesToGrid)
+ .wrapLabels(v.conf.wrapLabels)
+ .dragMode(v.conf.dragMode)
+ .pinMode(v.conf.pinMode)
+ .lassoMode(v.conf.lassoMode)
+ .zoomMode(v.conf.zoomMode)
+ .transform(v.conf.transform)
+ .autoRefresh(v.conf.autoRefresh)
+ .linkDistance(v.conf.linkDistance)
+ .charge(v.conf.charge)
+ .chargeDistance(v.conf.chargeDistance)
+ .gravity(v.conf.gravity)
+ .linkStrength(v.conf.linkStrength)
+ .friction(v.conf.friction)
+ .theta(v.conf.theta);
+
+
+ // start visualization
+ v.main.force
+ .nodes(v.data.nodes)
+ .links(v.data.links)
+ .size([v.tools.getGraphWidth(), v.tools.getGraphHeight()])
+ .start();
+
+ v.status.graphReady = true;
+ v.status.graphRendering = false;
+
+ if (v.status.customize) {
+ v.tools.createCustomizeWizard();
+ } else {
+ v.tools.createCustomizeLink();
+ }
+
+ // trigger render end event
+ v.tools.log("Event renderend triggered.");
+ v.tools.triggerApexEvent(document.querySelector("#" + v.dom.containerId),
+ "net_gobrechts_d3_force_renderend"
+ );
+ if (typeof(v.conf.onRenderEndFunction) === "function") {
+ v.conf.onRenderEndFunction.call(v.dom.svg);
+ }
+
+ v.tools.triggerApexEvent(document.querySelector("#" + v.dom.containerId), "apexafterrefresh");
+
+ return graph;
+ };
+
+ /**
+ * The `resume` method restarts only the force on your graph without a `render` cycle. This saves CPU time and can be useful if you change only things in your graph which do not need rendering to taking into effect:
+ *
+ * example.releaseFixedNodes().resume();
+ * @see {@link module:API.start}
+ * @see {@link module:API.render}
+ * @returns {Object} The graph object for method chaining.
+ */
+ graph.resume = function() {
+ if (v.status.graphReady) {
+ v.main.force.resume();
+ }
+ v.tools.createCustomizeWizardIfNotRendering();
+ return graph;
+ };
+
+ /**
+ * If true, a class named border is added to the SVG element, if false the class will be removed. The border itself is defined in the delivered CSS - you can overwrite it if the current style does not match your needs. No `render` or `resume` call needed to take into effect:
+ *
+ * example.showBorder(false);
+ * @param {boolean} [value=true] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.showBorder = function(value) {
+ if (!arguments.length) {
+ return v.conf.showBorder;
+ }
+ v.conf.showBorder = value;
+ if (v.status.graphStarted) {
+ v.dom.svg.classed("border", v.conf.showBorder);
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, a legend for all COLORVALUEs in the node data is rendered in the bottom left corner of the graph. No `render` or `resume` call needed to take into effect:
+ *
+ * example.showLegend(false);
+ * @param {boolean} [value=true] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.showLegend = function(value) {
+ if (!arguments.length) {
+ return v.conf.showLegend;
+ }
+ v.conf.showLegend = value;
+ if (v.status.graphStarted) {
+ if (v.conf.showLegend) {
+ v.tools.removeLegend();
+ v.tools.createLegend();
+ } else {
+ v.tools.removeLegend();
+ }
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, then links with the same source and target are rendered along a path around the node bottom. Needs a `render` call to take into effect:
+ *
+ * example.showSelfLinks(false).render();
+ * @param {boolean} [value=true] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.showSelfLinks = function(value) {
+ if (!arguments.length) {
+ return v.conf.showSelfLinks;
+ }
+ v.conf.showSelfLinks = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, you get an marker at the end of a link. Needs a `render` call to take into effect:
+ *
+ * example.showLinkDirection(false).render();
+ * @param {boolean} [value=true] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.showLinkDirection = function(value) {
+ if (!arguments.length) {
+ return v.conf.showLinkDirection;
+ }
+ v.conf.showLinkDirection = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true and you provided in your source data an attribute INFOSTRING, then a tooltip is shown by hovering a node. No `render` or `resume` call needed to take into effect:
+ *
+ * example.showTooltips(false);
+ * @param {boolean} [value=true] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.showTooltips = function(value) {
+ if (!arguments.length) {
+ return v.conf.showTooltips;
+ }
+ v.conf.showTooltips = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The position where tooltips are shown in the graph - can be `node`, `svgTopLeft` or `svgTopRight`. No `render` or `resume` call needed to take into effect:
+ *
+ * example.tooltipPosition('node');
+ * @param {string} [value=svgTopRight] - - The new config value.
+ * @returns {(string|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.tooltipPosition = function(value) {
+ if (!arguments.length) {
+ return v.conf.tooltipPosition;
+ }
+ v.conf.tooltipPosition = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Color scheme can be `color20`, `color20b`, `color20c`, `color10` or `direct`. The first four use the color functions provided by D3, which return up to 20 colors for the given keywords for your data attribute COLORVALUE - this can be a text like a department name or a postal zip code. With the last option you can provide direct css color values in your data like blue or #123456. No `render` or `resume` call needed to take into effect:
+ *
+ * example.colorScheme('color10');
+ * @param {string} [value=color20] - The new config value.
+ * @returns {(string|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.colorScheme = function(value) {
+ if (!arguments.length) {
+ return v.conf.colorScheme;
+ }
+ v.conf.colorScheme = value;
+ v.tools.setColorFunction();
+ if (v.status.graphStarted) {
+ v.main.nodes
+ .attr("fill", function(n) {
+ return (n.IMAGE ? "url(#" + v.tools.getPatternId(n) + ")" :
+ v.tools.color(n.COLORVALUE));
+ });
+ if (v.conf.showLegend) {
+ v.tools.removeLegend();
+ v.tools.createLegend();
+ }
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true and you provided in your node data an attribute LABEL, then a label is rendered on top of the node. Needs a `render` call to take into effect:
+ *
+ * example.showLabels(false).render();
+ * @see {@link module:API.wrapLabels}
+ * @see {@link module:API.wrappedLabelWidth}
+ * @see {@link module:API.wrappedLabelLineHeight}
+ * @param {boolean} [value=true] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.showLabels = function(value) {
+ if (!arguments.length) {
+ return v.conf.showLabels;
+ }
+ v.conf.showLabels = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true long labels are wrapped. Needs a `render` call to take into effect:
+ *
+ * example.wrapLabels(true).render();
+ * @see {@link module:API.showLabels}
+ * @see {@link module:API.labelSplitCharacter}
+ * @see {@link module:API.wrappedLabelWidth}
+ * @see {@link module:API.wrappedLabelLineHeight}
+ * @see {@link module:API.labelsCircular}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.wrapLabels = function(value) {
+ if (!arguments.length) {
+ return v.conf.wrapLabels;
+ }
+ v.conf.wrapLabels = value;
+ if (v.conf.wrapLabels) {
+ v.status.wrapLabelsOnNextTick = true;
+ }
+ if (v.status.graphStarted) {
+ v.main.labels.attr("lines", null);
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If set to a value other then `none` labels are splitted on this character. Needs `wrapLabels` to be true and a `render` call to take into effect. If both options `labelSplitCharacter` and `wrappedLabelWidth` are set, then `wrappedLabelWidth` is ignored.
+ *
+ * example.wrapLabels(true).labelSplitCharacter("^").render();
+ * @see {@link module:API.showLabels}
+ * @see {@link module:API.wrappedLabelWidth}
+ * @see {@link module:API.wrappedLabelLineHeight}
+ * @see {@link module:API.labelsCircular}
+ * @param {string} [value="none"] - The new config value.
+ * @returns {(string|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.labelSplitCharacter = function(value) {
+ if (!arguments.length) {
+ return v.conf.labelSplitCharacter;
+ }
+ v.conf.labelSplitCharacter = value;
+ if (v.conf.wrapLabels) {
+ v.status.wrapLabelsOnNextTick = true;
+ }
+ if (v.status.graphStarted) {
+ v.main.labels.attr("lines", null);
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The width of the labels, if option `wrapLabels` is set to true. Needs a `render` call to take into effect. This option is ignored when `labelSplitCharacter` is set to a value other then `none`.
+ *
+ * example.wrappedLabelWidth(40).render();
+ * @see {@link module:API.showLabels}
+ * @see {@link module:API.wrapLabels}
+ * @see {@link module:API.labelSplitCharacter}
+ * @see {@link module:API.wrappedLabelLineHeight}
+ * @see {@link module:API.labelsCircular}
+ * @param {number} [value=80] - The new config value.
+ * @returns {(number|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.wrappedLabelWidth = function(value) {
+ if (!arguments.length) {
+ return v.conf.wrappedLabelWidth;
+ }
+ v.conf.wrappedLabelWidth = value;
+ if (v.conf.wrapLabels) {
+ v.main.labels.attr("lines", null);
+ v.status.wrapLabelsOnNextTick = true;
+ }
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The line height of labels in `em`, if option `wrapLabels` is set to true. Needs a `render` call to take into effect:
+ *
+ * example.wrappedLabelLineHeight(1.5).render();
+ * @see {@link module:API.showLabels}
+ * @see {@link module:API.wrapLabels}
+ * @see {@link module:API.labelSplitCharacter}
+ * @see {@link module:API.wrappedLabelWidth}
+ * @see {@link module:API.labelsCircular}
+ * @param {number} [value=1.2] - The new config value.
+ * @returns {(number|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.wrappedLabelLineHeight = function(value) {
+ if (!arguments.length) {
+ return v.conf.wrappedLabelLineHeight;
+ }
+ v.conf.wrappedLabelLineHeight = value;
+ if (v.conf.wrapLabels) {
+ v.status.wrapLabelsOnNextTick = true;
+ }
+ if (v.status.graphStarted) {
+ v.main.labels.attr("lines", null);
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, then the labels are rendered along a path around the nodes.
+ *
+ * You can overwrite this attribute on node level by setting a attribute called LABELCIRCULAR on the node to true or false. As an example you can see this in the online demo on the node named KING.
+ *
+ * ATTENTION: If you set the LABELCIRCULAR attribute on a specific or all nodes, then the global configuration parameter labelsCircular has no effect on these nodes.
+ *
+ * Needs a `render` call to take into effect:
+ *
+ * example.labelsCircular(true).render();
+ * @see {@link module:API.showLabels}
+ * @see {@link module:API.labelDistance}
+ * @see {@link module:API.wrapLabels}
+ * @see {@link module:API.labelSplitCharacter}
+ * @see {@link module:API.wrappedLabelWidth}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.labelsCircular = function(value) {
+ if (!arguments.length) {
+ return v.conf.labelsCircular;
+ }
+ v.conf.labelsCircular = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The distance of a label from the nodes outer border. Needs a `render` call to take into effect:
+ *
+ * example.labelDistance(18).render();
+ * @see {@link module:API.labelsCircular}
+ * @see {@link module:API.wrapLabels}
+ * @param {number} [value=12] - The new config value.
+ * @returns {(number|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.labelDistance = function(value) {
+ if (!arguments.length) {
+ return v.conf.labelDistance;
+ }
+ v.conf.labelDistance = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If set to true the labels are aligned with a simulated annealing function to prevent overlapping when the graph is cooled down (correctly on the force end event and only on labels, who are not circular). Needs a `resume` call to take into effect:
+ *
+ * example.preventLabelOverlappingOnForceEnd(true).render();
+ * @see {@link module:API.labelPlacementIterations}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.preventLabelOverlappingOnForceEnd = function(value) {
+ if (!arguments.length) {
+ return v.conf.preventLabelOverlappingOnForceEnd;
+ }
+ v.conf.preventLabelOverlappingOnForceEnd = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The number of iterations for the preventLabelOverlappingOnForceEnd option - default is 250 - as higher the number, as higher the quality of the result. For details refer to the [description of the simulated annealing function of the author Evan Wang](https://github.com/tinker10/D3-Labeler). Needs a `resume` call to take into effect:
+ *
+ * example.preventLabelOverlappingOnForceEnd(true).resume();
+ * @see {@link module:API.labelPlacementIterations}
+ * @param {number} [value=250] - The new config value.
+ * @returns {(number|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.labelPlacementIterations = function(value) {
+ if (!arguments.length) {
+ return v.conf.labelPlacementIterations;
+ }
+ v.conf.labelPlacementIterations = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, the nodes are draggable. No `render` or `resume` call needed to take into effect:
+ *
+ * example.dragMode(false);
+ * @see {@link module:API.pinMode}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.dragMode = function(value) {
+ if (!arguments.length) {
+ return v.conf.dragMode;
+ }
+ v.conf.dragMode = value;
+ if (v.status.graphStarted) {
+ if (v.conf.dragMode) {
+ v.main.nodes.call(v.main.drag);
+ } else {
+ // http://stackoverflow.com/questions/13136355/d3-js-remove-force-drag-from-a-selection
+ v.main.nodes.on("mousedown.drag", null);
+ v.main.nodes.on("touchstart.drag", null);
+ }
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, the nodes are fixed (pinned) at the end of a drag event. No `render` or `resume` call needed to take into effect:
+ *
+ * example.pinMode(true);
+ * @see {@link module:API.releaseFixedNodes}
+ * @see {@link module:API.dragMode}
+ * @param {boolean} [value=true] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.pinMode = function(value) {
+ if (!arguments.length) {
+ return v.conf.pinMode;
+ }
+ v.conf.pinMode = value;
+ if (v.status.graphStarted) {
+ if (v.conf.pinMode) {
+ v.main.drag.on("dragstart", function(n) {
+ d3.select(this).classed("fixed", n.fixed = 1);
+ });
+ } else {
+ v.main.drag.on("dragstart", null);
+ }
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, you can select miltiple nodes with a lasso - think of a graphical multiselect :-). No `render` or `resume` call needed to take into effect:
+ *
+ * example.lassoMode(true);
+ * @see {@link module:API.zoomMode}
+ * @param {boolean} [value=true] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.lassoMode = function(value) {
+ if (!arguments.length) {
+ return v.conf.lassoMode;
+ }
+ v.conf.lassoMode = value;
+ if (v.status.graphStarted) {
+ if (v.conf.lassoMode) {
+ v.dom.graphOverlay.call(v.main.lasso);
+ v.main.lasso.items(v.main.nodes);
+ v.main.lasso.on("start", function() {
+ v.main.lasso.items().classed("selected", false);
+ v.tools.onLassoStart(v.main.lasso.items());
+ });
+ v.main.lasso.on("draw", function() {
+ v.main.lasso.items().filter(function(d) {
+ return d.possible === true;
+ })
+ .classed("selected", true);
+ v.main.lasso.items().filter(function(d) {
+ return d.possible === false;
+ })
+ .classed("selected", false);
+ });
+ v.main.lasso.on("end", function() {
+ v.main.lasso.items().filter(function(d) {
+ return d.selected === true;
+ })
+ .classed("selected", true);
+ v.main.lasso.items().filter(function(d) {
+ return d.selected === false;
+ })
+ .classed("selected", false);
+ v.tools.onLassoEnd(v.main.lasso.items());
+ });
+ // save lasso event for use in event proxy
+ v.events.mousedownLasso = v.dom.graphOverlay.on("mousedown.drag");
+ v.events.touchstartLasso = v.dom.graphOverlay.on("touchstart.drag");
+ //v.events.touchmoveDrag = v.dom.graphOverlay.on("touchmove.drag");
+ //v.events.touchendDrag = v.dom.graphOverlay.on("touchend.drag");
+
+ // register event proxy for relevant lasso events who conflict with force functions -> see also
+ // v.tools.lassoEventProxy
+ v.dom.graphOverlay.on("mousedown.drag", v.tools.lassoEventProxy(v.events.mousedownLasso));
+ v.dom.graphOverlay.on("touchstart.drag", v.tools.lassoEventProxy(v.events.touchstartLasso));
+ //v.dom.graphOverlay.on("touchmove.drag", v.tools.lassoEventProxy(v.events.touchmoveDrag));
+ //v.dom.graphOverlay.on("touchend.drag", v.tools.lassoEventProxy(v.events.touchendDrag));
+ } else {
+ v.dom.graphOverlay.on(".drag", null);
+ v.main.nodes.classed("selected", false);
+ }
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, you can zoom and pan the graph.
+ *
+ * ATTENTION: When zoomMode is set to true then the lassoMode is only working with the pressed alt or shift key.
+ *
+ * KNOWN BUG: In iOS it is after the first zoom event no more possible to drag a node - instead the whole graph is moved - this is, because iOS Safari provide a wrong event.target.tagName. Also a problem: your are not able to press the alt or shift key - if you want to use lasso and zoom together on a touch device, you have to provide a workaround. One possible way is to provide a button, which turns zoom mode on and off with the API zoomMode method - then the user has the choice between these two modes - not comfortable, but working.
+ *
+ * No `render` or `resume` call needed to take into effect:
+ *
+ * example.zoomMode(true);
+ * @see {@link module:API.zoom}
+ * @see {@link module:API.zoomSmooth}
+ * @see {@link module:API.transform}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.zoomMode = function(value) {
+ if (!arguments.length) {
+ return v.conf.zoomMode;
+ }
+ v.conf.zoomMode = value;
+ if (v.status.graphStarted) {
+ if (v.conf.zoomMode) {
+ v.main.zoom.scaleExtent([v.conf.minZoomFactor, v.conf.maxZoomFactor])
+ .size([v.tools.getGraphWidth(), v.tools.getGraphHeight()])
+ .on("zoom", v.main.zoomed);
+ v.dom.graphOverlay.call(v.main.zoom);
+ // save zoom events for use in event proxy
+ v.events.dblclickZoom = v.dom.graphOverlay.on("dblclick.zoom");
+ v.events.mousedownZoom = v.dom.graphOverlay.on("mousedown.zoom");
+ v.events.touchstartZoom = v.dom.graphOverlay.on("touchstart.zoom");
+ //v.events.touchmoveZoom = v.dom.graphOverlay.on("touchmove.zoom");
+ //v.events.touchendZoom = v.dom.graphOverlay.on("touchend.zoom");
+
+ // register event proxy for relevant zoom events which conflicts with force functions -> see also
+ // v.tools.zoomEventProxy
+ v.dom.graphOverlay.on("dblclick.zoom", v.tools.zoomEventProxy(v.events.dblclickZoom));
+ v.dom.graphOverlay.on("mousedown.zoom", v.tools.zoomEventProxy(v.events.mousedownZoom));
+ v.dom.graphOverlay.on("touchstart.zoom", v.tools.zoomEventProxy(v.events.touchstartZoom));
+ //v.dom.graphOverlay.on("touchmove.zoom", v.tools.zoomEventProxy(v.events.touchmoveZoom));
+ //v.dom.graphOverlay.on("touchend.zoom", v.tools.zoomEventProxy(v.events.touchendZoom));
+
+ // transform graph, if conf is not default
+ if (JSON.stringify(v.conf.transform) !== JSON.stringify(v.confDefaults.transform)) {
+ v.dom.graph.attr("transform", "translate(" + v.main.zoom.translate() + ")scale(" +
+ v.main.zoom.scale() + ")");
+ v.tools.writeConfObjectIntoWizard();
+ }
+ } else {
+ // http://stackoverflow.com/questions/22302919/
+ // unregister-zoom-listener-and-restore-scroll-ability-in-d3-js/22303160?noredirect=1#22303160
+ v.dom.graphOverlay.on(".zoom", null);
+ }
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * No `render` or `resume` call needed to take into effect::
+ *
+ * example.minZoomFactor(0.1);
+ * @see {@link module:API.maxZoomFactor}
+ * @param {number} [value=0.2] - The new config value.
+ * @returns {(number|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.minZoomFactor = function(value) {
+ if (!arguments.length) {
+ return v.conf.minZoomFactor;
+ }
+ v.conf.minZoomFactor = value;
+ if (v.status.graphReady) {
+ graph.zoomMode(v.conf.zoomMode);
+ }
+ return graph;
+ };
+
+ /**
+ * No `render` or `resume` call needed to take into effect::
+ *
+ * example.maxZoomFactor(10);
+ * @see {@link module:API.minZoomFactor}
+ * @param {number} [value=5] - The new config value.
+ * @returns {(number|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.maxZoomFactor = function(value) {
+ if (!arguments.length) {
+ return v.conf.maxZoomFactor;
+ }
+ v.conf.maxZoomFactor = value;
+ if (v.status.graphReady) {
+ graph.zoomMode(v.conf.zoomMode);
+ }
+ return graph;
+ };
+
+ /**
+ * The graph is centered to the given position and scaled to the calculated scale factor (effective graph with / viewportWidth).
+ *
+ * The reason to have a viewportWidth instead of a scale factor is, that you can rely on given data like the coordinates and radius of a node without calculating the scale factor by yourself - you define your desired viewport width and the zoom method is calculating the neccesary scale factor for this viewport width. If the calculated scale factor is less or greater then the configured minimum and maximum scale factors, then these configured scale factors are used. The reason for this a good user experience, since the graph would be otherwise falling back on these scale factors when the user is scaling the graph by mouse or touch events.
+ *
+ * No `render` or `resume` call needed to take into effect:
+ *
+ * var node = example.nodeDataById('8888');
+ * example.zoom(node.x, node.y, node.radius * 6); // default duration of 500ms
+ *
+ * var node = example.nodeDataById('9999');
+ * example.zoom(node.x, node.y, node.radius * 6, 1500); // duration of 1500ms
+ * @see {@link module:API.zoomMode}
+ * @see {@link module:API.minZoomFactor}
+ * @see {@link module:API.maxZoomFactor}
+ * @see {@link module:API.transform}
+ * @param {number} [centerX=graph width / 2] - The horizontal center position.
+ * @param {number} [centerY=graph height / 2] - The vertical center position.
+ * @param {number} [viewportWidth=graph width] - The desired viewport width.
+ * @param {number} [duration=500] - the duration of the transition
+ * @returns {Object} The graph object for method chaining.
+ */
+ graph.zoom = function(centerX, centerY, viewportWidth, duration) {
+ // http://bl.ocks.org/linssen/7352810
+ var translate, scale;
+ var width = v.tools.getGraphWidth(); // could be different then configured (responsive)
+ var height = v.tools.getGraphHeight();
+ centerX = (isNaN(centerX) ? width / 2 : parseInt(centerX));
+ centerY = (isNaN(centerY) ? height / 2 : parseInt(centerY));
+ viewportWidth = (isNaN(viewportWidth) ? width : parseInt(viewportWidth));
+ duration = (isNaN(duration) ? 500 : parseInt(duration));
+ scale = width / viewportWidth;
+ translate = [
+ width / 2 - centerX * scale,
+ height / 2 - centerY * scale
+ ];
+ v.main.interpolateZoom(translate, scale, duration);
+ return graph;
+ };
+
+ /**
+ * DEPRECATED: Please use zoom instead.
+ * @see {@link module:API.zoom}
+ * @see {@link module:API.zoomMode}
+ * @see {@link module:API.minZoomFactor}
+ * @see {@link module:API.maxZoomFactor}
+ * @see {@link module:API.transform}
+ * @param {number} [centerX=graph width / 2] - The horizontal center position.
+ * @param {number} [centerY=graph height / 2] - The vertical center position.
+ * @param {number} [viewportWidth=graph width] - The desired viewport width.
+ * @param {number} [duration=1500] - the duration of the transition
+ * @returns {Object} The graph object for method chaining.
+ */
+ graph.zoomSmooth = function(centerX, centerY, viewportWidth, duration) {
+ centerX = (isNaN(centerX) ? width / 2 : parseInt(centerX));
+ centerY = (isNaN(centerY) ? height / 2 : parseInt(centerY));
+ viewportWidth = (isNaN(viewportWidth) ? width : parseInt(viewportWidth));
+ duration = (isNaN(duration) ? 1500 : parseInt(duration));
+ graph.zoom(centerX, centerY, viewportWidth, duration);
+ return graph;
+ };
+
+ /**
+ * Behaves like a normal getter/setter (the `zoom` and `zoomSmooth` methods implements only setters) and can be used in the conf object to initialize the graph with different translate values/scale factors than [0,0]/1. The current transform value(an object) is rendered in the customization wizard conf object text area like all other options when the current value is different then the default value. No `render` or `resume` call needed to take into effect:
+ *
+ * //example.zoomMode(true);
+ * example.transform({"translate":[100,100],"scale":0.5});
+ * @see {@link module:API.zoomMode}
+ * @see {@link module:API.zoom}
+ * @see {@link module:API.zoomSmooth}
+ * @param {Object} [transform={“translate”:[0,0],“scale”:1}] - The new config value.
+ * @param {number} [duration=500] - The transition duration in milliseconds.
+ * @returns {Object} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.transform = function(transform, duration) {
+ if (!arguments.length) {
+ return {
+ "translate": v.main.zoom.translate(),
+ "scale": v.main.zoom.scale()
+ };
+ } else {
+ v.main.interpolateZoom(
+ transform.translate,
+ transform.scale,
+ (isNaN(duration) ? 500 : parseInt(duration))
+ );
+ }
+ return graph;
+ };
+
+ /**
+ * Helper/Command method - get the center position of the graph border box:
+ *
+ * example.centerPositionGraph();
+ * @returns {Array} An array with the x and y positions: [x, y].
+ */
+ graph.centerPositionGraph = function () {
+ var graphBox = v.dom.graph.node().getBBox();
+ return [
+ graphBox.x + graphBox.width / 2,
+ graphBox.y + graphBox.height / 2
+ ];
+ };
+
+ /**
+ * Helper/Command method - get the center position of the SVG viewport:
+ *
+ * example.centerPositionViewport();
+ * @returns {Array} An array with the x and y positions: [x, y].
+ */
+ graph.centerPositionViewport = function () {
+ var svg = {}, scale, translate;
+ svg.width = v.tools.getGraphWidth();
+ svg.height = v.tools.getGraphHeight();
+ scale = v.main.zoom.scale();
+ translate = v.main.zoom.translate();
+ return [
+ (svg.width / 2 - translate[0]) * 1 / scale,
+ (svg.height / 2 - translate[1]) * 1 / scale
+ ];
+ };
+
+ /**
+ * Helper/Command method - center the graph. No `render` or `resume` call needed to take into effect:
+ *
+ * example.center();
+ * @see {@link module:API.zoomMode}
+ * @see {@link module:API.zoomSmooth}
+ * @see {@link module:API.minZoomFactor}
+ * @see {@link module:API.maxZoomFactor}
+ * @see {@link module:API.transform}
+ * @see {@link module:API.zoomToFit}
+ * @see {@link module:API.zoomToFitOnForceEnd}
+ * @param {number} [duration=500] - The transition duration in milliseconds.
+ * @returns {Object} The graph object for method chaining.
+ */
+ graph.center = function (duration) {
+ var svg = {}, graphBox, translate, scale;
+ duration = (isNaN(duration) ? 500 : parseInt(duration));
+ svg.width = v.tools.getGraphWidth();
+ svg.height = v.tools.getGraphHeight();
+ graphBox = v.dom.graph.node().getBBox();
+ scale = v.main.zoom.scale();
+ // If the graph is hidden we get 0 for width and height. Zoom will then fail because
+ // the calculation results in NaN for the translation (x, y) and infinity for the scale.
+ if (graphBox.width > 0 && graphBox.height > 0) {
+ translate = [
+ (svg.width - graphBox.width * scale) / 2 - graphBox.x * scale,
+ (svg.height - graphBox.height * scale) / 2 - graphBox.y * scale
+ ];
+ v.main.interpolateZoom(translate, scale, duration);
+ }
+ return graph;
+ };
+
+ /**
+ * Helper/Command method - automatically zoom, so that the whole graph is visible and optimal sized. No `render` or `resume` call needed to take into effect:
+ *
+ * example.zoomToFit();
+ * @see {@link module:API.zoomMode}
+ * @see {@link module:API.zoomSmooth}
+ * @see {@link module:API.minZoomFactor}
+ * @see {@link module:API.maxZoomFactor}
+ * @see {@link module:API.transform}
+ * @see {@link module:API.zoomToFitOnForceEnd}
+ * @param {number} [duration=500] - The transition duration in milliseconds.
+ * @returns {Object} The graph object for method chaining.
+ */
+ graph.zoomToFit = function(duration) {
+ var svg = {}, graphBox, padding = 10, translate, scale;
+ duration = (isNaN(duration) ? 500 : parseInt(duration));
+ svg.width = v.tools.getGraphWidth();
+ svg.height = v.tools.getGraphHeight();
+ graphBox = v.dom.graph.node().getBBox();
+ // If the graph is hidden we get 0 for width and height. Zoom will then fail because
+ // the calculation results in NaN for the translation (x, y) and infinity for the scale.
+ if (graphBox.width > 0 && graphBox.height > 0) {
+ scale = Math.min((svg.height - 2 * padding) / graphBox.height,
+ (svg.width - 2 * padding) / graphBox.width);
+ translate = [
+ (svg.width - graphBox.width * scale) / 2 - graphBox.x * scale,
+ (svg.height - graphBox.height * scale) / 2 - graphBox.y * scale
+ ];
+ v.main.interpolateZoom(translate, scale, duration);
+ }
+ return graph;
+ };
+
+ /**
+ * Automatically zoom at force end, so that the whole graph is visible and optimal sized. If enabled it fires at every force end event. If you only want to resize your graph once than have a look at the command/helper method `zoomToFit`:
+ *
+ * //change config and resize once
+ * example.zoomToFitOnForceEnd(true).zoomToFit();
+ *
+ * //resize only once
+ * example.zoomToFit();
+ * @see {@link module:API.zoomMode}
+ * @see {@link module:API.zoomSmooth}
+ * @see {@link module:API.minZoomFactor}
+ * @see {@link module:API.maxZoomFactor}
+ * @see {@link module:API.transform}
+ * @see {@link module:API.zoomToFit}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.zoomToFitOnForceEnd = function(value) {
+ if (!arguments.length) {
+ return v.conf.zoomToFitOnForceEnd;
+ }
+ v.conf.zoomToFitOnForceEnd = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Automatically zoom at resize (API call of `width`, `height` or responsive change of parent container size with option `useDomParentWidth` set to true), so that the whole graph is visible and optimal sized.
+ *
+ * The event is harmonized/delayed for performance reasons. It could fire very often when for example the browser window is resized by the user. If the graph force simulation is running and not cooled down it is executed on the force end event. Also see the corresponding option `onResizeFunctionTimeout` which has a default value of 300 (milliseconds).
+ *
+ * If you only want to resize your graph once than have a look at the command/helper method `zoomToFit`:
+ *
+ * //change config and resize once
+ * example.zoomToFitOnResize(true).zoomToFit();
+ *
+ * //resize only once
+ * example.zoomToFit();
+ * @see {@link module:API.onResizeFunctionTimeout}
+ * @see {@link module:API.zoomMode}
+ * @see {@link module:API.zoomSmooth}
+ * @see {@link module:API.minZoomFactor}
+ * @see {@link module:API.maxZoomFactor}
+ * @see {@link module:API.transform}
+ * @see {@link module:API.zoomToFit}
+ * @see {@link module:API.zoomToFitOnForceEnd}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.zoomToFitOnResize = function(value) {
+ if (!arguments.length) {
+ return v.conf.zoomToFitOnResize;
+ }
+ v.conf.zoomToFitOnResize = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * When the graph is resized, the initial aspect ratio (width and height on first render cycle) is respected:
+ *
+ * //change config and resize height (width will change implicit based on initial aspect ratio)
+ * example.keepAspectRatioOnResize(true).height(400);
+ *
+ * @see {@link module:API.width}
+ * @see {@link module:API.height}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.keepAspectRatioOnResize = function(value) {
+ if (!arguments.length) {
+ return v.conf.keepAspectRatioOnResize;
+ }
+ v.conf.keepAspectRatioOnResize = value;
+ if (v.status.graphStarted) {
+ graph.width(v.conf.width);
+ graph.height(v.conf.height);
+ v.tools.removeLegend();
+ v.tools.createLegend();
+ v.tools.executeResize();
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Gets or sets the function for the resize event.
+ *
+ * No data is provided because this is a very generic event:
+ *
+ * example.onResizeFunction(
+ * function(){
+ * // your logic here
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Resize [D3 - Force Layout]” on your graph region.
+ * @see {@link module:API.onResizeFunctionTimeout}
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ */
+ graph.onResizeFunction = function(value) {
+ if (!arguments.length) {
+ return v.conf.onResizeFunction;
+ }
+ v.conf.onResizeFunction = value;
+ return graph;
+ };
+
+ /**
+ * The harmonized/delayed handling of the resize event to prevent performance issues - see also `zoomToFitOnResize`:
+ *
+ * example.onResizeFunctionTimeout(100).height(400);
+ * @see {@link module:API.onResizeFunction}
+ * @see {@link module:API.zoomToFitOnResize}
+ * @see {@link module:API.zoomMode}
+ * @see {@link module:API.zoomSmooth}
+ * @see {@link module:API.minZoomFactor}
+ * @see {@link module:API.maxZoomFactor}
+ * @see {@link module:API.transform}
+ * @see {@link module:API.zoomToFit}
+ * @see {@link module:API.zoomToFitOnForceEnd}
+ * @param {number} [value=300] - The new chart width value.
+ * @returns {(number|Object)} The current chart width value if no parameter is given or the graph object for method chaining.
+ */
+ graph.onResizeFunctionTimeout = function(value) {
+ if (!arguments.length) {
+ return v.conf.onResizeFunctionTimeout;
+ }
+ v.conf.onResizeFunctionTimeout = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, a loading indicator is shown when used as a APEX plugin during the AJAX calls. If you want to show the loading indicator in a standalone implementation you can show and hide the loading indicator directly with the API method `showLoadingIndicator`:
+ *
+ * example.showLoadingIndicatorOnAjaxCall(false);
+ * @see {@link module:API.showLoadingIndicator}
+ * @param {boolean} [value=true] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.showLoadingIndicatorOnAjaxCall = function(value) {
+ if (!arguments.length) {
+ return v.conf.showLoadingIndicatorOnAjaxCall;
+ }
+ v.conf.showLoadingIndicatorOnAjaxCall = value;
+ return graph;
+ };
+
+ /**
+ * Helper method to directly show or hide a loading indicator. The APEX plugin do this implicitly on AJAX calls when the option `showLoadingIndicatorOnAjaxCall` is set to true. No `render` or `resume` call needed to take into effect:
+ *
+ * // Show:
+ * example.showLoadingIndicator(true);
+ *
+ * // Hide:
+ * example.showLoadingIndicator(false);
+ * @see {@link module:API.showLoadingIndicatorOnAjaxCall}
+ * @param {boolean} - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.showLoadingIndicator = function(value) {
+ if (v.tools.parseBool(value)) {
+ v.dom.loading.style("display", "block");
+ } else {
+ v.dom.loading.style("display", "none");
+ }
+ return graph;
+ };
+
+ /**
+ * If true, fixed nodes are aligned to the nearest grid position on the drag end event. You can pin nodes, when `pinMode` is set to true or by delivering nodes with the attribute “fixed” set to true and “x” and “y” attributes for the position. If you have already fixed nodes on your graph you can also set this attribute at runtime and resume the force. Needs a `resume` call to take into effect:
+ *
+ * example.alignFixedNodesToGrid(true).resume();
+ * @see {@link module:API.gridSize}
+ * @see {@link module:API.pinMode}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.alignFixedNodesToGrid = function(value) {
+ var width, height;
+ if (!arguments.length) {
+ return v.conf.alignFixedNodesToGrid;
+ }
+ v.conf.alignFixedNodesToGrid = value;
+ if (v.status.graphStarted) {
+ width = v.tools.getGraphWidth();
+ height = v.tools.getGraphHeight();
+ // align fixed nodes to grid
+ if (v.conf.alignFixedNodesToGrid) {
+ // NO aligning on the very first start: this would overwrite user defined positions
+ if (v.status.graphReady) {
+ v.main.nodes.each(function(n) {
+ if (n.fixed) {
+ n.x = n.px = v.tools.getNearestGridPosition(n.x, width);
+ n.y = n.py = v.tools.getNearestGridPosition(n.y, height);
+ }
+ });
+ }
+ v.main.drag.on("dragend", function(n) {
+ n.x = n.px = v.tools.getNearestGridPosition(n.x, width);
+ n.y = n.py = v.tools.getNearestGridPosition(n.y, height);
+ });
+ } else {
+ v.main.drag.on("dragend", null);
+ }
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The grid size of the virtual grid for the option `alignFixedNodesToGrid`. Needs a `resume` call to take into effect:
+ *
+ * example.gridSize(100).alignFixedNodesToGrid(true).resume();
+ * @see {@link module:API.alignFixedNodesToGrid}
+ * @see {@link module:API.pinMode}
+ * @param {number} [value=50] - The new config value.
+ * @returns {(number|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.gridSize = function(value) {
+ if (!arguments.length) {
+ return v.conf.gridSize;
+ }
+ v.conf.gridSize = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Command method (has no get or set function). Moves all fixed nodes in the provided direction. Needs a `resume` call to take into effect:
+ *
+ * example.moveFixedNodes(10,-5).resume();
+ *
+ * The example adds 10 to x position and -5 to y position to all fixed nodes. ATTENTION: If alignFixedNodesToGrid is set to true this can have unexpected behavior - you must then provide values greater then gridSize halved to see any changes on your graph, otherwise the positions are falling back to the nearest (current) grid position.
+ * @see {@link module:API.pinMode}
+ * @see {@link module:API.alignFixedNodesToGrid}
+ * @param {number} [x=0] - x value - positive or negative
+ * @param {number} [y=0] - y value - positive or negative
+ * @returns {Object} The graph object for method chaining.
+ */
+ graph.moveFixedNodes = function(x, y) {
+ if (v.status.graphStarted) {
+ if (!x) {
+ x = 0;
+ }
+ if (!y) {
+ y = 0;
+ }
+ if (x !== 0 || y !== 0) {
+ v.main.nodes.each(function(n) {
+ if (n.fixed) {
+ n.x = n.px = (v.conf.alignFixedNodesToGrid ?
+ v.tools.getNearestGridPosition(n.x + x, v.conf.width) : n.x + x);
+ n.y = n.py = (v.conf.alignFixedNodesToGrid ?
+ v.tools.getNearestGridPosition(n.y + y, v.conf.width) : n.y + y);
+ }
+ });
+ }
+ }
+ return graph;
+ };
+
+ /**
+ * Command method (has no get or set function and expects no parameter): Release all fixed (pinned) nodes. Needs a `resume` call to take into effect:
+ *
+ * example.releaseFixedNodes().resume();
+ * @see {@link module:API.pinMode}
+ * @see {@link module:API.alignFixedNodesToGrid}
+ * @returns {Object} The graph object for method chaining.
+ */
+ graph.releaseFixedNodes = function() {
+ if (v.status.graphStarted) {
+ v.main.nodes.each(function(n) {
+ n.fixed = 0;
+ });
+ }
+ return graph;
+ };
+
+ /**
+ * Can be “none”, “click”, “dblclick” and “contextmenu” and defines, which event will release a node. This releasing of a node is sometimes a bit unstable (not on the code side, but on the visualizing side) and depends on the next tick event. You have to play around with this. If you want only release all nodes you can simply call the releaseFixedNodes method and resume the graph. No `render` or `resume` call needed to take into effect:
+ *
+ * example.nodeEventToStopPinMode("contextmenu");
+ * @see {@link module:API.releaseFixedNodes}
+ * @param {string} [value="contextmenu"] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.nodeEventToStopPinMode = function(value) {
+ if (!arguments.length) {
+ return v.conf.nodeEventToStopPinMode;
+ }
+ v.conf.nodeEventToStopPinMode = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, the context menu default browser action on the nodes are prevented. This could be useful, if you want to implement an own context menu for the nodes. xxx:
+ *
+ * example.onNodeContextmenuPreventDefault(true);
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.onNodeContextmenuPreventDefault = function(value) {
+ if (!arguments.length) {
+ return v.conf.onNodeContextmenuPreventDefault;
+ }
+ v.conf.onNodeContextmenuPreventDefault = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Can be “none”, “click”, “dblclick” or “contextmenu”. Works only for nodes with a non empty LINK attribute. No `render` or `resume` call needed to take into effect:
+ *
+ * example.nodeEventToOpenLink("click");
+ * @param {string} [value="dblclick"] - The new config value.
+ * @returns {(string|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.nodeEventToOpenLink = function(value) {
+ if (!arguments.length) {
+ return v.conf.nodeEventToOpenLink;
+ }
+ v.conf.nodeEventToOpenLink = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * This text is used as the link target, when a node has a LINK attribute.
+ *
+ * There are three special keywords: “none”, “nodeID” and “domContainerID”. If you use “none”, the link is opened in the same window/tab where your graph is currently shown. If you use “nodeID”, the ID of the currently clicked node is used as the target attribute, this means - you get one window/tab for each node in your graph - when you click a second time on the same node, the window/tab is reused. The same with the keyword “domContainerID” - you get one window/tab for each graph on your page - when you click a second time on the same node, the window/tab is reused.
+ *
+ * Anything else is not interpreted - your given text is simply used as the target attribute of the link. This is also the case for the second option in the customize wizard called “_blank”. If you use this, then each click on a node opens in a new window/tab. You are not restricted to use only the predefined select options. It is up to you to overwrite the value in your configuration object. As an example: If you want to have always the same window/tab for each click on a node, then simply provide a text here, that fit your needs e.g. “myOwnWindowName”.
+ *
+ * example.nodeLinkTarget("myOwnWindowName");
+ * @see {@link module:API.nodeEventToOpenLink}
+ * @param {string} [value="_blank"] - The new config value.
+ * @returns {(string|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.nodeLinkTarget = function(value) {
+ if (!arguments.length) {
+ return v.conf.nodeLinkTarget;
+ }
+ v.conf.nodeLinkTarget = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, the graph is refreshed automatically. This makes only sense when running as APEX plugin - here you have the SQL queries for loading new data with AJAX. If you run your code standalone, you have to provide new data as a parameter in the start or render method and therefore you have to use your own auto refresh logic. No `render` or `resume` call needed to take into effect:
+ *
+ * example.autoRefresh(true);
+ * @see {@link module:API.refreshInterval}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.autoRefresh = function(value) {
+ if (!arguments.length) {
+ return v.conf.autoRefresh;
+ }
+ v.conf.autoRefresh = value;
+ if (v.status.graphStarted) {
+ if (v.conf.autoRefresh && v.conf.refreshInterval && !v.conf.interval) {
+ v.conf.interval = window.setInterval(function() {
+ graph.start();
+ }, v.conf.refreshInterval);
+ v.tools.log("Auto refresh started with an interval of " + v.conf.refreshInterval + " milliseconds.");
+ } else if (!v.conf.autoRefresh && v.conf.interval) {
+ clearInterval(v.conf.interval);
+ v.conf.interval = null;
+ v.tools.log("Auto refresh stopped.");
+ }
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The refresh interval in milliseconds. No `render` or `resume` call needed to take into effect, but after changing the interval value you have to stop a current activated auto refresh and start it again to take the new value into effect:
+ *
+ * // only set the value and start auto refresh
+ * example.refreshInterval(4000).autoRefresh(true);
+ *
+ * // restart running auto refresh
+ * example.refreshInterval(2000).autoRefresh(false).autoRefresh(true);
+ * @see {@link module:API.autoRefresh}
+ * @param {number} [value=5000] - The new config value.
+ * @returns {(number|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.refreshInterval = function(value) {
+ if (!arguments.length) {
+ return v.conf.refreshInterval;
+ }
+ v.conf.refreshInterval = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, the width of the chart(SVG element) is aligned to its DOM parent element. No `render` or `resume` call needed to take into effect:
+ *
+ * example.useDomParentWidth(true);
+ * @see {@link module:API.setDomParentPaddingToZero}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.useDomParentWidth = function(value) {
+ if (!arguments.length) {
+ return v.conf.useDomParentWidth;
+ }
+ v.conf.useDomParentWidth = value;
+ if (v.status.graphStarted) {
+ if (v.conf.useDomParentWidth) {
+ v.tools.ResizeObserver.observe(v.dom.svgParent.node());
+ } else {
+ v.tools.ResizeObserver.unobserve(v.dom.svgParent.node());
+ }
+ // legend was not shown up correctly after option change of useDomParentWidth
+ if (v.conf.showLegend) {
+ v.tools.removeLegend();
+ v.tools.createLegend();
+ }
+ v.tools.executeResize();
+ }
+ return graph;
+ };
+
+ /**
+ * If true, the parent DOM element of the graph gets the style { padding: 0px; }. If set to false, this style is removed from the DOM parent of the graph. No `render` or `resume` call needed to take into effect:
+ *
+ * example.setDomParentPaddingToZero(true);
+ * @see {@link module:API.useDomParentWidth}
+ * @param {boolean} [value=false] - The new config value.
+ * @returns {(boolean|Object)} The current config value if no parameter is given or the graph object for method chaining.
+ */
+ graph.setDomParentPaddingToZero = function(value) {
+ if (!arguments.length) {
+ return v.conf.setDomParentPaddingToZero;
+ }
+ v.conf.setDomParentPaddingToZero = value;
+ if (v.status.graphStarted) {
+ if (v.conf.setDomParentPaddingToZero) {
+ v.dom.svgParent.style("padding", "0");
+ } else {
+ v.dom.svgParent.style("padding", null);
+ }
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Returns the current with of the graphs DOM parent. This method expects no parameter and terminates the method chain.
+ *
+ * If the option useDomParentWidth is set to true, then this is the effective width of the graph - independent of the configured width.
+ *
+ * example.domParentWidth();
+ * @returns {number} The current DOM parent width.
+ */
+ graph.domParentWidth = function() {
+ return v.tools.getSvgParentInnerWidth();
+ };
+
+ /**
+ * The width of the chart:
+ *
+ * example.width(800);
+ * @see {@link module:API.height}
+ * @param {number} [value=600] - The new chart width value.
+ * @returns {(number|Object)} The current chart width value if no parameter is given or the graph object for method chaining.
+ */
+ graph.width = function(value) {
+ if (!arguments.length) {
+ return v.conf.width;
+ }
+ v.conf.width = value;
+ if (v.status.graphStarted) {
+ if (v.conf.keepAspectRatioOnResize) {
+ v.conf.height = v.conf.width * 1 / v.status.aspectRatio;
+ }
+ v.tools.executeResize();
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The height of the chart:
+ *
+ * example.height(300);
+ * @see {@link module:API.width}
+ * @param {number} [value=400] - The new chart height value.
+ * @returns {(number|Object)} The current chart height value if no parameter is given or the graph object for method chaining.
+ */
+ graph.height = function(value) {
+ if (!arguments.length) {
+ return v.conf.height;
+ }
+ v.conf.height = value;
+ if (v.status.graphStarted) {
+ if (v.conf.keepAspectRatioOnResize) {
+ v.conf.width = v.conf.height * v.status.aspectRatio;
+ }
+ v.tools.executeResize();
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The minimum node radius. Each node radius is calculated by its SIZEVALUE attribute in a range between the minimum and the maximum node radius. Needs a `render` call to take into effect:
+ *
+ * example.minNodeRadius(2).render();
+ * @see {@link module:API.maxNodeRadius}
+ * @param {number} [value=6] - The new min node radius value.
+ * @returns {(number|Object)} The current min node radius value if no parameter is given or the graph object for method chaining.
+ */
+ graph.minNodeRadius = function(value) {
+ if (!arguments.length) {
+ return v.conf.minNodeRadius;
+ }
+ v.conf.minNodeRadius = value;
+ if (v.status.graphReady) {
+ v.tools.setRadiusFunction();
+ v.main.nodes.each(function(n) {
+ n.radius = v.tools.radius(n.SIZEVALUE);
+ });
+ v.main.nodes.attr("r", function(n) {
+ return n.radius;
+ });
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The maximum node radius. Each node radius is calculated by its SIZEVALUE attribute in a range between the minimum and the maximum node radius. Needs a `render` call to take into effect:
+ *
+ * example.maxNodeRadius(24).render();
+ * @see {@link module:API.minNodeRadius}
+ * @param {number} [value=18] - The new max node radius value.
+ * @returns {(number|Object)} The current max node radius value if no parameter is given or the graph object for method chaining.
+ */
+ graph.maxNodeRadius = function(value) {
+ if (!arguments.length) {
+ return v.conf.maxNodeRadius;
+ }
+ v.conf.maxNodeRadius = value;
+ if (v.status.graphReady) {
+ v.tools.setRadiusFunction();
+ v.main.nodes.each(function(n) {
+ n.radius = v.tools.radius(n.SIZEVALUE);
+ });
+ v.main.nodes.attr("r", function(n) {
+ return n.radius;
+ });
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The distance of the self link path around a node. Needs a `render` call to take into effect:
+ *
+ * example.selfLinkDistance(25).render();
+ * @see {@link module:API.linkDistance}
+ * @param {number} [value=20] - The new self link distance value.
+ * @returns {(number|Object)} The current self link distance value if no parameter is given or the graph object for method chaining.
+ */
+ graph.selfLinkDistance = function(value) {
+ if (!arguments.length) {
+ return v.conf.selfLinkDistance;
+ }
+ v.conf.selfLinkDistance = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * The distance between nodes centers. Needs a `render` call to take into effect:
+ *
+ * example.linkDistance(60).render();
+ * @see {@link module:API.selfLinkDistance}
+ * @param {number} [value=80] - The new link distance value.
+ * @returns {(number|Object)} The current link distance value if no parameter is given or the graph object for method chaining.
+ */
+ graph.linkDistance = function(value) {
+ if (!arguments.length) {
+ return v.conf.linkDistance;
+ }
+ v.conf.linkDistance = value;
+ if (v.status.graphStarted) {
+ v.main.force.linkDistance(v.conf.linkDistance);
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Gets or sets the charge strength to the specified value. For more informations have a look at the [D3 API Reference](https://github.com/d3/d3-3.x-api-reference/blob/master/Force-Layout.md#charge). Needs a `render` call to take into effect:
+ *
+ * example.charge(-200).render();
+ * @see {@link module:API.chargeDistance}
+ * @param {number} [value=-350] - The new charge value.
+ * @returns {(number|Object)} The current charge value if no parameter is given or the graph object for method chaining.
+ */
+ graph.charge = function(value) {
+ if (!arguments.length) {
+ return v.conf.charge;
+ }
+ v.conf.charge = value;
+ if (v.status.graphStarted) {
+ v.main.force.charge(v.conf.charge);
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Gets or sets the maximum distance over which charge forces are applied. For more informations have a look at the [D3 API Reference](https://github.com/d3/d3-3.x-api-reference/blob/master/Force-Layout.md#chargeDistance). This option is not shown in the customize wizard. Needs a `render` call to take into effect:
+ *
+ * example.chargeDistance(200).render();
+ * @see {@link module:API.charge}
+ * @param {number} [value=Infinity] - The new charge distance value.
+ * @returns {(number|Object)} The current charge distance value if no parameter is given or the graph object for method chaining.
+ */
+ graph.chargeDistance = function(value) {
+ if (!arguments.length) {
+ return v.conf.chargeDistance;
+ }
+ v.conf.chargeDistance = value;
+ if (v.status.graphStarted) {
+ v.main.force.chargeDistance(v.conf.chargeDistance);
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Gets or sets the gravitational strength to the specified numerical value. For more informations see the [D3 API Reference](https://github.com/d3/d3-3.x-api-reference/blob/master/Force-Layout.md#gravity). Needs a `render` call to take into effect:
+ *
+ * example.gravity(0.3).render();
+ * @param {number} [value=0.1] - The new gravity value.
+ * @returns {(number|Object)} The current gravity value if no parameter is given or the graph object for method chaining.
+ */
+ graph.gravity = function(value) {
+ if (!arguments.length) {
+ return v.conf.gravity;
+ }
+ v.conf.gravity = value;
+ if (v.status.graphStarted) {
+ v.main.force.gravity(v.conf.gravity);
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Gets or sets the strength (rigidity) of links to the specified value in the range [0,1]. For more informations see the [D3 API Reference](https://github.com/d3/d3-3.x-api-reference/blob/master/Force-Layout.md#linkStrength). Needs a `render` call to take into effect:
+ *
+ * example.linkStrength(0.1).render();
+ * @param {number} [value=1] - The new link strength value.
+ * @returns {(number|Object)} The current link strength value if no parameter is given or the graph object for method chaining.
+ */
+ graph.linkStrength = function(value) {
+ if (!arguments.length) {
+ return v.conf.linkStrength;
+ }
+ v.conf.linkStrength = value;
+ if (v.status.graphStarted) {
+ v.main.force.linkStrength(v.conf.linkStrength);
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Gets or sets the friction coefficient to the specified value. For more informations have a look at the [D3 API Reference](https://github.com/d3/d3-3.x-api-reference/blob/master/Force-Layout.md#friction). Needs a `render` call to take into effect:
+ *
+ * example.friction(0.4).render();
+ * @param {number} [value=0.9] - The new friction value.
+ * @returns {(number|Object)} The current friction value if no parameter is given or the graph object for method chaining.
+ */
+ graph.friction = function(value) {
+ if (!arguments.length) {
+ return v.conf.friction;
+ }
+ v.conf.friction = value;
+ if (v.status.graphStarted) {
+ v.main.force.friction(v.conf.friction);
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Gets or sets the Barnes–Hut approximation criterion to the specified value. For more informations see the [D3 API Reference](https://github.com/d3/d3-3.x-api-reference/blob/master/Force-Layout.md#theta). On smaller graphs with not so many nodes you will likely see no difference when you change this value. Needs a `render` call to take into effect:
+ *
+ * example.theta(0.1).render();
+ * @param {number} [value=0.8] - The new theta value.
+ * @returns {(number|Object)} The current theta value if no parameter is given or the graph object for method chaining.
+ */
+ graph.theta = function(value) {
+ if (!arguments.length) {
+ return v.conf.theta;
+ }
+ v.conf.theta = value;
+ if (v.status.graphStarted) {
+ v.main.force.theta(v.conf.theta);
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Gets or sets the maximum runtime in milliseconds for the force. This could be helpful when the graph is running to long with many node background images or when you want to stop the force early because all nodes are fixed and the running force is useless and costs only battery runtime.
+ *
+ * example.forceTimeLimit(100);
+ * @see {@link module:API.charge}
+ * @param {number} [value=Infinity] - The new force time limit value.
+ * @returns {(number|Object)} The current force time limit value if no parameter is given or the graph object for method chaining.
+ */
+ graph.forceTimeLimit = function(value) {
+ if (!arguments.length) {
+ return v.conf.forceTimeLimit;
+ }
+ v.conf.forceTimeLimit = value;
+ if (v.status.graphStarted) {
+ v.tools.createCustomizeWizardIfNotRendering();
+ }
+ return graph;
+ };
+
+ /**
+ * Gets or sets the current positions of all nodes. This lets you save and load a specific layout or modify the current positions (of fixed nodes - if you have no fixed nodes then the nodes will likely fall back to their previous positions because of the working forces). Works nice together with the `pinMode`. Needs a `resume` call to take into effect:
+ *
+ * // get current positions: Array of objects like [{"ID":"7839","x":200,"y":100,"fixed":1},...])
+ * var pos = example.positions();
+ * // set positions
+ * example.positions(pos.map(function(p){ p.x += 10; return p; })).resume();
+ *
+ * // all in one ;-)
+ * example.positions( example.positions().map(function(p){ p.x += 10; return p; }) ).resume();
+ * @see {@link module:API.pinMode}
+ * @param {Object} [positionsArray] - The new positions array.
+ * @returns {Object} The current positions array if no parameter is given or the graph object for method chaining.
+ */
+ graph.positions = function(positionsArray) {
+ if (!arguments.length) {
+ var positions = [];
+ v.data.nodes.forEach(function(n) {
+ positions.push({
+ "ID": n.ID,
+ "x": Math.round(n.x),
+ "y": Math.round(n.y),
+ "fixed": (n.fixed ? 1 : 0)
+ });
+ });
+ return positions;
+ } else {
+ if (v.status.graphReady) {
+ if (positionsArray.constructor === Array) {
+ positionsArray.forEach(function(n) {
+ if (v.data.idLookup[n.ID] !== undefined) {
+ v.data.idLookup[n.ID].fixed = v.tools.parseBool(n.fixed);
+ v.data.idLookup[n.ID].x = v.data.idLookup[n.ID].px = n.x;
+ v.data.idLookup[n.ID].y = v.data.idLookup[n.ID].py = n.y;
+ }
+ });
+ } else {
+ v.tools.logError("Unable to set node positions: positions method parameter must be an array of " +
+ "node positions");
+ }
+ } else {
+ v.conf.positions = positionsArray; // we do positioning later after start() is called
+ }
+ return graph;
+ }
+ };
+
+ /**
+ * Gets or sets the function for the link click event.
+ *
+ * In the first two parameters you get the event and the d3 node data, inside your function you have access to the DOM node with the this keyword:
+ *
+ * example.onLinkClickFunction(
+ * function(event, data){
+ * console.log("Link click - event:", event);
+ * console.log("Link click - data:", data);
+ * console.log("Link click - this:", this);
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Link Click [D3 - Force Layout]” on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
+ *
+ * console.log("Link click - event:", this.browserEvent);
+ * console.log("Link click - data:", this.data);
+ *
+ * Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
+ *
+ * Attention: It is not so easy to click a link, because the links are so narrow - if this option is needed I recommend to switch on the zoom mode - with zoom and pan it feels more natural to click links.
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ */
+ graph.onLinkClickFunction = function(eventFunction) {
+ if (!arguments.length) {
+ return v.conf.onLinkClickFunction;
+ }
+ v.conf.onLinkClickFunction = eventFunction;
+ return graph;
+ };
+
+ /**
+ * Gets or sets the function for the node mouseenter event.
+ *
+ * In the first two parameters you get the event and the d3 node data, inside your function you have access to the DOM node with the this keyword:
+ *
+ * example.onNodeMouseenterFunction(
+ * function(event, data){
+ * console.log("Node mouse enter - event:", event);
+ * console.log("Node mouse enter - data:", data);
+ * console.log("Node mouse enter - this:", this);
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Node Mouse Enter [D3 - Force Layout]” on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
+ *
+ * console.log("Node mouse enter - event:", this.browserEvent);
+ * console.log("Node mouse enter - data:", this.data);
+ *
+ * Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ */
+ graph.onNodeMouseenterFunction = function(eventFunction) {
+ if (!arguments.length) {
+ return v.conf.onNodeMouseenterFunction;
+ }
+ v.conf.onNodeMouseenterFunction = eventFunction;
+ return graph;
+ };
+
+ /**
+ * Gets or sets the function for the node mouseleave event.
+ *
+ * In the first two parameters you get the event and the d3 node data, inside your function you have access to the DOM node with the this keyword:
+ *
+ * example.onNodeMouseleaveFunction(
+ * function(event, data){
+ * console.log("Node mouse leave - event:", event);
+ * console.log("Node mouse leave - data:", data);
+ * console.log("Node mouse leave - this:", this);
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Node Mouse Leave [D3 - Force Layout]” on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
+ *
+ * console.log("Node mouse leave - event:", this.browserEvent);
+ * console.log("Node mouse leave - data:", this.data);
+ *
+ * Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ */
+ graph.onNodeMouseleaveFunction = function(value) {
+ if (!arguments.length) {
+ return v.conf.onNodeMouseleaveFunction;
+ }
+ v.conf.onNodeMouseleaveFunction = value;
+ return graph;
+ };
+
+ /**
+ * Gets or sets the function for the node click event.
+ *
+ * In the first two parameters you get the event and the d3 node data, inside your function you have access to the DOM node with the this keyword:
+ *
+ * example.onNodeClickFunction(
+ * function(event, data){
+ * console.log("Node click - event:", event);
+ * console.log("Node click - data:", data);
+ * console.log("Node click - this:", this);
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Node Click [D3 - Force Layout]” on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
+ *
+ * console.log("Node click - event:", this.browserEvent);
+ * console.log("Node click - data:", this.data);
+ *
+ * Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ */
+ graph.onNodeClickFunction = function(value) {
+ if (!arguments.length) {
+ return v.conf.onNodeClickFunction;
+ }
+ v.conf.onNodeClickFunction = value;
+ return graph;
+ };
+
+ /**
+ * Gets or sets the function for the node dblclick event.
+ *
+ * In the first two parameters you get the event and the d3 node data, inside your function you have access to the DOM node with the this keyword:
+ *
+ * example.onNodeDblclickFunction(
+ * function(event, data){
+ * console.log("Node double click - event:", event);
+ * console.log("Node double click - data:", data);
+ * console.log("Node double click - this:", this);
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Node Double Click [D3 - Force Layout]” on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
+ *
+ * console.log("Node double click - event:", this.browserEvent);
+ * console.log("Node double click - data:", this.data);
+ *
+ * Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ */
+ graph.onNodeDblclickFunction = function(value) {
+ if (!arguments.length) {
+ return v.conf.onNodeDblclickFunction;
+ }
+ v.conf.onNodeDblclickFunction = value;
+ return graph;
+ };
+
+ /**
+ * Gets or sets the function for the node contextmenu event.
+ *
+ * In the first two parameters you get the event and the d3 node data, inside your function you have access to the DOM node with the this keyword:
+ *
+ * example.onNodeContextmenuFunction(
+ * function(event, data){
+ * console.log("Node contextmenu - event:", event);
+ * console.log("Node contextmenu - data:", data);
+ * console.log("Node contextmenu - this:", this);
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Node Contextmenu [D3 - Force Layout]” on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
+ *
+ * console.log("Node contextmenu - event:", this.browserEvent);
+ * console.log("Node contextmenu - data:", this.data);
+ *
+ * Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ */
+ graph.onNodeContextmenuFunction = function(value) {
+ if (!arguments.length) {
+ return v.conf.onNodeContextmenuFunction;
+ }
+ v.conf.onNodeContextmenuFunction = value;
+ return graph;
+ };
+
+ /**
+ * Gets or sets the function for the lassostart event.
+ *
+ * In the first two parameters you get the event and the d3 lasso data, inside your function you have access to the DOM node with the this keyword. In case of the lasso this is refering the svg container element, because the lasso itself is not interesting:
+ *
+ * example.onLassoStartFunction(
+ * function(event, data){
+ * console.log("Lasso start - event:", event);
+ * console.log("Lasso start - data:", data);
+ * console.log("Lasso start - this:", this);
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Lasso Start [D3 - Force Layout]” on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
+ *
+ * console.log("Lasso start - event:", this.browserEvent);
+ * console.log("Lasso start - data:", this.data);
+ *
+ * Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ */
+ graph.onLassoStartFunction = function(value) {
+ if (!arguments.length) {
+ return v.conf.onLassoStartFunction;
+ }
+ v.conf.onLassoStartFunction = value;
+ return graph;
+ };
+
+ /**
+ * Gets or sets the function for the lassoend event.
+ *
+ * In the first two parameters you get the event and the d3 lasso data, inside your function you have access to the DOM node with the this keyword. In case of the lasso this is refering the svg container element, because the lasso itself is not interesting:
+ *
+ * example.onLassoEndFunction(
+ * function(event, data){
+ * console.log("Lasso end - event:", event);
+ * console.log("Lasso end - data:", data);
+ * console.log("Lasso end - this:", this);
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Lasso End [D3 - Force Layout]” on your graph region. If you do so, you can access the event and data by executing JavaScript code in this way:
+ *
+ * console.log("Lasso end - event:", this.browserEvent);
+ * console.log("Lasso end - data:", this.data);
+ *
+ * Please refer also to the APEX dynamic action documentation and keep in mind, that the data is the same in both ways but the event differs, because APEX provide a jQuery event and the Plugin the D3 original event.
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ */
+ graph.onLassoEndFunction = function(value) {
+ if (!arguments.length) {
+ return v.conf.onLassoEndFunction;
+ }
+ v.conf.onLassoEndFunction = value;
+ return graph;
+ };
+
+
+ /**
+ * Gets or sets the function for the renderend event.
+ *
+ * No data is provided because this is a very generic event. You can use the `nodes` and `links` API methods for a D3 array to modify directly the nodes or links:
+ *
+ * example.onRenderEndFunction(
+ * function(){
+ * example.nodes().filter(function (node) {
+ * return node.ID === "7839";
+ * }).style("fill", "blue");
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Render End [D3 - Force Layout]” on your graph region.
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ * @see {@link module:API.onForceStartFunction}
+ * @see {@link module:API.onForceEndFunction}
+ * @see {@link module:API.nodes}
+ * @see {@link module:API.links}
+ */
+ graph.onRenderEndFunction = function(value) {
+ if (!arguments.length) {
+ return v.conf.onRenderEndFunction;
+ }
+ v.conf.onRenderEndFunction = value;
+ return graph;
+ };
+
+ /**
+ * Gets or sets the function for the forcestart event.
+ *
+ * No data is provided because this is a very generic event:
+ *
+ * example.onForceStartFunction(
+ * function(){
+ * // your logic here.
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Force Start [D3 - Force Layout]” on your graph region.
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ * @see {@link module:API.onForceEndFunction}
+ * @see {@link module:API.onRenderEndFunction}
+ * @see {@link module:API.nodes}
+ * @see {@link module:API.links}
+ */
+ graph.onForceStartFunction = function(value) {
+ if (!arguments.length) {
+ return v.conf.onForceStartFunction;
+ }
+ v.conf.onForceStartFunction = value;
+ return graph;
+ };
+
+ /**
+ * Gets or sets the function for the forceend event.
+ *
+ * No data is provided because this is a very generic event:
+ *
+ * example.onForceEndFunction(
+ * function(){
+ * // your logic here.
+ * }
+ * );
+ *
+ * If used as APEX plugin you can also create an APEX dynamic action on the component event “Force End [D3 - Force Layout]” on your graph region.
+ * @param {Object} [eventFunction] - The new function.
+ * @returns {Object} The current function if no parameter is given or the graph object for method chaining.
+ * @see {@link module:API.onForceStartFunction}
+ * @see {@link module:API.onRenderEndFunction}
+ * @see {@link module:API.nodes}
+ * @see {@link module:API.links}
+ */
+ graph.onForceEndFunction = function(value) {
+ if (!arguments.length) {
+ return v.conf.onForceEndFunction;
+ }
+ v.conf.onForceEndFunction = value;
+ return graph;
+ };
+
+ /**
+ * Gets or sets the sample data. This makes only sense before the first start, because only on the first start without data available the sample data is used. After the first start you can provide new data with the start method. Example:
+ *
+ * //first start
+ * example.sampleData('...').start();
+ *
+ * //later
+ * example.start('...');
+ * @see {@link module:API.start}
+ * @param {(string|Object)} [data] - The new sample data as XML string, JSON string or JSON object.
+ * @returns {Object} The current sample data in JSON format if no parameter is given or the graph object for method chaining.
+ */
+ graph.sampleData = function(data) {
+ if (!arguments.length) {
+ return v.data.sampleData;
+ }
+ v.data.sampleData = data;
+ return graph;
+ };
+
+ /**
+ * Returns the current graph nodes as D3 array for direct modifications. This method expects no parameter and terminates the method chain. See also the [D3 docs](https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#operating-on-selections). Example:
+ *
+ * example.nodes().filter(function (node) {
+ * return node.ID === "7839";
+ * }).style("fill", "blue");
+ *
+ * example.nodes().filter(function (node) {
+ * return node.ID === "7839";
+ * }).classed("myOwnClass", true);
+ * @see {@link module:API.links}
+ * @see {@link module:API.selfLinks}
+ * @returns {Array} The current graph nodes.
+ */
+ graph.nodes = function() {
+ return v.main.nodes;
+ };
+
+ /**
+ * Returns the current graph links as D3 array for direct modifications. This method expects no parameter and terminates the method chain. See also the [D3 docs](https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#operating-on-selections). Example:
+ *
+ * example.links().filter(function (link) {
+ * return link.TOID === "7839";
+ * }).style("stroke", "red");
+ * @see {@link module:API.nodes}
+ * @see {@link module:API.selfLinks}
+ * @returns {Array} The current graph links.
+ */
+ graph.links = function() {
+ return v.main.links;
+ };
+
+ /**
+ * Returns the current graph selfLinks as D3 array for direct modifications. This method expects no parameter and terminates the method chain. See also the [D3 docs](https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#operating-on-selections). Example:
+ *
+ * example.selfLinks().style("stroke", "green");
+ * @see {@link module:API.nodes}
+ * @see {@link module:API.links}
+ * @returns {Array} The current graph links.
+ */
+ graph.selfLinks = function() {
+ return v.main.selfLinks;
+ };
+
+ /**
+ * Returns the current graph data as JSON object. This method expects no parameter and terminates the method chain. Example:
+ *
+ * //JSON object
+ * example.data();
+ *
+ * //stringified JSON object
+ * JSON.stringify(example.data());
+ * @see {@link module:API.nodeDataById}
+ * @see {@link module:API.start}
+ * @returns {Object} The current graph data.
+ */
+ graph.data = function() {
+ return v.data.dataConverted;
+ };
+
+ /**
+ * Returns the data from a specific node as JSON object. This method expects a node ID as parameter and terminates the method chain. Example:
+ *
+ * //get the data from the node with the ID 8888
+ * example.nodeDataById('8888');
+ *
+ * //get the data from the node with the ID 'myAlphanumericID'
+ * example.nodeDataById('myAlphanumericID');
+ * @see {@link module:API.data}
+ * @param {string} id - The node id.
+ * @returns {Object} The node data.
+ */
+ graph.nodeDataById = function(id) {
+ return v.data.idLookup[id];
+ };
+
+ /**
+ * Get or set the whole configuration with one call. Ouput includes all options, which are accessible via the API methods including the registered event functions:
+ *
+ * //get the current configuration
+ * example.options();
+ * //set the new configuration
+ * example.options( { pinMode: true, ... } );
+ * @see {@link module:API.optionsCustomizationWizard}
+ * @param {Object} [options] - Your new options.
+ * @returns {Object} Your current options or the graph object for method chaining.
+ */
+ graph.options = function(options) {
+ var key;
+ if (!arguments.length) {
+ var conf = {};
+ for (key in v.conf) {
+ if (v.conf.hasOwnProperty(key)) {
+ if (v.confDefaults.hasOwnProperty(key)) {
+ if ((v.confDefaults[key].type === "bool" ||
+ v.confDefaults[key].type === "number" ||
+ v.confDefaults[key].type === "text") &&
+ v.confDefaults[key].val !== v.conf[key]) {
+ conf[key] = v.conf[key];
+ } else if (v.confDefaults[key].type === "object" &&
+ JSON.stringify(v.confDefaults[key].val) !== JSON.stringify(v.conf[key])) {
+ conf[key] = v.conf[key];
+ }
+ } else if (!v.confDefaults.hasOwnProperty(key) &&
+ v.conf[key] !== undefined &&
+ v.conf[key] !== null) {
+ conf[key] = v.conf[key];
+ }
+ }
+ }
+ return conf;
+ } else {
+ v.tools.applyConfigurationObject(options);
+ return graph;
+ }
+ };
+
+ /**
+ * Get or set the whole configuration with one call. Output includes only the options, which are accessible via the customization wizard:
+ *
+ * //get the current configuration
+ * example.optionsCustomizationWizard();
+ * //set the new configuration
+ * example.optionsCustomizationWizard( { pinMode: true, ... } );
+ * @see {@link module:API.options}
+ * @param {Object} [options] - Your new options.
+ * @returns {Object} Your current options or the graph object for method chaining.
+ */
+ graph.optionsCustomizationWizard = function(options) {
+ var key;
+ if (!arguments.length) {
+ var conf = {};
+ for (key in v.confDefaults) {
+ if (v.confDefaults.hasOwnProperty(key)) {
+ if ((v.confDefaults[key].type === "bool" ||
+ v.confDefaults[key].type === "number" ||
+ v.confDefaults[key].type === "text") &&
+ v.confDefaults[key].val !== v.conf[key]) {
+ conf[key] = v.conf[key];
+ } else if (v.confDefaults[key].type === "object" &&
+ JSON.stringify(v.confDefaults[key].val) !== JSON.stringify(v.conf[key])) {
+ conf[key] = v.conf[key];
+ }
+ }
+ }
+ return conf;
+ } else {
+ v.tools.applyConfigurationObject(options);
+ return graph;
+ }
+ };
+
+ /**
+ * Gets or sets the customize mode. If true, the customizing wizard is opened, otherwise closed.
+ *
+ * example.customize(true);
+ * @see {@link module:API.debug}
+ * @param {boolean} [value] - The new mode.
+ * @returns {(boolean|Object)} The current mode if no parameter is given or the graph object for method chaining.
+ */
+ graph.customize = function(value) {
+ if (!arguments.length) {
+ return v.status.customize;
+ }
+ v.status.customize = value;
+ if (v.status.graphStarted) {
+ if (v.status.customize) {
+ v.tools.createCustomizeWizard();
+ v.tools.removeCustomizeLink();
+ } else {
+ v.tools.removeCustomizeWizard();
+ if (v.conf.debug) {
+ v.tools.createCustomizeLink();
+ }
+ }
+ }
+ return graph;
+ };
+
+ /**
+ * Gets or sets the debug mode. When debug is enabled, there is a link rendered in the SVG to start the customize wizard and debug messages are written to the console.
+ *
+ * example.debug(true);
+ * @see {@link module:API.customize}
+ * @param {boolean} [value] - The new mode.
+ * @returns {(boolean|Object)} The current mode if no parameter is given or the graph object for method chaining.
+ */
+ graph.debug = function(value) {
+ if (!arguments.length) {
+ return v.conf.debug;
+ }
+ v.conf.debug = value;
+ if (v.status.graphStarted) {
+ if (v.conf.debug) {
+ v.tools.createCustomizeLink();
+ } else {
+ v.tools.removeCustomizeLink();
+ }
+ }
+ return graph;
+ };
+
+ /**
+ * Returns the detected user agent. Expects no parameter and terminates the method chain:
+ *
+ * example.userAgent();
+ * @see {@link module:API.inspect}
+ * @returns {string} The detected user agent.
+ */
+ graph.userAgent = function() {
+ return v.status.userAgent;
+ };
+
+ /**
+ * Shows the current closure object, which holds all functions and data. This method expects no parameter and terminates the method chain:
+ *
+ * example.inspect();
+ * @see {@link module:API.userAgent}
+ * @returns {Object} The graph's internal object with all functions and data.
+ */
+ graph.inspect = function() {
+ return v;
+ };
+
+ /**
+ * Shows the current plugin version. This method expects no parameter and terminates the method chain:
+ *
+ * example.version();
+ * @see {@link module:API.userAgent}
+ * @returns {string} The plugin version.
+ */
+ graph.version = function() {
+ return v.version;
+ };
+
+ /*******************************************************************************************************************
+ * Startup code - runs on the initialization of a new chart - example:
+ * var myChart = net_gobrechts_d3_force( domContainerId, options, apexPluginId ).start();
+ */
+
+ v.main.init();
+
+ if (v.status.apexPluginId) {
+ // bind to the apexrefresh event, so that this region can be refreshed by a dynamic action
+ apex.jQuery("#" + v.dom.containerId).bind("apexrefresh", function() {
+ graph.start();
+ });
+ /*
+ //resume on window resize
+ apex.jQuery(window).on("apexwindowresized", function() {
+ graph.resume();
+ });
+ apex.jQuery("#t_Button_navControl").click(function() {
+ setTimeout(function() {
+ apex.jQuery(window).trigger("apexwindowresized");
+ }, 500);
+ });
+ */
+
+ }
+
+ // return the graph object for method chaining
+ return graph;
+
+}
diff --git a/main/scripts/d3-force-apex-plugin/dist/d3-force-3.1.0.min.js b/main/scripts/d3-force-apex-plugin/dist/d3-force-3.1.0.min.js
new file mode 100644
index 0000000..8d8397e
--- /dev/null
+++ b/main/scripts/d3-force-apex-plugin/dist/d3-force-3.1.0.min.js
@@ -0,0 +1,7 @@
+/**
+ * D3 Force Network Chart - v3.1.0 - 2019-04-28
+ * https://github.com/ogobrecht/d3-force-apex-plugin
+ * Copyright (c) 2015-2019 Ottmar Gobrecht - MIT license
+ */
+
+function netGobrechtsD3Force(t,e,o,n){"use strict";var g={conf:{},confDefaults:{},data:{},dom:{},events:{},lib:{},main:{},status:{},tools:{},version:"3.1.0"},p={};return g.main.init=function(){g.dom.containerId=t||"D3Force"+Math.floor(1e6*Math.random()),g.confUser=e||{},g.status.apexPluginId=o,g.status.apexPageItemsToSubmit=!(!n||""===n)&&n.replace(/\s/g,"").split(","),g.main.setupConfiguration(),g.main.setupDom(),g.main.setupFunctionReferences()},g.main.setupConfiguration=function(){g.conf.debug=g.status.apexPluginId&&1===apex.jQuery("#pdebug").length,g.status.debugPrefix="D3 Force in DOM container #"+g.dom.containerId+": ",g.status.customize=!1,g.status.customizeCurrentMenu="nodes",g.status.customizeCurrentTabPosition=null,g.status.forceTickCounter=0,g.status.forceStartTime=0,g.status.forceRunning=!1,g.status.graphStarted=!1,g.status.graphRendering=!1,g.status.graphReady=!1,g.status.graphOldPositions=null,g.status.sampleData=!1,g.status.wrapLabelsOnNextTick=!1,g.status.labelFontSize=null,g.status.resizeTriggered=!1,g.confDefaults.minNodeRadius={display:!0,relation:"node",type:"number",val:6,options:[12,11,10,9,8,7,6,5,4,3,2,1]},g.confDefaults.maxNodeRadius={display:!0,relation:"node",type:"number",val:18,options:[36,34,32,30,28,26,24,22,20,18,16,14,12]},g.confDefaults.colorScheme={display:!0,relation:"node",type:"text",val:"color20",options:["color20","color20b","color20c","color10","direct"]},g.confDefaults.dragMode={display:!0,relation:"node",type:"bool",val:!0,options:[!0,!1]},g.confDefaults.pinMode={display:!0,relation:"node",type:"bool",val:!1,options:[!0,!1]},g.confDefaults.nodeEventToStopPinMode={display:!0,relation:"node",type:"text",val:"contextmenu",options:["none","dblclick","contextmenu"]},g.confDefaults.onNodeContextmenuPreventDefault={display:!0,relation:"node",type:"bool",val:!1,options:[!0,!1]},g.confDefaults.nodeEventToOpenLink={display:!0,relation:"node",type:"text",val:"dblclick",options:["none","click","dblclick","contextmenu"]},g.confDefaults.nodeLinkTarget={display:!0,relation:"node",type:"text",val:"_blank",options:["none","_blank","nodeID","domContainerID"]},g.confDefaults.showLabels={display:!0,relation:"label",type:"bool",val:!0,options:[!0,!1]},g.confDefaults.wrapLabels={display:!0,relation:"label",type:"bool",val:!1,options:[!0,!1]},g.confDefaults.labelSplitCharacter={display:!0,relation:"label",type:"text",val:"none",options:["none","^","`","°","\\","|","/","#",":","::"]},g.confDefaults.wrappedLabelWidth={display:!0,relation:"label",type:"number",val:80,options:[200,190,180,170,160,150,140,130,120,110,100,90,80,70,60,50,40]},g.confDefaults.wrappedLabelLineHeight={display:!0,relation:"label",type:"number",val:1.2,options:[1.5,1.4,1.3,1.2,1.1,1]},g.confDefaults.labelsCircular={display:!0,relation:"label",type:"bool",val:!1,options:[!0,!1]},g.confDefaults.labelDistance={display:!0,relation:"label",type:"number",val:12,options:[30,28,26,24,22,20,18,16,14,12,10,8,6,4,2]},g.confDefaults.preventLabelOverlappingOnForceEnd={display:!0,relation:"label",type:"bool",val:!1,options:[!0,!1]},g.confDefaults.labelPlacementIterations={display:!0,relation:"label",type:"number",val:250,options:[2e3,1e3,500,250,125]},g.confDefaults.showTooltips={display:!0,relation:"node",type:"bool",val:!0,options:[!0,!1]},g.confDefaults.tooltipPosition={display:!0,relation:"node",type:"text",val:"svgTopRight",options:["node","svgTopLeft","svgTopRight"]},g.confDefaults.alignFixedNodesToGrid={display:!0,relation:"node",type:"bool",val:!1,options:[!0,!1]},g.confDefaults.gridSize={display:!0,relation:"node",type:"number",val:50,options:[150,140,130,120,110,100,90,80,70,60,50,40,30,20,10]},g.confDefaults.linkDistance={display:!0,relation:"link",type:"number",val:80,options:[120,110,100,90,80,70,60,50,40,30,20]},g.confDefaults.showLinkDirection={display:!0,relation:"link",type:"bool",val:!0,options:[!0,!1]},g.confDefaults.showSelfLinks={display:!0,relation:"link",type:"bool",val:!0,options:[!0,!1]},g.confDefaults.selfLinkDistance={display:!0,relation:"link",type:"number",val:20,options:[30,28,26,24,22,20,18,16,14,12,10,8]},g.confDefaults.useDomParentWidth={display:!0,relation:"graph",type:"bool",val:!1,options:[!0,!1]},g.confDefaults.width={display:!0,relation:"graph",type:"number",val:600,options:[1200,1150,1100,1050,1e3,950,900,850,800,750,700,650,600,550,500,450,400,350,300]},g.confDefaults.height={display:!0,relation:"graph",type:"number",val:400,options:[1200,1150,1100,1050,1e3,950,900,850,800,750,700,650,600,550,500,450,400,350,300]},g.confDefaults.setDomParentPaddingToZero={display:!0,relation:"graph",type:"bool",val:!1,options:[!0,!1]},g.confDefaults.showBorder={display:!0,relation:"graph",type:"bool",val:!0,options:[!0,!1]},g.confDefaults.showLegend={display:!0,relation:"graph",type:"bool",val:!0,options:[!0,!1]},g.confDefaults.showLoadingIndicatorOnAjaxCall={display:!0,relation:"graph",type:"bool",val:!0,options:[!0,!1]},g.confDefaults.lassoMode={display:!0,relation:"graph",type:"bool",val:!1,options:[!0,!1]},g.confDefaults.zoomMode={display:!0,relation:"graph",type:"bool",val:!1,options:[!0,!1]},g.confDefaults.minZoomFactor={display:!0,relation:"graph",type:"number",val:.2,options:[1,.9,.8,.7,.6,.5,.4,.3,.2,.1]},g.confDefaults.maxZoomFactor={display:!0,relation:"graph",type:"number",val:5,options:[10,9,8,7,6,5,4,3,2,1]},g.confDefaults.transform={display:!1,relation:"graph",type:"object",val:{translate:[0,0],scale:1}},g.confDefaults.zoomToFitOnForceEnd={display:!0,relation:"graph",type:"bool",val:!0,options:[!0,!1]},g.confDefaults.zoomToFitOnResize={display:!0,relation:"graph",type:"bool",val:!0,options:[!0,!1]},g.confDefaults.keepAspectRatioOnResize={display:!0,relation:"graph",type:"bool",val:!0,options:[!0,!1]},g.confDefaults.onResizeFunctionTimeout={display:!0,relation:"graph",type:"number",val:300,options:[1e3,900,800,700,600,500,400,300,200,100,0]},g.confDefaults.autoRefresh={display:!0,relation:"graph",type:"bool",val:!1,options:[!0,!1]},g.confDefaults.refreshInterval={display:!0,relation:"graph",type:"number",val:5e3,options:[6e4,3e4,15e3,1e4,5e3,2500]},g.confDefaults.forceTimeLimit={display:!0,relation:"graph",type:"number",val:1/0,options:[1/0,6400,3200,1600,800,400,200,100]},g.confDefaults.chargeDistance={display:!1,relation:"graph",type:"number",val:1/0,options:[1/0,25600,12800,6400,3200,1600,800,400,200,100],internal:!0},g.confDefaults.charge={display:!0,relation:"graph",type:"number",val:-350,options:[-1e3,-950,-900,-850,-800,-750,-700,-650,-600,-550,-500,-450,-400,-350,-300,-250,-200,-150,-100,-50,0],internal:!0},g.confDefaults.gravity={display:!0,relation:"graph",type:"number",val:.1,options:[1,.95,.9,.85,.8,.75,.7,.65,.6,.55,.5,.45,.4,.35,.3,.25,.2,.15,.1,.05,0],internal:!0},g.confDefaults.linkStrength={display:!0,relation:"graph",type:"number",val:1,options:[1,.95,.9,.85,.8,.75,.7,.65,.6,.55,.5,.45,.4,.35,.3,.25,.2,.15,.1,.05,0],internal:!0},g.confDefaults.friction={display:!0,relation:"graph",type:"number",val:.9,options:[1,.95,.9,.85,.8,.75,.7,.65,.6,.55,.5,.45,.4,.35,.3,.25,.2,.15,.1,.05,0],internal:!0},g.confDefaults.theta={display:!0,relation:"graph",type:"number",val:.8,options:[1,.95,.9,.85,.8,.75,.7,.65,.6,.55,.5,.45,.4,.35,.3,.25,.2,.15,.1,.05,0],internal:!0},g.conf.debug=void 0!==g.confUser.debug&&g.tools.parseBool(g.confUser.debug),g.conf.minNodeRadius=g.confUser.minNodeRadius||g.confDefaults.minNodeRadius.val,g.conf.maxNodeRadius=g.confUser.maxNodeRadius||g.confDefaults.maxNodeRadius.val,g.conf.colorScheme=g.confUser.colorScheme||g.confDefaults.colorScheme.val,g.conf.dragMode=void 0!==g.confUser.dragMode?g.tools.parseBool(g.confUser.dragMode):g.confDefaults.dragMode.val,g.conf.pinMode=void 0!==g.confUser.pinMode?g.tools.parseBool(g.confUser.pinMode):g.confDefaults.pinMode.val,g.conf.nodeEventToStopPinMode=g.confUser.nodeEventToStopPinMode||g.confDefaults.nodeEventToStopPinMode.val,g.conf.onNodeContextmenuPreventDefault=void 0!==g.confUser.onNodeContextmenuPreventDefault?g.tools.parseBool(g.confUser.onNodeContextmenuPreventDefault):g.confDefaults.onNodeContextmenuPreventDefault.val,g.conf.nodeEventToOpenLink=g.confUser.nodeEventToOpenLink||g.confDefaults.nodeEventToOpenLink.val,g.conf.nodeLinkTarget=g.confUser.nodeLinkTarget||g.confDefaults.nodeLinkTarget.val,g.conf.showLabels=void 0!==g.confUser.showLabels?g.tools.parseBool(g.confUser.showLabels):g.confDefaults.showLabels.val,g.conf.wrapLabels=void 0!==g.confUser.wrapLabels?g.tools.parseBool(g.confUser.wrapLabels):g.confDefaults.wrapLabels.val,g.conf.labelSplitCharacter=g.confUser.labelSplitCharacter||g.confDefaults.labelSplitCharacter.val,g.conf.wrappedLabelWidth=g.confUser.wrappedLabelWidth||g.confDefaults.wrappedLabelWidth.val,g.conf.wrappedLabelLineHeight=g.confUser.wrappedLabelLineHeight||g.confDefaults.wrappedLabelLineHeight.val,g.conf.labelsCircular=void 0!==g.confUser.labelsCircular?g.tools.parseBool(g.confUser.labelsCircular):g.confDefaults.labelsCircular.val,g.conf.labelDistance=g.confUser.labelDistance||g.confDefaults.labelDistance.val,g.conf.preventLabelOverlappingOnForceEnd=void 0!==g.confUser.preventLabelOverlappingOnForceEnd?g.tools.parseBool(g.confUser.preventLabelOverlappingOnForceEnd):g.confDefaults.preventLabelOverlappingOnForceEnd.val,g.conf.labelPlacementIterations=g.confUser.labelPlacementIterations||g.confDefaults.labelPlacementIterations.val,g.conf.showTooltips=void 0!==g.confUser.showTooltips?g.tools.parseBool(g.confUser.showTooltips):g.confDefaults.showTooltips.val,g.conf.tooltipPosition=g.confUser.tooltipPosition||g.confDefaults.tooltipPosition.val,g.conf.alignFixedNodesToGrid=void 0!==g.confUser.alignFixedNodesToGrid?g.tools.parseBool(g.confUser.alignFixedNodesToGrid):g.confDefaults.alignFixedNodesToGrid.val,g.conf.gridSize=g.confUser.gridSize&&0',g.status.userAgent=navigator.userAgent,g.status.userAgentIe9To11=!1,(-1!==navigator.appVersion.indexOf("MSIE 9")||-1!==navigator.appVersion.indexOf("MSIE 10")||-1!==g.status.userAgent.indexOf("Trident")&&-1!==g.status.userAgent.indexOf("rv:11"))&&(g.status.userAgentIe9To11=!0,g.tools.logError("Houston, we have a problem - user agent is IE 9, 10 or 11 - we have to provide a fix for markers: http://stackoverflow.com/questions/15588478/internet-explorer-10-not-showing-svg-path-d3-js-graph"))},g.main.setupDom=function(){var t,e;g.dom.body=d3.select("body"),null===document.querySelector("#"+g.dom.containerId)?g.dom.container=g.dom.body.append("div").attr("id",g.dom.containerId):(g.dom.container=d3.select("#"+g.dom.containerId),d3.selectAll("#"+g.dom.containerId+"_tooltip, #"+g.dom.containerId+"_customizing").remove()),null===document.querySelector("#"+g.dom.containerId+" svg")?g.dom.svg=g.dom.container.append("svg"):(g.dom.svg=d3.select("#"+g.dom.containerId+" svg"),d3.selectAll("#"+g.dom.containerId+" svg *").remove()),g.dom.svgParent=d3.select(g.dom.svg.node().parentNode),g.conf.setDomParentPaddingToZero&&g.dom.svgParent.style("padding","0"),t=g.tools.getGraphWidth(),e=g.tools.getGraphHeight(),g.dom.svg.attr("class","net_gobrechts_d3_force").classed("border",g.conf.showBorder).attr("width",t).attr("height",e),g.conf.useDomParentWidth&&g.dom.svg.attr("width",g.tools.getSvgParentInnerWidth()),g.dom.defs=g.dom.svg.append("defs"),g.dom.graphOverlay=g.dom.svg.append("g").attr("class","graphOverlay"),g.dom.graphOverlaySizeHelper=g.dom.graphOverlay.append("rect").attr("class","graphOverlaySizeHelper"),g.dom.graph=g.dom.graphOverlay.append("g").attr("class","graph"),g.dom.legend=g.dom.svg.append("g").attr("class","legend"),g.dom.loading=g.dom.svg.append("svg:g").attr("class","loading").style("display","none"),g.dom.loadingRect=g.dom.loading.append("svg:rect").attr("width",t).attr("height",e),g.dom.loadingText=g.dom.loading.append("svg:text").attr("x",t/2).attr("y",e/2).text("Loading..."),g.dom.defs.append("svg:marker").attr("id",g.dom.containerId+"_highlighted").attr("class","highlighted").attr("viewBox","0 0 10 10").attr("refX",10).attr("refY",5).attr("markerWidth",5).attr("markerHeight",5).attr("orient","auto").attr("markerUnits","strokeWidth").append("svg:path").attr("d","M0,0 L10,5 L0,10"),g.dom.defs.append("svg:marker").attr("id",g.dom.containerId+"_normal").attr("class","normal").attr("viewBox","0 0 10 10").attr("refX",10).attr("refY",5).attr("markerWidth",5).attr("markerHeight",5).attr("orient","auto").attr("markerUnits","strokeWidth").append("svg:path").attr("d","M0,0 L10,5 L0,10"),null===document.querySelector("#"+g.dom.containerId+"_tooltip")?g.dom.tooltip=g.dom.body.append("div").attr("id",g.dom.containerId+"_tooltip").attr("class","net_gobrechts_d3_force_tooltip").style("top","0px").style("left","0px"):g.dom.tooltip=d3.select("#"+g.dom.containerId+"_tooltip")},g.main.setupFunctionReferences=function(){g.main.force=d3.layout.force().on("start",function(){g.tools.log("Force started."),g.status.customize&&g.dom.customizePositions&&g.dom.customizePositions.text("Force started - wait for end event to show positions..."),g.status.forceTickCounter=0,g.status.forceStartTime=(new Date).getTime(),g.status.forceRunning=!0,g.tools.log("Event forcestart triggered."),g.tools.triggerApexEvent(document.querySelector("#"+g.dom.containerId),"net_gobrechts_d3_force_forcestart"),"function"==typeof g.conf.onForceStartFunction&&g.conf.onForceStartFunction.call(g.dom.svg)}).on("tick",function(){g.status.forceTickCounter+=1,g.status.userAgentIe9To11&&g.conf.showLinkDirection&&(g.main.links.each(function(){this.parentNode.insertBefore(this,this)}),g.main.selfLinks.each(function(){this.parentNode.insertBefore(this,this)})),g.main.selfLinks.attr("transform",function(t){return"translate("+t.source.x+","+t.source.y+")"}),g.main.links.attr("x1",function(t){return g.tools.adjustSourceX(t)}).attr("y1",function(t){return g.tools.adjustSourceY(t)}).attr("x2",function(t){return g.tools.adjustTargetX(t)}).attr("y2",function(t){return g.tools.adjustTargetY(t)}),g.conf.showLabels&&(g.main.labels.attr("x",function(t){return t.x}).attr("y",function(t){return t.y-t.radius-g.conf.labelDistance}),g.status.wrapLabelsOnNextTick&&(g.main.labels.call(g.tools.wrapLabels,g.conf.wrappedLabelWidth),g.status.wrapLabelsOnNextTick=!1),g.conf.wrapLabels&&g.main.labels.each(function(){var t=d3.select(this),e=t.attr("y")-(t.attr("lines")-1)*g.status.labelFontSize*g.conf.wrappedLabelLineHeight;t.attr("y",e).selectAll("tspan").attr("x",t.attr("x")).attr("y",e)}),g.main.labelPaths.attr("transform",function(t){return"translate("+t.x+","+t.y+")"}),g.main.linkLabelPaths.attr("d",function(t){return"M "+t.source.x+" "+t.source.y+" L "+t.target.x+" "+t.target.y}),g.main.linkLabels.attr("transform",function(t,e){if(t.target.xg.conf.forceTimeLimit&&g.main.force.stop()}).on("end",function(){g.conf.showLabels&&g.conf.preventLabelOverlappingOnForceEnd&&(g.data.simulatedAnnealingLabels=[],g.data.simulatedAnnealingAnchors=[],g.main.labels.each(function(t,e){var o=d3.select(this);g.data.simulatedAnnealingLabels[e]={width:this.getBBox().width,height:this.getBBox().height,x:t.x,y:o.attr("y")-(o.attr("lines")-1)*g.status.labelFontSize*g.conf.wrappedLabelLineHeight}}),g.main.nodes.filter(function(t){return!t.LABELCIRCULAR&&!g.conf.labelsCircular}).each(function(t,e){g.data.simulatedAnnealingAnchors[e]={x:t.x,y:t.y-t.radius-g.conf.labelDistance,r:.5}}),g.lib.labelerPlugin().label(g.data.simulatedAnnealingLabels).anchor(g.data.simulatedAnnealingAnchors).width(g.tools.getGraphWidth()).height(g.tools.getGraphHeight()).start(g.conf.labelPlacementIterations),g.main.labels.each(function(t,e){var o=d3.select(this),n=g.data.simulatedAnnealingLabels[e].x,a=g.data.simulatedAnnealingLabels[e].y;g.conf.wrapLabels?(a-=(o.attr("lines")-1)*g.status.labelFontSize*g.conf.wrappedLabelLineHeight,o.transition().duration(800).attr("x",n).attr("y",a).selectAll("tspan").attr("x",n).attr("y",a)):o.transition().duration(800).attr("x",n).attr("y",a)})),g.status.forceRunning=!1;var t=(new Date).getTime()-g.status.forceStartTime,e=(t/1e3).toFixed(1),o=Math.round(g.status.forceTickCounter/(t/1e3)),n=Math.round(t/g.status.forceTickCounter);g.status.customize&&g.dom.customizePositions&&g.dom.customizePositions.text(JSON.stringify(p.positions())),g.tools.log("Force ended."),g.tools.log(e+" seconds, "+g.status.forceTickCounter+" ticks to cool down ("+o+" ticks/s, "+n+" ms/tick)."),g.tools.log("Event forceend triggered."),g.tools.triggerApexEvent(document.querySelector("#"+g.dom.containerId),"net_gobrechts_d3_force_forceend"),g.conf.zoomToFitOnForceEnd?p.zoomToFit():g.conf.zoomMode||p.center(),"function"==typeof g.conf.onForceEndFunction&&g.conf.onForceEndFunction.call(g.dom.svg)}),g.main.drag=g.main.force.drag(),g.main.lasso=g.lib.lassoPlugin().closePathDistance(100).closePathSelect(!0).hoverSelect(!0).area(g.dom.graphOverlay).pathContainer(g.dom.svg),g.main.zoom=d3.behavior.zoom(),g.main.zoomed=function(){g.conf.transform={translate:g.main.zoom.translate(),scale:g.main.zoom.scale()},g.dom.graph.attr("transform","translate("+g.main.zoom.translate()+")scale("+g.main.zoom.scale()+")"),g.tools.writeConfObjectIntoWizard()},g.main.interpolateZoom=function(t,n,e){if(g.status.graphStarted)return ng.conf.maxZoomFactor&&(n=g.conf.maxZoomFactor),d3.transition().duration(e).tween("zoom",function(){var e=d3.interpolate(g.main.zoom.translate(),t),o=d3.interpolate(g.main.zoom.scale(),n);return function(t){g.main.zoom.scale(o(t)).translate(e(t)),g.main.zoomed()}})}},g.tools.parseBool=function(t){switch(String(t).trim().toLowerCase()){case"true":case"yes":case"1":return!0;case"false":case"no":case"0":case"":default:return!1}},g.tools.parseXml=function(t){var e=null;if(t)if(window.DOMParser)try{e=(new DOMParser).parseFromString(t,"text/xml")}catch(t){e=null,g.tools.logError("DOMParser - unable to parse XML: "+t.message)}else if(window.ActiveXObject)try{(e=new ActiveXObject("Microsoft.XMLDOM")).async=!1,e.loadXML(t)||g.tools.logError("Microsoft.XMLDOM - unable to parse XML: "+e.parseError.reason+e.parseError.srcText)}catch(t){e=null,g.tools.logError("Microsoft.XMLDOM - unable to parse XML: "+t.message)}return e},g.tools.xmlToJson=function(t){var n,e,a,o,s,r=null,i=function(t){if(n={},0g.conf.gridSize/2?t-o+g.conf.gridSize:t-o:e<=t?(n=e-(o=e%g.conf.gridSize))===e&&(n-=g.conf.gridSize):t<=g.conf.gridSize/2?n=g.conf.gridSize:e<=(n=(o=t%g.conf.gridSize)>g.conf.gridSize/2?t-o+g.conf.gridSize:t-o)&&(n-=g.conf.gridSize),n},g.tools.adjustSourceX=function(t){return t.source.x+Math.cos(g.tools.calcAngle(t))*t.source.radius},g.tools.adjustSourceY=function(t){return t.source.y+Math.sin(g.tools.calcAngle(t))*t.source.radius},g.tools.adjustTargetX=function(t){return t.target.x-Math.cos(g.tools.calcAngle(t))*t.target.radius},g.tools.adjustTargetY=function(t){return t.target.y-Math.sin(g.tools.calcAngle(t))*t.target.radius},g.tools.calcAngle=function(t){return Math.atan2(t.target.y-t.source.y,t.target.x-t.source.x)},g.tools.getSelfLinkPath=function(t){var e=t.source.radius,o=t.source.radius+g.conf.selfLinkDistance,n={source:{x:0,y:0,radius:e},target:{x:0-o/2,y:0+o,radius:e}},a={source:{x:0+o/2,y:0+o,radius:e},target:{x:0,y:0,radius:e}},s="M"+g.tools.adjustSourceX(n)+","+g.tools.adjustSourceY(n);return s+=" L"+(0-o/2)+","+(0+o),s+=" A"+o+","+o+" 0 0,0 "+(0+o/2)+","+(0+o),s+=" L"+g.tools.adjustTargetX(a)+","+g.tools.adjustTargetY(a)},g.tools.getLabelPath=function(t){var e=t.radius+g.conf.labelDistance,o="M"+(0-e)+",0";return o+=" a"+e+","+e+" 0 0,1 "+2*e+",0",o+=" a"+e+","+e+" 0 0,1 -"+2*e+",0"},g.tools.getPatternId=function(t){return g.dom.containerId+"_pattern_"+t.ID},g.tools.getLinkId=function(t){return t.FROMID+"_"+t.TOID},g.tools.getPathId=function(t){return g.dom.containerId+"_path_"+g.tools.getLinkId(t)},g.tools.openLink=function(t){"none"===g.conf.nodeLinkTarget?window.location.assign(t.LINK):"nodeID"===g.conf.nodeLinkTarget?window.open(t.LINK,t.ID).focus():"domContainerID"===g.conf.nodeLinkTarget?window.open(t.LINK,g.dom.containerId).focus():window.open(t.LINK,g.conf.nodeLinkTarget).focus()},g.tools.applyConfigurationObject=function(t){var e;for(e in t)t.hasOwnProperty(e)&&g.conf.hasOwnProperty(e)&&t[e]!==g.conf[e]&&p[e](t[e])},g.tools.zoomEventProxy=function(t){return function(){g.conf.dragMode&&(!g.conf.dragMode||"circle"===d3.event.target.tagName)||!g.conf.zoomMode||d3.event.altKey||d3.event.shiftKey||t.apply(this,arguments)}},g.tools.lassoEventProxy=function(t){return function(){g.conf.dragMode&&"circle"===d3.event.target.tagName||!g.conf.lassoMode||g.conf.zoomMode&&!d3.event.altKey&&!d3.event.shiftKey||t.apply(this,arguments)}},g.tools.showTooltip=function(t){var e;g.dom.tooltip.html(t).style("display","block"),"svgTopLeft"===g.conf.tooltipPosition?(e=g.tools.getOffsetRect(g.dom.svg.node()),g.dom.tooltip.style("top",e.top+(g.dom.svg.style("border-width")?parseInt(g.dom.svg.style("border-width")):1)+"px").style("left",e.left+(g.dom.svg.style("border-width")?parseInt(g.dom.svg.style("border-width")):1)+"px")):"svgTopRight"===g.conf.tooltipPosition?(e=g.tools.getOffsetRect(g.dom.svg.node()),g.dom.tooltip.style("top",e.top+parseInt(g.dom.svg.style("border-width")?parseInt(g.dom.svg.style("border-width")):1)+"px").style("left",e.left+parseInt(g.dom.svg.style("width"))+parseInt(g.dom.svg.style("border-width")?parseInt(g.dom.svg.style("border-width")):1)-parseInt(g.dom.tooltip.style("width"))-2*parseInt(g.dom.tooltip.style("border-width")?parseInt(g.dom.tooltip.style("border-width")):0)-parseInt(g.dom.tooltip.style("padding-left"))-parseInt(g.dom.tooltip.style("padding-right"))+"px")):g.dom.tooltip.style("left",d3.event.pageX+10+"px").style("top",d3.event.pageY+"px")},g.tools.hideTooltip=function(){g.dom.tooltip.style("display","none")},g.tools.onLinkClick=function(t){if(d3.event.defaultPrevented)return null;g.tools.log("Event linkclick triggered."),g.tools.triggerApexEvent(this,"net_gobrechts_d3_force_linkclick",t),"function"==typeof g.conf.onLinkClickFunction&&g.conf.onLinkClickFunction.call(this,d3.event,t)},g.tools.getMarkerUrl=function(t){return g.conf.showLinkDirection?"url(#"+g.dom.containerId+"_"+(t.COLOR?t.COLOR:"normal")+")":null},g.tools.getMarkerUrlHighlighted=function(){return g.conf.showLinkDirection?"url(#"+g.dom.containerId+"_highlighted)":null},g.tools.onLinkMouseenter=function(t){g.conf.showTooltips&&t.INFOSTRING&&g.tools.showTooltip(t.INFOSTRING)},g.tools.onLinkMouseleave=function(){g.conf.showTooltips&&g.tools.hideTooltip()},g.tools.onNodeMouseenter=function(e){g.main.nodes.classed("highlighted",function(t){return g.tools.neighboring(t,e)}),g.main.links.classed("highlighted",function(t){return t.source.ID===e.ID||t.target.ID===e.ID}).style("marker-end",function(t){return t.source.ID===e.ID||t.target.ID===e.ID?g.tools.getMarkerUrlHighlighted(t):g.tools.getMarkerUrl(t)}),g.main.selfLinks.classed("highlighted",function(t){return t.FROMID===e.ID}).style("marker-end",function(t){return t.source.ID===e.ID||t.target.ID===e.ID?g.tools.getMarkerUrlHighlighted(t):g.tools.getMarkerUrl(t)}),g.conf.showLabels&&(g.main.labels.classed("highlighted",function(t){return t.ID===e.ID}),g.main.labelsCircular.classed("highlighted",function(t){return t.ID===e.ID})),d3.select(this).classed("highlighted",!0),g.tools.log("Event nodemouseenter triggered."),g.tools.triggerApexEvent(this,"net_gobrechts_d3_force_mouseenter",e),"function"==typeof g.conf.onNodeMouseenterFunction&&g.conf.onNodeMouseenterFunction.call(this,d3.event,e),g.conf.showTooltips&&e.INFOSTRING&&g.tools.showTooltip(e.INFOSTRING)},g.tools.onNodeMouseleave=function(t){g.main.nodes.classed("highlighted",!1),g.main.links.classed("highlighted",!1).style("marker-end",g.tools.getMarkerUrl),g.main.selfLinks.classed("highlighted",!1).style("marker-end",g.tools.getMarkerUrl),g.conf.showLabels&&(g.main.labels.classed("highlighted",!1),g.main.labelsCircular.classed("highlighted",!1)),g.tools.log("Event nodemouseleave triggered."),g.tools.triggerApexEvent(this,"net_gobrechts_d3_force_mouseleave",t),"function"==typeof g.conf.onNodeMouseleaveFunction&&g.conf.onNodeMouseleaveFunction.call(this,d3.event,t),g.conf.showTooltips&&g.tools.hideTooltip()},g.tools.onNodeClick=function(t){if(d3.event.defaultPrevented)return null;t.LINK&&"click"===g.conf.nodeEventToOpenLink&&g.tools.openLink(t),"click"===g.conf.nodeEventToStopPinMode&&d3.select(this).classed("fixed",t.fixed=0),g.tools.log("Event nodeclick triggered."),g.tools.triggerApexEvent(this,"net_gobrechts_d3_force_click",t),"function"==typeof g.conf.onNodeClickFunction&&g.conf.onNodeClickFunction.call(this,d3.event,t)},g.tools.onNodeDblclick=function(t){t.LINK&&"dblclick"===g.conf.nodeEventToOpenLink&&g.tools.openLink(t),"dblclick"===g.conf.nodeEventToStopPinMode&&d3.select(this).classed("fixed",t.fixed=0),g.tools.log("Event nodedblclick triggered."),g.tools.triggerApexEvent(this,"net_gobrechts_d3_force_dblclick",t),"function"==typeof g.conf.onNodeDblclickFunction&&g.conf.onNodeDblclickFunction.call(this,d3.event,t)},g.tools.onNodeContextmenu=function(t){g.conf.onNodeContextmenuPreventDefault&&d3.event.preventDefault(),t.LINK&&"contextmenu"===g.conf.nodeEventToOpenLink&&g.tools.openLink(t),"contextmenu"===g.conf.nodeEventToStopPinMode&&d3.select(this).classed("fixed",t.fixed=0),g.tools.log("Event nodecontextmenu triggered."),g.tools.triggerApexEvent(this,"net_gobrechts_d3_force_contextmenu",t),"function"==typeof g.conf.onNodeContextmenuFunction&&g.conf.onNodeContextmenuFunction.call(this,d3.event,t)},g.tools.onLassoStart=function(t){var e={numberOfSelectedNodes:0,idsOfSelectedNodes:null};e.numberOfNodes=t.size(),e.nodes=t,g.tools.log("Event lassostart triggered."),g.tools.triggerApexEvent(document.querySelector("#"+g.dom.containerId),"net_gobrechts_d3_force_lassostart",e),"function"==typeof g.conf.onLassoStartFunction&&g.conf.onLassoStartFunction.call(g.dom.svg,d3.event,e)},g.tools.onLassoEnd=function(t){var e={numberOfSelectedNodes:0,idsOfSelectedNodes:""};e.numberOfNodes=t.size(),(e.nodes=t).each(function(t){t.selected&&(e.idsOfSelectedNodes+=t.ID+":",e.numberOfSelectedNodes++)}),e.idsOfSelectedNodes=0'+l+""),a=n.append("td"),s=a.append("select").attr("id",g.dom.containerId+"_"+l).attr("name",l).attr("value",g.conf[l]).attr("tabindex",d+1).classed("warning",g.confDefaults[l].internal).on("change",u),i=!1,f(l),i||(s.append("option").attr("value",g.conf[l]).attr("selected","selected").text(g.conf[l]),g.confDefaults[l].options.push(g.conf[l])),"pinMode"===l&&a.append("a").text(" release all").attr("href",null).on("click",c));g.dom.customizeOptionsTable.style("width",d3.select(g.dom.customizeOptionsTable).node()[0][0].clientWidth+"px"),o.append("span").html(" "),(o=e.append("td").style("vertical-align","top").style("padding-left","5px")).append("span").html('Your Configuration Object