component/axis.js

import * as d3 from "d3";
import { colorParse } from "../colorHelper.js";

/**
 * Reusable 3D Axis Component
 *
 * @module
 */
export default function() {

	/* Default Properties */
	let dimensions = { x: 40, y: 40, z: 40 };
	let color = "black";
	let classed = "d3X3dAxis";
	let labelPosition = "proximal";
	let labelInset = labelPosition === "distal" ? 1 : -1;

	/* Scale and Axis Options */
	let scale;
	let direction;
	let tickDirection;
	let tickArguments = [];
	let tickValues = null;
	let tickFormat = null;
	let tickSize = 1.5;
	let tickPadding = 2.0;

	/**
	 * Get Axis Direction Vector
	 *
	 * @private
	 * @param {string} axisDir
	 * @returns {number[]}
	 */
	const getAxisDirectionVector = function(axisDir) {
		const axisDirectionVectors = {
			x: [1, 0, 0],
			y: [0, 1, 0],
			z: [0, 0, 1]
		};

		return axisDirectionVectors[axisDir];
	};

	/**
	 * Get Axis Rotation Vector
	 *
	 * @private
	 * @param {string} axisDir
	 * @returns {number[]}
	 */
	const getAxisRotationVector = function(axisDir) {
		const axisRotationVectors = {
			x: [1, 1, 0, Math.PI],
			y: [0, 0, 0, 0],
			z: [0, 1, 1, Math.PI]
		};

		return axisRotationVectors[axisDir];
	};

	/**
	 * Constructor
	 *
	 * @constructor
	 * @alias axis
	 * @param {d3.selection} selection - The chart holder D3 selection.
	 */
	const my = function(selection) {
		selection.each(function() {

			const element = d3.select(this)
				.classed(classed, true);

			const range = scale.range();
			const range0 = range[0];
			const range1 = range[range.length - 1];

			const axisDirectionVector = getAxisDirectionVector(direction);
			const tickDirectionVector = getAxisDirectionVector(tickDirection);
			const axisRotationVector = getAxisRotationVector(direction);
			const tickRotationVector = getAxisRotationVector(tickDirection);

			/*
			// FIXME: Currently the tickArguments option does not work.
			const tickValuesDefault = scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain();
			tickValues = tickValues === null ? tickValuesDefault : tickValues;
			*/
			tickValues = scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain();

			const tickFormatDefault = scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments) : (d) => d;
			tickFormat = tickFormat === null ? tickFormatDefault : tickFormat;

			const makeSolid = (el, color) => {
				el.append("Appearance")
					.append("Material")
					.attr("diffuseColor", colorParse(color) || "0 0 0")
					.attr("transparency", "0");
			};

			const shape = (el, radius, height, color) => {
				const shape = el.append("Shape");

				shape.append("Cylinder")
					.attr("radius", radius)
					.attr("height", height);

				shape.call(makeSolid, colorParse(color));
			};

			// Main Lines
			const domain = element.selectAll(".domain")
				.data([null]);

			domain.enter()
				.append("Transform")
				.attr("class", "domain")
				.attr("rotation", axisRotationVector.join(" "))
				.attr("translation", axisDirectionVector.map((d) => (d * (range0 + range1) / 2)).join(" "))
				.call(shape, 0.1, range1 - range0, color)
				.merge(domain);

			domain.exit()
				.remove();

			// Tick Lines
			const ticks = element.selectAll(".tickLine")
				.data(tickValues, (d) => d);

			ticks.enter()
				.append("Transform")
				.attr("class", "tickLine")
				.attr("translation", (t) => (axisDirectionVector.map((a) => (scale(t) * a)).join(" ")))
				.append("Transform")
				.attr("translation", tickDirectionVector.map((d) => (d * tickSize / 2)).join(" "))
				.attr("rotation", tickRotationVector.join(" "))
				.call(shape, 0.05, tickSize, "#f3f3f3")
				.merge(ticks);

			ticks.transition()
				.attr("translation", (t) => (axisDirectionVector.map((a) => (scale(t) * a)).join(" ")));

			ticks.exit()
				.remove();

			// Tick Labels
			const labels = element.selectAll(".tickLabel")
				.data(tickValues, (d) => d);

			labels.enter()
				.append("Transform")
				.attr("class", "tickLabel")
				.attr("translation", (t) => (axisDirectionVector.map((a) => (scale(t) * a)).join(" ")))
				.append("Transform")
				.attr("translation", tickDirectionVector.map((d, i) => (labelInset * d * tickPadding) + (((labelInset + 1) / 2) * tickSize * tickDirectionVector[i])))
				.append("Billboard")
				.attr("axisOfRotation", "0 0 0")
				.append("Shape")
				.call(makeSolid, "black")
				.append("Text")
				.attr("string", (d) => `"${tickFormat(d)}"`)
				.append("FontStyle")
				.attr("size", 1.3)
				.attr("family", "\"SANS\"")
				.attr("style", "BOLD")
				.attr("justify", "\"MIDDLE\" \"MIDDLE\"")
				.merge(labels);

			labels.transition()
				.attr("translation", (t) => (axisDirectionVector.map((a) => (scale(t) * a)).join(" ")))
				.select("Transform")
				.attr("translation", tickDirectionVector.map((d, i) => (labelInset * d * tickPadding) + (((labelInset + 1) / 2) * tickSize * tickDirectionVector[i])))
				.on("start", function() {
					d3.select(this)
						.select("Billboard")
						.select("Shape")
						.select("Text")
						.attr("string", (d) => `"${tickFormat(d)}"`);
				});

			labels.exit()
				.remove();

		});
	};

	/**
	 * Dimensions Getter / Setter
	 *
	 * @param {{x: number, y: number, z: number}} _v - 3D object dimensions.
	 * @returns {*}
	 */
	my.dimensions = function(_v) {
		if (!arguments.length) return dimensions;
		dimensions = _v;
		return my;
	};

	/**
	 * Scale Getter / Setter
	 *
	 * @param {d3.scale} _v - D3 Scale.
	 * @returns {*}
	 */
	my.scale = function(_v) {
		if (!arguments.length) return scale;
		scale = _v;
		return my;
	};

	/**
	 * Direction Getter / Setter
	 *
	 * @param {string} _v - Direction of Axis (e.g. "x", "y", "z").
	 * @returns {*}
	 */
	my.direction = function(_v) {
		if (!arguments.length) return direction;
		direction = _v;
		return my;
	};

	/**
	 * Tick Direction Getter / Setter
	 *
	 * @param {string} _v - Direction of Ticks (e.g. "x", "y", "z").
	 * @returns {*}
	 */
	my.tickDirection = function(_v) {
		if (!arguments.length) return tickDirection;
		tickDirection = _v;
		return my;
	};

	/**
	 * Tick Arguments Getter / Setter
	 *
	 * @param {Array} _v - Tick arguments.
	 * @returns {Array<*>}
	 */
	my.tickArguments = function(_v) {
		if (!arguments.length) return tickArguments;
		tickArguments = _v;
		return my;
	};

	/**
	 * Tick Values Getter / Setter
	 *
	 * @param {Array} _v - Tick values.
	 * @returns {*}
	 */
	my.tickValues = function(_v) {
		if (!arguments.length) return tickValues;
		tickValues = _v;
		return my;
	};

	/**
	 * Tick Format Getter / Setter
	 *
	 * @param {string} _v - Tick format.
	 * @returns {*}
	 */
	my.tickFormat = function(_v) {
		if (!arguments.length) return tickFormat;
		tickFormat = _v;
		return my;
	};

	/**
	 * Tick Size Getter / Setter
	 *
	 * @param {number} _v - Tick length.
	 * @returns {*}
	 */
	my.tickSize = function(_v) {
		if (!arguments.length) return tickSize;
		tickSize = _v;
		return my;
	};

	/**
	 * Tick Padding Getter / Setter
	 *
	 * @param {number} _v - Tick padding size.
	 * @returns {*}
	 */
	my.tickPadding = function(_v) {
		if (!arguments.length) return tickPadding;
		tickPadding = _v;
		return my;
	};

	/**
	 * Color Getter / Setter
	 *
	 * @param {string} _v - Color (e.g. "red" or "#ff0000").
	 * @returns {*}
	 */
	my.color = function(_v) {
		if (!arguments.length) return color;
		color = _v;
		return my;
	};

	/**
	 * Label Position Getter / Setter
	 *
	 * @param {string} _v - Position ("proximal" or "distal")
	 * @returns {*}
	 */
	my.labelPosition = function(_v) {
		if (!arguments.length) return labelPosition;
		labelPosition = _v;
		labelInset = labelPosition === "distal" ? 1 : -1;
		return my;
	};

	return my;
}