From 9dfd64bc2a4e6e34d6b08e0e7207830b99e10008 Mon Sep 17 00:00:00 2001 From: Kris Salvador Date: Tue, 1 May 2018 15:28:25 -0400 Subject: [PATCH] Clean up and refactor (#59) * Add prettier & eslint code style rules * Remove legacy code and evaluate any todos * Only send valid XYPlot props to children. Resolves #22 * Check against master list of scaleTypes * Implement invertX and invertY scale * Alphabetically sort props displayed on docs * Update documentation for x and y axis labels and titles * Clean up code and todos * includeXZero and includeYZero props + test --- docs/src/ComponentDocs.js | 89 +++---- docs/src/docs/XAxisLabels/propDocs.json | 148 ++++++++---- docs/src/docs/XYPlot/propDocs.json | 80 ++----- docs/src/docs/YAxisLabels/propDocs.json | 59 +++-- src/AreaChart.js | 2 +- src/MarkerLineChart.js | 26 -- src/TreeMap.js | 1 + src/XAxis.js | 1 - src/XAxisLabels.js | 81 ++++++- src/XAxisTitle.js | 24 ++ src/XYPlot.js | 62 ++--- src/YAxis.js | 1 - src/YAxisLabels.js | 74 +++++- src/YAxisTitle.js | 27 ++- src/utils/Scale.js | 4 +- src/utils/resolveXYScales.js | 107 +++++---- src/utils/xyPropsEqual.js | 2 +- tests/jsdom/spec/SankeyDiagram.spec.js | 288 ++++++++++++++++------- tests/jsdom/spec/XYPlot.spec.js | 156 +++++------- tests/jsdom/spec/examples.spec.js | 8 +- tests/jsdom/spec/resolveXYScales.spec.js | 141 +++++++---- 21 files changed, 853 insertions(+), 528 deletions(-) diff --git a/docs/src/ComponentDocs.js b/docs/src/ComponentDocs.js index c77a7acf..3cdc0bf9 100644 --- a/docs/src/ComponentDocs.js +++ b/docs/src/ComponentDocs.js @@ -1,56 +1,63 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import _ from 'lodash'; -import remark from 'remark'; -import remarkReact from 'remark-react'; +import React from "react"; +import ReactDOM from "react-dom"; +import _ from "lodash"; +import remark from "remark"; +import remarkReact from "remark-react"; export default class ComponentDocs extends React.Component { render() { - const {name, propDocs, children} = this.props; + const { name, propDocs, children } = this.props; + const sortedProps = _(_.get(propDocs, "props")) + .toPairs() + .sortBy(0) + .fromPairs() + .value(); - return
-
-

{name}

-
- - {propDocs.description ? + return ( +
-

- {renderMarkdown(propDocs.description)} -

+

{name}

- : null - } -
-

{name} props:

- {_.map(_.get(propDocs, 'props'), (propInfo, propKey) => { - return
- {propKey}: {_.get(propInfo, 'type.name', 'unknown')} - {propInfo.description ?
: null} - {propInfo.description ? - - {renderMarkdown(propInfo.description)} - - : null} + {propDocs.description ? ( +
+

+ {renderMarkdown(propDocs.description)} +

+
+ ) : null} - {propInfo.defaultValue ? -
- default value: {propInfo.defaultValue.value} +
+

{name} props:

+ {_.map(sortedProps, (propInfo, propKey) => { + return ( +
+ {propKey}:{" "} + {_.get(propInfo, "type.name", "unknown")} + {propInfo.description ?
: null} + {propInfo.description ? ( + + {renderMarkdown(propInfo.description)} + + ) : null} + {propInfo.defaultValue ? ( +
+ default value: {propInfo.defaultValue.value} +
+ ) : null}
- : null} -
- })} -
+ ); + })} +
- {children} -
+ {children} +
+ ); } } -function renderMarkdown(markdownText = '') { +function renderMarkdown(markdownText = "") { return remark() .use(remarkReact) - .processSync(markdownText) - .contents; -} \ No newline at end of file + .processSync(markdownText).contents; +} diff --git a/docs/src/docs/XAxisLabels/propDocs.json b/docs/src/docs/XAxisLabels/propDocs.json index e4c43542..1e754ee2 100644 --- a/docs/src/docs/XAxisLabels/propDocs.json +++ b/docs/src/docs/XAxisLabels/propDocs.json @@ -59,117 +59,179 @@ } ], "props": { - "xScale": { - "type": { - "name": "func" - }, - "required": false, - "description": "" - }, - "onMouseEnterLabel": { - "type": { - "name": "func" - }, - "required": false, - "description": "" - }, - "onMouseMoveLabel": { + "height": { "type": { - "name": "func" + "name": "number" }, "required": false, - "description": "" - }, - "onMouseLeaveLabel": { - "type": { - "name": "func" - }, - "required": false, - "description": "" - }, - "height": { + "description": "", "defaultValue": { "value": "250", "computed": false } }, "position": { + "type": { + "name": "string" + }, + "required": false, + "description": "", "defaultValue": { "value": "\"bottom\"", "computed": false } }, "placement": { + "type": { + "name": "string" + }, + "required": false, + "description": "Placement of labels in regards to the x axis. Accepted options are \"above\" or \"below\"", "defaultValue": { "value": "undefined", "computed": true } }, + "xScale": { + "type": { + "name": "func" + }, + "required": false, + "description": "D3 scale for X axis - provided by XYPlot" + }, + "spacingTop": { + "type": { + "name": "number" + }, + "required": false, + "description": "Spacing - provided by XYPlot" + }, + "spacingBottom": { + "type": { + "name": "number" + }, + "required": false, + "description": "Spacing - provided by XYPlot" + }, "distance": { + "type": { + "name": "number" + }, + "required": false, + "description": "Label distance from X Axis", "defaultValue": { "value": "4", "computed": false } }, - "nice": { - "defaultValue": { - "value": "true", - "computed": false - } - }, "tickCount": { + "type": { + "name": "number" + }, + "required": false, + "description": "Number of ticks on axis", "defaultValue": { "value": "10", "computed": false } }, "ticks": { + "type": { + "name": "array" + }, + "required": false, + "description": "Custom ticks to display", "defaultValue": { "value": "null", "computed": false } }, - "labelClassName": { + "labelStyle": { + "type": { + "name": "object" + }, + "required": false, + "description": "Object declaring styles for label.\n\nDisclaimer: labelStyle will merge its defaults with the given labelStyle prop\nin order to ensure that our collision library measureText is able to calculate the\nsmallest amount of possible collissions along the axis. It's therefore dependent on\nfontFamily, size and fontStyle to always be passed in. If you're looking to have a centralized\nstylesheet, we suggest creating a styled label component that wraps XAxisLabels with your preferred styles.", "defaultValue": { - "value": "\"\"", + "value": "{\n fontFamily: \"Helvetica, sans-serif\",\n fontSize: \"14px\",\n lineHeight: 1,\n textAnchor: \"middle\"\n}", "computed": false } }, - "labelStyle": { + "labelClassName": { + "type": { + "name": "string" + }, + "required": false, + "description": "", "defaultValue": { - "value": "{\n fontFamily: \"Helvetica, sans-serif\",\n fontSize: \"14px\",\n lineHeight: 1,\n textAnchor: \"middle\"\n}", + "value": "\"\"", "computed": false } }, "format": { + "type": { + "name": "string" + }, + "required": false, + "description": "Format to use for the labels\n\nFor example, given labels with real numbers one can pass in 0.[0] to round to the first significant digit", "defaultValue": { "value": "undefined", "computed": true } }, "formats": { + "type": { + "name": "array" + }, + "required": false, + "description": "Formats to use for the labels in priority order. XAxisLabels will try to be smart about which format\nto use that keeps the labels distinct and provides the least amount of collisions when rendered.\n\nFor example, given labels with real numbers one can pass in 0.[0] to round to the first significant digit", "defaultValue": { "value": "undefined", "computed": true } }, "labels": { + "type": { + "name": "array" + }, + "required": false, + "description": "Custom labels provided. Note that each object in the array has to be of shape\n`{\n value,\n text,\n height,\n width\n}`\nvalue - value you'd like this label to be aligned with\ntext - text you'd like displayed\nheight - height of the given label\nwidth - width of the given label", "defaultValue": { "value": "undefined", "computed": true } }, - "spacingTop": { + "nice": { + "type": { + "name": "bool" + }, + "required": false, + "description": "Round ticks to capture extent of given x and y Domain", "defaultValue": { - "value": "0", + "value": "true", "computed": false } }, - "spacingBottom": { - "defaultValue": { - "value": "0", - "computed": false - } + "onMouseEnterLabel": { + "type": { + "name": "func" + }, + "required": false, + "description": "" + }, + "onMouseMoveLabel": { + "type": { + "name": "func" + }, + "required": false, + "description": "" + }, + "onMouseLeaveLabel": { + "type": { + "name": "func" + }, + "required": false, + "description": "" } } } \ No newline at end of file diff --git a/docs/src/docs/XYPlot/propDocs.json b/docs/src/docs/XYPlot/propDocs.json index 9ee1d510..51da29d1 100644 --- a/docs/src/docs/XYPlot/propDocs.json +++ b/docs/src/docs/XYPlot/propDocs.json @@ -79,33 +79,41 @@ "required": false, "description": "" }, - "xScale": { + "xScaleType": { "type": { - "name": "func" + "name": "string" }, "required": false, - "description": "d3 scales for the X and Y axes of the chart, in {x, y} object format.\n(optional, normally determined automatically by XYPlot)" + "description": "" }, - "yScale": { + "yScaleType": { "type": { - "name": "func" + "name": "string" }, "required": false, "description": "" }, - "xScaleType": { + "invertXScale": { "type": { - "name": "string" + "name": "bool" }, "required": false, - "description": "" + "description": "Whether or not to invert the x and y scales", + "defaultValue": { + "value": "false", + "computed": false + } }, - "yScaleType": { + "invertYScale": { "type": { - "name": "string" + "name": "bool" }, "required": false, - "description": "" + "description": "", + "defaultValue": { + "value": "false", + "computed": false + } }, "margin": { "type": { @@ -170,56 +178,6 @@ "required": false, "description": "" }, - "paddingTop": { - "type": { - "name": "number" - }, - "required": false, - "description": "" - }, - "paddingBottom": { - "type": { - "name": "number" - }, - "required": false, - "description": "" - }, - "paddingLeft": { - "type": { - "name": "number" - }, - "required": false, - "description": "" - }, - "paddingRight": { - "type": { - "name": "number" - }, - "required": false, - "description": "" - }, - "invertXScale": { - "type": { - "name": "bool" - }, - "required": false, - "description": "", - "defaultValue": { - "value": "false", - "computed": false - } - }, - "invertYScale": { - "type": { - "name": "bool" - }, - "required": false, - "description": "", - "defaultValue": { - "value": "false", - "computed": false - } - }, "onMouseMove": { "type": { "name": "func" diff --git a/docs/src/docs/YAxisLabels/propDocs.json b/docs/src/docs/YAxisLabels/propDocs.json index edac1230..116a6c4f 100644 --- a/docs/src/docs/YAxisLabels/propDocs.json +++ b/docs/src/docs/YAxisLabels/propDocs.json @@ -64,7 +64,7 @@ "name": "func" }, "required": false, - "description": "" + "description": "D3 scale for Y axis - provided by XYPlot" }, "height": { "type": { @@ -124,14 +124,14 @@ ] }, "required": false, - "description": "" + "description": "Placement of labels in regards to the x axis. Accepted options are \"before\" or \"after\"" }, "distance": { "type": { "name": "number" }, "required": false, - "description": "", + "description": "Label distance from Y Axis", "defaultValue": { "value": "4", "computed": false @@ -142,7 +142,7 @@ "name": "bool" }, "required": false, - "description": "", + "description": "Round ticks to capture extent of given y Domain from XYPlot", "defaultValue": { "value": "true", "computed": false @@ -153,7 +153,7 @@ "name": "number" }, "required": false, - "description": "", + "description": "Number of ticks on axis", "defaultValue": { "value": "10", "computed": false @@ -164,31 +164,31 @@ "name": "array" }, "required": false, - "description": "", + "description": "Custom ticks to display", "defaultValue": { "value": "null", "computed": false } }, - "labelClassName": { + "labelStyle": { "type": { - "name": "string" + "name": "object" }, "required": false, - "description": "", + "description": "Object declaring styles for label.\n\nDisclaimer: labelStyle will merge its defaults with the given labelStyle prop\nin order to ensure that our collision library measureText is able to calculate the\nsmallest amount of possible collissions along the axis. It's therefore dependent on\nfontFamily, size and fontStyle to always be passed in. If you're looking to have a centralized\nstylesheet, we suggest creating a styled label component that wraps XAxisLabels with your preferred styles.", "defaultValue": { - "value": "\"\"", + "value": "{\n fontFamily: \"Helvetica, sans-serif\",\n fontSize: \"14px\",\n lineHeight: 1,\n textAnchor: \"end\"\n}", "computed": false } }, - "labelStyle": { + "labelClassName": { "type": { - "name": "object" + "name": "string" }, "required": false, "description": "", "defaultValue": { - "value": "{\n fontFamily: \"Helvetica, sans-serif\",\n fontSize: \"14px\",\n lineHeight: 1,\n textAnchor: \"end\"\n}", + "value": "\"\"", "computed": false } }, @@ -197,22 +197,14 @@ "name": "number" }, "required": false, - "description": "", - "defaultValue": { - "value": "0", - "computed": false - } + "description": "Spacing - provided by XYPlot" }, "spacingRight": { "type": { "name": "number" }, "required": false, - "description": "", - "defaultValue": { - "value": "0", - "computed": false - } + "description": "Spacing - provided by XYPlot" }, "onMouseEnterLabel": { "type": { @@ -234,6 +226,27 @@ }, "required": false, "description": "" + }, + "format": { + "type": { + "name": "string" + }, + "required": false, + "description": "Format to use for the labels\n\nFor example, given labels with real numbers one can pass in 0.[0] to round to the first significant digit" + }, + "formats": { + "type": { + "name": "array" + }, + "required": false, + "description": "Formats to use for the labels in priority order. XAxisLabels will try to be smart about which format\nto use that keeps the labels distinct and provides the least amount of collisions when rendered.\n\nFor example, given labels with real numbers one can pass in 0.[0] to round to the first significant digit" + }, + "labels": { + "type": { + "name": "array" + }, + "required": false, + "description": "Custom labels provided. Note that each object in the array has to be of shape\n`{\n value,\n text,\n height,\n width\n}`\nvalue - value you'd like this label to be aligned with\ntext - text you'd like displayed\nheight - height of the given label\nwidth - width of the given label" } } } \ No newline at end of file diff --git a/src/AreaChart.js b/src/AreaChart.js index 45349bd2..ee89ff15 100644 --- a/src/AreaChart.js +++ b/src/AreaChart.js @@ -11,7 +11,7 @@ import * as CustomPropTypes from "./utils/CustomPropTypes"; // a filled path drawn between two lines (datasets). // todo horizontal prop, for filling area horizontally? -// todo support categorical data? +// todo support ordinal (like days of the week) data? // todo build StackedAreaChart that composes multiple AreaCharts export default class AreaChart extends React.Component { diff --git a/src/MarkerLineChart.js b/src/MarkerLineChart.js index d03f6851..135a6f4e 100644 --- a/src/MarkerLineChart.js +++ b/src/MarkerLineChart.js @@ -69,32 +69,6 @@ export default class MarkerLineChart extends React.Component { lineLength: 10 }; - // todo reimplement padding/spacing - /* - static getOptions(props) { - const {data, getX, getXEnd, getY, getYEnd, scaleType, orientation, lineLength} = props; - const tickType = getTickType(props); - const isVertical = (orientation === 'vertical'); - const accessors = {x: makeAccessor(getX), y: makeAccessor(getY)}; - const endAccessors = {x: makeAccessor(getXEnd), y: makeAccessor(getYEnd)}; - - let options = {domain: {}, spacing: {}}; - - if(tickType === 'RangeValue') { // set range domain for range type - let rangeAxis = isVertical ? 'x' : 'y'; - options.domain[rangeAxis] = - rangeAxisDomain(data, accessors[rangeAxis], endAccessors[rangeAxis], scaleType[rangeAxis]); - } else { - // the value, and therefore the center of the marker line, may fall exactly on the axis min or max, - // therefore marker lines need (0.5*lineLength) spacing so they don't hang over the edge of the chart - const halfLine = Math.ceil(0.5 * lineLength); - options.spacing = isVertical ? {left: halfLine, right: halfLine} : {top: halfLine, bottom: halfLine}; - } - - return options; - } - */ - static getSpacing(props) { const tickType = getTickType(props); //no spacing for rangeValue marker charts since line start and end are set explicitly diff --git a/src/TreeMap.js b/src/TreeMap.js index 20612de9..276db048 100644 --- a/src/TreeMap.js +++ b/src/TreeMap.js @@ -167,6 +167,7 @@ class TreeMap extends React.Component { const { width, height, data, sticky } = this.props; // if height, width, or the data changes, or if the treemap is not sticky, re-initialize the layout function + // todo reevaluate this logic if ( !sticky || width != newProps.width || diff --git a/src/XAxis.js b/src/XAxis.js index dafff592..779790d7 100644 --- a/src/XAxis.js +++ b/src/XAxis.js @@ -87,7 +87,6 @@ export default class XAxis extends React.Component { } static getMargin(props) { - // todo figure out margin if labels change after margin? const { ticksProps, labelsProps, titleProps } = getAxisChildProps(props); let margins = []; diff --git a/src/XAxisLabels.js b/src/XAxisLabels.js index 4ce0212d..e6492250 100644 --- a/src/XAxisLabels.js +++ b/src/XAxisLabels.js @@ -81,7 +81,81 @@ function resolveXLabelsForValues(scale, values, formats, style, force = true) { class XAxisLabels extends React.Component { static propTypes = { + height: PropTypes.number, + /*** + * Position of x axis labels. Accepted options are "top" or "bottom" + */ + position: PropTypes.oneOf(["top", "bottom"]), + /** + * Placement of labels in regards to the x axis. Accepted options are "above" or "below" + */ + placement: PropTypes.oneOf(["below", "above"]), + /** + * D3 scale for X axis - provided by XYPlot + */ xScale: PropTypes.func, + /** + * Spacing - provided by XYPlot + */ + spacingTop: PropTypes.number, + /** + * Spacing - provided by XYPlot + */ + spacingBottom: PropTypes.number, + /** + * Label distance from X Axis + */ + distance: PropTypes.number, + /** + * Number of ticks on axis + */ + tickCount: PropTypes.number, + /** + * Custom ticks to display + */ + ticks: PropTypes.array, + /** + * Object declaring styles for label. + * + * Disclaimer: labelStyle will merge its defaults with the given labelStyle prop + * in order to ensure that our collision library measureText is able to calculate the + * smallest amount of possible collissions along the axis. It's therefore dependent on + * fontFamily, size and fontStyle to always be passed in. If you're looking to have a centralized + * stylesheet, we suggest creating a styled label component that wraps XAxisLabels with your preferred styles. + */ + labelStyle: PropTypes.object, + labelClassName: PropTypes.string, + /** + * Format to use for the labels + * + * For example, given labels with real numbers one can pass in 0.[0] to round to the first significant digit + */ + format: PropTypes.string, + /** + * Formats to use for the labels in priority order. XAxisLabels will try to be smart about which format + * to use that keeps the labels distinct and provides the least amount of collisions when rendered. + * + * For example, given labels with real numbers one can pass in 0.[0] to round to the first significant digit + */ + formats: PropTypes.array, + /** + * Custom labels provided. Note that each object in the array has to be of shape + * `{ + * value, + * text, + * height, + * width + * }` + * value - value you'd like this label to be aligned with + * text - text you'd like displayed + * height - height of the given label + * width - width of the given label + */ + labels: PropTypes.array, + /** + * Round ticks to capture extent of given x Domain from XYPlot + */ + nice: PropTypes.bool, // Label Handling onMouseEnterLabel: PropTypes.func, onMouseMoveLabel: PropTypes.func, @@ -104,9 +178,7 @@ class XAxisLabels extends React.Component { }, format: undefined, formats: undefined, - labels: undefined, - spacingTop: 0, - spacingBottom: 0 + labels: undefined }; shouldComponentUpdate(nextProps, nextState) { @@ -192,7 +264,7 @@ class XAxisLabels extends React.Component { // doing this will require communicating the updated ticks/tickCount back to the parent element... const { labels } = resolveXLabelsForValues(xScale, ticks, formats, style); - // console.log('found labels', labels); + return labels; } @@ -219,6 +291,7 @@ class XAxisLabels extends React.Component { ? `translate(0, ${height + spacingBottom})` : `translate(0, ${-spacingTop})`; // todo: position: 'zero' to position along the zero line + // example include having both positive and negative areas and youd like labels just on zero line return ( diff --git a/src/XAxisTitle.js b/src/XAxisTitle.js index 0b37f5ed..92e8471b 100644 --- a/src/XAxisTitle.js +++ b/src/XAxisTitle.js @@ -7,13 +7,37 @@ export default class XAxisTitle extends React.Component { static propTypes = { height: PropTypes.number, width: PropTypes.number, + /** + * Title distance from X Axis + */ distance: PropTypes.number, + /** + * Position of title in regards to the x axis. Accepted options are "top" or "bottom" + */ position: PropTypes.oneOf(["top", "bottom"]), + /** + * Placement of title in regards to the x axis. Accepted options are "above" or "below" + */ placement: PropTypes.oneOf(["above", "below"]), alignment: PropTypes.oneOf(["left", "center", "right"]), rotate: PropTypes.bool, + /** + * Object declaring styles for label. + * + * Disclaimer: labelStyle will merge its defaults with the given labelStyle prop + * in order to ensure that our collision library measureText is able to calculate the + * smallest amount of possible collissions along the axis. It's therefore dependent on + * fontFamily, size and fontStyle to always be passed in. If you're looking to have a centralized + * stylesheet, we suggest creating a styled title component that wraps XAxisTitle with your preferred styles. + */ style: PropTypes.object, + /** + * Spacing - provided by XYPlot + */ spacingTop: PropTypes.number, + /** + * Spacing - provided by XYPlot + */ spacingBottom: PropTypes.number }; static defaultProps = { diff --git a/src/XYPlot.js b/src/XYPlot.js index 05557485..cd1a021c 100644 --- a/src/XYPlot.js +++ b/src/XYPlot.js @@ -107,20 +107,28 @@ class XYPlot extends React.Component { */ xDomain: PropTypes.array, yDomain: PropTypes.array, - /** - * d3 scales for the X and Y axes of the chart, in {x, y} object format. - * (optional, normally determined automatically by XYPlot) - */ - xScale: PropTypes.func, - yScale: PropTypes.func, xScaleType: PropTypes.string, yScaleType: PropTypes.string, + /** + * Whether or not to invert the x and y scales + */ + invertXScale: PropTypes.bool, + invertYScale: PropTypes.bool, + + /** + * Whether or not to coerce 0 into your x domain + */ + includeXZero: PropTypes.bool, + /** + * Whether or not to coerce 0 into your y domain + */ + includeYZero: PropTypes.bool, + /** * */ - margin: PropTypes.object, marginTop: PropTypes.number, marginBottom: PropTypes.number, marginLeft: PropTypes.number, @@ -132,13 +140,11 @@ class XYPlot extends React.Component { spacingLeft: PropTypes.number, spacingRight: PropTypes.number, - paddingTop: PropTypes.number, - paddingBottom: PropTypes.number, - paddingLeft: PropTypes.number, - paddingRight: PropTypes.number, - - invertXScale: PropTypes.bool, - invertYScale: PropTypes.bool, + // todo implement padding (helper for spacing) + // paddingTop: PropTypes.number, + // paddingBottom: PropTypes.number, + // paddingLeft: PropTypes.number, + // paddingRight: PropTypes.number, onMouseMove: PropTypes.func, onMouseEnter: PropTypes.func, @@ -150,16 +156,10 @@ class XYPlot extends React.Component { static defaultProps = { width: 400, height: 250, - // invertScale: {x: false, y: false}, invertXScale: false, - invertYScale: false - // emptyLabel: "Unknown", - - // these values are inferred from data if not provided, therefore empty defaults - // scaleType: {}, - // domain: {}, - // margin: {}, - //spacing: {top: 0, bottom: 0, left: 0, right: 0} + invertYScale: false, + includeXZero: false, + includeYZero: false }; onXYMouseEvent = (callbackKey, event) => { @@ -186,7 +186,10 @@ class XYPlot extends React.Component { spacingTop, spacingBottom, spacingLeft, - spacingRight + spacingRight, + // Passed in as prop from resolveXYScales + xScale, + yScale } = this.props; // subtract margin + spacing from width/height to obtain inner width/height of panel & chart area // panelSize is the area including chart + spacing but NOT margin @@ -218,10 +221,15 @@ class XYPlot extends React.Component { const handlers = _.fromPairs( handlerNames.map(n => [n, methodIfFuncProp(n, this.props, this)]) ); - + const scales = { + xScale, + yScale + }; + const xyPlotPropKeys = _.keys(XYPlot.propTypes); const propsToPass = { - ..._.omit(this.props, ["children"]), - ...chartSize + ..._.pick(this.props, xyPlotPropKeys), + ...chartSize, + ...scales }; return ( diff --git a/src/YAxis.js b/src/YAxis.js index 0afcb339..f28a7dc2 100644 --- a/src/YAxis.js +++ b/src/YAxis.js @@ -86,7 +86,6 @@ export default class YAxis extends React.Component { } static getMargin(props) { - // todo figure out margin if labels change after margin? const { ticksProps, labelsProps, titleProps } = getAxisChildProps(props); let margins = []; diff --git a/src/YAxisLabels.js b/src/YAxisLabels.js index abe4f2d4..3c2be54b 100644 --- a/src/YAxisLabels.js +++ b/src/YAxisLabels.js @@ -59,26 +59,86 @@ function resolveYLabelsForValues(scale, values, formats, style, force = true) { class YAxisLabels extends React.Component { static propTypes = { + /** + * D3 scale for Y axis - provided by XYPlot + */ yScale: PropTypes.func, height: PropTypes.number, width: PropTypes.number, + /*** + * Position of y axis labels. Accepted options are "left" or "right" + */ position: PropTypes.oneOf(["left", "right"]), + /** + * Placement of labels in regards to the x axis. Accepted options are "before" or "after" + */ placement: PropTypes.oneOf(["before", "after"]), + /** + * Label distance from Y Axis + */ distance: PropTypes.number, + /** + * Round ticks to capture extent of given y Domain from XYPlot + */ nice: PropTypes.bool, + /** + * Number of ticks on axis + */ tickCount: PropTypes.number, + /** + * Custom ticks to display + */ ticks: PropTypes.array, - labelClassName: PropTypes.string, + /** + * Object declaring styles for label. + * + * Disclaimer: labelStyle will merge its defaults with the given labelStyle prop + * in order to ensure that our collision library measureText is able to calculate the + * smallest amount of possible collissions along the axis. It's therefore dependent on + * fontFamily, size and fontStyle to always be passed in. If you're looking to have a centralized + * stylesheet, we suggest creating a styled label component that wraps YAxisLabels with your preferred styles. + */ labelStyle: PropTypes.object, + labelClassName: PropTypes.string, + /** + * Spacing - provided by XYPlot + */ spacingLeft: PropTypes.number, + /** + * Spacing - provided by XYPlot + */ spacingRight: PropTypes.number, // Label Handling onMouseEnterLabel: PropTypes.func, onMouseMoveLabel: PropTypes.func, - onMouseLeaveLabel: PropTypes.func - // format: undefined, - // formats: undefined, - // labels: undefined + onMouseLeaveLabel: PropTypes.func, + /** + * Format to use for the labels + * + * For example, given labels with real numbers one can pass in 0.[0] to round to the first significant digit + */ + format: PropTypes.string, + /** + * Formats to use for the labels in priority order. XAxisLabels will try to be smart about which format + * to use that keeps the labels distinct and provides the least amount of collisions when rendered. + * + * For example, given labels with real numbers one can pass in 0.[0] to round to the first significant digit + */ + formats: PropTypes.array, + /** + * Custom labels provided. Note that each object in the array has to be of shape + * `{ + * value, + * text, + * height, + * width + * }` + * value - value you'd like this label to be aligned with + * text - text you'd like displayed + * height - height of the given label + * width - width of the given label + */ + labels: PropTypes.array }; static defaultProps = { height: 250, @@ -94,9 +154,7 @@ class YAxisLabels extends React.Component { fontSize: "14px", lineHeight: 1, textAnchor: "end" - }, - spacingLeft: 0, - spacingRight: 0 + } }; shouldComponentUpdate(nextProps, nextState) { diff --git a/src/YAxisTitle.js b/src/YAxisTitle.js index a4fba3fb..196efa08 100644 --- a/src/YAxisTitle.js +++ b/src/YAxisTitle.js @@ -7,15 +7,40 @@ export default class YAxisTitle extends React.Component { static propTypes = { height: PropTypes.number, width: PropTypes.number, + /** + * Title distance from Y Axis + */ distance: PropTypes.number, + /** + * Position of title in regards to the y axis. Accepted options are "left" or "right" + */ position: PropTypes.oneOf(["left", "right"]), alignment: PropTypes.oneOf(["top", "middle", "bottom"]), - placement: PropTypes.oneOf(["before", "after"]), + /** + * Placement of title in regards to the x axis. Accepted options are "above" or "below" + */ + placement: PropTypes.oneOf(["above", "below"]), rotate: PropTypes.bool, + /** + * Object declaring styles for label. + * + * Disclaimer: style will merge its defaults with the given style prop + * in order to ensure that our collision library measureText is able to calculate the + * smallest amount of possible collissions along the axis. It's therefore dependent on + * fontFamily, size and fontStyle to always be passed in. If you're looking to have a centralized + * stylesheet, we suggest creating a styled title component that wraps YAxisTitle with your preferred styles. + */ style: PropTypes.object, + /** + * Spacing - provided by XYPlot + */ spacingLeft: PropTypes.number, + /** + * Spacing - provided by XYPlot + */ spacingRight: PropTypes.number }; + static defaultProps = { height: 250, width: 400, diff --git a/src/utils/Scale.js b/src/utils/Scale.js index 5c743422..7ddda586 100644 --- a/src/utils/Scale.js +++ b/src/utils/Scale.js @@ -93,6 +93,7 @@ export function getTickDomain(scale, { ticks, tickCount, nice } = {}) { const scaleType = inferScaleType(scale); // bug - d3 linearScale.copy().nice() modifies original scale, so we must create a new scale instead of copy()ing // todo replace this with d3-scale from d3 v4.0 + // check if d3 still has this issue if (nice && scaleType !== "ordinal") { scale = initScale(scaleType) .domain(scale.domain()) @@ -113,6 +114,5 @@ export function scaleEqual(scaleA, scaleB) { ? scaleA === scaleB // safe fallback : // check scale equality _.isEqual(scaleA.domain(), scaleB.domain()) && - _.isEqual(scaleA.range(), scaleB.range()) && - _.isEqual(getScaleTicks(scaleA), getScaleTicks(scaleB)); // todo is this necessary? + _.isEqual(scaleA.range(), scaleB.range()); } diff --git a/src/utils/resolveXYScales.js b/src/utils/resolveXYScales.js index c17951a5..98f49068 100644 --- a/src/utils/resolveXYScales.js +++ b/src/utils/resolveXYScales.js @@ -44,8 +44,9 @@ function componentName(Component) { } function isValidScaleType(scaleType) { - // todo: check against whitelist? - return _.isString(scaleType); + const validScaleTypes = ["ordinal", "time", "log", "pow", "linear"]; + + return _.includes(validScaleTypes, scaleType); } function areValidScales(scales) { return _.every(scales, isValidScale); @@ -72,6 +73,7 @@ function omitNullUndefined(obj) { } // not currently being used but potentially has some learnings +// attempt at condensing all the resolve functions below // function resolveXYPropsOnComponentOrChildren(propKeys, props, reducers = {}, validators = {}, result = {}) { // const isDone = (o) => (_.every(propKeys, k => _.isObject(o[k]) && _.every(['x', 'y'], xy => _.has(o[k][xy])))); // result = _.pick({...props, ...result}, propKeys); @@ -120,11 +122,6 @@ function omitNullUndefined(obj) { export default function resolveXYScales(ComposedComponent) { return class extends React.Component { - static defaultProps = _.defaults(ComposedComponent.defaultProps, { - invertXScale: false, - invertYScale: false - }); - // todo better way for HOC's to pass statics through? static getScaleType = ComposedComponent.getScaleType; static getSpacing = ComposedComponent.getSpacing; @@ -232,7 +229,7 @@ export default function resolveXYScales(ComposedComponent) { } _resolveDomain(props, Component, xScaleType, yScaleType) { - let { xDomain, yDomain } = props; + let { xDomain, yDomain, includeXZero, includeYZero } = props; const xDataType = dataTypeFromScaleType(xScaleType); const yDataType = dataTypeFromScaleType(yScaleType); @@ -272,13 +269,11 @@ export default function resolveXYScales(ComposedComponent) { ); if (!isYDone() && isValidDomain(componentYDomain, yDataType)) yDomain = componentYDomain; - - if (isDone()) return { xDomain, yDomain }; } // if Component has data or datasets props, // use the default domainFromDatasets function to determine a domain from them - if (_.isArray(props.data) || _.isArray(props.datasets)) { + if (!isDone() && (_.isArray(props.data) || _.isArray(props.datasets))) { const datasets = _.isArray(props.datasets) ? props.datasets : [props.data]; @@ -296,13 +291,12 @@ export default function resolveXYScales(ComposedComponent) { yDataType ); } - if (isDone()) return { xDomain, yDomain }; } // if Component has children, // recurse through descendants to resolve their domains the same way, // and combine them into a single domain, if there are multiple - if (React.Children.count(props.children)) { + if (!isDone() && React.Children.count(props.children)) { let childrenDomains = mapOverChildren( props.children, this._resolveDomain.bind(this), @@ -324,7 +318,29 @@ export default function resolveXYScales(ComposedComponent) { } } - // if(!isDone()) console.warn(`resolveXYScales was unable to resolve both domains. xDomain: ${xDomain}, yDomain: ${yDomain}`); + if (!isDone()) { + console.warn( + `resolveXYScales was unable to resolve both domains. xDomain: ${xDomain}, yDomain: ${yDomain}` + ); + } else { + if (includeXZero && !_.inRange(0, ...xDomain)) { + // If both are negative set max of domain to 0 + if (xDomain[0] < 0 && xDomain[1] < 0) { + xDomain[1] = 0; + } else { + xDomain[0] = 0; + } + } + + if (includeYZero && !_.inRange(0, ...yDomain)) { + // If both are negative set max of domain to 0 + if (yDomain[0] < 0 && yDomain[1] < 0) { + yDomain[1] = 0; + } else { + yDomain[0] = 0; + } + } + } return { xDomain, yDomain }; } @@ -334,7 +350,6 @@ export default function resolveXYScales(ComposedComponent) { Component, { xScaleType, yScaleType, xDomain, yDomain, xScale, yScale } ) { - // todo resolve directly from ticks/tickCount props? if (_.isFunction(Component.getTickDomain)) { const componentTickDomains = Component.getTickDomain({ xScaleType, @@ -546,6 +561,8 @@ export default function resolveXYScales(ComposedComponent) { height, xScaleType, yScaleType, + invertXScale, + invertYScale, xDomain, yDomain, xScale, @@ -583,7 +600,13 @@ export default function resolveXYScales(ComposedComponent) { xScale = initScale(xScaleType) .domain(xDomain) .range(xRange); + + // reverse scale domain if `invertXScale` is passed + if (invertXScale) { + xScale.domain(xScale.domain().reverse()); + } } + if (!isValidScale(yScale)) { const yRange = innerRangeY(innerChartHeight, spacing).map( v => v - (spacing.top || 0) @@ -591,24 +614,12 @@ export default function resolveXYScales(ComposedComponent) { yScale = initScale(yScaleType) .domain(yDomain) .range(yRange); - } - - // todo - ticks, nice and getDomain should be an axis prop instead, and axis should have getDomain - // set `nice` option to round scale domains to nicer numbers - // const kTickCount = tickCount ? tickCount[k] : 10; - // if(nice && nice[k] && _.isFunction(kScale.nice)) kScale.nice(kTickCount); - - // extend scale domain to include custom `ticks` if passed - // - // if(ticks[k]) { - // const dataType = dataTypeFromScaleType(scaleType[k]); - // const tickDomain = domainFromData(ticks[k], _.identity, dataType); - // kScale.domain(combineDomains([kScale.domain(), tickDomain]), dataType); - // } - - // reverse scale domain if `invertScale` is passed - // if(invertScale[k]) kScale.domain(kScale.domain().reverse()); + // reverse scale domain if `invertYScale` is passed + if (invertYScale) { + yScale.domain(yScale.domain().reverse()); + } + } return { xScale, yScale }; }; @@ -617,12 +628,6 @@ export default function resolveXYScales(ComposedComponent) { const { props } = this; const { width, height, invertXScale, invertYScale } = props; - // short-circuit if scales provided - // todo warn/throw if bad scales are passed - // todo also pass domain/scaleType/etc from scales?? - if (isValidScale(props.xScale) && isValidScale(props.yScale)) - return ; - // scales not provided, so we have to resolve them // first resolve scale types and domains // const scaleType = this._resolveScaleType(props, ComposedComponent); @@ -651,6 +656,8 @@ export default function resolveXYScales(ComposedComponent) { yScaleType, xDomain, yDomain, + invertXScale, + invertYScale, scaleX: props.scaleX, scaleY: props.scaleY, marginTop: props.marginTop, @@ -711,7 +718,12 @@ export default function resolveXYScales(ComposedComponent) { xScale: tempScale.xScale, yScale: tempScale.yScale }), - { marginTop: 0, marginBottom: 0, marginLeft: 0, marginRight: 0 } + { + marginTop: 0, + marginBottom: 0, + marginLeft: 0, + marginRight: 0 + } ); const { @@ -728,7 +740,12 @@ export default function resolveXYScales(ComposedComponent) { xScale: tempScale.xScale, yScale: tempScale.yScale }), - { spacingTop: 0, spacingBottom: 0, spacingLeft: 0, spacingRight: 0 } + { + spacingTop: 0, + spacingBottom: 0, + spacingLeft: 0, + spacingRight: 0 + } ); // create real scales from resolved margins @@ -746,12 +763,6 @@ export default function resolveXYScales(ComposedComponent) { const { xScale, yScale } = this._makeScales(scaleOptions); const passedProps = _.assign({}, this.props, { - //legacy - // scale: {x: xScale, y: yScale}, - // domain: {x: xDomain, y: yDomain}, - // scaleType: {x: xScaleType, y: yScaleType}, - // margin: {top: marginTop, bottom: marginBottom, left: marginLeft, right: marginRight}, - // 0.4.0 xScale, yScale, xDomain, @@ -769,10 +780,6 @@ export default function resolveXYScales(ComposedComponent) { }); return ; - // todo spacing/padding - // todo includeZero - // todo purerender/shouldcomponentupdate? - // todo resolve margins if scales are present // todo throw if cannot resolve scaleType // todo throw if cannot resolve domain // todo check to make sure margins didn't change after scales resolved? diff --git a/src/utils/xyPropsEqual.js b/src/utils/xyPropsEqual.js index 4b3794e4..bed26253 100644 --- a/src/utils/xyPropsEqual.js +++ b/src/utils/xyPropsEqual.js @@ -17,7 +17,7 @@ export const defaultPropKeysToDeepCheck = [ "spacing", "domain", "style", - "data" + "data" // not worth deepchecking data due to perf issues ]; export default function xyPropsEqual( diff --git a/tests/jsdom/spec/SankeyDiagram.spec.js b/tests/jsdom/spec/SankeyDiagram.spec.js index c915c369..877b9b89 100644 --- a/tests/jsdom/spec/SankeyDiagram.spec.js +++ b/tests/jsdom/spec/SankeyDiagram.spec.js @@ -1,12 +1,12 @@ import React from "react"; import * as d3 from "d3"; import _ from "lodash"; -import {mount, shallow} from "enzyme"; +import { mount, shallow } from "enzyme"; import chai from "chai"; import sinon from "sinon"; import sinonChai from "sinon-chai"; chai.use(sinonChai); -const {expect} = chai; +const { expect } = chai; // use rewire to test internal SankeyNode/Link/etc. components const rewire = require("rewire"); @@ -20,14 +20,20 @@ const SankeyLinkLabel = Sankey.__get__("SankeyLinkLabel"); function getSampleData() { return { - nodes: [{name: "Apples"}, {name: "Bananas"}, {name: "Cherries"}, {name: "Dates"}, {name: "Elderberries"}], + nodes: [ + { name: "Apples" }, + { name: "Bananas" }, + { name: "Cherries" }, + { name: "Dates" }, + { name: "Elderberries" } + ], links: [ - {source: 0, target: 2, value: 0.5}, - {source: 0, target: 3, value: 0.5}, - {source: 1, target: 2, value: 0.5}, - {source: 1, target: 3, value: 0.5}, - {source: 2, target: 4, value: 1}, - {source: 3, target: 4, value: 1} + { source: 0, target: 2, value: 0.5 }, + { source: 0, target: 3, value: 0.5 }, + { source: 1, target: 2, value: 0.5 }, + { source: 1, target: 3, value: 0.5 }, + { source: 2, target: 4, value: 1 }, + { source: 3, target: 4, value: 1 } ] }; } @@ -35,33 +41,33 @@ function getSampleData() { function getSampleDataWithId() { return { nodes: [ - {id: "a", label: "Apples"}, - {id: "b", label: "Bananas"}, - {id: "c", label: "Cherries"}, - {id: "d", label: "Dates"}, - {id: "e", label: "Elderberries"} + { id: "a", label: "Apples" }, + { id: "b", label: "Bananas" }, + { id: "c", label: "Cherries" }, + { id: "d", label: "Dates" }, + { id: "e", label: "Elderberries" } ], links: [ - {source: "a", target: "c", value: 0.5}, - {source: "a", target: "d", value: 0.5}, - {source: "b", target: "c", value: 0.5}, - {source: "b", target: "d", value: 0.5}, - {source: "c", target: "e", value: 1}, - {source: "d", target: "e", value: 1} + { source: "a", target: "c", value: 0.5 }, + { source: "a", target: "d", value: 0.5 }, + { source: "b", target: "c", value: 0.5 }, + { source: "b", target: "d", value: 0.5 }, + { source: "c", target: "e", value: 1 }, + { source: "d", target: "e", value: 1 } ] }; } describe("SankeyDiagram", () => { it("renders a Sankey Diagram", () => { - const {nodes, links} = getSampleData(); - const props = {width: 600, height: 400, nodes, links}; + const { nodes, links } = getSampleData(); + const props = { width: 600, height: 400, nodes, links }; const chart = mount(); const svg = chart.find("svg"); expect(svg).to.have.length(1); + // todo check shouldClone // get sampleData again since it has been mutated by the component - // todo don't mutate incoming nodes/links? const sampleData = getSampleData(); const sankeyNodes = chart.find(SankeyNode); const sankeyLinks = chart.find(SankeyLink); @@ -82,7 +88,10 @@ describe("SankeyDiagram", () => { expect(nodeProps.node.sourceLinks).to.have.length(sourceLinks.length); expect(nodeProps.node.targetLinks).to.be.an("array"); expect(nodeProps.node.targetLinks).to.have.length(targetLinks.length); - const expectedNodeValue = Math.max(_.sumBy(sourceLinks, l => l.value), _.sumBy(targetLinks, l => l.value)); + const expectedNodeValue = Math.max( + _.sumBy(sourceLinks, l => l.value), + _.sumBy(targetLinks, l => l.value) + ); expect(nodeProps.node.value).to.equal(expectedNodeValue); expect(nodeProps.node.x0).to.be.finite; expect(nodeProps.node.x1).to.be.finite; @@ -122,7 +131,7 @@ describe("SankeyDiagram", () => { width: 600, height: 400, className: "woof", - style: {paddingLeft: 30} + style: { paddingLeft: 30 } }; const chart = mount(); const svg = chart.find("svg"); @@ -137,7 +146,7 @@ describe("SankeyDiagram", () => { it("uses shouldClone prop to determine whether to clone or mutate nodes/links data", () => { const dataToClone = getSampleData(); const cloneProps = { - ...dataToClone, + ...dataToClone, width: 600, height: 400, shouldClone: true @@ -178,13 +187,20 @@ describe("SankeyDiagram", () => { sankeyNodes.forEach((node, i) => { const nodeProps = node.props(); expect(nodeProps.node).to.be.an("object"); - const sourceLinks = sampleData.links.filter(link => link.source === nodeProps.node.id); - const targetLinks = sampleData.links.filter(link => link.target === nodeProps.node.id); + const sourceLinks = sampleData.links.filter( + link => link.source === nodeProps.node.id + ); + const targetLinks = sampleData.links.filter( + link => link.target === nodeProps.node.id + ); expect(nodeProps.node.sourceLinks).to.be.an("array"); expect(nodeProps.node.sourceLinks).to.have.length(sourceLinks.length); expect(nodeProps.node.targetLinks).to.be.an("array"); expect(nodeProps.node.targetLinks).to.have.length(targetLinks.length); - const expectedNodeValue = Math.max(_.sumBy(sourceLinks, l => l.value), _.sumBy(targetLinks, l => l.value)); + const expectedNodeValue = Math.max( + _.sumBy(sourceLinks, l => l.value), + _.sumBy(targetLinks, l => l.value) + ); expect(nodeProps.node.value).to.equal(expectedNodeValue); expect(nodeProps.node.x0).to.be.finite; expect(nodeProps.node.x1).to.be.finite; @@ -217,7 +233,7 @@ describe("SankeyDiagram", () => { }); it("uses showNodes boolean or accessor prop to determine whether to render nodes", () => { - const size = {width: 600, height: 400}; + const size = { width: 600, height: 400 }; const showNodesProps = { ...size, ...getSampleData(), @@ -244,7 +260,7 @@ describe("SankeyDiagram", () => { }); it("uses showLinks boolean or accessor prop to determine whether to render links", () => { - const size = {width: 600, height: 400}; + const size = { width: 600, height: 400 }; const showLinksProps = { ...size, ...getSampleData(), @@ -303,8 +319,12 @@ describe("SankeyDiagram", () => { const chart = mount(); const sankeyNodes = chart.find(SankeyNode); expect(sankeyNodes).to.have.length(5); - expect(sankeyNodes.at(1).props().node.y0 - sankeyNodes.at(0).props().node.y1).to.equal(37); - expect(sankeyNodes.at(3).props().node.y0 - sankeyNodes.at(2).props().node.y1).to.equal(37); + expect( + sankeyNodes.at(1).props().node.y0 - sankeyNodes.at(0).props().node.y1 + ).to.equal(37); + expect( + sankeyNodes.at(3).props().node.y0 - sankeyNodes.at(2).props().node.y1 + ).to.equal(37); }); // todo: test nodeAlignment? how? @@ -315,7 +335,7 @@ describe("SankeyDiagram", () => { width: 600, height: 400, nodeClassName: "doggo", - nodeStyle: {fill: "orange"}, + nodeStyle: { fill: "orange" }, onMouseEnterNode: sinon.spy(), onMouseLeaveNode: sinon.spy(), onMouseMoveNode: sinon.spy(), @@ -347,7 +367,7 @@ describe("SankeyDiagram", () => { width: 600, height: 400, linkClassName: "kitten", - linkStyle: {fill: "tomato"}, + linkStyle: { fill: "tomato" }, onMouseEnterLink: sinon.spy(), onMouseLeaveLink: sinon.spy(), onMouseMoveLink: sinon.spy(), @@ -374,13 +394,15 @@ describe("SankeyDiagram", () => { }); it("uses showNodeLabels boolean or accessor prop to determine whether to render node labels", () => { - const size = {width: 600, height: 400}; + const size = { width: 600, height: 400 }; const showNodeLabelsProps = { ...size, ...getSampleData(), showNodeLabels: true }; - const showNodeLabelsChart = mount(); + const showNodeLabelsChart = mount( + + ); expect(showNodeLabelsChart.find(SankeyNodeLabel)).to.have.length(5); const hideNodeLabelsProps = { @@ -388,7 +410,9 @@ describe("SankeyDiagram", () => { ...getSampleData(), showNodeLabels: false }; - const hideNodeLabelsChart = mount(); + const hideNodeLabelsChart = mount( + + ); expect(hideNodeLabelsChart.find(SankeyNodeLabel)).to.have.length(0); const showSomeNodeLabelsProps = { @@ -396,18 +420,22 @@ describe("SankeyDiagram", () => { ...getSampleData(), showNodeLabels: node => node.index < 3 }; - const showSomeNodeLabelsChart = mount(); + const showSomeNodeLabelsChart = mount( + + ); expect(showSomeNodeLabelsChart.find(SankeyNodeLabel)).to.have.length(3); }); it("uses showLinkLabels boolean or accessor prop to determine whether to render link labels", () => { - const size = {width: 600, height: 400}; + const size = { width: 600, height: 400 }; const showLinkLabelsProps = { ...size, ...getSampleData(), showLinkLabels: true }; - const showLinkLabelsChart = mount(); + const showLinkLabelsChart = mount( + + ); expect(showLinkLabelsChart.find(SankeyLinkLabel)).to.have.length(6); const hideLinkLabelsProps = { @@ -415,7 +443,9 @@ describe("SankeyDiagram", () => { ...getSampleData(), showLinkLabels: false }; - const hideLinkLabelsChart = mount(); + const hideLinkLabelsChart = mount( + + ); expect(hideLinkLabelsChart.find(SankeyLinkLabel)).to.have.length(0); const showSomeLinkLabelsProps = { @@ -423,7 +453,9 @@ describe("SankeyDiagram", () => { ...getSampleData(), showLinkLabels: (link, graph) => link.target.index === 2 }; - const showSomeLinkLabelsChart = mount(); + const showSomeLinkLabelsChart = mount( + + ); expect(showSomeLinkLabelsChart.find(SankeyLinkLabel)).to.have.length(2); }); @@ -436,7 +468,7 @@ describe("SankeyDiagram", () => { y1: 100 }; it("renders a rectangle with the position & size of the current node", () => { - const node = mount(); + const node = mount(); const rect = node.find("rect"); expect(rect).to.have.length(1); expect(rect.props().x).to.equal(30); @@ -447,8 +479,12 @@ describe("SankeyDiagram", () => { }); it("passes nodeClassName and nodeStyle through to the node rectangle element", () => { const className = "foo-bar-node"; - const style = {fill: "coral"}; - const nodeProps = {node: basicNodeObj, nodeClassName: className, nodeStyle: style}; + const style = { fill: "coral" }; + const nodeProps = { + node: basicNodeObj, + nodeClassName: className, + nodeStyle: style + }; const node = mount(); const rect = node.find("rect"); expect(rect.props().className).to.contain(className); @@ -457,8 +493,12 @@ describe("SankeyDiagram", () => { }); it("calls nodeClassName & nodeStyle to get class & style, if they are functions", () => { const className = (node, graph) => `i-${node.index}-x0-${node.x0}`; - const style = (node, graph) => ({strokeWidth: `${node.x1}px`}); - const nodeProps = {node: basicNodeObj, nodeClassName: className, nodeStyle: style}; + const style = (node, graph) => ({ strokeWidth: `${node.x1}px` }); + const nodeProps = { + node: basicNodeObj, + nodeClassName: className, + nodeStyle: style + }; const node = mount(); const rect = node.find("rect"); expect(rect.props().className).to.contain("i-5-x0-30"); @@ -468,7 +508,7 @@ describe("SankeyDiagram", () => { it("attaches mouse event handlers (enter, leave, move, down, up, click) to the node rectangle", () => { const nodeProps = { node: basicNodeObj, - graph: {nodes: [], links: []}, + graph: { nodes: [], links: [] }, onMouseEnterNode: sinon.spy(), onMouseLeaveNode: sinon.spy(), onMouseMoveNode: sinon.spy(), @@ -520,9 +560,9 @@ describe("SankeyDiagram", () => { describe("SankeyLink", () => { const linkPath = "M10 10"; - const linkObj = {width: 20}; + const linkObj = { width: 20 }; it("renders a link path", () => { - const link = mount(); + const link = mount(); const path = link.find("path"); expect(path).to.have.length(1); expect(path.props().d).to.equal(linkPath); @@ -531,8 +571,12 @@ describe("SankeyDiagram", () => { }); it("passes linkClassName and linkStyle through to the path element", () => { const linkClassName = "foo-bar-link"; - const linkStyle = {fill: "thistle"}; - const link = mount(); + const linkStyle = { fill: "thistle" }; + const link = mount( + + ); const path = link.find("path"); expect(path.props().className).to.contain(linkClassName); expect(path.props().style).to.be.an("object"); @@ -540,8 +584,8 @@ describe("SankeyDiagram", () => { }); it("calls linkClassName & linkStyle to get class & style, if they are functions", () => { const linkClassName = (link, graph) => `w-${link.width}`; - const linkStyle = (link, graph) => ({borderWidth: link.width}); - const linkProps = {link: linkObj, linkClassName, linkStyle}; + const linkStyle = (link, graph) => ({ borderWidth: link.width }); + const linkProps = { link: linkObj, linkClassName, linkStyle }; const link = mount(); const path = link.find("path"); expect(path.props().className).to.contain("w-20"); @@ -551,7 +595,7 @@ describe("SankeyDiagram", () => { it("attaches mouse event handlers (enter, leave, move, down, up, click) to the link path", () => { const linkProps = { link: linkObj, - graph: {nodes: [], links: []}, + graph: { nodes: [], links: [] }, onMouseEnterLink: sinon.spy(), onMouseLeaveLink: sinon.spy(), onMouseMoveLink: sinon.spy(), @@ -611,7 +655,11 @@ describe("SankeyDiagram", () => { name: "Sour Lemons" }; it("renders a node label in a element", () => { - const label = mount( "ok"}} />); + const label = mount( + "ok" }} + /> + ); const text = label.find("text"); expect(text).to.have.length(1); expect(text.props().x).to.be.finite; @@ -648,7 +696,9 @@ describe("SankeyDiagram", () => { + nodeLabelText: node => ( + + ) }} /> ); @@ -659,10 +709,17 @@ describe("SankeyDiagram", () => { it("passes nodeLabelClassName and nodeLabelStyle through to the text element", () => { const nodeLabelClassName = "my-fun-node-label"; - const nodeLabelStyle = {fill: "salmon"}; + const nodeLabelStyle = { fill: "salmon" }; const nodeLabelText = node => node.name; const label = mount( - + ); const text = label.find("text"); expect(text).to.have.length(1); @@ -672,10 +729,19 @@ describe("SankeyDiagram", () => { }); it("calls nodeLabelClassName & nodeLabelStyle to get class & style, if they are functions", () => { const nodeLabelClassName = node => `node-label-${node.id}`; - const nodeLabelStyle = node => ({fill: node.id === "lemons" ? "orange" : "purple"}); + const nodeLabelStyle = node => ({ + fill: node.id === "lemons" ? "orange" : "purple" + }); const nodeLabelText = node => node.name; const label = mount( - + ); const text = label.find("text"); expect(text).to.have.length(1); @@ -685,71 +751,112 @@ describe("SankeyDiagram", () => { }); it("uses nodeLabelPlacement to determine the label's position", () => { const nodeLabelText = node => node.name; - const commonProps = {node: basicNodeObj, nodeLabelText}; + const commonProps = { node: basicNodeObj, nodeLabelText }; - const labelBefore = mount(); + const labelBefore = mount( + + ); const labelBeforeText = labelBefore.find("text"); expect(labelBeforeText.props().x).to.be.at.most(30); expect(labelBeforeText.props().y).to.equal(70); - expect(labelBeforeText.props().style.alignmentBaseline).to.equal("middle"); + expect(labelBeforeText.props().style.alignmentBaseline).to.equal( + "middle" + ); expect(labelBeforeText.props().style.textAnchor).to.equal("end"); - const labelAfter = mount(); + const labelAfter = mount( + + ); const labelAfterText = labelAfter.find("text"); expect(labelAfterText.props().x).to.be.at.least(50); expect(labelAfterText.props().y).to.equal(70); expect(labelAfterText.props().style.alignmentBaseline).to.equal("middle"); expect(labelAfterText.props().style.textAnchor).to.equal("start"); - const labelAbove = mount(); + const labelAbove = mount( + + ); const labelAboveText = labelAbove.find("text"); expect(labelAboveText.props().x).to.equal(40); expect(labelAboveText.props().y).to.be.at.most(40); - expect(labelAboveText.props().style.alignmentBaseline).to.equal("baseline"); + expect(labelAboveText.props().style.alignmentBaseline).to.equal( + "baseline" + ); expect(labelAboveText.props().style.textAnchor).to.equal("middle"); - const labelBelow = mount(); + const labelBelow = mount( + + ); const labelBelowText = labelBelow.find("text"); expect(labelBelowText.props().x).to.equal(40); expect(labelBelowText.props().y).to.be.at.least(100); - expect(labelBelowText.props().style.alignmentBaseline).to.equal("hanging"); + expect(labelBelowText.props().style.alignmentBaseline).to.equal( + "hanging" + ); expect(labelBelowText.props().style.textAnchor).to.equal("middle"); - const conditionalPlacement = node => (node.id === "lemons" ? "below" : "above"); + const conditionalPlacement = node => + node.id === "lemons" ? "below" : "above"; const labelConditional = mount( - + ); // should resolve to 'below', so use same tests as 'below' const labelConditionalText = labelConditional.find("text"); expect(labelConditionalText.props().x).to.equal(40); expect(labelConditionalText.props().y).to.be.at.least(100); - expect(labelConditionalText.props().style.alignmentBaseline).to.equal("hanging"); + expect(labelConditionalText.props().style.alignmentBaseline).to.equal( + "hanging" + ); expect(labelConditionalText.props().style.textAnchor).to.equal("middle"); }); it("uses nodeLabelDistance to determine node label's distance from the node", () => { const nodeLabelText = node => node.name; - const commonProps = {node: basicNodeObj, nodeLabelText, nodeLabelDistance: 9}; + const commonProps = { + node: basicNodeObj, + nodeLabelText, + nodeLabelDistance: 9 + }; - const labelBefore = mount(); + const labelBefore = mount( + + ); const labelBeforeText = labelBefore.find("text"); expect(labelBeforeText.props().x).to.equal(21); - const labelAfter = mount(); + const labelAfter = mount( + + ); const labelAfterText = labelAfter.find("text"); expect(labelAfterText.props().x).to.equal(59); - const labelAbove = mount(); + const labelAbove = mount( + + ); const labelAboveText = labelAbove.find("text"); expect(labelAboveText.props().y).to.equal(31); - const labelBelow = mount(); + const labelBelow = mount( + + ); const labelBelowText = labelBelow.find("text"); expect(labelBelowText.props().y).to.equal(109); const getDistance = node => 7; const labelDynamic = mount( - + ); const labelDynamicText = labelDynamic.find("text"); expect(labelDynamicText.props().y).to.equal(107); @@ -757,10 +864,14 @@ describe("SankeyDiagram", () => { }); describe("SankeyLinkLabel", () => { - const basicLinkObj = {source: 2, target: 5, value: 99}; + const basicLinkObj = { source: 2, target: 5, value: 99 }; it("renders a link label", () => { - const props = {link: basicLinkObj, linkPathId: "myLinkPath", linkLabelText: () => "r2d2"}; + const props = { + link: basicLinkObj, + linkPathId: "myLinkPath", + linkLabelText: () => "r2d2" + }; const label = mount(); const text = label.find("text"); expect(text).to.have.length(1); @@ -775,8 +886,8 @@ describe("SankeyDiagram", () => { linkPathId: "myLinkPath", linkLabelText: () => "r2d2", linkLabelClassName: "link-zelda", - linkLabelStyle: {fill: "orange"}, - linkLabelAttributes: {textAnchor: "end"} + linkLabelStyle: { fill: "orange" }, + linkLabelAttributes: { textAnchor: "end" } }; const label = mount(); const text = label.find("text"); @@ -791,9 +902,9 @@ describe("SankeyDiagram", () => { link: basicLinkObj, linkPathId: "myLinkPath", linkLabelText: () => "r2d2", - linkLabelClassName: (link) => `link-${link.source}-${link.target}`, - linkLabelStyle: () => ({fill: "thistle"}), - linkLabelAttributes: () => ({textAnchor: "start"}) + linkLabelClassName: link => `link-${link.source}-${link.target}`, + linkLabelStyle: () => ({ fill: "thistle" }), + linkLabelAttributes: () => ({ textAnchor: "start" }) }; const label = mount(); const text = label.find("text"); @@ -808,15 +919,16 @@ describe("SankeyDiagram", () => { link: basicLinkObj, linkPathId: "myLinkPath", linkLabelText: () => "r2d2", - linkLabelStartOffset: '27%' + linkLabelStartOffset: "27%" }; const label = mount(); const text = label.find("text"); expect(text).to.have.length(1); const textPath = text.find("textPath"); expect(textPath).to.have.length(1); - expect(textPath.props().startOffset).to.equal('27%'); + expect(textPath.props().startOffset).to.equal("27%"); }); }); // todo test terminals + // test their properties & rendered correctly }); diff --git a/tests/jsdom/spec/XYPlot.spec.js b/tests/jsdom/spec/XYPlot.spec.js index f66943fe..f2f5a2f4 100644 --- a/tests/jsdom/spec/XYPlot.spec.js +++ b/tests/jsdom/spec/XYPlot.spec.js @@ -1,103 +1,55 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import * as d3 from 'd3'; -import {expect} from 'chai'; -import {mount, shallow} from 'enzyme'; - -import {XYPlot, LineChart} from '../../../src/index.js'; - -const commonXYProps = {xDomain: [0, 10], yDomain: [0, 100]}; - -describe('XYPlot', () => { - it('renders SVG with given width & height (or a default)', () => { - const chart = mount(); - const node = chart.find('svg').getNode(); - expect(node.tagName.toLowerCase()).to.equal('svg'); - expect(node.getAttribute('width')).to.equal('600'); - expect(node.getAttribute('height')).to.equal('800'); - - const chart2 = mount(); - const node2 = chart2.find('svg').getNode(); - expect(node2.tagName.toLowerCase()).to.equal('svg'); - expect(parseInt(node2.getAttribute('width'))).to.be.a('number').and.to.be.above(0); - expect(parseInt(node2.getAttribute('height'))).to.be.a('number').and.to.be.above(0); - }); - - it('renders inner chart area with given margin', () => { - const size = 400; - const margin = {marginTop: 10, marginBottom: 20, marginLeft: 30, marginRight: 40}; - const chart = mount(); - const inner = chart.find('.chart-inner').getNode(); - const bg = chart.find('.plot-background').getNode(); - expect(inner.getAttribute('transform').replace(/\s/, '')) - .to.contain(`translate(${margin.marginLeft},${margin.marginTop})`); - expect(parseInt(bg.getAttribute('width'))).to.equal(size - (margin.marginLeft + margin.marginRight)); - expect(parseInt(bg.getAttribute('height'))).to.equal(size - (margin.marginTop + margin.marginBottom)); - }); - - // TODO: TEST MOUSE EVENTS! - - // - // it('creates a top/bottom/left/right object from single value, if object is not given for directional props', () => { - // const size = 400; - // const margin = 50; - // const chart = mount(); - // const inner = chart.find('.chart-inner').getNode(); - // const bg = chart.find('.plot-background').getNode(); - // expect(inner.getAttribute('transform').replace(/\s/, '')) - // .to.contain(`translate(${margin},${margin})`); - // expect(parseInt(bg.getAttribute('width'))).to.equal(size - (margin + margin)); - // expect(parseInt(bg.getAttribute('height'))).to.equal(size - (margin + margin)); - // }); - - /* - spacing: PropTypes.fourDirections, - - // axis types - number, time or ordinal - axisType: PropTypes.xyObjectOf(PropTypes.axisType), - // scale domains may be provided, otherwise will be inferred from data - domain: PropTypes.xyObjectOf(PropTypes.dataArray), - // whether or not to extend the scales to end on nice values (see docs for d3 scale.linear.nice()) - nice: PropTypes.xyObjectOf(PropTypes.bool), - - // approximate # of ticks to include on each axis - 10 is default - // (actual # may be slightly different, to get nicest intervals) - tickCount: PropTypes.xyObjectOf(PropTypes.number), - // or alternatively, you can pass an array of the exact tick values to use on each axis - ticks: PropTypes.xyObjectOf(PropTypes.dataArray), - // size of axis ticks - tickLength: PropTypes.xyObjectOf(PropTypes.number), - - // axis value labels will be created for each tick, unless you specify a different list of values to label - labelValues: PropTypes.xyObjectOf(PropTypes.dataArray), - // format to use for the axis value labels. can be a function or a string. - // if function, called on each label. - // if string, interpreted as momentjs formats for time axes, or numeraljs formats for number axes - labelFormat: PropTypes.xyObjectOf(PropTypes.stringFormatter), - // padding between axis value labels and the axis/ticks - labelPadding: PropTypes.xyObjectOf(PropTypes.number), - - // should we draw axis value labels - showLabels: PropTypes.xyObjectOf(PropTypes.bool), - // should we draw the grid lines in the main chart space - showGrid: PropTypes.xyObjectOf(PropTypes.bool), - // should we draw the little tick lines along the axis - showTicks: PropTypes.xyObjectOf(PropTypes.bool), - // should we draw a line showing where zero is - showZero: PropTypes.xyObjectOf(PropTypes.bool), - - // - axisLabel: PropTypes.xyObjectOf(PropTypes.string), - axisLabelAlign: PropTypes.xyObjectOf(PropTypes.shape({ - horizontal: PropTypes.oneOf(['left', 'center', 'right']), - vertical: PropTypes.oneOf(['top', 'bottom']) - })), - axisLabelPadding: PropTypes.xyObjectOf(PropTypes.number), - - // todo more interaction - onMouseMove: PropTypes.func, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func - */ - +import React from "react"; +import ReactDOM from "react-dom"; +import * as d3 from "d3"; +import { expect } from "chai"; +import { mount, shallow } from "enzyme"; + +import { XYPlot, LineChart } from "../../../src/index.js"; + +const commonXYProps = { xDomain: [0, 10], yDomain: [0, 100] }; + +describe("XYPlot", () => { + it("renders SVG with given width & height (or a default)", () => { + const chart = mount(); + const node = chart.find("svg").getNode(); + expect(node.tagName.toLowerCase()).to.equal("svg"); + expect(node.getAttribute("width")).to.equal("600"); + expect(node.getAttribute("height")).to.equal("800"); + + const chart2 = mount(); + const node2 = chart2.find("svg").getNode(); + expect(node2.tagName.toLowerCase()).to.equal("svg"); + expect(parseInt(node2.getAttribute("width"))) + .to.be.a("number") + .and.to.be.above(0); + expect(parseInt(node2.getAttribute("height"))) + .to.be.a("number") + .and.to.be.above(0); + }); + + it("renders inner chart area with given margin", () => { + const size = 400; + const margin = { + marginTop: 10, + marginBottom: 20, + marginLeft: 30, + marginRight: 40 + }; + const chart = mount( + + ); + const inner = chart.find(".chart-inner").getNode(); + const bg = chart.find(".plot-background").getNode(); + expect(inner.getAttribute("transform").replace(/\s/, "")).to.contain( + `translate(${margin.marginLeft},${margin.marginTop})` + ); + expect(parseInt(bg.getAttribute("width"))).to.equal( + size - (margin.marginLeft + margin.marginRight) + ); + expect(parseInt(bg.getAttribute("height"))).to.equal( + size - (margin.marginTop + margin.marginBottom) + ); + }); + + // TODO: TEST MOUSE EVENTS! Use sinon.spy()! }); diff --git a/tests/jsdom/spec/examples.spec.js b/tests/jsdom/spec/examples.spec.js index 3ad8f344..a3a2e0bc 100644 --- a/tests/jsdom/spec/examples.spec.js +++ b/tests/jsdom/spec/examples.spec.js @@ -1,10 +1,12 @@ -import _ from 'lodash'; -import React from 'react'; +import _ from "lodash"; +import React from "react"; // import TestUtils from 'react-addons-test-utils'; -import {expect} from 'chai'; +import { expect } from "chai"; // TODO: fix this to work with new examples structure?? +// test the examples render + // import {examples} from '../../../docs/src/Examples.jsx'; // describe('examples', () => { diff --git a/tests/jsdom/spec/resolveXYScales.spec.js b/tests/jsdom/spec/resolveXYScales.spec.js index ac7f682e..af367d63 100644 --- a/tests/jsdom/spec/resolveXYScales.spec.js +++ b/tests/jsdom/spec/resolveXYScales.spec.js @@ -1,11 +1,11 @@ import _ from "lodash"; import React from "react"; import * as d3 from "d3"; -import {expect} from "chai"; -import {mount, shallow} from "enzyme"; +import { expect } from "chai"; +import { mount, shallow } from "enzyme"; -import {isValidScale} from "../../../src/utils/Scale"; -import {innerRangeX, innerRangeY} from "../../../src/utils/Margin"; +import { isValidScale } from "../../../src/utils/Scale"; +import { innerRangeX, innerRangeY } from "../../../src/utils/Margin"; import resolveXYScales from "../../../src/utils/resolveXYScales"; @@ -28,11 +28,17 @@ function expectXYScales(scales) { }); } -function expectXYScaledComponent(rendered, {width, height, scaleType, domain, margin, range}) { +function expectXYScaledComponent( + rendered, + { width, height, scaleType, domain, margin, range } +) { // checks that a given rendered component has been created with XY scales/margin // that match the expected domain, range & margin // if range not provided, it should be width/height minus margins - range = range || {x: innerRangeX(width, margin), y: innerRangeY(height, margin)}; + range = range || { + x: innerRangeX(width, margin), + y: innerRangeY(height, margin) + }; expect(scaleType).to.be.an("object"); console.log("expected domains", domain); console.log("expected range", range); @@ -59,11 +65,17 @@ function expectXYScaledComponent(rendered, {width, height, scaleType, domain, ma }); } -function expectXYScaledComponentEnzyme(rendered, {width, height, scaleType, domain, margin, range}) { +function expectXYScaledComponentEnzyme( + rendered, + { width, height, scaleType, domain, margin, range } +) { // checks that a given rendered component has been created with XY scales/margin // that match the expected domain, range & margin // if range not provided, it should be width/height minus margins - range = range || {x: innerRangeX(width, margin), y: innerRangeY(height, margin)}; + range = range || { + x: innerRangeX(width, margin), + y: innerRangeY(height, margin) + }; expect(scaleType).to.be.an("object"); expect(rendered.props().margin).to.deep.equal(margin); @@ -89,16 +101,20 @@ function expectXYScaledComponentEnzyme(rendered, {width, height, scaleType, doma } describe("resolveXYScales", () => { - const customScaleType = {xScaleType: "ordinal", yScaleType: "linear"}; - const customDomain = {xDomain: [-5, 5], yDomain: [0, 10]}; - const customMargin = {marginTop: 10, marginBottom: 20, marginLeft: 30, marginRight: 40}; + const customScaleType = { xScaleType: "ordinal", yScaleType: "linear" }; + const customDomain = { xDomain: [-5, 5], yDomain: [0, 10] }; + const customMargin = { + marginTop: 10, + marginBottom: 20, + marginLeft: 30, + marginRight: 40 + }; const width = 500; const height = 400; // test fixture component classes class ComponentWithChildren extends React.Component { render() { - //console.log(this.props.scale.x.range()) return
{this.props.children}
; } } @@ -143,36 +159,29 @@ describe("resolveXYScales", () => { xDomain, yDomain } = this.props; - const newChildren = React.Children.map(this.props.children, (child, i) => { - return React.cloneElement(child, { - width, - height, - xScale, - yScale, - xScaleType, - yScaleType, - marginTop, - marginBottom, - marginLeft, - marginRight, - xDomain, - yDomain - }); - }); + const newChildren = React.Children.map( + this.props.children, + (child, i) => { + return React.cloneElement(child, { + width, + height, + xScale, + yScale, + xScaleType, + yScaleType, + marginTop, + marginBottom, + marginLeft, + marginRight, + xDomain, + yDomain + }); + } + ); return
{newChildren}
; } } const XYContainerChart = resolveXYScales(ContainerChart); - // - // const XYChartWithObjectProps = resolveObjectProps(resolveXYScales(Chart), - // ['domain', 'scale', 'scaleType'], ['x', 'y'] - // ); - // const XYChartWithCustomMarginAndObjectProps = resolveObjectProps(resolveXYScales(ChartWithCustomMargin), - // ['domain', 'scale', 'scaleType'], ['x', 'y'] - // ); - // const XYContainerChartWithObjectProps = resolveObjectProps(resolveXYScales(ContainerChart), - // ['domain', 'scale', 'scaleType'], ['x', 'y'] - // ); it("passes XY scales and margins through if both are provided", () => { const props = { @@ -192,7 +201,14 @@ describe("resolveXYScales", () => { const wrapped = mount(); const rendered = wrapped.find(Chart); - ["xScale", "yScale", "marginTop", "marginBottom", "marginLeft", "marginRight"].forEach(propKey => { + [ + "xScale", + "yScale", + "marginTop", + "marginBottom", + "marginLeft", + "marginRight" + ].forEach(propKey => { expectRefAndDeepEqual(rendered.props()[propKey], props[propKey]); }); }); @@ -216,13 +232,18 @@ describe("resolveXYScales", () => { const renderedXScale = rendered.props().xScale; const renderedYScale = rendered.props().yScale; - // expectXYScales(renderedScale); expect(isValidScale(renderedXScale)).to.equal(true); expect(isValidScale(renderedYScale)).to.equal(true); expect(renderedXScale.domain()).to.deep.equal(props.xDomain); expect(renderedYScale.domain()).to.deep.equal(props.yDomain); - expect(renderedXScale.range()).to.deep.equal([0, width - (props.marginLeft + props.marginRight)]); - expect(renderedYScale.range()).to.deep.equal([height - (props.marginTop + props.marginBottom), 0]); + expect(renderedXScale.range()).to.deep.equal([ + 0, + width - (props.marginLeft + props.marginRight) + ]); + expect(renderedYScale.range()).to.deep.equal([ + height - (props.marginTop + props.marginBottom), + 0 + ]); }); it("infers scaleType from Component.getScaleType", () => { @@ -396,6 +417,32 @@ describe("resolveXYScales", () => { expect(rendered.props().yDomain).to.deep.equal([0, 5]); }); + it("x and y domain includes 0 given inferred domain from children data", () => { + const props = { + width, + height, + includeXZero: true, + includeYZero: true, + xScaleType: "linear", + yScaleType: "linear", + marginTop: 11, + marginBottom: 22, + marginLeft: 33, + marginRight: 44 + }; + const tree = ( + + d[0]} y={d => d[1]} /> + d[0]} y={d => d[1]} /> + + ); + const wrapped = mount(tree); + const rendered = wrapped.find(ContainerChart); + + expect(rendered.props().xDomain).to.deep.equal([0, 10]); + expect(rendered.props().yDomain).to.deep.equal([0, 14]); + }); + it("infers margin from Component.getMargin", () => { const props = { width, @@ -459,7 +506,7 @@ describe("resolveXYScales", () => { }); it("infers scaleType & domain from data, margin from getMargin", () => { - const containerProps = {width, height}; + const containerProps = { width, height }; const chartProps = { data: [[12, "a"], [18, "b"], [22, "c"]], x: d => d[0], @@ -515,11 +562,15 @@ describe("resolveXYScales", () => { marginRight: 44 }; - const invertXChart = mount().find(Chart); + const invertXChart = mount().find( + Chart + ); expect(invertXChart.props().xDomain).to.deep.equal([3, -3]); expect(invertXChart.props().yDomain).to.deep.equal([0, 10]); - const invertYChart = mount().find(Chart); + const invertYChart = mount().find( + Chart + ); expect(invertYChart.props().xDomain).to.deep.equal([-3, 3]); expect(invertYChart.props().yDomain).to.deep.equal([10, 0]); });