All files / components/Axis Axis.jsx

100% Statements 16/16
100% Branches 36/36
100% Functions 2/2
100% Lines 15/15
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176            120x               120x                           120x                                                                                                               120x                                             35x   35x     35x 35x 35x   35x       35x 6x 14x     35x                                 190x                                                                  
import _ from 'lodash';
import React from 'react';
import { lucidClassNames } from '../../util/style-helpers';
import { discreteTicks } from '../../util/chart-helpers';
import { createClass, omitProps } from '../../util/component-types';
 
const cx = lucidClassNames.bind('&-Axis');
 
const {
	string,
	array,
	func,
	number,
	oneOf,
} = React.PropTypes;
 
/**
 * {"categories": ["visualizations", "chart primitives"]}
 *
 * *For use within an `svg`*
 *
 * Axes are used to help render human-readable reference marks on charts. They
 * can either be horizontal or vertical and really only need a scale to be able
 * to draw properly.
 *
 * This component is a very close sister to d3's svg axis and most of the logic
 * was ported from there.
 */
const Axis = createClass({
	displayName: 'Axis',
 
	propTypes: {
		/**
		 * Appended to the component-specific class names set on the root element.
		 */
		className: string,
		/**
		 * Must be a d3 scale. Lucid exposes the `lucid.d3Scale` library for use
		 * here.
		 */
		scale: func.isRequired,
		/**
		 * Size of the ticks for each discrete tick mark.
		 */
		innerTickSize: number,
		/**
		 * Size of the tick marks found at the beginning and end of the axis. It's
		 * common to set this to `0` to remove them.
		 */
		outerTickSize: number,
		/**
		 * An optional function that can format ticks. Generally this shouldn't be
		 * needed since d3 has very good default formatters for most data.
		 *
		 * Signature: `(tick) => {}`
		 */
		tickFormat: func,
		/**
		 * If you need fine grained control over the axis ticks, you can pass them
		 * in this array.
		 */
		ticks: array,
		/**
		 * Determines the spacing between each tick and its text.
		 */
		tickPadding: number,
		/**
		 * Determines the orientation of the ticks. `left` and `right` will
		 * generate a vertical axis, whereas `top` and `bottom` will generate a
		 * horizontal axis.
		 */
		orient: oneOf(['top', 'bottom', 'left', 'right']),
		/**
		 * Control the number of ticks displayed.
		 *
		 * If the scale is time based or linear, this number acts a "hint" per the
		 * default behavior of D3. If it's an ordinal scale, this number is treated
		 * as an absolute number of ticks to display and is powered by our own
		 * utility function `discreteTicks`.
		 */
		tickCount: number,
	},
 
	getDefaultProps() {
		return {
			innerTickSize: 6, // same as d3
			outerTickSize: 6, // same as d3
			tickPadding: 3, // same as d3
			orient: 'bottom',
			tickCount: null,
		};
	},
 
	render() {
		const {
			scale,
			className,
			orient,
			tickCount,
			ticks = scale.ticks
				? scale.ticks(tickCount)
				: discreteTicks(scale.domain(), tickCount), // ordinal scales don't have `ticks` but they do have `domains`
			innerTickSize,
			outerTickSize,
			tickFormat = scale.tickFormat ? scale.tickFormat() : _.identity,
			tickPadding,
			...passThroughs,
		} = this.props;
 
		const tickSpacing = Math.max(innerTickSize, 0) + tickPadding;
 
		// Domain
		const range = scale.range();
		const sign = orient === 'top' || orient === 'left' ? -1 : 1;
		const isH = orient === 'top' || orient === 'bottom'; // is horizontal
 
		let scaleNormalized = scale;
 
		// Only band scales have `bandwidth`, this conditional helps center the
		// ticks on the bands
		if (scale.bandwidth) {
			const bandModifier = scale.bandwidth() / 2;
			scaleNormalized = (d) => scale(d) + bandModifier;
		}
 
		return (
			<g
				{...omitProps(passThroughs, Axis)}
				className={cx(className, '&')}
			>
			{isH ? (
				<path
					className={cx('&-domain')}
					d={`M${range[0]},${sign * outerTickSize}V0H${range[1]}V${sign * outerTickSize}`}
				/>
			) : (
				<path
					className={cx('&-domain')}
					d={`M${sign * outerTickSize},${range[0]}H0V${range[1]}H${sign * outerTickSize}`}
				/>
			)}
				{_.map(ticks, (tick) =>
					<g
						key={tick}
						transform={`translate(${isH ? scaleNormalized(tick) : 0}, ${isH ? 0 : scaleNormalized(tick)})`}
					>
						<line
							className={cx('&-tick')}
							x2={isH ? 0 : sign * innerTickSize}
							y2={isH ? sign * innerTickSize : 0}
						/>
						<text
							className={cx('&-tick-text')}
							x={isH ? 0 : sign * tickSpacing}
							y={isH ? sign * tickSpacing : 0}
							dy={isH
								? sign < 0 ? '0em' : '.71em' // magic d3 number
								: '.32em' // magic d3 number
							}
							style={{
								textAnchor: isH
									? 'middle'
									: sign < 0 ? 'end' : 'start',
							}}
						>
							{tickFormat(tick)}
						</text>
					</g>
				)}
			</g>
		);
	},
});
 
export default Axis;