nv.d3.scatter.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. // Licensed to Cloudera, Inc. under one
  2. // or more contributor license agreements. See the NOTICE file
  3. // distributed with this work for additional information
  4. // regarding copyright ownership. Cloudera, Inc. licenses this file
  5. // to you under the Apache License, Version 2.0 (the
  6. // "License"); you may not use this file except in compliance
  7. // with the License. You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing, software
  12. // distributed under the License is distributed on an "AS IS" BASIS,
  13. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. // See the License for the specific language governing permissions and
  15. // limitations under the License.
  16. nv.models.scatter = function() {
  17. "use strict";
  18. //============================================================
  19. // Public Variables with Default Settings
  20. //------------------------------------------------------------
  21. var margin = {top: 0, right: 0, bottom: 0, left: 0}
  22. , width = 960
  23. , height = 500
  24. , color = nv.utils.defaultColor() // chooses color
  25. , id = Math.floor(Math.random() * 100000) //Create semi-unique ID incase user doesn't select one
  26. , x = d3.scale.linear()
  27. , y = d3.scale.linear()
  28. , z = d3.scale.linear() //linear because d3.svg.shape.size is treated as area
  29. , getX = function(d) { return d.x } // accessor to get the x value
  30. , getY = function(d) { return d.y } // accessor to get the y value
  31. , getSize = function(d) { return d.size || 1} // accessor to get the point size
  32. , getShape = function(d) { return d.shape || 'circle' } // accessor to get point shape
  33. , onlyCircles = true // Set to false to use shapes
  34. , forceX = [] // List of numbers to Force into the X scale (ie. 0, or a max / min, etc.)
  35. , forceY = [] // List of numbers to Force into the Y scale
  36. , forceSize = [] // List of numbers to Force into the Size scale
  37. , interactive = true // If true, plots a voronoi overlay for advanced point intersection
  38. , pointKey = null
  39. , pointActive = function(d) { return !d.notActive } // any points that return false will be filtered out
  40. , padData = false // If true, adds half a data points width to front and back, for lining up a line chart with a bar chart
  41. , padDataOuter = .1 //outerPadding to imitate ordinal scale outer padding
  42. , clipEdge = false // if true, masks points within x and y scale
  43. , clipVoronoi = true // if true, masks each point with a circle... can turn off to slightly increase performance
  44. , clipRadius = function() { return 25 } // function to get the radius for voronoi point clips
  45. , xDomain = null // Override x domain (skips the calculation from data)
  46. , yDomain = null // Override y domain
  47. , xRange = null // Override x range
  48. , yRange = null // Override y range
  49. , sizeDomain = null // Override point size domain
  50. , sizeRange = null
  51. , singlePoint = false
  52. , dispatch = d3.dispatch('elementClick', 'elementMouseover', 'elementMouseout')
  53. , useVoronoi = true
  54. ;
  55. //============================================================
  56. //============================================================
  57. // Private Variables
  58. //------------------------------------------------------------
  59. var x0, y0, z0 // used to store previous scales
  60. , timeoutID
  61. , needsUpdate = false // Flag for when the points are visually updating, but the interactive layer is behind, to disable tooltips
  62. ;
  63. //============================================================
  64. function chart(selection) {
  65. selection.each(function(data) {
  66. var availableWidth = width - margin.left - margin.right,
  67. availableHeight = height - margin.top - margin.bottom,
  68. container = d3.select(this);
  69. //add series index to each data point for reference
  70. data.forEach(function(series, i) {
  71. series.values.forEach(function(point) {
  72. point.series = i;
  73. });
  74. });
  75. //------------------------------------------------------------
  76. // Setup Scales
  77. // remap and flatten the data for use in calculating the scales' domains
  78. var seriesData = (xDomain && yDomain && sizeDomain) ? [] : // if we know xDomain and yDomain and sizeDomain, no need to calculate.... if Size is constant remember to set sizeDomain to speed up performance
  79. d3.merge(
  80. data.map(function(d) {
  81. return d.values.map(function(d,i) {
  82. return { x: getX(d,i), y: getY(d,i), size: getSize(d,i) }
  83. })
  84. })
  85. );
  86. x .domain(xDomain || d3.extent(seriesData.map(function(d) { return d.x; }).concat(forceX)))
  87. if (padData && data[0])
  88. x.range(xRange || [(availableWidth * padDataOuter + availableWidth) / (2 *data[0].values.length), availableWidth - availableWidth * (1 + padDataOuter) / (2 * data[0].values.length) ]);
  89. //x.range([availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5) / data[0].values.length ]);
  90. else
  91. x.range(xRange || [0, availableWidth]);
  92. y .domain(yDomain || d3.extent(seriesData.map(function(d) { return d.y }).concat(forceY)))
  93. .range(yRange || [availableHeight, 0]);
  94. z .domain(sizeDomain || d3.extent(seriesData.map(function(d) { return d.size }).concat(forceSize)))
  95. .range(sizeRange || [16, 256]);
  96. // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point
  97. if (x.domain()[0] === x.domain()[1] || y.domain()[0] === y.domain()[1]) singlePoint = true;
  98. if (x.domain()[0] === x.domain()[1])
  99. x.domain()[0] ?
  100. x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01])
  101. : x.domain([-1,1]);
  102. if (y.domain()[0] === y.domain()[1])
  103. y.domain()[0] ?
  104. y.domain([y.domain()[0] - y.domain()[0] * 0.01, y.domain()[1] + y.domain()[1] * 0.01])
  105. : y.domain([-1,1]);
  106. if ( isNaN(x.domain()[0])) {
  107. x.domain([-1,1]);
  108. }
  109. if ( isNaN(y.domain()[0])) {
  110. y.domain([-1,1]);
  111. }
  112. x0 = x0 || x;
  113. y0 = y0 || y;
  114. z0 = z0 || z;
  115. //------------------------------------------------------------
  116. //------------------------------------------------------------
  117. // Setup containers and skeleton of chart
  118. var wrap = container.selectAll('g.nv-wrap.nv-scatter').data([data]);
  119. var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-scatter nv-chart-' + id + (singlePoint ? ' nv-single-point' : ''));
  120. var defsEnter = wrapEnter.append('defs');
  121. var gEnter = wrapEnter.append('g');
  122. var g = wrap.select('g');
  123. gEnter.append('g').attr('class', 'nv-groups');
  124. gEnter.append('g').attr('class', 'nv-point-paths');
  125. wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  126. //------------------------------------------------------------
  127. defsEnter.append('clipPath')
  128. .attr('id', 'nv-edge-clip-' + id)
  129. .append('rect');
  130. wrap.select('#nv-edge-clip-' + id + ' rect')
  131. .attr('width', availableWidth)
  132. .attr('height', (availableHeight > 0) ? availableHeight : 0);
  133. g .attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + id + ')' : '');
  134. function updateInteractiveLayer() {
  135. if (!interactive) return false;
  136. var eventElements;
  137. var vertices = d3.merge(data.map(function(group, groupIndex) {
  138. return group.values
  139. .map(function(point, pointIndex) {
  140. // *Adding noise to make duplicates very unlikely
  141. // *Injecting series and point index for reference
  142. /* *Adding a 'jitter' to the points, because there's an issue in d3.geom.voronoi.
  143. */
  144. var pX = getX(point,pointIndex);
  145. var pY = getY(point,pointIndex);
  146. return [x(pX)+ Math.random() * 1e-7,
  147. y(pY)+ Math.random() * 1e-7,
  148. groupIndex,
  149. pointIndex, point]; //temp hack to add noise untill I think of a better way so there are no duplicates
  150. })
  151. .filter(function(pointArray, pointIndex) {
  152. return pointActive(pointArray[4], pointIndex); // Issue #237.. move filter to after map, so pointIndex is correct!
  153. })
  154. })
  155. );
  156. //inject series and point index for reference into voronoi
  157. if (useVoronoi === true) {
  158. if (clipVoronoi) {
  159. var pointClipsEnter = wrap.select('defs').selectAll('.nv-point-clips')
  160. .data([id])
  161. .enter();
  162. pointClipsEnter.append('clipPath')
  163. .attr('class', 'nv-point-clips')
  164. .attr('id', 'nv-points-clip-' + id);
  165. var pointClips = wrap.select('#nv-points-clip-' + id).selectAll('circle')
  166. .data(vertices);
  167. pointClips.enter().append('circle')
  168. .attr('r', clipRadius);
  169. pointClips.exit().remove();
  170. pointClips
  171. .attr('cx', function(d) { return d[0] })
  172. .attr('cy', function(d) { return d[1] });
  173. wrap.select('.nv-point-paths')
  174. .attr('clip-path', 'url(#nv-points-clip-' + id + ')');
  175. }
  176. if(vertices.length) {
  177. // Issue #283 - Adding 2 dummy points to the voronoi b/c voronoi requires min 3 points to work
  178. vertices.push([x.range()[0] - 20, y.range()[0] - 20, null, null]);
  179. vertices.push([x.range()[1] + 20, y.range()[1] + 20, null, null]);
  180. vertices.push([x.range()[0] - 20, y.range()[0] + 20, null, null]);
  181. vertices.push([x.range()[1] + 20, y.range()[1] - 20, null, null]);
  182. }
  183. var bounds = d3.geom.polygon([
  184. [-10,-10],
  185. [-10,height + 10],
  186. [width + 10,height + 10],
  187. [width + 10,-10]
  188. ]);
  189. var voronoi = d3.geom.voronoi(vertices).map(function(d, i) {
  190. return {
  191. 'data': bounds.clip(d),
  192. 'series': vertices[i][2],
  193. 'point': vertices[i][3]
  194. }
  195. });
  196. var pointPaths = wrap.select('.nv-point-paths').selectAll('path')
  197. .data(voronoi);
  198. pointPaths.enter().append('path')
  199. .attr('class', function(d,i) { return 'nv-path-'+i; });
  200. pointPaths.exit().remove();
  201. pointPaths
  202. .attr('d', function(d) {
  203. if (d.data.length === 0)
  204. return 'M 0 0'
  205. else
  206. return 'M' + d.data.join('L') + 'Z';
  207. });
  208. var mouseEventCallback = function(d,mDispatch) {
  209. if (needsUpdate) return 0;
  210. var series = data[d.series];
  211. if (typeof series === 'undefined') return;
  212. var point = series.values[d.point];
  213. mDispatch({
  214. point: point,
  215. series: series,
  216. pos: [x(getX(point, d.point)) + margin.left, y(getY(point, d.point)) + margin.top],
  217. seriesIndex: d.series,
  218. pointIndex: d.point
  219. });
  220. };
  221. pointPaths
  222. .on('click', function(d) {
  223. mouseEventCallback(d, dispatch.elementClick);
  224. })
  225. .on('mouseover', function(d) {
  226. mouseEventCallback(d, dispatch.elementMouseover);
  227. })
  228. .on('mouseout', function(d, i) {
  229. mouseEventCallback(d, dispatch.elementMouseout);
  230. });
  231. } else {
  232. /*
  233. // bring data in form needed for click handlers
  234. var dataWithPoints = vertices.map(function(d, i) {
  235. return {
  236. 'data': d,
  237. 'series': vertices[i][2],
  238. 'point': vertices[i][3]
  239. }
  240. });
  241. */
  242. // add event handlers to points instead voronoi paths
  243. wrap.select('.nv-groups').selectAll('.nv-group')
  244. .selectAll('.nv-point')
  245. //.data(dataWithPoints)
  246. //.style('pointer-events', 'auto') // recativate events, disabled by css
  247. .on('click', function(d,i) {
  248. //nv.log('test', d, i);
  249. if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point
  250. var series = data[d.series],
  251. point = series.values[i];
  252. dispatch.elementClick({
  253. point: point,
  254. series: series,
  255. pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
  256. seriesIndex: d.series,
  257. pointIndex: i
  258. });
  259. })
  260. .on('mouseover', function(d,i) {
  261. if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point
  262. var series = data[d.series],
  263. point = series.values[i];
  264. dispatch.elementMouseover({
  265. point: point,
  266. series: series,
  267. pos: [x(getX(point, i)) + margin.left, y(getY(point, i)) + margin.top],
  268. seriesIndex: d.series,
  269. pointIndex: i
  270. });
  271. })
  272. .on('mouseout', function(d,i) {
  273. if (needsUpdate || !data[d.series]) return 0; //check if this is a dummy point
  274. var series = data[d.series],
  275. point = series.values[i];
  276. dispatch.elementMouseout({
  277. point: point,
  278. series: series,
  279. seriesIndex: d.series,
  280. pointIndex: i
  281. });
  282. });
  283. }
  284. needsUpdate = false;
  285. }
  286. needsUpdate = true;
  287. var groups = wrap.select('.nv-groups').selectAll('.nv-group')
  288. .data(function(d) { return d }, function(d) { return d.key });
  289. groups.enter().append('g')
  290. .style('stroke-opacity', 1e-6)
  291. .style('fill-opacity', 1e-6);
  292. groups.exit()
  293. .remove();
  294. groups
  295. .attr('class', function(d,i) { return 'nv-group nv-series-' + i })
  296. .classed('hover', function(d) { return d.hover });
  297. groups
  298. .transition()
  299. .style('fill', function(d,i) { return color(d, i) })
  300. .style('stroke', function(d,i) { return color(d, i) })
  301. .style('stroke-opacity', 1)
  302. .style('fill-opacity', .5);
  303. if (onlyCircles) {
  304. var points = groups.selectAll('circle.nv-point')
  305. .data(function(d) { return d.values }, pointKey);
  306. points.enter().append('circle')
  307. .style('fill', function (d,i) { return d.color })
  308. .style('stroke', function (d,i) { return d.color })
  309. .attr('cx', function(d,i) { return nv.utils.NaNtoZero(x0(getX(d,i))) })
  310. .attr('cy', function(d,i) { return nv.utils.NaNtoZero(y0(getY(d,i))) })
  311. .attr('r', function(d,i) { return Math.sqrt(z(getSize(d,i))/Math.PI) });
  312. points.exit().remove();
  313. groups.exit().selectAll('path.nv-point').transition()
  314. .attr('cx', function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) })
  315. .attr('cy', function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) })
  316. .remove();
  317. points.each(function(d,i) {
  318. d3.select(this)
  319. .classed('nv-point', true)
  320. .classed('nv-point-' + i, true)
  321. .classed('hover',false)
  322. ;
  323. });
  324. points.transition()
  325. .attr('cx', function(d,i) { return nv.utils.NaNtoZero(x(getX(d,i))) })
  326. .attr('cy', function(d,i) { return nv.utils.NaNtoZero(y(getY(d,i))) })
  327. .attr('r', function(d,i) { return Math.sqrt(z(getSize(d,i))/Math.PI) });
  328. } else {
  329. var points = groups.selectAll('path.nv-point')
  330. .data(function(d) { return d.values });
  331. points.enter().append('path')
  332. .style('fill', function (d,i) { return d.color })
  333. .style('stroke', function (d,i) { return d.color })
  334. .attr('transform', function(d,i) {
  335. return 'translate(' + x0(getX(d,i)) + ',' + y0(getY(d,i)) + ')'
  336. })
  337. .attr('d',
  338. d3.svg.symbol()
  339. .type(getShape)
  340. .size(function(d,i) { return z(getSize(d,i)) })
  341. );
  342. points.exit().remove();
  343. groups.exit().selectAll('path.nv-point')
  344. .transition()
  345. .attr('transform', function(d,i) {
  346. return 'translate(' + x(getX(d,i)) + ',' + y(getY(d,i)) + ')'
  347. })
  348. .remove();
  349. points.each(function(d,i) {
  350. d3.select(this)
  351. .classed('nv-point', true)
  352. .classed('nv-point-' + i, true)
  353. .classed('hover',false)
  354. ;
  355. });
  356. points.transition()
  357. .attr('transform', function(d,i) {
  358. //nv.log(d,i,getX(d,i), x(getX(d,i)));
  359. return 'translate(' + x(getX(d,i)) + ',' + y(getY(d,i)) + ')'
  360. })
  361. .attr('d',
  362. d3.svg.symbol()
  363. .type(getShape)
  364. .size(function(d,i) { return z(getSize(d,i)) })
  365. );
  366. }
  367. // Delay updating the invisible interactive layer for smoother animation
  368. clearTimeout(timeoutID); // stop repeat calls to updateInteractiveLayer
  369. timeoutID = setTimeout(updateInteractiveLayer, 300);
  370. //updateInteractiveLayer();
  371. //store old scales for use in transitions on update
  372. x0 = x.copy();
  373. y0 = y.copy();
  374. z0 = z.copy();
  375. });
  376. return chart;
  377. }
  378. //============================================================
  379. // Event Handling/Dispatching (out of chart's scope)
  380. //------------------------------------------------------------
  381. chart.clearHighlights = function() {
  382. //Remove the 'hover' class from all highlighted points.
  383. d3.selectAll(".nv-chart-" + id + " .nv-point.hover").classed("hover",false);
  384. };
  385. chart.highlightPoint = function(seriesIndex,pointIndex,isHoverOver) {
  386. d3.select(".nv-chart-" + id + " .nv-series-" + seriesIndex + " .nv-point-" + pointIndex)
  387. .classed("hover",isHoverOver);
  388. };
  389. dispatch.on('elementMouseover.point', function(d) {
  390. if (interactive) chart.highlightPoint(d.seriesIndex,d.pointIndex,true);
  391. });
  392. dispatch.on('elementMouseout.point', function(d) {
  393. if (interactive) chart.highlightPoint(d.seriesIndex,d.pointIndex,false);
  394. });
  395. //============================================================
  396. //============================================================
  397. // Expose Public Variables
  398. //------------------------------------------------------------
  399. chart.dispatch = dispatch;
  400. chart.options = nv.utils.optionsFunc.bind(chart);
  401. chart.x = function(_) {
  402. if (!arguments.length) return getX;
  403. getX = d3.functor(_);
  404. return chart;
  405. };
  406. chart.y = function(_) {
  407. if (!arguments.length) return getY;
  408. getY = d3.functor(_);
  409. return chart;
  410. };
  411. chart.size = function(_) {
  412. if (!arguments.length) return getSize;
  413. getSize = d3.functor(_);
  414. return chart;
  415. };
  416. chart.margin = function(_) {
  417. if (!arguments.length) return margin;
  418. margin.top = typeof _.top != 'undefined' ? _.top : margin.top;
  419. margin.right = typeof _.right != 'undefined' ? _.right : margin.right;
  420. margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom;
  421. margin.left = typeof _.left != 'undefined' ? _.left : margin.left;
  422. return chart;
  423. };
  424. chart.width = function(_) {
  425. if (!arguments.length) return width;
  426. width = _;
  427. return chart;
  428. };
  429. chart.height = function(_) {
  430. if (!arguments.length) return height;
  431. height = _;
  432. return chart;
  433. };
  434. chart.xScale = function(_) {
  435. if (!arguments.length) return x;
  436. x = _;
  437. return chart;
  438. };
  439. chart.yScale = function(_) {
  440. if (!arguments.length) return y;
  441. y = _;
  442. return chart;
  443. };
  444. chart.zScale = function(_) {
  445. if (!arguments.length) return z;
  446. z = _;
  447. return chart;
  448. };
  449. chart.xDomain = function(_) {
  450. if (!arguments.length) return xDomain;
  451. xDomain = _;
  452. return chart;
  453. };
  454. chart.yDomain = function(_) {
  455. if (!arguments.length) return yDomain;
  456. yDomain = _;
  457. return chart;
  458. };
  459. chart.sizeDomain = function(_) {
  460. if (!arguments.length) return sizeDomain;
  461. sizeDomain = _;
  462. return chart;
  463. };
  464. chart.xRange = function(_) {
  465. if (!arguments.length) return xRange;
  466. xRange = _;
  467. return chart;
  468. };
  469. chart.yRange = function(_) {
  470. if (!arguments.length) return yRange;
  471. yRange = _;
  472. return chart;
  473. };
  474. chart.sizeRange = function(_) {
  475. if (!arguments.length) return sizeRange;
  476. sizeRange = _;
  477. return chart;
  478. };
  479. chart.forceX = function(_) {
  480. if (!arguments.length) return forceX;
  481. forceX = _;
  482. return chart;
  483. };
  484. chart.forceY = function(_) {
  485. if (!arguments.length) return forceY;
  486. forceY = _;
  487. return chart;
  488. };
  489. chart.forceSize = function(_) {
  490. if (!arguments.length) return forceSize;
  491. forceSize = _;
  492. return chart;
  493. };
  494. chart.interactive = function(_) {
  495. if (!arguments.length) return interactive;
  496. interactive = _;
  497. return chart;
  498. };
  499. chart.pointKey = function(_) {
  500. if (!arguments.length) return pointKey;
  501. pointKey = _;
  502. return chart;
  503. };
  504. chart.pointActive = function(_) {
  505. if (!arguments.length) return pointActive;
  506. pointActive = _;
  507. return chart;
  508. };
  509. chart.padData = function(_) {
  510. if (!arguments.length) return padData;
  511. padData = _;
  512. return chart;
  513. };
  514. chart.padDataOuter = function(_) {
  515. if (!arguments.length) return padDataOuter;
  516. padDataOuter = _;
  517. return chart;
  518. };
  519. chart.clipEdge = function(_) {
  520. if (!arguments.length) return clipEdge;
  521. clipEdge = _;
  522. return chart;
  523. };
  524. chart.clipVoronoi= function(_) {
  525. if (!arguments.length) return clipVoronoi;
  526. clipVoronoi = _;
  527. return chart;
  528. };
  529. chart.useVoronoi= function(_) {
  530. if (!arguments.length) return useVoronoi;
  531. useVoronoi = _;
  532. if (useVoronoi === false) {
  533. clipVoronoi = false;
  534. }
  535. return chart;
  536. };
  537. chart.clipRadius = function(_) {
  538. if (!arguments.length) return clipRadius;
  539. clipRadius = _;
  540. return chart;
  541. };
  542. chart.color = function(_) {
  543. if (!arguments.length) return color;
  544. color = nv.utils.getColor(_);
  545. return chart;
  546. };
  547. chart.shape = function(_) {
  548. if (!arguments.length) return getShape;
  549. getShape = _;
  550. return chart;
  551. };
  552. chart.onlyCircles = function(_) {
  553. if (!arguments.length) return onlyCircles;
  554. onlyCircles = _;
  555. return chart;
  556. };
  557. chart.id = function(_) {
  558. if (!arguments.length) return id;
  559. id = _;
  560. return chart;
  561. };
  562. chart.singlePoint = function(_) {
  563. if (!arguments.length) return singlePoint;
  564. singlePoint = _;
  565. return chart;
  566. };
  567. //============================================================
  568. return chart;
  569. }