component/particles.js

  1. import * as d3 from "d3";
  2. import dataTransform from "../dataTransform.js";
  3. import { dispatch } from "../events.js";
  4. import { colorParse } from "../colorHelper.js";
  5. /**
  6. * Reusable 3D Particle Plot Component
  7. *
  8. * @module
  9. */
  10. export default function() {
  11. /* Default Properties */
  12. let dimensions = { x: 40, y: 40, z: 40 };
  13. let colors = d3.schemeRdYlGn[8];
  14. let color;
  15. let classed = "d3X3dBubbles";
  16. let mappings;
  17. /* Scales */
  18. let xScale;
  19. let yScale;
  20. let zScale;
  21. let colorScale;
  22. /**
  23. * Array to String
  24. *
  25. * @private
  26. * @param {array} arr
  27. * @returns {string}
  28. */
  29. const array2dToString = function(arr) {
  30. return arr.reduce((a, b) => a.concat(b), [])
  31. .reduce((a, b) => a.concat(b), [])
  32. .join(" ");
  33. };
  34. /**
  35. * Initialise Data and Scales
  36. *
  37. * @private
  38. * @param {Array} data - Chart data.
  39. */
  40. const init = function(data) {
  41. let newData = {};
  42. ['x', 'y', 'z', 'color'].forEach((dimension) => {
  43. let set = {
  44. key: dimension,
  45. values: []
  46. };
  47. data.values.forEach((d) => {
  48. let key = mappings[dimension];
  49. let value = d.values.find((v) => v.key === key).value;
  50. set.values.push({ key: key, value: value });
  51. });
  52. newData[dimension] = dataTransform(set).summary();
  53. });
  54. let extentX = newData.x.valueExtent;
  55. let extentY = newData.y.valueExtent;
  56. let extentZ = newData.z.valueExtent;
  57. let extentColor = newData.color.valueExtent;
  58. if (typeof xScale === "undefined") {
  59. xScale = d3.scaleLinear()
  60. .domain(extentX)
  61. .range([0, dimensions.x]);
  62. }
  63. if (typeof yScale === "undefined") {
  64. yScale = d3.scaleLinear()
  65. .domain(extentY)
  66. .range([0, dimensions.y]);
  67. }
  68. if (typeof zScale === "undefined") {
  69. zScale = d3.scaleLinear()
  70. .domain(extentZ)
  71. .range([0, dimensions.z]);
  72. }
  73. if (color) {
  74. colorScale = d3.scaleQuantize()
  75. .domain(extentColor)
  76. .range([color, color]);
  77. } else if (typeof colorScale === "undefined") {
  78. colorScale = d3.scaleQuantize()
  79. .domain(extentColor)
  80. .range(colors);
  81. }
  82. };
  83. /**
  84. * Constructor
  85. *
  86. * @constructor
  87. * @alias particles
  88. * @param {d3.selection} selection - The chart holder D3 selection.
  89. */
  90. const my = function(selection) {
  91. selection.each(function(data) {
  92. init(data);
  93. const element = d3.select(this)
  94. .classed(classed, true)
  95. .attr("id", (d) => d.key);
  96. const particleData = function(data) {
  97. const pointCoords = function(Y) {
  98. return Y.values.map(function(d) {
  99. let xVal = d.values.find((v) => v.key === mappings.x).value;
  100. let yVal = d.values.find((v) => v.key === mappings.y).value;
  101. let zVal = d.values.find((v) => v.key === mappings.z).value;
  102. return [xScale(xVal), yScale(yVal), zScale(zVal)];
  103. })
  104. };
  105. const pointColors = function(Y) {
  106. return Y.values.map(function(d) {
  107. let colorVal = d.values.find((v) => v.key === mappings.color).value;
  108. let color = d3.color(colorScale(colorVal));
  109. return colorParse(color);
  110. })
  111. };
  112. data.point = array2dToString(pointCoords(data));
  113. data.color = array2dToString(pointColors(data));
  114. return [data];
  115. };
  116. const shape = (el) => {
  117. const shape = el.append("Shape");
  118. /*
  119. // FIXME: x3dom cannot have empty IFS nodes, we must to use .html() rather than .append() & .attr().
  120. const appearance = shape.append("Appearance");
  121. appearance.append("PointProperties")
  122. .attr("colorMode", "POINT_COLOR")
  123. .attr("pointSizeMinValue", 1)
  124. .attr("pointSizeMaxValue", 100)
  125. .attr("pointSizeScaleFactor", 5);
  126. const pointset = shape.append("PointSet");
  127. pointset.append("Coordinate")
  128. .attr("point", (d) => d.point);
  129. pointset.append("Color")
  130. .attr("color", (d) => d.color);
  131. */
  132. shape.html((d) => `
  133. <Appearance>
  134. <PointProperties colorMode="POINT_COLOR" pointSizeMinValue="1" pointSizeMaxValue="100" pointSizeScaleFactor="5"></PointProperties>
  135. </Appearance>
  136. <PointSet>
  137. <Coordinate point="${d.point}"></Coordinate>
  138. <Color color="${d.color}"></Color>
  139. </IndexedFaceset>
  140. `);
  141. };
  142. const particles = element.selectAll(".particle")
  143. .data((d) => particleData(d), (d) => d.key);
  144. particles.enter()
  145. .append("Group")
  146. .classed("particle", true)
  147. .call(shape)
  148. .merge(particles);
  149. const particleTransition = particles.transition().select("Shape");
  150. particleTransition.select("PointSet")
  151. .select("Coordinate")
  152. .attr("point", (d) => d.point);
  153. particleTransition.select("PointSet")
  154. .select("Color")
  155. .attr("color", (d) => d.color);
  156. });
  157. };
  158. /**
  159. * Dimensions Getter / Setter
  160. *
  161. * @param {{x: number, y: number, z: number}} _v - 3D object dimensions.
  162. * @returns {*}
  163. */
  164. my.dimensions = function(_v) {
  165. if (!arguments.length) return dimensions;
  166. dimensions = _v;
  167. return this;
  168. };
  169. /**
  170. * X Scale Getter / Setter
  171. *
  172. * @param {d3.scale} _v - D3 scale.
  173. * @returns {*}
  174. */
  175. my.xScale = function(_v) {
  176. if (!arguments.length) return xScale;
  177. xScale = _v;
  178. return my;
  179. };
  180. /**
  181. * Y Scale Getter / Setter
  182. *
  183. * @param {d3.scale} _v - D3 scale.
  184. * @returns {*}
  185. */
  186. my.yScale = function(_v) {
  187. if (!arguments.length) return yScale;
  188. yScale = _v;
  189. return my;
  190. };
  191. /**
  192. * Z Scale Getter / Setter
  193. *
  194. * @param {d3.scale} _v - D3 scale.
  195. * @returns {*}
  196. */
  197. my.zScale = function(_v) {
  198. if (!arguments.length) return zScale;
  199. zScale = _v;
  200. return my;
  201. };
  202. /**
  203. * Color Scale Getter / Setter
  204. *
  205. * @param {d3.scale} _v - D3 color scale.
  206. * @returns {*}
  207. */
  208. my.colorScale = function(_v) {
  209. if (!arguments.length) return colorScale;
  210. colorScale = _v;
  211. return my;
  212. };
  213. /**
  214. * Color Getter / Setter
  215. *
  216. * @param {string} _v - Color (e.g. "red" or "#ff0000").
  217. * @returns {*}
  218. */
  219. my.color = function(_v) {
  220. if (!arguments.length) return color;
  221. color = _v;
  222. return my;
  223. };
  224. /**
  225. * Colors Getter / Setter
  226. *
  227. * @param {Array} _v - Array of colours used by color scale.
  228. * @returns {*}
  229. */
  230. my.colors = function(_v) {
  231. if (!arguments.length) return colors;
  232. colors = _v;
  233. return my;
  234. };
  235. /**
  236. * Mappings Getter / Setter
  237. *
  238. * @param {Object} _v - Map properties to colour etc.
  239. * @returns {*}
  240. */
  241. my.mappings = function(_v) {
  242. if (!arguments.length) return mappings;
  243. mappings = _v;
  244. return my;
  245. };
  246. /**
  247. * Dispatch On Getter
  248. *
  249. * @returns {*}
  250. */
  251. my.on = function() {
  252. let value = dispatch.on.apply(dispatch, arguments);
  253. return value === dispatch ? my : value;
  254. };
  255. return my;
  256. }