d3.parsets.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. // Parallel Sets by Jason Davies, http://www.jasondavies.com/
  2. // Functionality based on http://eagereyes.org/parallel-sets
  3. (function() {
  4. d3.parsets = function() {
  5. var dispatch = d3.dispatch("sortDimensions", "sortCategories"),
  6. dimensions_ = autoDimensions,
  7. dimensionFormat = String,
  8. tooltip_ = defaultTooltip,
  9. categoryTooltip = defaultCategoryTooltip,
  10. value_,
  11. spacing = 20,
  12. width,
  13. height,
  14. tension = 1,
  15. tension0,
  16. duration = 500;
  17. function parsets(selection) {
  18. selection.each(function(data, i) {
  19. var g = d3.select(this),
  20. ordinal = d3.scaleOrdinal(),
  21. dragging = false,
  22. dimensionNames = dimensions_.call(this, data, i),
  23. dimensions = [],
  24. tree = {children: {}, count: 0},
  25. nodes,
  26. total,
  27. ribbon;
  28. d3.select(window).on("mousemove.parsets." + ++parsetsId, unhighlight);
  29. if (tension0 == null) tension0 = tension;
  30. g.selectAll(".ribbon, .ribbon-mouse")
  31. .data(["ribbon", "ribbon-mouse"], String)
  32. .enter().append("g")
  33. .attr("class", String);
  34. updateDimensions();
  35. if (tension != tension0) {
  36. var t = d3.transition(g);
  37. if (t.tween) t.tween("ribbon", tensionTween);
  38. else tensionTween()(1);
  39. }
  40. function tensionTween() {
  41. var i = d3.interpolateNumber(tension0, tension);
  42. return function(t) {
  43. tension0 = i(t);
  44. ribbon.attr("d", ribbonPath);
  45. };
  46. }
  47. function updateDimensions() {
  48. // Cache existing bound dimensions to preserve sort order.
  49. var dimension = g.selectAll("g.dimension"),
  50. cache = {};
  51. dimension.each(function(d) { cache[d.name] = d; });
  52. dimensionNames.forEach(function(d) {
  53. if (!cache.hasOwnProperty(d)) {
  54. cache[d] = {name: d, categories: []};
  55. }
  56. dimensions.push(cache[d]);
  57. });
  58. dimensions.sort(compareY);
  59. // Populate tree with existing nodes.
  60. g.select(".ribbon").selectAll("path")
  61. .each(function(d) {
  62. var path = d.path.split("\0"),
  63. node = tree,
  64. n = path.length - 1;
  65. for (var i = 0; i < n; i++) {
  66. var p = path[i];
  67. node = node.children.hasOwnProperty(p) ? node.children[p]
  68. : node.children[p] = {children: {}, count: 0};
  69. }
  70. node.children[d.name] = d;
  71. });
  72. tree = buildTree(tree, data, dimensions.map(dimensionName), value_);
  73. cache = dimensions.map(function(d) {
  74. var t = {};
  75. d.categories.forEach(function(c) {
  76. t[c.name] = c;
  77. });
  78. return t;
  79. });
  80. (function categories(d, i) {
  81. if (!d.children) return;
  82. var dim = dimensions[i],
  83. t = cache[i];
  84. for (var k in d.children) {
  85. if (!t.hasOwnProperty(k)) {
  86. dim.categories.push(t[k] = {name: k});
  87. }
  88. categories(d.children[k], i + 1);
  89. }
  90. })(tree, 0);
  91. ordinal.domain([]).range(d3.range(dimensions[0].categories.length));
  92. nodes = layout(tree, dimensions, ordinal);
  93. total = getTotal(dimensions);
  94. dimensions.forEach(function(d) {
  95. d.count = total;
  96. });
  97. dimension = dimension.data(dimensions, dimensionName);
  98. var dEnter = dimension.enter().append("g")
  99. .attr("class", "dimension")
  100. .attr("transform", function(d) { return "translate(0," + d.y + ")"; })
  101. .on("mousedown.parsets", cancelEvent);
  102. dimension = dEnter.merge(dimension);
  103. dimension.each(function(d) {
  104. d.y0 = d.y;
  105. d.categories.forEach(function(d) { d.x0 = d.x; });
  106. });
  107. dEnter.append("rect")
  108. .attr("width", width)
  109. .attr("y", -45)
  110. .attr("height", 45);
  111. var textEnter = dEnter.append("text")
  112. .attr("class", "dimension")
  113. .attr("transform", "translate(0,-25)");
  114. textEnter.append("tspan")
  115. .attr("class", "name")
  116. .text(dimensionFormatName);
  117. textEnter.append("tspan")
  118. .attr("class", "sort alpha")
  119. .attr("dx", "2em")
  120. .text("alpha »")
  121. .on("mousedown.parsets", cancelEvent);
  122. textEnter.append("tspan")
  123. .attr("class", "sort size")
  124. .attr("dx", "2em")
  125. .text("size »")
  126. .on("mousedown.parsets", cancelEvent);
  127. dimension
  128. .call(d3.drag()
  129. .on("start", function(d) {
  130. dragging = true;
  131. d.y0 = d.y;
  132. })
  133. .on("drag", function(d) {
  134. d.y0 = d.y = d3.event.y;
  135. for (var i = 1; i < dimensions.length; i++) {
  136. if (height * dimensions[i].y < height * dimensions[i - 1].y) {
  137. dimensions.sort(compareY);
  138. dimensionNames = dimensions.map(dimensionName);
  139. ordinal.domain([]).range(d3.range(dimensions[0].categories.length));
  140. nodes = layout(tree = buildTree({children: {}, count: 0}, data, dimensionNames, value_), dimensions, ordinal);
  141. total = getTotal(dimensions);
  142. g.selectAll(".ribbon, .ribbon-mouse").selectAll("path").remove();
  143. updateRibbons();
  144. updateCategories(dimension);
  145. dimension.transition().duration(duration)
  146. .attr("transform", translateY)
  147. .tween("ribbon", ribbonTweenY);
  148. dispatch.call("sortDimensions");
  149. break;
  150. }
  151. }
  152. d3.select(this)
  153. .attr("transform", "translate(0," + d.y + ")")
  154. .transition();
  155. ribbon.filter(function(r) { return r.source.dimension === d || r.target.dimension === d; })
  156. .attr("d", ribbonPath);
  157. })
  158. .on("end", function(d) {
  159. dragging = false;
  160. unhighlight();
  161. var y0 = 45,
  162. dy = (height - y0 - 2) / (dimensions.length - 1);
  163. dimensions.forEach(function(d, i) {
  164. d.y = y0 + i * dy;
  165. });
  166. transition(d3.select(this))
  167. .attr("transform", "translate(0," + d.y + ")")
  168. .tween("ribbon", ribbonTweenY);
  169. }));
  170. dimension.select("text").select("tspan.sort.alpha")
  171. .on("click.parsets", sortBy("alpha", function(a, b) { return a.name < b.name ? 1 : -1; }, dimension));
  172. dimension.select("text").select("tspan.sort.size")
  173. .on("click.parsets", sortBy("size", function(a, b) { return a.count - b.count; }, dimension));
  174. dimension.transition().duration(duration)
  175. .attr("transform", function(d) { return "translate(0," + d.y + ")"; })
  176. .tween("ribbon", ribbonTweenY);
  177. dimension.exit().remove();
  178. updateCategories(dimension);
  179. updateRibbons();
  180. }
  181. function sortBy(type, f, dimension) {
  182. return function(d) {
  183. var direction = this.__direction = -(this.__direction || 1);
  184. d3.select(this).text(direction > 0 ? type + " »" : "« " + type);
  185. d.categories.sort(function() { return direction * f.apply(this, arguments); });
  186. nodes = layout(tree, dimensions, ordinal);
  187. updateCategories(dimension);
  188. updateRibbons();
  189. dispatch.call("sortCategories");
  190. };
  191. }
  192. function updateRibbons() {
  193. ribbon = g.select(".ribbon").selectAll("path")
  194. .data(nodes, function(d) { return d.path; });
  195. var ribbonEnter = ribbon.enter().append("path")
  196. .each(function(d) {
  197. d.source.x0 = d.source.x;
  198. d.target.x0 = d.target.x;
  199. })
  200. .attr("class", function(d) { return "category-" + d.major; })
  201. .attr("d", ribbonPath);
  202. ribbon.exit().remove();
  203. ribbon = ribbonEnter.merge(ribbon);
  204. ribbon.sort(function(a, b) { return b.count - a.count; });
  205. var mouse = g.select(".ribbon-mouse").selectAll("path")
  206. .data(nodes, function(d) { return d.path; });
  207. mouse.enter().append("path")
  208. .on("mousemove.parsets", function(d) {
  209. ribbon.classed("active", false);
  210. if (dragging) return;
  211. highlight(d = d.node, true);
  212. showTooltip(tooltip_.call(this, d));
  213. d3.event.stopPropagation();
  214. })
  215. .merge(mouse)
  216. .sort(function(a, b) { return b.count - a.count; })
  217. .attr("d", ribbonPathStatic);
  218. mouse.exit().remove();
  219. }
  220. // Animates the x-coordinates only of the relevant ribbon paths.
  221. function ribbonTweenX(d) {
  222. var nodes = [d],
  223. r = ribbon.filter(function(r) {
  224. var s, t;
  225. if (r.source.node === d) nodes.push(s = r.source);
  226. if (r.target.node === d) nodes.push(t = r.target);
  227. return s || t;
  228. }),
  229. i = nodes.map(function(d) { return d3.interpolateNumber(d.x0, d.x); }),
  230. n = nodes.length;
  231. return function(t) {
  232. for (var j = 0; j < n; j++) nodes[j].x0 = i[j](t);
  233. r.attr("d", ribbonPath);
  234. };
  235. }
  236. // Animates the y-coordinates only of the relevant ribbon paths.
  237. function ribbonTweenY(d) {
  238. var r = ribbon.filter(function(r) { return r.source.dimension.name == d.name || r.target.dimension.name == d.name; }),
  239. i = d3.interpolateNumber(d.y0, d.y);
  240. return function(t) {
  241. d.y0 = i(t);
  242. r.attr("d", ribbonPath);
  243. };
  244. }
  245. // Highlight a node and its descendants, and optionally its ancestors.
  246. function highlight(d, ancestors) {
  247. if (dragging) return;
  248. var highlight = [];
  249. (function recurse(d) {
  250. highlight.push(d);
  251. for (var k in d.children) recurse(d.children[k]);
  252. })(d);
  253. highlight.shift();
  254. if (ancestors) while (d) highlight.push(d), d = d.parent;
  255. ribbon.filter(function(d) {
  256. var active = highlight.indexOf(d.node) >= 0;
  257. if (active) this.parentNode.appendChild(this);
  258. return active;
  259. }).classed("active", true);
  260. }
  261. // Unhighlight all nodes.
  262. function unhighlight() {
  263. if (dragging) return;
  264. ribbon.classed("active", false);
  265. hideTooltip();
  266. }
  267. function updateCategories(g) {
  268. var category = g.selectAll("g.category")
  269. .data(function(d) { return d.categories; }, function(d) { return d.name; });
  270. var categoryEnter = category.enter().append("g")
  271. .attr("class", "category")
  272. .attr("transform", function(d) { return "translate(" + d.x + ")"; });
  273. category.exit().remove();
  274. category = categoryEnter.merge(category)
  275. .on("mousemove.parsets", function(d) {
  276. ribbon.classed("active", false);
  277. if (dragging) return;
  278. d.nodes.forEach(function(d) { highlight(d); });
  279. showTooltip(categoryTooltip.call(this, d));
  280. d3.event.stopPropagation();
  281. })
  282. .on("mouseout.parsets", unhighlight)
  283. .on("mousedown.parsets", cancelEvent)
  284. .call(d3.drag()
  285. .on("start", function(d) {
  286. dragging = true;
  287. d.x0 = d.x;
  288. })
  289. .on("drag", function(d) {
  290. d.x = d3.event.x;
  291. var categories = d.dimension.categories;
  292. for (var i = 0, c = categories[0]; ++i < categories.length;) {
  293. if (c.x + c.dx / 2 > (c = categories[i]).x + c.dx / 2) {
  294. categories.sort(function(a, b) { return a.x + a.dx / 2 - b.x - b.dx / 2; });
  295. nodes = layout(tree, dimensions, ordinal);
  296. updateRibbons();
  297. updateCategories(g);
  298. highlight(d.node);
  299. dispatch.call("sortCategories");
  300. break;
  301. }
  302. }
  303. var x = 0,
  304. p = spacing / (categories.length - 1);
  305. categories.forEach(function(e) {
  306. if (d === e) e.x0 = d3.event.x;
  307. e.x = x;
  308. x += e.count / total * (width - spacing) + p;
  309. });
  310. d3.select(this)
  311. .attr("transform", function(d) { return "translate(" + d.x0 + ")"; })
  312. .transition();
  313. ribbon.filter(function(r) { return r.source.node === d || r.target.node === d; })
  314. .attr("d", ribbonPath);
  315. })
  316. .on("end", function(d) {
  317. dragging = false;
  318. unhighlight();
  319. updateRibbons();
  320. transition(d3.select(this))
  321. .attr("transform", "translate(" + d.x + ")")
  322. .tween("ribbon", ribbonTweenX);
  323. }));
  324. category.transition().duration(duration)
  325. .attr("transform", function(d) { return "translate(" + d.x + ")"; })
  326. .tween("ribbon", ribbonTweenX);
  327. categoryEnter.append("rect")
  328. .attr("width", function(d) { return d.dx; })
  329. .attr("y", -20)
  330. .attr("height", 20);
  331. categoryEnter.append("line")
  332. .style("stroke-width", 2);
  333. categoryEnter.append("text")
  334. .attr("dy", "-.3em");
  335. category.select("rect")
  336. .attr("width", function(d) { return d.dx; })
  337. .attr("class", function(d) {
  338. return "category-" + (d.dimension === dimensions[0] ? ordinal(d.name) : "background");
  339. });
  340. category.select("line")
  341. .attr("x2", function(d) { return d.dx; });
  342. category.select("text")
  343. .text(truncateText(function(d) { return d.name; }, function(d) { return d.dx; }));
  344. }
  345. });
  346. }
  347. parsets.dimensionFormat = function(_) {
  348. if (!arguments.length) return dimensionFormat;
  349. dimensionFormat = _;
  350. return parsets;
  351. };
  352. parsets.dimensions = function(_) {
  353. if (!arguments.length) return dimensions_;
  354. dimensions_ = functor(_);
  355. return parsets;
  356. };
  357. parsets.value = function(_) {
  358. if (!arguments.length) return value_;
  359. value_ = functor(_);
  360. return parsets;
  361. };
  362. parsets.width = function(_) {
  363. if (!arguments.length) return width;
  364. width = +_;
  365. return parsets;
  366. };
  367. parsets.height = function(_) {
  368. if (!arguments.length) return height;
  369. height = +_;
  370. return parsets;
  371. };
  372. parsets.spacing = function(_) {
  373. if (!arguments.length) return spacing;
  374. spacing = +_;
  375. return parsets;
  376. };
  377. parsets.tension = function(_) {
  378. if (!arguments.length) return tension;
  379. tension = +_;
  380. return parsets;
  381. };
  382. parsets.duration = function(_) {
  383. if (!arguments.length) return duration;
  384. duration = +_;
  385. return parsets;
  386. };
  387. parsets.tooltip = function(_) {
  388. if (!arguments.length) return tooltip;
  389. tooltip_ = _ == null ? defaultTooltip : _;
  390. return parsets;
  391. };
  392. parsets.categoryTooltip = function(_) {
  393. if (!arguments.length) return categoryTooltip;
  394. categoryTooltip = _ == null ? defaultCategoryTooltip : _;
  395. return parsets;
  396. };
  397. var body = d3.select("body");
  398. var tooltip = body.append("div")
  399. .style("display", "none")
  400. .attr("class", "parsets tooltip");
  401. parsets.on = function() {
  402. var value = dispatch.on.apply(dispatch, arguments);
  403. return value === dispatch ? parsets : value;
  404. };
  405. return parsets.value(1).width(960).height(600);
  406. function dimensionFormatName(d, i) {
  407. return dimensionFormat.call(this, d.name, i);
  408. }
  409. function showTooltip(html) {
  410. var m = d3.mouse(body.node());
  411. tooltip
  412. .style("display", null)
  413. .style("left", m[0] + 30 + "px")
  414. .style("top", m[1] - 20 + "px")
  415. .html(html);
  416. }
  417. function hideTooltip() {
  418. tooltip.style("display", "none");
  419. }
  420. function transition(g) {
  421. return duration ? g.transition().duration(duration).ease(parsetsEase) : g;
  422. }
  423. function layout(tree, dimensions, ordinal) {
  424. var nodes = [],
  425. nd = dimensions.length,
  426. y0 = 45,
  427. dy = (height - y0 - 2) / (nd - 1);
  428. dimensions.forEach(function(d, i) {
  429. d.categories.forEach(function(c) {
  430. c.dimension = d;
  431. c.count = 0;
  432. c.nodes = [];
  433. });
  434. d.y = y0 + i * dy;
  435. });
  436. // Compute per-category counts.
  437. var total = (function rollup(d, i) {
  438. if (!d.children) return d.count;
  439. var dim = dimensions[i],
  440. total = 0;
  441. dim.categories.forEach(function(c) {
  442. var child = d.children[c.name];
  443. if (!child) return;
  444. c.nodes.push(child);
  445. var count = rollup(child, i + 1);
  446. c.count += count;
  447. total += count;
  448. });
  449. return total;
  450. })(tree, 0);
  451. // Stack the counts.
  452. dimensions.forEach(function(d) {
  453. d.categories = d.categories.filter(function(d) { return d.count; });
  454. var x = 0,
  455. p = spacing / (d.categories.length - 1);
  456. d.categories.forEach(function(c) {
  457. c.x = x;
  458. c.dx = c.count / total * (width - spacing);
  459. c.in = {dx: 0};
  460. c.out = {dx: 0};
  461. x += c.dx + p;
  462. });
  463. });
  464. var dim = dimensions[0];
  465. dim.categories.forEach(function(c) {
  466. var k = c.name;
  467. if (tree.children.hasOwnProperty(k)) {
  468. recurse(c, {node: tree.children[k], path: k}, 1, ordinal(k));
  469. }
  470. });
  471. function recurse(p, d, depth, major) {
  472. var node = d.node,
  473. dimension = dimensions[depth];
  474. dimension.categories.forEach(function(c) {
  475. var k = c.name;
  476. if (!node.children.hasOwnProperty(k)) return;
  477. var child = node.children[k];
  478. child.path = d.path + "\0" + k;
  479. var target = child.target || {node: c, dimension: dimension};
  480. target.x = c.in.dx;
  481. target.dx = child.count / total * (width - spacing);
  482. c.in.dx += target.dx;
  483. var source = child.source || {node: p, dimension: dimensions[depth - 1]};
  484. source.x = p.out.dx;
  485. source.dx = target.dx;
  486. p.out.dx += source.dx;
  487. child.node = child;
  488. child.source = source;
  489. child.target = target;
  490. child.major = major;
  491. nodes.push(child);
  492. if (depth + 1 < dimensions.length) recurse(c, child, depth + 1, major);
  493. });
  494. }
  495. return nodes;
  496. }
  497. // Dynamic path string for transitions.
  498. function ribbonPath(d) {
  499. var s = d.source,
  500. t = d.target;
  501. return ribbonPathString(s.node.x0 + s.x0, s.dimension.y0, s.dx, t.node.x0 + t.x0, t.dimension.y0, t.dx, tension0);
  502. }
  503. // Static path string for mouse handlers.
  504. function ribbonPathStatic(d) {
  505. var s = d.source,
  506. t = d.target;
  507. return ribbonPathString(s.node.x + s.x, s.dimension.y, s.dx, t.node.x + t.x, t.dimension.y, t.dx, tension);
  508. }
  509. function ribbonPathString(sx, sy, sdx, tx, ty, tdx, tension) {
  510. var m0, m1;
  511. return (tension === 1 ? [
  512. "M", [sx, sy],
  513. "L", [tx, ty],
  514. "h", tdx,
  515. "L", [sx + sdx, sy],
  516. "Z"]
  517. : ["M", [sx, sy],
  518. "C", [sx, m0 = tension * sy + (1 - tension) * ty], " ",
  519. [tx, m1 = tension * ty + (1 - tension) * sy], " ", [tx, ty],
  520. "h", tdx,
  521. "C", [tx + tdx, m1], " ", [sx + sdx, m0], " ", [sx + sdx, sy],
  522. "Z"]).join("");
  523. }
  524. function compareY(a, b) {
  525. a = height * a.y, b = height * b.y;
  526. return a < b ? -1 : a > b ? 1 : a >= b ? 0 : a <= a ? -1 : b <= b ? 1 : NaN;
  527. }
  528. };
  529. d3.parsets.tree = buildTree;
  530. function autoDimensions(d) {
  531. return d.length ? d3.keys(d[0]).sort() : [];
  532. }
  533. function cancelEvent() {
  534. d3.event.stopPropagation();
  535. d3.event.preventDefault();
  536. }
  537. function dimensionName(d) { return d.name; }
  538. function getTotal(dimensions) {
  539. return dimensions[0].categories.reduce(function(a, d) {
  540. return a + d.count;
  541. }, 0);
  542. }
  543. // Given a text function and width function, truncates the text if necessary to
  544. // fit within the given width.
  545. function truncateText(text, width) {
  546. return function(d, i) {
  547. var t = this.textContent = text(d, i),
  548. w = width(d, i);
  549. if (this.getComputedTextLength() < w) return t;
  550. this.textContent = "…" + t;
  551. var lo = 0,
  552. hi = t.length + 1,
  553. x;
  554. while (lo < hi) {
  555. var mid = lo + hi >> 1;
  556. if ((x = this.getSubStringLength(0, mid)) < w) lo = mid + 1;
  557. else hi = mid;
  558. }
  559. return lo > 1 ? t.substr(0, lo - 2) + "…" : "";
  560. };
  561. }
  562. var percent = d3.format("%"),
  563. comma = d3.format(",f"),
  564. parsetsEase = d3.easeElastic,
  565. parsetsId = 0;
  566. // Construct tree of all category counts for a given ordered list of
  567. // dimensions. Similar to d3.nest, except we also set the parent.
  568. function buildTree(root, data, dimensions, value) {
  569. zeroCounts(root);
  570. var n = data.length,
  571. nd = dimensions.length;
  572. for (var i = 0; i < n; i++) {
  573. var d = data[i],
  574. v = +value(d, i),
  575. node = root;
  576. for (var j = 0; j < nd; j++) {
  577. var dimension = dimensions[j],
  578. category = d[dimension],
  579. children = node.children;
  580. node.count += v;
  581. node = children.hasOwnProperty(category) ? children[category]
  582. : children[category] = {
  583. children: j === nd - 1 ? null : {},
  584. count: 0,
  585. parent: node,
  586. dimension: dimension,
  587. name: category
  588. };
  589. }
  590. node.count += v;
  591. }
  592. return root;
  593. }
  594. function zeroCounts(d) {
  595. d.count = 0;
  596. if (d.children) {
  597. for (var k in d.children) zeroCounts(d.children[k]);
  598. }
  599. }
  600. function identity(d) { return d; }
  601. function translateY(d) { return "translate(0," + d.y + ")"; }
  602. function defaultTooltip(d) {
  603. var count = d.count,
  604. path = [];
  605. while (d.parent) {
  606. if (d.name) path.unshift(d.name);
  607. d = d.parent;
  608. }
  609. return path.join(" → ") + "<br>" + comma(count) + " (" + percent(count / d.count) + ")";
  610. }
  611. function defaultCategoryTooltip(d) {
  612. return d.name + "<br>" + comma(d.count) + " (" + percent(d.count / d.dimension.count) + ")";
  613. }
  614. function functor(v) {
  615. return typeof v === "function" ? v : function() {
  616. return v;
  617. };
  618. }
  619. })();