|
|
@ -7,80 +7,22 @@ var Camera = require('./Camera'); |
|
|
|
var Filter = require('./Filter'); |
|
|
|
var Slider = require('./Slider'); |
|
|
|
var StepNumber = require('./StepNumber'); |
|
|
|
var Settings = require('./Settings'); |
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// Definitions private to module
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
/// enumerate the available styles
|
|
|
|
Graph3d.STYLE = { |
|
|
|
BAR : 0, |
|
|
|
BARCOLOR: 1, |
|
|
|
BARSIZE : 2, |
|
|
|
DOT : 3, |
|
|
|
DOTLINE : 4, |
|
|
|
DOTCOLOR: 5, |
|
|
|
DOTSIZE : 6, |
|
|
|
GRID : 7, |
|
|
|
LINE : 8, |
|
|
|
SURFACE : 9 |
|
|
|
}; |
|
|
|
Graph3d.STYLE = Settings.STYLE; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Field names in the options hash which are of relevance to the user. |
|
|
|
* |
|
|
|
* Specifically, these are the fields which require no special handling, |
|
|
|
* and can be directly copied over. |
|
|
|
*/ |
|
|
|
var OPTIONKEYS = [ |
|
|
|
'width', |
|
|
|
'height', |
|
|
|
'filterLabel', |
|
|
|
'legendLabel', |
|
|
|
'xLabel', |
|
|
|
'yLabel', |
|
|
|
'zLabel', |
|
|
|
'xValueLabel', |
|
|
|
'yValueLabel', |
|
|
|
'zValueLabel', |
|
|
|
'showGrid', |
|
|
|
'showPerspective', |
|
|
|
'showShadow', |
|
|
|
'keepAspectRatio', |
|
|
|
'verticalRatio', |
|
|
|
'showAnimationControls', |
|
|
|
'animationInterval', |
|
|
|
'animationPreload', |
|
|
|
'animationAutoStart', |
|
|
|
'axisColor', |
|
|
|
'gridColor', |
|
|
|
'xCenter', |
|
|
|
'yCenter' |
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Field names in the options hash which are of relevance to the user. |
|
|
|
* |
|
|
|
* Same as OPTIONKEYS, but internally these fields are stored with |
|
|
|
* prefix 'default' in the name. |
|
|
|
* Following label is used in the settings to describe values which |
|
|
|
* should be determined by the code while running, from the current |
|
|
|
* data and graph style. |
|
|
|
* |
|
|
|
* Using 'undefined' directly achieves the same thing, but this is |
|
|
|
* more descriptive by describing the intent. |
|
|
|
*/ |
|
|
|
var PREFIXEDOPTIONKEYS = [ |
|
|
|
'xBarWidth', |
|
|
|
'yBarWidth', |
|
|
|
'valueMin', |
|
|
|
'valueMax', |
|
|
|
'xMin', |
|
|
|
'xMax', |
|
|
|
'xStep', |
|
|
|
'yMin', |
|
|
|
'yMax', |
|
|
|
'yStep', |
|
|
|
'zMin', |
|
|
|
'zMax', |
|
|
|
'zStep' |
|
|
|
]; |
|
|
|
var autoByDefault = undefined; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
@ -89,15 +31,8 @@ var PREFIXEDOPTIONKEYS = [ |
|
|
|
* These are the values used when a Graph3d instance is initialized |
|
|
|
* without custom settings. |
|
|
|
* |
|
|
|
* If a field is not in this list, a default value of 'undefined' can |
|
|
|
* be assumed. Of course, it does no harm to set a field explicitly to |
|
|
|
* 'undefined' here. |
|
|
|
* |
|
|
|
* A value of 'undefined' here normally means: |
|
|
|
* |
|
|
|
* 'derive from current data and graph style' |
|
|
|
* |
|
|
|
* In the code, this is indicated by the comment 'auto by default'. |
|
|
|
* If a field is not in this list, a default value of 'autoByDefault' |
|
|
|
* is assumed, which is just an alias for 'undefined'. |
|
|
|
*/ |
|
|
|
var DEFAULTS = { |
|
|
|
width : '400px', |
|
|
@ -114,29 +49,27 @@ var DEFAULTS = { |
|
|
|
showPerspective : true, |
|
|
|
showShadow : false, |
|
|
|
keepAspectRatio : true, |
|
|
|
verticalRatio : 0.5, // 0.1 to 1.0, where 1.0 results in a 'cube'
|
|
|
|
verticalRatio : 0.5, // 0.1 to 1.0, where 1.0 results in a 'cube'
|
|
|
|
|
|
|
|
showAnimationControls: undefined, // auto by default
|
|
|
|
animationInterval : 1000, // milliseconds
|
|
|
|
showAnimationControls: autoByDefault, |
|
|
|
animationInterval : 1000, // milliseconds
|
|
|
|
animationPreload : false, |
|
|
|
animationAutoStart : undefined, // auto by default
|
|
|
|
animationAutoStart : autoByDefault, |
|
|
|
|
|
|
|
axisColor : '#4D4D4D', |
|
|
|
gridColor : '#D3D3D3', |
|
|
|
xCenter : '55%', |
|
|
|
yCenter : '50%', |
|
|
|
|
|
|
|
// Following require special handling, therefore not mentioned in the OPTIONKEYS tables.
|
|
|
|
|
|
|
|
style : Graph3d.STYLE.DOT, |
|
|
|
tooltip : false, |
|
|
|
showLegend : undefined, // auto by default (based on graph style)
|
|
|
|
backgroundColor : undefined, |
|
|
|
showLegend : autoByDefault, // determined by graph style
|
|
|
|
backgroundColor : autoByDefault, |
|
|
|
|
|
|
|
dataColor : { |
|
|
|
fill : '#7DC1FF', |
|
|
|
stroke : '#3267D2', |
|
|
|
strokeWidth: 1 // px
|
|
|
|
strokeWidth: 1 // px
|
|
|
|
}, |
|
|
|
|
|
|
|
cameraPosition : { |
|
|
@ -145,103 +78,22 @@ var DEFAULTS = { |
|
|
|
distance : 1.7 |
|
|
|
}, |
|
|
|
|
|
|
|
// Following stored internally with field prefix 'default'
|
|
|
|
// All these are 'auto by default'
|
|
|
|
|
|
|
|
xBarWidth : undefined, |
|
|
|
yBarWidth : undefined, |
|
|
|
valueMin : undefined, |
|
|
|
valueMax : undefined, |
|
|
|
xMin : undefined, |
|
|
|
xMax : undefined, |
|
|
|
xStep : undefined, |
|
|
|
yMin : undefined, |
|
|
|
yMax : undefined, |
|
|
|
yStep : undefined, |
|
|
|
zMin : undefined, |
|
|
|
zMax : undefined, |
|
|
|
zStep : undefined |
|
|
|
xBarWidth : autoByDefault, |
|
|
|
yBarWidth : autoByDefault, |
|
|
|
valueMin : autoByDefault, |
|
|
|
valueMax : autoByDefault, |
|
|
|
xMin : autoByDefault, |
|
|
|
xMax : autoByDefault, |
|
|
|
xStep : autoByDefault, |
|
|
|
yMin : autoByDefault, |
|
|
|
yMax : autoByDefault, |
|
|
|
yStep : autoByDefault, |
|
|
|
zMin : autoByDefault, |
|
|
|
zMax : autoByDefault, |
|
|
|
zStep : autoByDefault |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Make first letter of parameter upper case. |
|
|
|
* |
|
|
|
* Source: http://stackoverflow.com/a/1026087
|
|
|
|
*/ |
|
|
|
function capitalize(str) { |
|
|
|
if (str === undefined || str === "") { |
|
|
|
return str; |
|
|
|
} |
|
|
|
|
|
|
|
return str.charAt(0).toUpperCase() + str.slice(1); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Add a prefix to a field name, taking style guide into account |
|
|
|
*/ |
|
|
|
function prefixFieldName(prefix, fieldName) { |
|
|
|
if (prefix === undefined || prefix === "") { |
|
|
|
return fieldName; |
|
|
|
} |
|
|
|
|
|
|
|
return prefix + capitalize(fieldName); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Forcibly copy fields from src to dst in a controlled manner. |
|
|
|
* |
|
|
|
* A given field in dst will always be overwitten. If this field |
|
|
|
* is undefined or not present in src, the field in dst will |
|
|
|
* be explicitly set to undefined. |
|
|
|
* |
|
|
|
* The intention here is to be able to reset all option fields. |
|
|
|
* |
|
|
|
* Only the fields mentioned in array 'fields' will be handled. |
|
|
|
* |
|
|
|
* @param fields array with names of fields to copy |
|
|
|
* @param prefix optional; prefix to use for the target fields. |
|
|
|
*/ |
|
|
|
function forceCopy(src, dst, fields, prefix) { |
|
|
|
var srcKey; |
|
|
|
var dstKey; |
|
|
|
|
|
|
|
for (var i in fields) { |
|
|
|
srcKey = fields[i]; |
|
|
|
dstKey = prefixFieldName(prefix, srcKey); |
|
|
|
|
|
|
|
dst[dstKey] = src[srcKey]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Copy fields from src to dst in a safe and controlled manner. |
|
|
|
* |
|
|
|
* Only the fields mentioned in array 'fields' will be copied over, |
|
|
|
* and only if these are actually defined. |
|
|
|
* |
|
|
|
* @param fields array with names of fields to copy |
|
|
|
* @param prefix optional; prefix to use for the target fields. |
|
|
|
*/ |
|
|
|
function safeCopy(src, dst, fields, prefix) { |
|
|
|
var srcKey; |
|
|
|
var dstKey; |
|
|
|
|
|
|
|
for (var i in fields) { |
|
|
|
srcKey = fields[i]; |
|
|
|
if (src[srcKey] === undefined) continue; |
|
|
|
|
|
|
|
dstKey = prefixFieldName(prefix, srcKey); |
|
|
|
|
|
|
|
dst[dstKey] = src[srcKey]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// Class Graph3d
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
@ -272,27 +124,7 @@ function Graph3d(container, data, options) { |
|
|
|
// create a frame and canvas
|
|
|
|
this.create(); |
|
|
|
|
|
|
|
//
|
|
|
|
// Set Defaults
|
|
|
|
//
|
|
|
|
|
|
|
|
// Handle the defaults which can be simply copied over
|
|
|
|
forceCopy(DEFAULTS, this, OPTIONKEYS); |
|
|
|
forceCopy(DEFAULTS, this, PREFIXEDOPTIONKEYS, 'default'); |
|
|
|
|
|
|
|
// Following are internal fields, not part of the user settings
|
|
|
|
this.margin = 10; // px
|
|
|
|
this.showGrayBottom = false; // TODO: this does not work correctly
|
|
|
|
this.showTooltip = false; |
|
|
|
this.dotSizeRatio = 0.02; // size of the dots as a fraction of the graph width
|
|
|
|
this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
|
|
|
|
|
|
|
|
// Handle the more complex ('special') fields
|
|
|
|
this._setSpecialSettings(DEFAULTS, this); |
|
|
|
|
|
|
|
//
|
|
|
|
// End Set Defaults
|
|
|
|
//
|
|
|
|
Settings.setDefaults(DEFAULTS, this); |
|
|
|
|
|
|
|
// the column indexes
|
|
|
|
this.colX = undefined; |
|
|
@ -459,223 +291,6 @@ Graph3d.prototype._calcTranslations = function(points, sort) { |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// Methods for handling settings
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Special handling for certain parameters |
|
|
|
* |
|
|
|
* 'Special' here means: setting requires more than a simple copy |
|
|
|
*/ |
|
|
|
Graph3d.prototype._setSpecialSettings = function(src, dst) { |
|
|
|
if (src.backgroundColor !== undefined) { |
|
|
|
this._setBackgroundColor(src.backgroundColor, dst); |
|
|
|
} |
|
|
|
|
|
|
|
this._setDataColor(src.dataColor, dst); |
|
|
|
this._setStyle(src.style, dst); |
|
|
|
this._setShowLegend(src.showLegend, dst); |
|
|
|
this._setCameraPosition(src.cameraPosition, dst); |
|
|
|
|
|
|
|
// As special fields go, this is an easy one; just a translation of the name.
|
|
|
|
// Can't use this.tooltip directly, because that field exists internally
|
|
|
|
if (src.tooltip !== undefined) { |
|
|
|
dst.showTooltip = src.tooltip; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Set the value of setting 'showLegend' |
|
|
|
* |
|
|
|
* This depends on the value of the style fields, so it must be called |
|
|
|
* after the style field has been initialized. |
|
|
|
*/ |
|
|
|
Graph3d.prototype._setShowLegend = function(showLegend, dst) { |
|
|
|
if (showLegend === undefined) { |
|
|
|
// If the default was auto, make a choice for this field
|
|
|
|
var isAutoByDefault = (DEFAULTS.showLegend === undefined); |
|
|
|
|
|
|
|
if (isAutoByDefault) { |
|
|
|
// these styles default to having legends
|
|
|
|
var isLegendGraphStyle = this.style === Graph3d.STYLE.DOTCOLOR |
|
|
|
|| this.style === Graph3d.STYLE.DOTSIZE; |
|
|
|
|
|
|
|
this.showLegend = isLegendGraphStyle; |
|
|
|
} else { |
|
|
|
// Leave current value as is
|
|
|
|
} |
|
|
|
} else { |
|
|
|
dst.showLegend = showLegend; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
Graph3d.prototype._setStyle = function(style, dst) { |
|
|
|
if (style === undefined) { |
|
|
|
return; // Nothing to do
|
|
|
|
} |
|
|
|
|
|
|
|
var styleNumber; |
|
|
|
|
|
|
|
if (typeof style === 'string') { |
|
|
|
styleNumber = this._getStyleNumber(style); |
|
|
|
|
|
|
|
if (styleNumber === -1 ) { |
|
|
|
throw new Error('Style \'' + style + '\' is invalid'); |
|
|
|
} |
|
|
|
} else { |
|
|
|
// Do a pedantic check on style number value
|
|
|
|
var valid = false; |
|
|
|
for (var n in Graph3d.STYLE) { |
|
|
|
if (Graph3d.STYLE[n] === style) { |
|
|
|
valid = true; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (!valid) { |
|
|
|
throw new Error('Style \'' + style + '\' is invalid'); |
|
|
|
} |
|
|
|
|
|
|
|
styleNumber = style; |
|
|
|
} |
|
|
|
|
|
|
|
dst.style = styleNumber; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Set the background styling for the graph |
|
|
|
* @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor |
|
|
|
*/ |
|
|
|
Graph3d.prototype._setBackgroundColor = function(backgroundColor, dst) { |
|
|
|
var fill = 'white'; |
|
|
|
var stroke = 'gray'; |
|
|
|
var strokeWidth = 1; |
|
|
|
|
|
|
|
if (typeof(backgroundColor) === 'string') { |
|
|
|
fill = backgroundColor; |
|
|
|
stroke = 'none'; |
|
|
|
strokeWidth = 0; |
|
|
|
} |
|
|
|
else if (typeof(backgroundColor) === 'object') { |
|
|
|
if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; |
|
|
|
if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; |
|
|
|
if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; |
|
|
|
} |
|
|
|
else { |
|
|
|
throw new Error('Unsupported type of backgroundColor'); |
|
|
|
} |
|
|
|
|
|
|
|
dst.frame.style.backgroundColor = fill; |
|
|
|
dst.frame.style.borderColor = stroke; |
|
|
|
dst.frame.style.borderWidth = strokeWidth + 'px'; |
|
|
|
dst.frame.style.borderStyle = 'solid'; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
Graph3d.prototype._setDataColor = function(dataColor, dst) { |
|
|
|
if (dataColor === undefined) { |
|
|
|
return; // Nothing to do
|
|
|
|
} |
|
|
|
|
|
|
|
if (dst.dataColor === undefined) { |
|
|
|
dst.dataColor = {}; |
|
|
|
} |
|
|
|
|
|
|
|
if (typeof dataColor === 'string') { |
|
|
|
dst.dataColor.fill = dataColor; |
|
|
|
dst.dataColor.stroke = dataColor; |
|
|
|
} |
|
|
|
else { |
|
|
|
if (dataColor.fill) { |
|
|
|
dst.dataColor.fill = dataColor.fill; |
|
|
|
} |
|
|
|
if (dataColor.stroke) { |
|
|
|
dst.dataColor.stroke = dataColor.stroke; |
|
|
|
} |
|
|
|
if (dataColor.strokeWidth !== undefined) { |
|
|
|
dst.dataColor.strokeWidth = dataColor.strokeWidth; |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
Graph3d.prototype._setCameraPosition = function(cameraPosition, dst) { |
|
|
|
var camPos = cameraPosition; |
|
|
|
if (camPos === undefined) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (dst.camera === undefined) { |
|
|
|
dst.camera = new Camera(); |
|
|
|
} |
|
|
|
|
|
|
|
dst.camera.setArmRotation(camPos.horizontal, camPos.vertical); |
|
|
|
dst.camera.setArmLength(camPos.distance); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
// Public methods for specific settings
|
|
|
|
//
|
|
|
|
|
|
|
|
/** |
|
|
|
* Set the rotation and distance of the camera |
|
|
|
* @param {Object} pos An object with the camera position. The object |
|
|
|
* contains three parameters: |
|
|
|
* - horizontal {Number} |
|
|
|
* The horizontal rotation, between 0 and 2*PI. |
|
|
|
* Optional, can be left undefined. |
|
|
|
* - vertical {Number} |
|
|
|
* The vertical rotation, between 0 and 0.5*PI |
|
|
|
* if vertical=0.5*PI, the graph is shown from the |
|
|
|
* top. Optional, can be left undefined. |
|
|
|
* - distance {Number} |
|
|
|
* The (normalized) distance of the camera to the |
|
|
|
* center of the graph, a value between 0.71 and 5.0. |
|
|
|
* Optional, can be left undefined. |
|
|
|
*/ |
|
|
|
Graph3d.prototype.setCameraPosition = function(pos) { |
|
|
|
this._setCameraPosition(pos, this); |
|
|
|
this.redraw(); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// End methods for handling settings
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Retrieve the style index from given styleName |
|
|
|
* @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' |
|
|
|
* @return {Number} styleNumber Enumeration value representing the style, or -1 |
|
|
|
* when not found |
|
|
|
*/ |
|
|
|
Graph3d.prototype._getStyleNumber = function(styleName) { |
|
|
|
switch (styleName) { |
|
|
|
case 'dot': return Graph3d.STYLE.DOT; |
|
|
|
case 'dot-line': return Graph3d.STYLE.DOTLINE; |
|
|
|
case 'dot-color': return Graph3d.STYLE.DOTCOLOR; |
|
|
|
case 'dot-size': return Graph3d.STYLE.DOTSIZE; |
|
|
|
case 'line': return Graph3d.STYLE.LINE; |
|
|
|
case 'grid': return Graph3d.STYLE.GRID; |
|
|
|
case 'surface': return Graph3d.STYLE.SURFACE; |
|
|
|
case 'bar': return Graph3d.STYLE.BAR; |
|
|
|
case 'bar-color': return Graph3d.STYLE.BARCOLOR; |
|
|
|
case 'bar-size': return Graph3d.STYLE.BARSIZE; |
|
|
|
} |
|
|
|
|
|
|
|
return -1; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Determine the indexes of the data columns, based on the given style and data |
|
|
|
* @param {DataSet} data |
|
|
@ -1210,16 +825,7 @@ Graph3d.prototype.setOptions = function (options) { |
|
|
|
|
|
|
|
this.animationStop(); |
|
|
|
|
|
|
|
if (options !== undefined) { |
|
|
|
// retrieve parameter values
|
|
|
|
|
|
|
|
// Handle the parameters which can be simply copied over
|
|
|
|
safeCopy(options, this, OPTIONKEYS); |
|
|
|
safeCopy(options, this, PREFIXEDOPTIONKEYS, 'default'); |
|
|
|
|
|
|
|
// Handle the more complex ('special') fields
|
|
|
|
this._setSpecialSettings(options, this); |
|
|
|
} |
|
|
|
Settings.setOptions(options, this); |
|
|
|
|
|
|
|
this.setSize(this.width, this.height); |
|
|
|
|
|
|
@ -2575,4 +2181,36 @@ function getMouseY (event) { |
|
|
|
return event.targetTouches[0] && event.targetTouches[0].clientY || 0; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// Public methods for specific settings
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
/** |
|
|
|
* Set the rotation and distance of the camera |
|
|
|
* @param {Object} pos An object with the camera position. The object |
|
|
|
* contains three parameters: |
|
|
|
* - horizontal {Number} |
|
|
|
* The horizontal rotation, between 0 and 2*PI. |
|
|
|
* Optional, can be left undefined. |
|
|
|
* - vertical {Number} |
|
|
|
* The vertical rotation, between 0 and 0.5*PI |
|
|
|
* if vertical=0.5*PI, the graph is shown from the |
|
|
|
* top. Optional, can be left undefined. |
|
|
|
* - distance {Number} |
|
|
|
* The (normalized) distance of the camera to the |
|
|
|
* center of the graph, a value between 0.71 and 5.0. |
|
|
|
* Optional, can be left undefined. |
|
|
|
*/ |
|
|
|
Graph3d.prototype.setCameraPosition = function(pos) { |
|
|
|
Settings.setCameraPosition(pos, this); |
|
|
|
this.redraw(); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// End public methods for specific settings
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = Graph3d; |