diff --git a/packages/vx-axis/package.json b/packages/vx-axis/package.json index b489b3bf4..bdbc8a4ef 100644 --- a/packages/vx-axis/package.json +++ b/packages/vx-axis/package.json @@ -5,6 +5,7 @@ "sideEffects": false, "main": "lib/index.js", "module": "esm/index.js", + "types": "lib/index.d.ts", "scripts": { "docs": "cd ./docs && ../../../node_modules/.bin/react-docgen ../src/axis/ | ../../../scripts/buildDocs.sh" }, @@ -30,6 +31,8 @@ }, "homepage": "https://github.com/hshoff/vx#readme", "dependencies": { + "@types/classnames": "^2.2.9", + "@types/react": "*", "@vx/group": "0.0.192", "@vx/point": "0.0.192", "@vx/shape": "0.0.192", @@ -37,6 +40,9 @@ "classnames": "^2.2.5", "prop-types": "^15.6.0" }, + "devDependencies": { + "@vx/scale": "0.0.192" + }, "peerDependencies": { "react": "^16.3.0-0" }, diff --git a/packages/vx-axis/src/axis/Axis.jsx b/packages/vx-axis/src/axis/Axis.tsx similarity index 55% rename from packages/vx-axis/src/axis/Axis.jsx rename to packages/vx-axis/src/axis/Axis.tsx index 145f525eb..99248ae8b 100644 --- a/packages/vx-axis/src/axis/Axis.jsx +++ b/packages/vx-axis/src/axis/Axis.tsx @@ -1,46 +1,21 @@ import React from 'react'; -import PropTypes from 'prop-types'; import cx from 'classnames'; import { Line } from '@vx/shape'; import { Point } from '@vx/point'; import { Group } from '@vx/group'; import { Text } from '@vx/text'; import center from '../utils/center'; -import identity from '../utils/identity'; import getLabelTransform from '../utils/labelTransform'; import ORIENT from '../constants/orientation'; +import toString from '../utils/toString'; +import toNumberOrUndefined from '../utils/toNumberOrUndefined'; +import { SharedAxisProps, AxisOrientation } from '../types'; -const propTypes = { - axisClassName: PropTypes.string, - axisLineClassName: PropTypes.string, - hideAxisLine: PropTypes.bool, - hideTicks: PropTypes.bool, - hideZero: PropTypes.bool, - label: PropTypes.string, - labelClassName: PropTypes.string, - labelOffset: PropTypes.number, - labelProps: PropTypes.object, - left: PropTypes.number, - numTicks: PropTypes.number, - orientation: PropTypes.oneOf([ORIENT.top, ORIENT.right, ORIENT.bottom, ORIENT.left]), - rangePadding: PropTypes.number, - scale: PropTypes.func.isRequired, - stroke: PropTypes.string, - strokeWidth: PropTypes.number, - strokeDasharray: PropTypes.string, - tickClassName: PropTypes.string, - tickFormat: PropTypes.func, - tickLabelProps: PropTypes.func, - tickLength: PropTypes.number, - tickStroke: PropTypes.string, - tickTransform: PropTypes.string, - tickValues: PropTypes.array, - tickComponent: PropTypes.func, - top: PropTypes.number, - children: PropTypes.func, +export type AxisProps = SharedAxisProps & { + orientation?: AxisOrientation; }; -export default function Axis({ +export default function Axis({ children, axisClassName, axisLineClassName, @@ -54,14 +29,14 @@ export default function Axis({ textAnchor: 'middle', fontFamily: 'Arial', fontSize: 10, - fill: 'black', + fill: '#222', }, left = 0, numTicks = 10, orientation = ORIENT.bottom, rangePadding = 0, scale, - stroke = 'black', + stroke = '#222', strokeWidth = 1, strokeDasharray, tickClassName, @@ -70,38 +45,36 @@ export default function Axis({ textAnchor: 'middle', fontFamily: 'Arial', fontSize: 10, - fill: 'black', + fill: '#222', }), tickLength = 8, - tickStroke = 'black', + tickStroke = '#222', tickTransform, tickValues, tickComponent, top = 0, -}) { - let values = scale.ticks ? scale.ticks(numTicks) : scale.domain(); - if (tickValues) values = tickValues; - let format = scale.tickFormat ? scale.tickFormat() : identity; - if (tickFormat) format = tickFormat; +}: AxisProps) { + const values = tickValues || (scale.ticks ? scale.ticks(numTicks) : scale.domain()); + const format = tickFormat || (scale.tickFormat ? scale.tickFormat() : toString); const range = scale.range(); - const range0 = range[0] + 0.5 - rangePadding; - const range1 = range[range.length - 1] + 0.5 + rangePadding; + const range0 = Number(range[0]) + 0.5 - rangePadding; + const range1 = Number(range[range.length - 1]) + 0.5 + rangePadding; - const horizontal = orientation !== ORIENT.left && orientation !== ORIENT.right; const isLeft = orientation === ORIENT.left; const isTop = orientation === ORIENT.top; + const axisIsHorizontal = isTop || orientation === ORIENT.bottom; const tickSign = isLeft || isTop ? -1 : 1; - const position = (scale.bandwidth ? center : identity)(scale.copy()); + const position = center(scale.copy()); const axisFromPoint = new Point({ - x: horizontal ? range0 : 0, - y: horizontal ? 0 : range0, + x: axisIsHorizontal ? range0 : 0, + y: axisIsHorizontal ? 0 : range0, }); const axisToPoint = new Point({ - x: horizontal ? range1 : 0, - y: horizontal ? 0 : range1, + x: axisIsHorizontal ? range1 : 0, + y: axisIsHorizontal ? 0 : range1, }); let tickLabelFontSize = 10; // track the max tick label size to compute label offset @@ -112,7 +85,7 @@ export default function Axis({ {children({ axisFromPoint, axisToPoint, - horizontal, + horizontal: axisIsHorizontal, tickSign, numTicks, label, @@ -121,13 +94,14 @@ export default function Axis({ tickFormat: format, tickPosition: position, ticks: values.map((value, index) => { + const scaledValue = toNumberOrUndefined(position(value)); const from = new Point({ - x: horizontal ? position(value) : 0, - y: horizontal ? 0 : position(value), + x: axisIsHorizontal ? scaledValue : 0, + y: axisIsHorizontal ? 0 : scaledValue, }); const to = new Point({ - x: horizontal ? position(value) : tickSign * tickLength, - y: horizontal ? tickLength * tickSign : position(value), + x: axisIsHorizontal ? scaledValue : tickSign * tickLength, + y: axisIsHorizontal ? tickLength * tickSign : scaledValue, }); return { value, @@ -145,20 +119,30 @@ export default function Axis({ return ( {values.map((val, index) => { - if (hideZero && val === 0) return null; - + if ( + hideZero && + ((typeof val === 'number' && val === 0) || (typeof val === 'string' && val === '0')) + ) { + return null; + } + const scaledValue = toNumberOrUndefined(position(val)); const tickFromPoint = new Point({ - x: horizontal ? position(val) : 0, - y: horizontal ? 0 : position(val), + x: axisIsHorizontal ? scaledValue : 0, + y: axisIsHorizontal ? 0 : scaledValue, }); const tickToPoint = new Point({ - x: horizontal ? position(val) : tickSign * tickLength, - y: horizontal ? tickLength * tickSign : position(val), + x: axisIsHorizontal ? scaledValue : tickSign * tickLength, + y: axisIsHorizontal ? tickLength * tickSign : scaledValue, }); const tickLabelPropsObj = tickLabelProps(val, index); - tickLabelFontSize = Math.max(tickLabelFontSize, tickLabelPropsObj.fontSize || 0); + tickLabelFontSize = Math.max( + tickLabelFontSize, + (typeof tickLabelPropsObj.fontSize === 'number' && tickLabelPropsObj.fontSize) || 0, + ); + const tickYCoord = tickToPoint.y + (axisIsHorizontal && !isTop ? tickLabelFontSize : 0); + const formattedValue = format(val, index); return ( } {tickComponent ? ( tickComponent({ - x: tickToPoint.x, - y: tickToPoint.y + (horizontal && !isTop ? tickLabelFontSize : 0), - formattedValue: format(val, index), ...tickLabelPropsObj, + x: tickToPoint.x, + y: tickYCoord, + formattedValue, }) ) : ( - - {format(val, index)} + + {formattedValue} )} @@ -216,5 +196,3 @@ export default function Axis({ ); } - -Axis.propTypes = propTypes; diff --git a/packages/vx-axis/src/axis/AxisBottom.jsx b/packages/vx-axis/src/axis/AxisBottom.jsx deleted file mode 100644 index 3d0fc0d4f..000000000 --- a/packages/vx-axis/src/axis/AxisBottom.jsx +++ /dev/null @@ -1,178 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; -import Axis from './Axis'; -import ORIENT from '../constants/orientation'; - -const propTypes = { - /** - * The class name applied to the outermost axis group element. - */ - axisClassName: PropTypes.string, - /** - * The class name applied to the axis line element. - */ - axisLineClassName: PropTypes.string, - /** - * If true, will hide the axis line. - */ - hideAxisLine: PropTypes.bool, - /** - * If true, will hide the ticks (but not the tick labels). - */ - hideTicks: PropTypes.bool, - /** - * If true, will hide the '0' value tick and tick label. - */ - hideZero: PropTypes.bool, - /** - * The text for the axis label. - */ - label: PropTypes.string, - /** - * The class name applied to the axis label text element. - */ - labelClassName: PropTypes.string, - /** - * Pixel offset of the axis label (does not include tick label font size, which is accounted for automatically) - */ - labelOffset: PropTypes.number, - /** - * Props applied to the axis label component. - */ - labelProps: PropTypes.object, - /** - * A left pixel offset applied to the entire axis. - */ - left: PropTypes.number, - /** - * The number of ticks wanted for the axis (note this is approximate) - */ - numTicks: PropTypes.number, - /** - * Pixel padding to apply to both sides of the axis. - */ - rangePadding: PropTypes.number, - /** - * A [d3](https://github.com/d3/d3-scale) or [vx](https://github.com/hshoff/vx/tree/master/packages/vx-scale) scale function. - */ - scale: PropTypes.func.isRequired, - /** - * The color for the stroke of the lines. - */ - stroke: PropTypes.string, - /** - * The pixel value for the width of the lines. - */ - strokeWidth: PropTypes.number, - /** - * The pattern of dashes in the stroke. - */ - strokeDasharray: PropTypes.string, - /** - * The class name applied to each tick group. - */ - tickClassName: PropTypes.string, - /** - * A [d3 formatter](https://github.com/d3/d3-scale/blob/master/README.md#continuous_tickFormat) for the tick text. - */ - tickFormat: PropTypes.func, - /** - * A function that returns props for a given tick label. - */ - tickLabelProps: PropTypes.func, - /** - * The length of the tick lines. - */ - tickLength: PropTypes.number, - /** - * The color for the tick's stroke value. - */ - tickStroke: PropTypes.string, - /** - * A custom SVG transform value to be applied to each tick group. - */ - tickTransform: PropTypes.string, - /** - * An array of values that determine the number and values of the ticks. Falls back to `scale.ticks()` or `.domain()`. - */ - tickValues: PropTypes.array, - tickComponent: PropTypes.func, - /** - * A top pixel offset applied to the entire axis. - */ - top: PropTypes.number, - /** - * For more control over rendering or to add event handlers to datum, pass a function as children. - */ - children: PropTypes.func, -}; - -export default function AxisBottom({ - children, - axisClassName, - axisLineClassName, - hideAxisLine, - hideTicks, - hideZero, - label, - labelClassName, - labelOffset = 8, - labelProps, - left, - numTicks, - rangePadding, - scale, - stroke, - strokeWidth, - strokeDasharray, - tickClassName, - tickFormat, - tickLabelProps = (/** tickValue, index */) => ({ - dy: '0.25em', - fill: 'black', - fontFamily: 'Arial', - fontSize: 10, - textAnchor: 'middle', - }), - tickLength = 8, - tickStroke, - tickTransform, - tickValues, - tickComponent, - top, -}) { - return ( - - ); -} - -AxisBottom.propTypes = propTypes; diff --git a/packages/vx-axis/src/axis/AxisBottom.tsx b/packages/vx-axis/src/axis/AxisBottom.tsx new file mode 100644 index 000000000..3bde70bdb --- /dev/null +++ b/packages/vx-axis/src/axis/AxisBottom.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import cx from 'classnames'; +import Axis from './Axis'; +import ORIENT from '../constants/orientation'; +import { SharedAxisProps } from '../types'; + +export type AxisBottomProps = SharedAxisProps; + +export default function AxisBottom({ + children, + axisClassName, + axisLineClassName, + hideAxisLine, + hideTicks, + hideZero, + label, + labelClassName, + labelOffset = 8, + labelProps, + left, + numTicks, + rangePadding, + scale, + stroke, + strokeWidth, + strokeDasharray, + tickClassName, + tickFormat, + tickLabelProps = (/** tickValue, index */) => ({ + dy: '0.25em', + fill: '#222', + fontFamily: 'Arial', + fontSize: 10, + textAnchor: 'middle', + }), + tickLength = 8, + tickStroke, + tickTransform, + tickValues, + tickComponent, + top, +}: AxisBottomProps) { + return ( + + ); +} diff --git a/packages/vx-axis/src/axis/AxisLeft.jsx b/packages/vx-axis/src/axis/AxisLeft.tsx similarity index 60% rename from packages/vx-axis/src/axis/AxisLeft.jsx rename to packages/vx-axis/src/axis/AxisLeft.tsx index 9a1d7f962..061bf7f92 100644 --- a/packages/vx-axis/src/axis/AxisLeft.jsx +++ b/packages/vx-axis/src/axis/AxisLeft.tsx @@ -1,39 +1,12 @@ import React from 'react'; -import PropTypes from 'prop-types'; import cx from 'classnames'; import Axis from './Axis'; import ORIENT from '../constants/orientation'; +import { SharedAxisProps } from '../types'; -const propTypes = { - axisClassName: PropTypes.string, - axisLineClassName: PropTypes.string, - hideAxisLine: PropTypes.bool, - hideTicks: PropTypes.bool, - hideZero: PropTypes.bool, - label: PropTypes.string, - labelClassName: PropTypes.string, - labelOffset: PropTypes.number, - labelProps: PropTypes.object, - left: PropTypes.number, - numTicks: PropTypes.number, - rangePadding: PropTypes.number, - scale: PropTypes.func.isRequired, - stroke: PropTypes.string, - strokeWidth: PropTypes.number, - strokeDasharray: PropTypes.string, - tickClassName: PropTypes.string, - tickFormat: PropTypes.func, - tickLabelProps: PropTypes.func, - tickLength: PropTypes.number, - tickStroke: PropTypes.string, - tickTransform: PropTypes.string, - tickValues: PropTypes.array, - tickComponent: PropTypes.func, - top: PropTypes.number, - children: PropTypes.func, -}; +export type AxisLeftProps = SharedAxisProps; -export default function AxisLeft({ +export default function AxisLeft({ children, axisClassName, axisLineClassName, @@ -56,7 +29,7 @@ export default function AxisLeft({ tickLabelProps = (/** tickValue, index */) => ({ dx: '-0.25em', dy: '0.25em', - fill: 'black', + fill: '#222', fontFamily: 'Arial', fontSize: 10, textAnchor: 'end', @@ -67,7 +40,7 @@ export default function AxisLeft({ tickValues, tickComponent, top, -}) { +}: AxisLeftProps) { return ( ); } - -AxisLeft.propTypes = propTypes; diff --git a/packages/vx-axis/src/axis/AxisRight.jsx b/packages/vx-axis/src/axis/AxisRight.tsx similarity index 60% rename from packages/vx-axis/src/axis/AxisRight.jsx rename to packages/vx-axis/src/axis/AxisRight.tsx index 11445cf72..510d36ee0 100644 --- a/packages/vx-axis/src/axis/AxisRight.jsx +++ b/packages/vx-axis/src/axis/AxisRight.tsx @@ -1,39 +1,12 @@ import React from 'react'; -import PropTypes from 'prop-types'; import cx from 'classnames'; import Axis from './Axis'; import ORIENT from '../constants/orientation'; +import { SharedAxisProps } from '../types'; -const propTypes = { - axisClassName: PropTypes.string, - axisLineClassName: PropTypes.string, - hideAxisLine: PropTypes.bool, - hideTicks: PropTypes.bool, - hideZero: PropTypes.bool, - label: PropTypes.string, - labelClassName: PropTypes.string, - labelOffset: PropTypes.number, - labelProps: PropTypes.object, - left: PropTypes.number, - numTicks: PropTypes.number, - rangePadding: PropTypes.number, - scale: PropTypes.func.isRequired, - stroke: PropTypes.string, - strokeWidth: PropTypes.number, - strokeDasharray: PropTypes.string, - tickClassName: PropTypes.string, - tickFormat: PropTypes.func, - tickLabelProps: PropTypes.func, - tickLength: PropTypes.number, - tickStroke: PropTypes.string, - tickTransform: PropTypes.string, - tickValues: PropTypes.array, - tickComponent: PropTypes.func, - top: PropTypes.number, - children: PropTypes.func, -}; +export type AxisRightProps = SharedAxisProps; -export default function AxisRight({ +export default function AxisRight({ children, axisClassName, axisLineClassName, @@ -56,7 +29,7 @@ export default function AxisRight({ tickLabelProps = (/** tickValue, index */) => ({ dx: '0.25em', dy: '0.25em', - fill: 'black', + fill: '#222', fontFamily: 'Arial', fontSize: 10, textAnchor: 'start', @@ -67,7 +40,7 @@ export default function AxisRight({ tickValues, tickComponent, top, -}) { +}: AxisRightProps) { return ( ); } - -AxisRight.propTypes = propTypes; diff --git a/packages/vx-axis/src/axis/AxisTop.jsx b/packages/vx-axis/src/axis/AxisTop.tsx similarity index 60% rename from packages/vx-axis/src/axis/AxisTop.jsx rename to packages/vx-axis/src/axis/AxisTop.tsx index d0cdadf87..0c6302420 100644 --- a/packages/vx-axis/src/axis/AxisTop.jsx +++ b/packages/vx-axis/src/axis/AxisTop.tsx @@ -1,39 +1,12 @@ import React from 'react'; -import PropTypes from 'prop-types'; import cx from 'classnames'; import Axis from './Axis'; import ORIENT from '../constants/orientation'; +import { SharedAxisProps } from '../types'; -const propTypes = { - axisClassName: PropTypes.string, - axisLineClassName: PropTypes.string, - hideAxisLine: PropTypes.bool, - hideTicks: PropTypes.bool, - hideZero: PropTypes.bool, - label: PropTypes.string, - labelClassName: PropTypes.string, - labelOffset: PropTypes.number, - labelProps: PropTypes.object, - left: PropTypes.number, - numTicks: PropTypes.number, - rangePadding: PropTypes.number, - scale: PropTypes.func.isRequired, - stroke: PropTypes.string, - strokeWidth: PropTypes.number, - strokeDasharray: PropTypes.string, - tickClassName: PropTypes.string, - tickFormat: PropTypes.func, - tickLabelProps: PropTypes.func, - tickLength: PropTypes.number, - tickStroke: PropTypes.string, - tickTransform: PropTypes.string, - tickValues: PropTypes.array, - tickComponent: PropTypes.func, - top: PropTypes.number, - children: PropTypes.func, -}; +export type AxisTopProps = SharedAxisProps; -export default function AxisTop({ +export default function AxisTop({ children, axisClassName, axisLineClassName, @@ -55,7 +28,7 @@ export default function AxisTop({ tickFormat, tickLabelProps = (/** tickValue, index */) => ({ dy: '-0.25em', - fill: 'black', + fill: '#222', fontFamily: 'Arial', fontSize: 10, textAnchor: 'middle', @@ -66,7 +39,7 @@ export default function AxisTop({ tickValues, tickComponent, top, -}) { +}: AxisTopProps) { return ( ); } - -AxisTop.propTypes = propTypes; diff --git a/packages/vx-axis/src/constants/orientation.js b/packages/vx-axis/src/constants/orientation.js deleted file mode 100644 index 7f35efa2e..000000000 --- a/packages/vx-axis/src/constants/orientation.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - top: 'top', - left: 'left', - right: 'right', - bottom: 'bottom', -}; diff --git a/packages/vx-axis/src/constants/orientation.ts b/packages/vx-axis/src/constants/orientation.ts new file mode 100644 index 000000000..57189db18 --- /dev/null +++ b/packages/vx-axis/src/constants/orientation.ts @@ -0,0 +1,10 @@ +import { AxisOrientation } from '../types'; + +const orientation: Record = { + top: 'top', + left: 'left', + right: 'right', + bottom: 'bottom', +}; + +export default orientation; diff --git a/packages/vx-axis/src/index.js b/packages/vx-axis/src/index.ts similarity index 100% rename from packages/vx-axis/src/index.js rename to packages/vx-axis/src/index.ts diff --git a/packages/vx-axis/src/types.ts b/packages/vx-axis/src/types.ts new file mode 100644 index 000000000..f766db892 --- /dev/null +++ b/packages/vx-axis/src/types.ts @@ -0,0 +1,123 @@ +import { TextProps } from '@vx/text/lib/Text'; + +export type AxisOrientation = 'top' | 'right' | 'bottom' | 'left'; + +export type FormattedValue = string | number | undefined; + +export type TickFormatter = (value: ScaleInput, tickIndex: number) => FormattedValue; + +export type TickLabelProps = (val: ScaleInput, index: number) => Partial; + +export type TickRendererProps = Partial & { + x: number; + y: number; + formattedValue: FormattedValue; +}; + +export type SharedAxisProps = { + /** The class name applied to the outermost axis group element. */ + axisClassName?: string; + /** The class name applied to the axis line element. */ + axisLineClassName?: string; + /** If true, will hide the axis line. */ + hideAxisLine?: boolean; + /** If true, will hide the ticks (but not the tick labels). */ + hideTicks?: boolean; + /** If true, will hide the '0' value tick and tick label. */ + hideZero?: boolean; + /** The text for the axis label. */ + label?: string; + /** The class name applied to the axis label text element. */ + labelClassName?: string; + /** Pixel offset of the axis label (does not include tick label font size, which is accounted for automatically) */ + labelOffset?: number; + /** Props applied to the axis label component. */ + labelProps?: Partial; + /** A left pixel offset applied to the entire axis. */ + left?: number; + /** The number of ticks wanted for the axis (note this is approximate) */ + numTicks?: number; + /** Pixel padding to apply to both sides of the axis. */ + rangePadding?: number; + /** A [d3](https://github.com/d3/d3-scale) or [vx](https://github.com/hshoff/vx/tree/master/packages/vx-scale) scale function. */ + scale: GenericScale; + /** The color for the stroke of the lines. */ + stroke?: string; + /** The pixel value for the width of the lines. */ + strokeWidth?: number; + /** The pattern of dashes in the stroke. */ + strokeDasharray?: string; + /** The class name applied to each tick group. */ + tickClassName?: string; + /** A [d3 formatter](https://github.com/d3/d3-scale/blob/master/README.md#continuous_tickFormat) for the tick text. */ + tickFormat?: TickFormatter; + /** A function that returns props for a given tick label. */ + tickLabelProps?: TickLabelProps; + /** The length of the tick lines. */ + tickLength?: number; + /** The color for the tick's stroke value. */ + tickStroke?: string; + /** A custom SVG transform value to be applied to each tick group. */ + tickTransform?: string; + /** An array of values that determine the number and values of the ticks. Falls back to `scale.ticks()` or `.domain()`. */ + tickValues?: ScaleInput[]; + /** Override the component used to render tick labels (instead of from @vx/text) */ + tickComponent?: (tickRendererProps: TickRendererProps) => React.ReactNode; + /** A top pixel offset applied to the entire axis. */ + top?: number; + /** For more control over rendering or to add event handlers to datum, pass a function as children. */ + children?: (renderProps: ChildRenderProps) => React.ReactNode; +}; + +// In order to plot values on an axis, Output must be numeric or coercible to a number. +// Some scales return undefined. +export type ScaleOutput = number | { valueOf(): number } | undefined; + +export type GenericScale = + | ScaleNoRangeRound + | ScaleWithRangeRound; + +interface ScaleNoRangeRound { + (value: ScaleInput): ScaleOutput | [ScaleOutput, ScaleOutput]; // quantize scales return an array + domain(): ScaleInput[] | [ScaleInput, ScaleInput]; + domain(scaleInput: ScaleInput[] | [ScaleInput, ScaleInput]): this; + range(): ScaleOutput[] | [ScaleOutput, ScaleOutput]; + range(scaleOutput: ScaleOutput[] | [ScaleOutput, ScaleOutput]): this; + ticks?: (count: number) => ScaleInput[] | [ScaleInput, ScaleInput]; + bandwidth?: () => number; + round?: () => boolean; + tickFormat?: () => (input: ScaleInput) => FormattedValue; + copy(): this; +} + +// We cannot have optional methods AND overloads, so define a separate type for rangeRound +interface ScaleWithRangeRound extends ScaleNoRangeRound { + rangeRound(): ScaleOutput[] | [ScaleOutput, ScaleOutput]; + rangeRound(scaleOutput: ScaleOutput[] | [ScaleOutput, ScaleOutput]): this; +} + +export interface Point { + x: number; + y: number; +} + +export type ChildRenderProps = { + axisFromPoint: Point; + axisToPoint: Point; + horizontal: boolean; + /** Axis coordinate sign, -1 for left or top orientation. */ + tickSign: 1 | -1; + numTicks: number; + label?: string; + rangePadding: number; + tickLength: number; + tickFormat: TickFormatter; + tickPosition: (value: ScaleInput) => ScaleOutput; + ticks: { + value: ScaleInput; + index: number; + from: Point; + to: Point; + formattedValue: FormattedValue; + }[]; +}; diff --git a/packages/vx-axis/src/utils/center.js b/packages/vx-axis/src/utils/center.js deleted file mode 100644 index dd033fff1..000000000 --- a/packages/vx-axis/src/utils/center.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function center(scale) { - let offset = scale.bandwidth() / 2; - if (scale.round()) offset = Math.round(offset); - return d => { - return scale(d) + offset; - }; -} diff --git a/packages/vx-axis/src/utils/center.ts b/packages/vx-axis/src/utils/center.ts new file mode 100644 index 000000000..83683be4c --- /dev/null +++ b/packages/vx-axis/src/utils/center.ts @@ -0,0 +1,18 @@ +import { GenericScale } from '../types'; + +/** + * Returns a function that applies a centering transform to a scaled value, + * if `Output` is of type `number` and `scale.bandwidth()` is defined + */ +export default function center(scale: GenericScale) { + let offset = scale.bandwidth ? scale.bandwidth() / 2 : 0; + if (scale.round && scale.round()) offset = Math.round(offset); + + return (d: ScaleInput) => { + const scaledValue = scale(d); + if (typeof scaledValue === 'number') return scaledValue + offset; + // quantize scales return an array of values + if (Array.isArray(scaledValue)) return Number(scaledValue[0]) + offset; + return scaledValue; + }; +} diff --git a/packages/vx-axis/src/utils/identity.js b/packages/vx-axis/src/utils/identity.js deleted file mode 100644 index 0c86fdf01..000000000 --- a/packages/vx-axis/src/utils/identity.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function identity(x) { - return x; -} diff --git a/packages/vx-axis/src/utils/labelTransform.js b/packages/vx-axis/src/utils/labelTransform.js deleted file mode 100644 index 3a53c2a1d..000000000 --- a/packages/vx-axis/src/utils/labelTransform.js +++ /dev/null @@ -1,32 +0,0 @@ -import ORIENT from '../constants/orientation'; - -export default function labelTransform({ - labelOffset, - labelProps, - orientation, - range, - tickLabelFontSize, - tickLength, -}) { - const sign = orientation === ORIENT.left || orientation === ORIENT.top ? -1 : 1; - - let x; - let y; - let transform = null; - - if (orientation === ORIENT.top || orientation === ORIENT.bottom) { - x = (range[0] + range[range.length - 1]) / 2; - y = - sign * - (tickLength + - labelOffset + - tickLabelFontSize + - (orientation === ORIENT.bottom ? labelProps.fontSize : 0)); - } else { - x = sign * ((range[0] + range[range.length - 1]) / 2); - y = -(tickLength + labelOffset); - transform = `rotate(${sign * 90})`; - } - - return { x, y, transform }; -} diff --git a/packages/vx-axis/src/utils/labelTransform.ts b/packages/vx-axis/src/utils/labelTransform.ts new file mode 100644 index 000000000..ce964d543 --- /dev/null +++ b/packages/vx-axis/src/utils/labelTransform.ts @@ -0,0 +1,43 @@ +import { TextProps } from '@vx/text/lib/Text'; +import ORIENT from '../constants/orientation'; +import { AxisOrientation, ScaleOutput } from '../types'; + +export interface TransformArgs { + labelOffset: number; + labelProps: Partial; + orientation: AxisOrientation; + range: ScaleOutput[]; + tickLabelFontSize: number; + tickLength: number; +} + +export default function labelTransform({ + labelOffset, + labelProps, + orientation, + range, + tickLabelFontSize, + tickLength, +}: TransformArgs) { + const sign = orientation === ORIENT.left || orientation === ORIENT.top ? -1 : 1; + + let x; + let y; + let transform; + + if (orientation === ORIENT.top || orientation === ORIENT.bottom) { + const yBottomOffset = + orientation === ORIENT.bottom && typeof labelProps.fontSize === 'number' + ? labelProps.fontSize + : 0; + + x = (Number(range[0]) + Number(range[range.length - 1])) / 2; + y = sign * (tickLength + labelOffset + tickLabelFontSize + yBottomOffset); + } else { + x = sign * ((Number(range[0]) + Number(range[range.length - 1])) / 2); + y = -(tickLength + labelOffset); + transform = `rotate(${sign * 90})`; + } + + return { x, y, transform }; +} diff --git a/packages/vx-axis/src/utils/toNumberOrUndefined.ts b/packages/vx-axis/src/utils/toNumberOrUndefined.ts new file mode 100644 index 000000000..46beaafe0 --- /dev/null +++ b/packages/vx-axis/src/utils/toNumberOrUndefined.ts @@ -0,0 +1,6 @@ +export default function toNumberOrUndefined( + val?: number | { valueOf(): number }, +): number | undefined { + if (typeof val === 'undefined') return val; + return Number(val); +} diff --git a/packages/vx-axis/src/utils/toString.ts b/packages/vx-axis/src/utils/toString.ts new file mode 100644 index 000000000..aab868cc9 --- /dev/null +++ b/packages/vx-axis/src/utils/toString.ts @@ -0,0 +1,3 @@ +export default function toString(x?: T) { + return x && x.toString(); +} diff --git a/packages/vx-axis/test/Axis.test.jsx b/packages/vx-axis/test/Axis.test.tsx similarity index 96% rename from packages/vx-axis/test/Axis.test.jsx rename to packages/vx-axis/test/Axis.test.tsx index c727e42ae..fc96501f3 100644 --- a/packages/vx-axis/test/Axis.test.jsx +++ b/packages/vx-axis/test/Axis.test.tsx @@ -3,15 +3,16 @@ import { shallow } from 'enzyme'; import { Line } from '@vx/shape'; import { Text } from '@vx/text'; -import { scaleBand, scaleLinear } from '../../vx-scale/src/index.ts'; +import { scaleBand, scaleLinear } from '@vx/scale'; import { Axis } from '../src'; +import { GenericScale } from '../src/types'; const axisProps = { - orientation: 'left', + orientation: 'left' as const, scale: scaleLinear({ rangeRound: [10, 0], domain: [0, 10], - }), + }) as GenericScale, label: 'test axis', }; @@ -196,9 +197,10 @@ describe('', () => { test('it should use center if scale is band', () => { const overrideAxisProps = { + orientation: 'bottom' as const, scale: scaleBand({ rangeRound: [10, 0], - domain: [0, 10], + domain: ['a', 'b'], }), }; const wrapper = shallow(); diff --git a/packages/vx-axis/test/AxisBottom.test.jsx b/packages/vx-axis/test/AxisBottom.test.tsx similarity index 95% rename from packages/vx-axis/test/AxisBottom.test.jsx rename to packages/vx-axis/test/AxisBottom.test.tsx index cf21de8e6..e5c8066d3 100644 --- a/packages/vx-axis/test/AxisBottom.test.jsx +++ b/packages/vx-axis/test/AxisBottom.test.tsx @@ -1,14 +1,15 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { scaleLinear } from '../../vx-scale/src/index.ts'; +import { scaleLinear } from '../../vx-scale/src'; import { Axis, AxisBottom } from '../src'; +import { GenericScale } from '../src/types'; const axisProps = { scale: scaleLinear({ rangeRound: [10, 0], domain: [0, 10], - }), + }) as GenericScale, }; describe('', () => { diff --git a/packages/vx-axis/test/AxisLeft.test.jsx b/packages/vx-axis/test/AxisLeft.test.tsx similarity index 95% rename from packages/vx-axis/test/AxisLeft.test.jsx rename to packages/vx-axis/test/AxisLeft.test.tsx index 5c98ad8b3..f9256a954 100644 --- a/packages/vx-axis/test/AxisLeft.test.jsx +++ b/packages/vx-axis/test/AxisLeft.test.tsx @@ -1,14 +1,15 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { scaleLinear } from '../../vx-scale/src/index.ts'; +import { scaleLinear } from '../../vx-scale/src'; import { Axis, AxisLeft } from '../src'; +import { GenericScale } from '../src/types'; const axisProps = { scale: scaleLinear({ rangeRound: [10, 0], domain: [0, 10], - }), + }) as GenericScale, }; describe('', () => { diff --git a/packages/vx-axis/test/AxisRight.test.jsx b/packages/vx-axis/test/AxisRight.test.tsx similarity index 95% rename from packages/vx-axis/test/AxisRight.test.jsx rename to packages/vx-axis/test/AxisRight.test.tsx index ed6895213..07b04aa7f 100644 --- a/packages/vx-axis/test/AxisRight.test.jsx +++ b/packages/vx-axis/test/AxisRight.test.tsx @@ -1,14 +1,15 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { scaleLinear } from '../../vx-scale/src/index.ts'; +import { scaleLinear } from '../../vx-scale/src'; import { Axis, AxisRight } from '../src'; +import { GenericScale } from '../src/types'; const axisProps = { scale: scaleLinear({ rangeRound: [10, 0], domain: [0, 10], - }), + }) as GenericScale, }; describe('', () => { diff --git a/packages/vx-axis/test/AxisTop.test.jsx b/packages/vx-axis/test/AxisTop.test.tsx similarity index 95% rename from packages/vx-axis/test/AxisTop.test.jsx rename to packages/vx-axis/test/AxisTop.test.tsx index b60071878..b26285cf0 100644 --- a/packages/vx-axis/test/AxisTop.test.jsx +++ b/packages/vx-axis/test/AxisTop.test.tsx @@ -1,14 +1,15 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { scaleLinear } from '../../vx-scale/src/index.ts'; +import { scaleLinear } from '../../vx-scale/src'; import { Axis, AxisTop } from '../src'; +import { GenericScale } from '../src/types'; const axisProps = { scale: scaleLinear({ rangeRound: [10, 0], domain: [0, 10], - }), + }) as GenericScale, }; describe('', () => { diff --git a/packages/vx-axis/test/Orientation.test.js b/packages/vx-axis/test/Orientation.test.ts similarity index 100% rename from packages/vx-axis/test/Orientation.test.js rename to packages/vx-axis/test/Orientation.test.ts diff --git a/packages/vx-axis/test/scales.test.tsx b/packages/vx-axis/test/scales.test.tsx new file mode 100644 index 000000000..0ce6ba82e --- /dev/null +++ b/packages/vx-axis/test/scales.test.tsx @@ -0,0 +1,161 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { + scaleBand, + scaleLinear, + scaleLog, + scaleOrdinal, + scalePoint, + scalePower, + scaleQuantile, + scaleQuantize, + scaleSymlog, + scaleThreshold, + scaleTime, + scaleUtc, +} from '@vx/scale'; +import { Axis } from '../src'; +import { GenericScale } from '../src/types'; + +const axisProps = { + orientation: 'left' as const, + label: 'test axis', +}; + +function setup(scale: GenericScale) { + return () => shallow(); +} + +describe('Axis scales', () => { + it('should render with scaleBand', () => { + expect( + setup( + scaleBand({ + rangeRound: [10, 0], + domain: ['a', 'b', 'c'], + }) as GenericScale, + ), + ).not.toThrow(); + }); + + it('should render with scaleLinear', () => { + expect( + setup( + scaleLinear({ + rangeRound: [10, 0], + domain: [0, 10], + }) as GenericScale, + ), + ).not.toThrow(); + }); + + it('should render with scaleLog', () => { + expect( + setup( + scaleLog({ + rangeRound: [10, 0], + domain: [1, 10, 100, 1000], + }) as GenericScale, + ), + ).not.toThrow(); + }); + + it('should render with scaleOrdinal', () => { + expect( + setup( + scaleOrdinal({ + range: [0, 10], + domain: ['a', 'b', 'c'], + }) as GenericScale, + ), + ).not.toThrow(); + }); + + it('should render with scalePoint', () => { + expect( + setup( + scalePoint({ + rangeRound: [0, 10], + domain: ['a', 'b', 'c'], + }) as GenericScale, + ), + ).not.toThrow(); + }); + + it('should render with scalePower', () => { + expect( + setup( + scalePower({ + range: [1, 2, 3, 4, 5], + domain: [1, 10, 100, 1000, 10000], + }) as GenericScale, + ), + ).not.toThrow(); + }); + + it('should render with scaleQuantile', () => { + expect( + setup( + scaleQuantile({ + range: [0, 2, 4, 6, 8, 10], + domain: [1, 10, 100, 1000, 10000], + }) as GenericScale, + ), + ).not.toThrow(); + }); + + it('should render with scaleQuantize', () => { + expect( + setup( + scaleQuantize({ + range: [1, 10], + domain: [1, 10], + }) as GenericScale, + ), + ).not.toThrow(); + }); + + it('should render with scaleSymlog', () => { + expect( + setup( + scaleSymlog({ + range: [1, 10], + domain: [1, 10], + }) as GenericScale, + ), + ).not.toThrow(); + }); + + it('should render with scaleThreshold', () => { + expect( + setup( + scaleThreshold({ + range: [1, 10], + domain: [1, 10], + }) as GenericScale, + ), + ).not.toThrow(); + }); + + it('should render with scaleTime', () => { + expect( + setup( + scaleTime({ + range: [1, 10], + domain: [new Date('2020-01-01'), new Date('2020-01-05')], + }) as GenericScale, + ), + ).not.toThrow(); + }); + + it('should render with scaleUtc', () => { + expect( + setup( + scaleUtc({ + range: [1, 10], + domain: [new Date('2020-01-01'), new Date('2020-01-05')], + }) as GenericScale, + ), + ).not.toThrow(); + }); +}); diff --git a/packages/vx-text/src/Text.tsx b/packages/vx-text/src/Text.tsx index 483bf4f98..db8592d53 100644 --- a/packages/vx-text/src/Text.tsx +++ b/packages/vx-text/src/Text.tsx @@ -19,8 +19,11 @@ interface WordsWithWidth { } type SVGTSpanProps = React.SVGAttributes; +type SVGTextProps = React.SVGAttributes; export type TextProps = { + /** className to apply to the SVGText element. */ + className?: string; /** Whether to scale the fontSize to accomodate the specified width. */ scaleToFit?: boolean; /** Rotational angle of the text. */ @@ -46,7 +49,11 @@ export type TextProps = { /** Cap height of the text. */ capHeight?: SVGTSpanProps['capHeight']; /** Font size of text. */ - fontSize?: SVGTSpanProps['fontSize']; + fontSize?: SVGTextProps['fontSize']; + /** Font family of text. */ + fontFamily?: SVGTextProps['fontFamily']; + /** Fill color of text. */ + fill?: SVGTextProps['fill']; /** Maximum width to occupy (approximate as words are not split). */ width?: number; /** String (or number coercible to one) to be styled and positioned. */