component/axis.js

  1. import * as d3 from "d3";
  2. import { colorParse } from "../colorHelper.js";
  3. /**
  4. * Reusable 3D Axis Component
  5. *
  6. * @module
  7. */
  8. export default function() {
  9. /* Default Properties */
  10. let dimensions = { x: 40, y: 40, z: 40 };
  11. let color = "black";
  12. let classed = "d3X3dAxis";
  13. let labelPosition = "proximal";
  14. let labelInset = labelPosition === "distal" ? 1 : -1;
  15. /* Scale and Axis Options */
  16. let scale;
  17. let direction;
  18. let tickDirection;
  19. let tickArguments = [];
  20. let tickValues = null;
  21. let tickFormat = null;
  22. let tickSize = 1.5;
  23. let tickPadding = 2.0;
  24. /**
  25. * Get Axis Direction Vector
  26. *
  27. * @private
  28. * @param {string} axisDir
  29. * @returns {number[]}
  30. */
  31. const getAxisDirectionVector = function(axisDir) {
  32. const axisDirectionVectors = {
  33. x: [1, 0, 0],
  34. y: [0, 1, 0],
  35. z: [0, 0, 1]
  36. };
  37. return axisDirectionVectors[axisDir];
  38. };
  39. /**
  40. * Get Axis Rotation Vector
  41. *
  42. * @private
  43. * @param {string} axisDir
  44. * @returns {number[]}
  45. */
  46. const getAxisRotationVector = function(axisDir) {
  47. const axisRotationVectors = {
  48. x: [1, 1, 0, Math.PI],
  49. y: [0, 0, 0, 0],
  50. z: [0, 1, 1, Math.PI]
  51. };
  52. return axisRotationVectors[axisDir];
  53. };
  54. /**
  55. * Constructor
  56. *
  57. * @constructor
  58. * @alias axis
  59. * @param {d3.selection} selection - The chart holder D3 selection.
  60. */
  61. const my = function(selection) {
  62. selection.each(function() {
  63. const element = d3.select(this)
  64. .classed(classed, true);
  65. const range = scale.range();
  66. const range0 = range[0];
  67. const range1 = range[range.length - 1];
  68. const axisDirectionVector = getAxisDirectionVector(direction);
  69. const tickDirectionVector = getAxisDirectionVector(tickDirection);
  70. const axisRotationVector = getAxisRotationVector(direction);
  71. const tickRotationVector = getAxisRotationVector(tickDirection);
  72. /*
  73. // FIXME: Currently the tickArguments option does not work.
  74. const tickValuesDefault = scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain();
  75. tickValues = tickValues === null ? tickValuesDefault : tickValues;
  76. */
  77. tickValues = scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain();
  78. const tickFormatDefault = scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments) : (d) => d;
  79. tickFormat = tickFormat === null ? tickFormatDefault : tickFormat;
  80. const makeSolid = (el, color) => {
  81. el.append("Appearance")
  82. .append("Material")
  83. .attr("diffuseColor", colorParse(color) || "0 0 0")
  84. .attr("transparency", "0");
  85. };
  86. const shape = (el, radius, height, color) => {
  87. const shape = el.append("Shape");
  88. shape.append("Cylinder")
  89. .attr("radius", radius)
  90. .attr("height", height);
  91. shape.call(makeSolid, colorParse(color));
  92. };
  93. // Main Lines
  94. const domain = element.selectAll(".domain")
  95. .data([null]);
  96. domain.enter()
  97. .append("Transform")
  98. .attr("class", "domain")
  99. .attr("rotation", axisRotationVector.join(" "))
  100. .attr("translation", axisDirectionVector.map((d) => (d * (range0 + range1) / 2)).join(" "))
  101. .call(shape, 0.1, range1 - range0, color)
  102. .merge(domain);
  103. domain.exit()
  104. .remove();
  105. // Tick Lines
  106. const ticks = element.selectAll(".tickLine")
  107. .data(tickValues, (d) => d);
  108. ticks.enter()
  109. .append("Transform")
  110. .attr("class", "tickLine")
  111. .attr("translation", (t) => (axisDirectionVector.map((a) => (scale(t) * a)).join(" ")))
  112. .append("Transform")
  113. .attr("translation", tickDirectionVector.map((d) => (d * tickSize / 2)).join(" "))
  114. .attr("rotation", tickRotationVector.join(" "))
  115. .call(shape, 0.05, tickSize, "#f3f3f3")
  116. .merge(ticks);
  117. ticks.transition()
  118. .attr("translation", (t) => (axisDirectionVector.map((a) => (scale(t) * a)).join(" ")));
  119. ticks.exit()
  120. .remove();
  121. // Tick Labels
  122. const labels = element.selectAll(".tickLabel")
  123. .data(tickValues, (d) => d);
  124. labels.enter()
  125. .append("Transform")
  126. .attr("class", "tickLabel")
  127. .attr("translation", (t) => (axisDirectionVector.map((a) => (scale(t) * a)).join(" ")))
  128. .append("Transform")
  129. .attr("translation", tickDirectionVector.map((d, i) => (labelInset * d * tickPadding) + (((labelInset + 1) / 2) * tickSize * tickDirectionVector[i])))
  130. .append("Billboard")
  131. .attr("axisOfRotation", "0 0 0")
  132. .append("Shape")
  133. .call(makeSolid, "black")
  134. .append("Text")
  135. .attr("string", (d) => `"${tickFormat(d)}"`)
  136. .append("FontStyle")
  137. .attr("size", 1.3)
  138. .attr("family", "\"SANS\"")
  139. .attr("style", "BOLD")
  140. .attr("justify", "\"MIDDLE\" \"MIDDLE\"")
  141. .merge(labels);
  142. labels.transition()
  143. .attr("translation", (t) => (axisDirectionVector.map((a) => (scale(t) * a)).join(" ")))
  144. .select("Transform")
  145. .attr("translation", tickDirectionVector.map((d, i) => (labelInset * d * tickPadding) + (((labelInset + 1) / 2) * tickSize * tickDirectionVector[i])))
  146. .on("start", function() {
  147. d3.select(this)
  148. .select("Billboard")
  149. .select("Shape")
  150. .select("Text")
  151. .attr("string", (d) => `"${tickFormat(d)}"`);
  152. });
  153. labels.exit()
  154. .remove();
  155. });
  156. };
  157. /**
  158. * Dimensions Getter / Setter
  159. *
  160. * @param {{x: number, y: number, z: number}} _v - 3D object dimensions.
  161. * @returns {*}
  162. */
  163. my.dimensions = function(_v) {
  164. if (!arguments.length) return dimensions;
  165. dimensions = _v;
  166. return my;
  167. };
  168. /**
  169. * Scale Getter / Setter
  170. *
  171. * @param {d3.scale} _v - D3 Scale.
  172. * @returns {*}
  173. */
  174. my.scale = function(_v) {
  175. if (!arguments.length) return scale;
  176. scale = _v;
  177. return my;
  178. };
  179. /**
  180. * Direction Getter / Setter
  181. *
  182. * @param {string} _v - Direction of Axis (e.g. "x", "y", "z").
  183. * @returns {*}
  184. */
  185. my.direction = function(_v) {
  186. if (!arguments.length) return direction;
  187. direction = _v;
  188. return my;
  189. };
  190. /**
  191. * Tick Direction Getter / Setter
  192. *
  193. * @param {string} _v - Direction of Ticks (e.g. "x", "y", "z").
  194. * @returns {*}
  195. */
  196. my.tickDirection = function(_v) {
  197. if (!arguments.length) return tickDirection;
  198. tickDirection = _v;
  199. return my;
  200. };
  201. /**
  202. * Tick Arguments Getter / Setter
  203. *
  204. * @param {Array} _v - Tick arguments.
  205. * @returns {Array<*>}
  206. */
  207. my.tickArguments = function(_v) {
  208. if (!arguments.length) return tickArguments;
  209. tickArguments = _v;
  210. return my;
  211. };
  212. /**
  213. * Tick Values Getter / Setter
  214. *
  215. * @param {Array} _v - Tick values.
  216. * @returns {*}
  217. */
  218. my.tickValues = function(_v) {
  219. if (!arguments.length) return tickValues;
  220. tickValues = _v;
  221. return my;
  222. };
  223. /**
  224. * Tick Format Getter / Setter
  225. *
  226. * @param {string} _v - Tick format.
  227. * @returns {*}
  228. */
  229. my.tickFormat = function(_v) {
  230. if (!arguments.length) return tickFormat;
  231. tickFormat = _v;
  232. return my;
  233. };
  234. /**
  235. * Tick Size Getter / Setter
  236. *
  237. * @param {number} _v - Tick length.
  238. * @returns {*}
  239. */
  240. my.tickSize = function(_v) {
  241. if (!arguments.length) return tickSize;
  242. tickSize = _v;
  243. return my;
  244. };
  245. /**
  246. * Tick Padding Getter / Setter
  247. *
  248. * @param {number} _v - Tick padding size.
  249. * @returns {*}
  250. */
  251. my.tickPadding = function(_v) {
  252. if (!arguments.length) return tickPadding;
  253. tickPadding = _v;
  254. return my;
  255. };
  256. /**
  257. * Color Getter / Setter
  258. *
  259. * @param {string} _v - Color (e.g. "red" or "#ff0000").
  260. * @returns {*}
  261. */
  262. my.color = function(_v) {
  263. if (!arguments.length) return color;
  264. color = _v;
  265. return my;
  266. };
  267. /**
  268. * Label Position Getter / Setter
  269. *
  270. * @param {string} _v - Position ("proximal" or "distal")
  271. * @returns {*}
  272. */
  273. my.labelPosition = function(_v) {
  274. if (!arguments.length) return labelPosition;
  275. labelPosition = _v;
  276. labelInset = labelPosition === "distal" ? 1 : -1;
  277. return my;
  278. };
  279. return my;
  280. }