From 0443b8b166560f2a8f93eb791a9d36c1ae4ac144 Mon Sep 17 00:00:00 2001 From: Jon de la Motte Date: Wed, 11 May 2016 09:37:58 -0700 Subject: [PATCH 1/3] Added charts - Improved the color palette page - Updated contrib docs with new _lucidIsPrivate convention - Several new private components that power charts - Cleaned up variables.less - Updated generic tests to handle function props that don't start with `on` - Added new colors for charts - Added new utilities to support charts --- CONTRIBUTING.md | 1 + gulp/docs.js | 18 +- package.json | 4 + src/components/Axis/Axis.jsx | 176 +++++++ src/components/Axis/Axis.less | 13 + src/components/Axis/Axis.spec.js | 13 + src/components/Axis/examples/2.bottom.jsx | 28 ++ src/components/Axis/examples/3.top.jsx | 28 ++ src/components/Axis/examples/4.left.jsx | 24 + src/components/Axis/examples/5.right.jsx | 24 + src/components/Axis/examples/6.time.jsx | 45 ++ src/components/Axis/examples/7.ordinal.jsx | 28 ++ src/components/AxisLabel/AxisLabel.jsx | 89 ++++ src/components/AxisLabel/AxisLabel.less | 11 + src/components/AxisLabel/AxisLabel.spec.js | 7 + src/components/AxisLabel/examples/basic.jsx | 80 +++ src/components/Bar/Bar.jsx | 98 ++++ src/components/Bar/Bar.less | 13 + src/components/Bar/Bar.spec.js | 7 + src/components/Bar/examples/basic.jsx | 42 ++ src/components/BarChart/BarChar.spec.jsx | 107 ++++ src/components/BarChart/BarChart.jsx | 297 +++++++++++ src/components/BarChart/examples/1.basic.jsx | 26 + .../BarChart/examples/2.limited-ticks.jsx | 37 ++ .../BarChart/examples/3.grouped.jsx | 23 + .../BarChart/examples/4.stacked.jsx | 24 + .../BarChart/examples/5.all-the-things.jsx | 43 ++ src/components/Bars/Bars.jsx | 190 +++++++ src/components/Bars/Bars.spec.js | 18 + src/components/Bars/examples/basic.jsx | 58 +++ src/components/Line/Line.jsx | 69 +++ src/components/Line/Line.less | 11 + src/components/Line/Line.spec.js | 7 + src/components/Line/examples/basic.jsx | 19 + src/components/LineChart/LineChart.jsx | 468 ++++++++++++++++++ src/components/LineChart/LineChart.spec.jsx | 120 +++++ src/components/LineChart/examples/1.basic.jsx | 17 + src/components/LineChart/examples/2.multi.jsx | 21 + .../LineChart/examples/3.dual-axis.jsx | 33 ++ .../LineChart/examples/4.stacked.jsx | 24 + .../LineChart/examples/5.all-the-things.jsx | 42 ++ src/components/Lines/Lines.jsx | 179 +++++++ src/components/Lines/Lines.spec.js | 18 + src/components/Lines/examples/stacked.jsx | 56 +++ src/components/Point/Point.jsx | 122 +++++ src/components/Point/Point.less | 15 + src/components/Point/Point.spec.js | 7 + src/components/Point/examples/basic.jsx | 40 ++ src/components/Point/examples/with-stroke.jsx | 44 ++ src/components/Points/Points.jsx | 187 +++++++ src/components/Points/Points.less | 0 src/components/Points/Points.spec.js | 18 + src/components/Points/examples/basic.jsx | 51 ++ src/docs/containers/colors.jsx | 374 +++++++------- src/docs/containers/colors.less | 331 ++++--------- src/docs/index.html | 1 + src/docs/index.jsx | 31 +- src/index.js | 5 + src/styles/components.less | 5 + src/styles/variables.less | 152 ++++-- src/util/chart-helpers.js | 171 +++++++ src/util/chart-helpers.spec.js | 231 +++++++++ src/util/generic-tests.jsx | 24 +- 63 files changed, 3991 insertions(+), 474 deletions(-) create mode 100644 src/components/Axis/Axis.jsx create mode 100644 src/components/Axis/Axis.less create mode 100644 src/components/Axis/Axis.spec.js create mode 100644 src/components/Axis/examples/2.bottom.jsx create mode 100644 src/components/Axis/examples/3.top.jsx create mode 100644 src/components/Axis/examples/4.left.jsx create mode 100644 src/components/Axis/examples/5.right.jsx create mode 100644 src/components/Axis/examples/6.time.jsx create mode 100644 src/components/Axis/examples/7.ordinal.jsx create mode 100644 src/components/AxisLabel/AxisLabel.jsx create mode 100644 src/components/AxisLabel/AxisLabel.less create mode 100644 src/components/AxisLabel/AxisLabel.spec.js create mode 100644 src/components/AxisLabel/examples/basic.jsx create mode 100644 src/components/Bar/Bar.jsx create mode 100644 src/components/Bar/Bar.less create mode 100644 src/components/Bar/Bar.spec.js create mode 100644 src/components/Bar/examples/basic.jsx create mode 100644 src/components/BarChart/BarChar.spec.jsx create mode 100644 src/components/BarChart/BarChart.jsx create mode 100644 src/components/BarChart/examples/1.basic.jsx create mode 100644 src/components/BarChart/examples/2.limited-ticks.jsx create mode 100644 src/components/BarChart/examples/3.grouped.jsx create mode 100644 src/components/BarChart/examples/4.stacked.jsx create mode 100644 src/components/BarChart/examples/5.all-the-things.jsx create mode 100644 src/components/Bars/Bars.jsx create mode 100644 src/components/Bars/Bars.spec.js create mode 100644 src/components/Bars/examples/basic.jsx create mode 100644 src/components/Line/Line.jsx create mode 100644 src/components/Line/Line.less create mode 100644 src/components/Line/Line.spec.js create mode 100644 src/components/Line/examples/basic.jsx create mode 100644 src/components/LineChart/LineChart.jsx create mode 100644 src/components/LineChart/LineChart.spec.jsx create mode 100644 src/components/LineChart/examples/1.basic.jsx create mode 100644 src/components/LineChart/examples/2.multi.jsx create mode 100644 src/components/LineChart/examples/3.dual-axis.jsx create mode 100644 src/components/LineChart/examples/4.stacked.jsx create mode 100644 src/components/LineChart/examples/5.all-the-things.jsx create mode 100644 src/components/Lines/Lines.jsx create mode 100644 src/components/Lines/Lines.spec.js create mode 100644 src/components/Lines/examples/stacked.jsx create mode 100644 src/components/Point/Point.jsx create mode 100644 src/components/Point/Point.less create mode 100644 src/components/Point/Point.spec.js create mode 100644 src/components/Point/examples/basic.jsx create mode 100644 src/components/Point/examples/with-stroke.jsx create mode 100644 src/components/Points/Points.jsx create mode 100644 src/components/Points/Points.less create mode 100644 src/components/Points/Points.spec.js create mode 100644 src/components/Points/examples/basic.jsx create mode 100644 src/util/chart-helpers.js create mode 100644 src/util/chart-helpers.spec.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c945ab757..f4e2c81a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,6 +68,7 @@ describe('MyNewComponent', () => { - Components should use start case e.g.: - "MyComponent" - "AnotherComponentHere.jsx" +- We hang the `_lucidIsPrivate` boolean off our `statics` on our React classes to indicate that the component isn't intended for external consumption yet. ## Tests diff --git a/gulp/docs.js b/gulp/docs.js index 53ad22bef..e8ec7a44d 100644 --- a/gulp/docs.js +++ b/gulp/docs.js @@ -36,10 +36,10 @@ function findParentNodeIdentifier(path) { } function getDocsForPath(definitionPath, name) { - return reactDocgen.parse('', function (ast, recast) { + return reactDocgen.parse('', function (/* ast, recast */) { return definitionPath; }, reactDocgen.defaultHandlers.concat([ - function (documentation, definition) { + function (documentation /*, definition */) { documentation.set('displayName', name); } ])); @@ -73,9 +73,8 @@ module.exports = { var componentName = extractComponentName(file); - console.log('Docgen parsing %s...', file); - var definitionMap; + var isPrivateComponent = false var exportIdentiferName; var componentSource = fs.readFileSync(file) var docs = reactDocgen.parse( @@ -86,6 +85,14 @@ module.exports = { recast.visit(ast, { visitObjectExpression: function (path) { _.forEach(path.get('properties').value, function (property) { + if (property.key.name === 'statics') { + _.forEach(property.value.properties, function (staticsProperty) { + if (staticsProperty.key.name === '_lucidIsPrivate') { + isPrivateComponent = true; + } + }); + } + if (property.key.name === 'render') { var identifier = findParentNodeIdentifier(path); if (identifier) { @@ -104,7 +111,7 @@ module.exports = { }, // Handlers, a series of functions through which the documentation is // built up. - reactDocgen.defaultHandlers.concat(function (documentation, definition) { + reactDocgen.defaultHandlers.concat(function (documentation /*, definition */) { // TODO: determine composition from the `import` statements See // existing handlers for examples: // https://github.com/reactjs/react-docgen/blob/dca8ec9d57b4833f7ddb3164bedf4d74578eee1e/src/handlers/propTypeCompositionHandler.js @@ -112,6 +119,7 @@ module.exports = { return getDocsForPath(definitionMap[childComponentId], childComponentId); }); documentation.set('childComponents', childComponentDocs); + documentation.set('isPrivateComponent', isPrivateComponent); }) ); diff --git a/package.json b/package.json index 3b6a42c57..5cbf8ff88 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,10 @@ "license": "Apache-2.0", "dependencies": { "classnames": "^2.2.3", + "d3-scale": "^0.6.4", + "d3-shape": "^0.6.0", + "d3-time": "^0.2.5", + "d3-time-format": "^0.3.2", "lodash": "^4.11.0", "react-addons-css-transition-group": "^0.14.0", "reselect": "^2.5.1" diff --git a/src/components/Axis/Axis.jsx b/src/components/Axis/Axis.jsx new file mode 100644 index 000000000..8d78c0a3e --- /dev/null +++ b/src/components/Axis/Axis.jsx @@ -0,0 +1,176 @@ +import _ from 'lodash'; +import React from 'react'; +import { lucidClassNames } from '../../util/style-helpers'; +import { discreteTicks } from '../../util/chart-helpers'; +import { createClass } from '../../util/component-types'; + +const cx = lucidClassNames.bind('&-Axis'); + +const { + array, + func, + number, + oneOf, + any, +} = React.PropTypes; + +/** + * {"categories": ["visualizations", "chart primitives"]} + * + * Axes and allies + * + * This component is a very close sister to `d3.avg.axis` and most of the logic + * was ported from d3. + */ +const Axis = createClass({ + displayName: 'Axis', + + statics: { + _lucidIsPrivate: true, + }, + + propTypes: { + /** + * Classes are appended to root element along with existing classes using + * the `classnames` library. + */ + className: any, + /** + * Must be a D3 scale. + */ + scale: func.isRequired, + /** + * Size of the ticks for each discrete tick mark. + */ + innerTickSize: number, + /** + * Size of the tick marks found at the beginning and end of the axis. It's + * common to set this to `0` to remove them. + */ + outerTickSize: number, + /** + * An optional function that can format ticks. Generally this shouldn't be + * needed since d3 has very good default formatters for most data. + * + * Signature: `(tick) => {}` + */ + tickFormat: func, + /** + * If you need fine grained control over the axis ticks, you can pass them + * in this array. + */ + ticks: array, + /** + * Determines the spacing between each tick and its text. + */ + tickPadding: number, + /** + * Determines the orientation of the ticks. `left` and `right` will + * generate a vertical axis, whereas `top` and `bottom` will generate a + * horizontal axis. + */ + orient: oneOf(['top', 'bottom', 'left', 'right']), + /** + * Control the number of ticks displayed. + * + * Usually when using an ordinal scale you should show a tick mark for + * every value, but there are some rare cases when you want to only show a + * sampling of the ticks. By passing in a number to we'll generate an + * evenly spaced number of ticks and display them for ordinal and + * continuous scales. + */ + tickCount: number, + }, + + getDefaultProps() { + return { + innerTickSize: 6, // same as d3 + outerTickSize: 6, // same as d3 + tickPadding: 3, // same as d3 + tickFormat: undefined, // purposefully `undefined` so we can drop through to destructuring defaults + orient: 'bottom', + tickCount: null, + }; + }, + + render() { + const { + scale, + className, + orient, + tickCount, + ticks = scale.ticks + ? scale.ticks(tickCount) + : discreteTicks(scale.domain(), tickCount), // ordinal scales don't have `ticks` but they do have `domains` + innerTickSize, + outerTickSize, + tickFormat = scale.tickFormat ? scale.tickFormat() : _.identity, + tickPadding, + ...passThroughs, + } = this.props; + + const tickSpacing = Math.max(innerTickSize, 0) + tickPadding; + + // Domain + const range = scale.range(); + const sign = orient === 'top' || orient === 'left' ? -1 : 1; + const isH = orient === 'top' || orient === 'bottom'; + + let scaleNormalized = scale; + + // Only ordinal scales have `bandwidth` + if (scale.bandwidth) { + const bandModifier = scale.bandwidth() / 2; + scaleNormalized = (d) => scale(d) + bandModifier; + } + + return ( + + {isH ? ( + + ) : ( + + )} + {_.map(ticks, (tick) => + + + + {tickFormat(tick)} + + + )} + + ); + } +}); + +export default Axis; diff --git a/src/components/Axis/Axis.less b/src/components/Axis/Axis.less new file mode 100644 index 000000000..ab6a43919 --- /dev/null +++ b/src/components/Axis/Axis.less @@ -0,0 +1,13 @@ +.lucid-Axis { + &-tick-text { + font: 11px sans-serif; + fill: @color-textColor; + } + + &-domain, + &-tick { + fill: none; + stroke: @color-gray; + shape-rendering: crispEdges; + } +} diff --git a/src/components/Axis/Axis.spec.js b/src/components/Axis/Axis.spec.js new file mode 100644 index 000000000..4685a2647 --- /dev/null +++ b/src/components/Axis/Axis.spec.js @@ -0,0 +1,13 @@ +import d3Scale from 'd3-scale'; +import { common } from '../../util/generic-tests'; + +import Axis from './Axis'; + +describe('Axis', () => { + common(Axis, { + exemptFunctionProps: ['scale', 'tickFormat'], + getDefaultProps: () => ({ + scale: d3Scale.scaleLinear(), + }) + }); +}); diff --git a/src/components/Axis/examples/2.bottom.jsx b/src/components/Axis/examples/2.bottom.jsx new file mode 100644 index 000000000..87e534d05 --- /dev/null +++ b/src/components/Axis/examples/2.bottom.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import d3Scale from 'd3-scale'; + +import Axis from '../Axis'; + +const margin = {right: 20, left: 20}; +const width = 400; +const height = 50; +const innerWidth = width - margin.right - margin.left; +const x = d3Scale.scaleLinear() + .domain([0, 100000]) + .range([0, innerWidth]); + +export default React.createClass({ + render() { + return ( + + + + + + ); + } +}); diff --git a/src/components/Axis/examples/3.top.jsx b/src/components/Axis/examples/3.top.jsx new file mode 100644 index 000000000..69ac9d8af --- /dev/null +++ b/src/components/Axis/examples/3.top.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import d3Scale from 'd3-scale'; + +import Axis from '../Axis'; + +const margin = {right: 20, left: 20}; +const width = 400; +const height = 50; +const innerWidth = width - margin.right - margin.left; +const x = d3Scale.scaleLinear() + .domain([0, 100000]) + .range([0, innerWidth]); + +export default React.createClass({ + render() { + return ( + + + + + + ); + } +}); diff --git a/src/components/Axis/examples/4.left.jsx b/src/components/Axis/examples/4.left.jsx new file mode 100644 index 000000000..51c3d15fa --- /dev/null +++ b/src/components/Axis/examples/4.left.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import d3Scale from 'd3-scale'; + +import Axis from '../Axis'; + +const margin = {top: 10, bottom: 10}; +const width = 50; +const height = 200; +const innerHeight = height - margin.top - margin.bottom; +const y = d3Scale.scaleLinear() + .domain([0, 100000]) + .range([innerHeight, 0]); + +export default React.createClass({ + render() { + return ( + + + + + + ); + } +}); diff --git a/src/components/Axis/examples/5.right.jsx b/src/components/Axis/examples/5.right.jsx new file mode 100644 index 000000000..d994200f1 --- /dev/null +++ b/src/components/Axis/examples/5.right.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import d3Scale from 'd3-scale'; + +import Axis from '../Axis'; + +const margin = {top: 10, bottom: 10}; +const width = 50; +const height = 200; +const innerHeight = height - margin.top - margin.bottom; +const y = d3Scale.scaleLinear() + .domain([0, 100000]) + .range([innerHeight, 0]); + +export default React.createClass({ + render() { + return ( + + + + + + ); + } +}); diff --git a/src/components/Axis/examples/6.time.jsx b/src/components/Axis/examples/6.time.jsx new file mode 100644 index 000000000..995cc3751 --- /dev/null +++ b/src/components/Axis/examples/6.time.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import d3Scale from 'd3-scale'; +import d3Time from 'd3-time'; + +import Axis from '../Axis'; + +const margin = {right: 10, left: 30}; +const width = 400; +const height = 200; +const innerWidth = width - margin.right - margin.left; + +const x = d3Scale.scaleTime() + .domain([new Date('2015-01-01'), new Date('2017-02-01')]) + .range([0, innerWidth]); + +export default React.createClass({ + render() { + return ( + + + + + + + + + + + + + + ); + } +}); diff --git a/src/components/Axis/examples/7.ordinal.jsx b/src/components/Axis/examples/7.ordinal.jsx new file mode 100644 index 000000000..f73ca900b --- /dev/null +++ b/src/components/Axis/examples/7.ordinal.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import d3Scale from 'd3-scale'; + +import Axis from '../Axis'; + +const margin = {right: 20, left: 20}; +const width = 400; +const height = 40; +const innerWidth = width - margin.right - margin.left; + +const x = d3Scale.scaleBand() + .domain(['a', 'b', 'c', 'd']) + .range([0, innerWidth]); + +export default React.createClass({ + render() { + return ( + + + + + + ); + } +}); diff --git a/src/components/AxisLabel/AxisLabel.jsx b/src/components/AxisLabel/AxisLabel.jsx new file mode 100644 index 000000000..64d98ebb6 --- /dev/null +++ b/src/components/AxisLabel/AxisLabel.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { lucidClassNames } from '../../util/style-helpers'; +import { createClass } from '../../util/component-types'; + +const cx = lucidClassNames.bind('&-AxisLabel'); + +const { + number, + string, + oneOf, + any, +} = React.PropTypes; + +/** + * {"categories": ["visualizations", "chart primitives"]} + * + * Labels for axes. + */ +const AxisLabel = createClass({ + displayName: 'AxisLabel', + + statics: { + _lucidIsPrivate: true, + }, + + propTypes: { + /** + * Classes are appended to root element along with existing classes using + * the `classnames` library. + */ + className: any, + /** + * Height of the margin this label should fit into. + */ + height: number, + /** + * Width of the margin this label should fit into. + */ + width: number, + /** + * Zero-based color, defaults to -1 which is black. + */ + color: number, + /** + * Contents of the label, should only ever be a string since we use a `text` + * under the hood. + */ + label: string, + /** + * Determine orientation of the label. + */ + orient: oneOf(['top', 'bottom', 'left', 'right']), + }, + + getDefaultProps() { + return { + color: -1, + }; + }, + + render() { + const { + height, + width, + orient, + label, + color, + className, + ...passThroughs, + } = this.props; + + const isH = orient === 'top' || orient === 'bottom'; + + return ( + + {label} + + ); + } +}); + +export default AxisLabel; diff --git a/src/components/AxisLabel/AxisLabel.less b/src/components/AxisLabel/AxisLabel.less new file mode 100644 index 000000000..e199f4b84 --- /dev/null +++ b/src/components/AxisLabel/AxisLabel.less @@ -0,0 +1,11 @@ +.lucid-AxisLabel { + text-anchor: middle; + + &-color--1 { fill: @color-textColor; } + &-color-0 { fill: @color-chart-0; } + &-color-1 { fill: @color-chart-1; } + &-color-2 { fill: @color-chart-2; } + &-color-3 { fill: @color-chart-3; } + &-color-4 { fill: @color-chart-4; } + &-color-5 { fill: @color-chart-5; } +} diff --git a/src/components/AxisLabel/AxisLabel.spec.js b/src/components/AxisLabel/AxisLabel.spec.js new file mode 100644 index 000000000..435b68590 --- /dev/null +++ b/src/components/AxisLabel/AxisLabel.spec.js @@ -0,0 +1,7 @@ +import { common } from '../../util/generic-tests'; + +import AxisLabel from './AxisLabel'; + +describe('AxisLabel', () => { + common(AxisLabel); +}); diff --git a/src/components/AxisLabel/examples/basic.jsx b/src/components/AxisLabel/examples/basic.jsx new file mode 100644 index 000000000..26492bbfb --- /dev/null +++ b/src/components/AxisLabel/examples/basic.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import AxisLabel from '../AxisLabel'; + +const width = 1000; +const height = 400; +const margin = { top: 50, right: 50, bottom: 50, left: 50}; + +const innerWidth = width - margin.right - margin.left; +const innerHeight = height - margin.top - margin.bottom; + +export default React.createClass({ + render() { + return ( + + {/* dotted outline for the svg */} + + + {/* show the inner part of the chart */} + + + + + + + + + + + + + + + + + + + + + ); + } +}); + diff --git a/src/components/Bar/Bar.jsx b/src/components/Bar/Bar.jsx new file mode 100644 index 000000000..55bac8741 --- /dev/null +++ b/src/components/Bar/Bar.jsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { lucidClassNames } from '../../util/style-helpers'; +import { createClass } from '../../util/component-types'; + +const cx = lucidClassNames.bind('&-Bar'); + +const { + number, + bool, + any, +} = React.PropTypes; + +/** + * {"categories": ["visualizations", "geoms"]} + * + * Bars are typically used for bar charts. + * + */ +const Bar = createClass({ + displayName: 'Bar', + + statics: { + _lucidIsPrivate: true, + }, + + propTypes: { + /** + * Classes are appended to root element along with existing classes using + * the `classnames` library. + */ + className: any, + /** + * x coordinate. + */ + x: number, + /** + * y coordinate. + */ + y: number, + /** + * Height of the bar. + */ + height: number, + /** + * Width of the bar. + */ + width: number, + /** + * Determines if the bar has a white stroke around it. + */ + hasStroke: bool, + /** + * Zero-based set of colors. It's recommended that you pass the index of + * your array for colors. + */ + color: number, + }, + + getDefaultProps() { + return { + x: 0, + y: 0, + height: 0, + width: 0, + color: 0, + }; + }, + + render() { + const { + className, + color, + hasStroke, + height, + width, + x, + y, + ...passThroughs, + } = this.props; + + const colorIndex = color % 6; + + return ( + + ); + } +}); + +export default Bar; diff --git a/src/components/Bar/Bar.less b/src/components/Bar/Bar.less new file mode 100644 index 000000000..8c3997e40 --- /dev/null +++ b/src/components/Bar/Bar.less @@ -0,0 +1,13 @@ +.lucid-Bar { + &-has-stroke { + stroke: @color-white; + stroke-width: 2; + } + + &-color-0 { fill: @color-chart-0; } + &-color-1 { fill: @color-chart-1; } + &-color-2 { fill: @color-chart-2; } + &-color-3 { fill: @color-chart-3; } + &-color-4 { fill: @color-chart-4; } + &-color-5 { fill: @color-chart-5; } +} diff --git a/src/components/Bar/Bar.spec.js b/src/components/Bar/Bar.spec.js new file mode 100644 index 000000000..b98ddeb87 --- /dev/null +++ b/src/components/Bar/Bar.spec.js @@ -0,0 +1,7 @@ +import { common } from '../../util/generic-tests'; + +import Bar from './Bar'; + +describe('Bar', () => { + common(Bar); +}); diff --git a/src/components/Bar/examples/basic.jsx b/src/components/Bar/examples/basic.jsx new file mode 100644 index 000000000..b973662a7 --- /dev/null +++ b/src/components/Bar/examples/basic.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import Bar from '../Bar'; + +const svgProps = { + width: 20, + height: 100 +}; + +const pointProps = { + x: 5, + y: 0, + width: 10, + height: 100, +}; + +export default React.createClass({ + render() { + return ( +
+ + + + + + + + + + + + + + + + + + + +
+ ); + } +}); diff --git a/src/components/BarChart/BarChar.spec.jsx b/src/components/BarChart/BarChar.spec.jsx new file mode 100644 index 000000000..223268d77 --- /dev/null +++ b/src/components/BarChart/BarChar.spec.jsx @@ -0,0 +1,107 @@ +// Note: these tests are basically pin tests, given that we're rendering svgs, +// these tests serve to ensure that the rendered output is exactly at the +// author inteded. As a consequence, you may need to re-pin these tests if you +// change things. + +import React from 'react'; +import { mount } from 'enzyme'; +import { common } from '../../util/generic-tests'; +import describeWithDOM from '../../util/describe-with-dom'; +import assert from 'assert'; + +import BarChart from './BarChart'; + +describeWithDOM('BarChart', () => { + let wrapper; + + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); + + common(BarChart, { + exemptFunctionProps: [ + 'xAxisFormatter', + 'yAxisFormatter', + ], + getDefaultProps: () => ({ + data: [ + { x: 'Monday' , y: 1 , y2: 2} , + { x: 'Tuesday' , y: 4 , y2: 4 } , + { x: 'Wednesday' , y: 8 , y2: 1 } , + { x: 'Thursday' , y: 20 , y2: 15 } , + { x: 'Friday' , y: 10 , y2: 2 } , + ], + }) + }); + + describe('render', () => { + it('should render a basic chart', () => { + wrapper = mount( + + ); + + assert.equal(wrapper.find('.lucid-Bar').length, 5, 'did not find the correct number of bars'); + assert.equal(wrapper.find('.lucid-Axis').length, 2, 'did not find the correct number of axes'); + }); + + it('should render a chart with multiple series', () => { + wrapper = mount( + + ); + + assert.equal(wrapper.find('.lucid-Bar').length, 10, 'did not find the correct number of bars'); + }); + + it('should have the correct html', () => { + wrapper = mount( + 'x axis tick'} + + yAxisFields={['rev', 'imps']} + yAxisTitle='Metrics' + yAxisTickCount={4} + yAxisFormatter={() => 'y axis tick'} + /> + ); + + assert.equal(wrapper.html(), 'x axis tickx axis tickDatey axis ticky axis ticky axis ticky axis tickMetrics') + }); + }); +}); + diff --git a/src/components/BarChart/BarChart.jsx b/src/components/BarChart/BarChart.jsx new file mode 100644 index 000000000..3ceab96fc --- /dev/null +++ b/src/components/BarChart/BarChart.jsx @@ -0,0 +1,297 @@ +import _ from 'lodash'; +import React from 'react'; +import { lucidClassNames } from '../../util/style-helpers'; +import { createClass } from '../../util/component-types'; +import { + maxByFields, + maxByFieldsStacked, +} from '../../util/chart-helpers'; +import d3Scale from 'd3-scale'; + +import Axis from '../Axis/Axis'; +import AxisLabel from '../AxisLabel/AxisLabel'; +import Bars from '../Bars/Bars'; + +const cx = lucidClassNames.bind('&-BarChart'); + +const { + any, + arrayOf, + func, + number, + object, + shape, + string, + array, + bool, +} = React.PropTypes; + +/** + * {"categories": ["visualizations", "charts"]} + * + * Bar charts are great for showing data that fits neatly in to "buckets". The + * x axis data must be strings, and the y axis data must be numeric. + */ +const BarChart = createClass({ + displayName: 'BarChart', + + propTypes: { + /** + * Classes are appended to root element along with existing classes using + * the `classnames` library. + */ + className: any, + /** + * Height of the chart. + */ + height: number, + /** + * Width of the chart. + */ + width: number, + /** + * An object defining the margins of the chart. These margins typically + * contain the axis and labels. + */ + margin: shape({ + top: number, + right: number, + bottom: number, + left: number, + }), + /** + * Data for the chart. E.g. + * + * [ + * { x: 'Monday' , y: 1 } , + * { x: 'Tuesday' , y: 2 } , + * { x: 'Wednesday' , y: 3 } , + * { x: 'Thursday' , y: 2 } , + * { x: 'Friday' , y: 5 } , + * ] + */ + data: arrayOf(object).isRequired, + /** + * An object with human readable names for fields that will be used for + * tooltips and legends which are *not yet implemented*. E.g: + * + * { + * x: 'Revenue', + * y: 'Impressions', + * } + * + * legend: object, + */ + + + /** + * The field we should look up your x data by. Your actual x data must be + * strings. + */ + xAxisField: string, + /** + * There are some cases where you need to only show a "sampling" of ticks + * on the x axis. This number will control that. + */ + xAxisTickCount: number, + /** + * An optional function used to format your x axis data. If you don't + * provide anything, we'll use an identity function. + */ + xAxisFormatter: func, + /** + * Set a title for the x axis. + */ + xAxisTitle: string, + /** + * Set a color for the x axis title. This takes any number 0 or greater and + * it converts it to a color in our color palette. + */ + xAxisTitleColor: number, + + + /** + * An array of your y axis fields. Typically this will just be a single + * item unless you need to display grouped or stacked bars. + */ + yAxisFields: array.isRequired, + /** + * The minimum number the y axis should display. Typically this + * should be be `0`. + */ + yAxisMin: number, + /** + * The maximum number the y axis should display. This should almost always + * be the largest number from your dataset. + */ + yAxisMax: number, + /** + * An optional function used to format your y axis data. If you don't + * provide anything, we use the default D3 number formatter. + */ + yAxisFormatter: func, + /** + * Stack the y axis data instead of showing it as groups. This is only + * useful if you have multiple `yAxisFields`. Stacking will cause the chart + * to be aggregated by sum. + */ + yAxisIsStacked: bool, + /** + * There are some cases where you need to only show a "sampling" of ticks + * on the y axis. This number will control that. + */ + yAxisTickCount: number, + /** + * Set a title for the y axis. + */ + yAxisTitle: string, + /** + * Set a color for the y axis title. This takes any number 0 or greater and + * it converts it to a color in our color palette. + */ + yAxisTitleColor: number, + }, + + getDefaultProps() { + return { + height: 400, + width: 1000, + margin: { + top: 10, + right: 20, + bottom: 50, + left: 80, + }, + + xAxisField: 'x', + xAxisTickCount: null, + xAxisTitle: null, + xAxisTitleColor: -1, + + yAxisFields: ['y'], + yAxisTickCount: null, + yAxisIsStacked: false, + yAxisMin: 0, + yAxisTitle: null, + yAxisTitleColor: -1, + }; + }, + + render() { + const { + className, + height, + width, + margin, + data, + + xAxisField, + xAxisFormatter, + xAxisTitle, + xAxisTitleColor, + xAxisTickCount, + + yAxisFields, + yAxisFormatter, + yAxisTitle, + yAxisTitleColor, + yAxisIsStacked, + yAxisTickCount, + yAxisMin, + yAxisMax = yAxisIsStacked + ? maxByFieldsStacked(data, yAxisFields) + : maxByFields(data, yAxisFields), + + ...passThroughs, + } = this.props; + + if (_.isEmpty(data)) { + return null; + } + + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const paddingInner = yAxisFields.length > 1 ? 0.3 : 0.05; + + const xScale = d3Scale.scaleBand() + .domain(_.map(data, xAxisField)) + .range([0, innerWidth]) + .paddingInner(paddingInner) + .paddingOuter(0.5); + + const yScale = d3Scale.scaleLinear() + .domain([yAxisMin, yAxisMax]) + .range([innerHeight, 0]); + + return ( + + {/* x axis */} + + + + + {/* x axis title */} + + {xAxisTitle ? ( + + ) : null} + + + {/* y axis */} + + + + + {/* y axis title */} + + {yAxisTitle ? ( + + ) : null} + + + {/* bars */} + + + ); + } +}); + +export default BarChart; diff --git a/src/components/BarChart/examples/1.basic.jsx b/src/components/BarChart/examples/1.basic.jsx new file mode 100644 index 000000000..684002b73 --- /dev/null +++ b/src/components/BarChart/examples/1.basic.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import BarChart from '../BarChart'; + +const data = [ + { x: '2015-01-01', y: 1 }, + { x: '2015-01-02', y: 2 }, + { x: '2015-01-03', y: 3 }, + { x: '2015-01-04', y: 5 }, +]; + +export default React.createClass({ + render() { + return ( +
+ +
+ ); + } +}); diff --git a/src/components/BarChart/examples/2.limited-ticks.jsx b/src/components/BarChart/examples/2.limited-ticks.jsx new file mode 100644 index 000000000..4b8eadfe5 --- /dev/null +++ b/src/components/BarChart/examples/2.limited-ticks.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import BarChart from '../BarChart'; + +const data = [ + { x: '2015-01-01', y: 1 }, + { x: '2015-01-02', y: 2 }, + { x: '2015-01-03', y: 3 }, + { x: '2015-01-04', y: 5 }, + { x: '2015-01-05', y: 2 }, + { x: '2015-01-06', y: 3 }, + { x: '2015-01-07', y: 2 }, + { x: '2015-01-08', y: 2 }, + { x: '2015-01-09', y: 5 }, + { x: '2015-01-10', y: 3 }, + { x: '2015-01-11', y: 4 }, + { x: '2015-01-12', y: 4 }, + { x: '2015-01-13', y: 5 }, + { x: '2015-01-14', y: 3 }, + { x: '2015-01-15', y: 4 }, + { x: '2015-01-16', y: 3 }, + { x: '2015-01-17', y: 6 }, +]; + +export default React.createClass({ + render() { + return ( +
+ +
+ ); + } +}); diff --git a/src/components/BarChart/examples/3.grouped.jsx b/src/components/BarChart/examples/3.grouped.jsx new file mode 100644 index 000000000..75857e83b --- /dev/null +++ b/src/components/BarChart/examples/3.grouped.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import BarChart from '../BarChart'; + +const data = [ + { x: 'Monday', apples: 10, pears: 20, peaches: 35 }, + { x: 'Tuesday', apples: 20, pears: 5, peaches: 20 }, + { x: 'Wednesday', apples: 5, pears: 15, peaches: 5 }, +]; + +export default React.createClass({ + render() { + return ( +
+ +
+ ); + } +}); diff --git a/src/components/BarChart/examples/4.stacked.jsx b/src/components/BarChart/examples/4.stacked.jsx new file mode 100644 index 000000000..2adcaf0ac --- /dev/null +++ b/src/components/BarChart/examples/4.stacked.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import BarChart from '../BarChart'; + +const data = [ + { x: 'Monday', apples: 10, pears: 20, peaches: 35 }, + { x: 'Tuesday', apples: 20, pears: 5, peaches: 20 }, + { x: 'Wednesday', apples: 5, pears: 15, peaches: 5 }, +]; + +export default React.createClass({ + render() { + return ( +
+ +
+ ); + } +}); diff --git a/src/components/BarChart/examples/5.all-the-things.jsx b/src/components/BarChart/examples/5.all-the-things.jsx new file mode 100644 index 000000000..1d842ce7a --- /dev/null +++ b/src/components/BarChart/examples/5.all-the-things.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import BarChart from '../BarChart'; + +const data = [ + { day: 'monday' , apples: 2000 , oranges: 3000 } , + { day: 'tuesday' , apples: 2000 , oranges: 5000 } , + { day: 'wednesday' , apples: 3000 , oranges: 2000 } , + { day: 'thursday' , apples: 5000 , oranges: 6000 } , +]; +const yFormatter = (d) => `${d / 1000}k`; +const xFormatter = (d) => d.toUpperCase().slice(0, 3); + +export default React.createClass({ + render() { + return ( + + ); + } +}); diff --git a/src/components/Bars/Bars.jsx b/src/components/Bars/Bars.jsx new file mode 100644 index 000000000..d80781e60 --- /dev/null +++ b/src/components/Bars/Bars.jsx @@ -0,0 +1,190 @@ +import _ from 'lodash'; +import React from 'react'; +import { lucidClassNames } from '../../util/style-helpers'; +import { groupByFields } from '../../util/chart-helpers'; +import { createClass } from '../../util/component-types'; +import d3Scale from 'd3-scale'; +import d3Shape from 'd3-shape'; + +import Bar from '../Bar/Bar'; + +const cx = lucidClassNames.bind('&-Bars'); + +const { + any, + arrayOf, + func, + number, + object, + bool, + string, +} = React.PropTypes; + +/** + * {"categories": ["visualizations", "chart primitives"]} + * + * Bars for your rectangular viewing pleasure. + */ +const Bars = createClass({ + displayName: 'Bars', + + statics: { + _lucidIsPrivate: true, + }, + + propTypes: { + /** + * Classes are appended to root element along with existing classes using + * the `classnames` library. + */ + className: any, + /** + * Top position + */ + top: number, + /** + * Left position + */ + left: number, + /** + * De-normalized data + * + * ``` + * [ + * { x: 'one', y0: 1, y1: 2, y2: 3, y3: 5 }, + * { x: 'two', y0: 2, y1: 3, y2: 4, y3: 6 }, + * { x: 'three', y0: 2, y1: 4, y2: 5, y3: 6 }, + * { x: 'four', y0: 3, y1: 6, y2: 7, y3: 7 }, + * { x: 'five', y0: 4, y1: 8, y2: 9, y3: 8 }, + * ] + * ``` + */ + data: arrayOf(object).isRequired, + /** + * The scale for the x axis. This must be a d3-scale scale. + */ + xScale: func.isRequired, + /** + * The scale for the y axis. This must be a d3-scale scale. + */ + yScale: func.isRequired, + /** + * The field we should look up your x data by. + */ + xField: string, + /** + * The field(s) we should look up your y data by. Each entry represents a + * series. Your actual y data should be numeric. + */ + yFields: arrayOf(string), + /** + * This will stack the data instead of grouping it. In order to stack the + * data we have to calculate a new domain for the y scale that is based on + * the `sum` of the data. + */ + isStacked: bool, + /** + * Sometimes you might not want the colors to start rotating at the blue + * color, this number will be added the bar index in determining which + * color the bars are. + */ + colorOffset: number, + }, + + getDefaultProps() { + return { + top: 0, + left: 0, + xField: 'x', + yFields: ['y'], + isStacked: false, + colorOffset: 0, + }; + }, + + render() { + const { + className, + data, + left, + top, + xScale, + xField, + yScale: yScaleOriginal, + yFields, + isStacked, + ...passThroughs, + } = this.props; + + // Copy the original so we can mutate it + let yScale = yScaleOriginal.copy(); + + // If we are stacked, we need to calculate a new domain based on the sum of + // the various series' y data. One row per series. + const transformedData = isStacked + ? d3Shape.stack().keys(yFields)(data) + : groupByFields(data, yFields); + + // If we are stacked, we need to calculate a new domain based on the sum of + // the various group's y data + if (isStacked) { + yScale.domain([ + yScale.domain()[0], + _.chain(transformedData).last().flatten().max().value() + ]); + } + + const yScaleHeight = _.max(yScale.range()); + + return ( + + {_.map(transformedData, (d, dIndex) => { + // TODO: this could probably be cleaned and DRY'd up. It's a bit odd + // to have an if statement in the middle of the jsx. + if (isStacked) { + return ( + + {_.map(d, (series, seriesIndex) => ( + + ))} + + ); + } else { + const innerXScale = d3Scale.scaleBand() + .domain(_.times(yFields.length)) + .range([0, xScale.bandwidth()]) + .round(true); + + return ( + + {_.map(d, (y, yIndex) => ( + + ))} + + ); + } + })} + + ); + } +}); + +export default Bars; diff --git a/src/components/Bars/Bars.spec.js b/src/components/Bars/Bars.spec.js new file mode 100644 index 000000000..406219766 --- /dev/null +++ b/src/components/Bars/Bars.spec.js @@ -0,0 +1,18 @@ +import d3Scale from 'd3-scale'; +import { common } from '../../util/generic-tests'; + +import Bars from './Bars'; + +describe('Bars', () => { + common(Bars, { + exemptFunctionProps: [ + 'xScale', + 'yScale', + ], + getDefaultProps: () => ({ + data: [{x: 'one', y: 2}], + xScale: d3Scale.scaleBand(), + yScale: d3Scale.scaleLinear(), + }) + }); +}); diff --git a/src/components/Bars/examples/basic.jsx b/src/components/Bars/examples/basic.jsx new file mode 100644 index 000000000..deed53c34 --- /dev/null +++ b/src/components/Bars/examples/basic.jsx @@ -0,0 +1,58 @@ +import _ from 'lodash'; +import React from 'react'; +import Bars from '../Bars'; +import d3Scale from 'd3-scale'; + +const width = 1000; +const height = 400; + +const data = [ + { x: 'one', y0: 1, y1: 2, y2: 3, y3: 5 }, + { x: 'two', y0: 2, y1: 3, y2: 4, y3: 6 }, + { x: 'three', y0: 2, y1: 4, y2: 5, y3: 6 }, + { x: 'four', y0: 3, y1: 6, y2: 7, y3: 7 }, + { x: 'five', y0: 4, y1: 8, y2: 9, y3: 8 }, + { x: 'six', y0: 20, y1: 8, y2: 9, y3: 1 }, +]; + +const yFields = ['y0', 'y1', 'y2', 'y3']; +const yMax = _.max(_.reduce(yFields, (acc, field) => { + return acc.concat(_.map(data, field)); +}, [])); + +const xScale = d3Scale.scaleBand() + .domain(_.map(data, 'x')) + .range([0, width]) + .round(true) + .paddingInner(0.1); + +const yScale = d3Scale.scaleLinear() + .domain([0, yMax]) + .range([height, 0]); + +export default React.createClass({ + render() { + return ( +
+ + + + + + + +
+ ); + } +}); diff --git a/src/components/Line/Line.jsx b/src/components/Line/Line.jsx new file mode 100644 index 000000000..6817cd6fe --- /dev/null +++ b/src/components/Line/Line.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { lucidClassNames } from '../../util/style-helpers'; +import { createClass } from '../../util/component-types'; + +const cx = lucidClassNames.bind('&-Line'); + +const { + number, + string, + any, +} = React.PropTypes; + +/** + * {"categories": ["visualizations", "geoms"]} + * + * Lines are great. If I told you they aren't, I'd by li'n. + * + */ +const Line = createClass({ + displayName: 'Line', + + statics: { + _lucidIsPrivate: true, + }, + + propTypes: { + /** + * Classes are appended to root element along with existing classes using + * the `classnames` library. + */ + className: any, + /** + * The path for the line. + */ + d: string, + /** + * Zero-based set of colors. It's recommended that you pass the index of + * your array for colors. + */ + color: number, + }, + + getDefaultProps() { + return { + color: 0, + }; + }, + + render() { + const { + className, + color, + d, + ...passThroughs, + } = this.props; + + const colorIndex = color % 6; + + return ( + + ); + } +}); + +export default Line; diff --git a/src/components/Line/Line.less b/src/components/Line/Line.less new file mode 100644 index 000000000..21af5116a --- /dev/null +++ b/src/components/Line/Line.less @@ -0,0 +1,11 @@ +.lucid-Line { + stroke: @color-chart-0; + stroke-width: 2; + + &-color-0 { stroke: @color-chart-0; fill: @color-chart-0; } + &-color-1 { stroke: @color-chart-1; fill: @color-chart-1; } + &-color-2 { stroke: @color-chart-2; fill: @color-chart-2; } + &-color-3 { stroke: @color-chart-3; fill: @color-chart-3; } + &-color-4 { stroke: @color-chart-4; fill: @color-chart-4; } + &-color-5 { stroke: @color-chart-5; fill: @color-chart-5; } +} diff --git a/src/components/Line/Line.spec.js b/src/components/Line/Line.spec.js new file mode 100644 index 000000000..028882dee --- /dev/null +++ b/src/components/Line/Line.spec.js @@ -0,0 +1,7 @@ +import { common } from '../../util/generic-tests'; + +import Line from './Line'; + +describe('Line', () => { + common(Line); +}); diff --git a/src/components/Line/examples/basic.jsx b/src/components/Line/examples/basic.jsx new file mode 100644 index 000000000..e8cd7afa0 --- /dev/null +++ b/src/components/Line/examples/basic.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import Line from '../Line'; + +export default React.createClass({ + render() { + return ( +
+ + + + + + + + +
+ ); + } +}); diff --git a/src/components/LineChart/LineChart.jsx b/src/components/LineChart/LineChart.jsx new file mode 100644 index 000000000..8a8a47484 --- /dev/null +++ b/src/components/LineChart/LineChart.jsx @@ -0,0 +1,468 @@ +import _ from 'lodash'; +import React from 'react'; +import { lucidClassNames } from '../../util/style-helpers'; +import { createClass } from '../../util/component-types'; +import { + minByFields, + maxByFields, + maxByFieldsStacked, + formatDate, +} from '../../util/chart-helpers'; +import d3Scale from 'd3-scale'; + +import Axis from '../Axis/Axis'; +import AxisLabel from '../AxisLabel/AxisLabel'; +import Lines from '../Lines/Lines'; +import Points from '../Points/Points'; + +const cx = lucidClassNames.bind('&-LineChart'); + +const { + any, + arrayOf, + func, + instanceOf, + number, + object, + shape, + string, + bool, +} = React.PropTypes; + +/** + * {"categories": ["visualizations", "charts"]} + * + * The line chart presents data over time. Currently only dates are supported + * on the x axis and numeric values on the y. If you need discrete values on + * the x axis, consider using the `BarChart` instead. + */ +const LineChart = createClass({ + displayName: 'LineChart', + + propTypes: { + /** + * Classes are appended to root element along with existing classes using + * the `classnames` library. + */ + className: any, + /** + * Height of the chart. + */ + height: number, + /** + * Width of the chart. + */ + width: number, + /** + * An object defining the margins of the chart. These margins will contain + * the axis and labels. + */ + margin: shape({ + top: number, + right: number, + bottom: number, + left: number, + }), + /** + * Data for the chart. E.g. + * + * [ + * { x: new Date('2015-01-01') , y: 1 } , + * { x: new Date('2015-01-02') , y: 2 } , + * { x: new Date('2015-01-03') , y: 3 } , + * { x: new Date('2015-01-04') , y: 2 } , + * { x: new Date('2015-01-05') , y: 5 } , + * ] + */ + data: arrayOf(object).isRequired, + /** + * An object with human readable names for fields that will be used for + * tooltips and legends which are *not yet implemented*. E.g: + * + * { + * x: 'Revenue', + * y: 'Impressions', + * } + * + * legend: object, + */ + + + /** + * The field we should look up your x data by. The data must be valid + * javascript dates. + */ + xAxisField: string, + /** + * The minimum date the x axis should display. Typically this will be the + * smallest items from your dataset. + */ + xAxisMin: instanceOf(Date), + /** + * The maximum date the x axis should display. This should almost always be + * the largest date from your dataset. + */ + xAxisMax: instanceOf(Date), + /** + * An optional function used to format your x axis data. If you don't + * provide anything, we use the default D3 date variable formatter. + */ + xAxisFormatter: func, + /** + * There are some cases where you need to only show a "sampling" of ticks + * on the x axis. This number will control that. + */ + xAxisTickCount: number, + /** + * Set a title for the x axis. + */ + xAxisTitle: string, + /** + * Set a color for the x axis title. This takes any number 0 or greater and + * it converts it to a color in our color palette. + */ + xAxisTitleColor: number, + + + /** + * An array of your y axis fields. Typically this will just be a single + * item unless you need to display multiple lines. + */ + yAxisFields: arrayOf(string), + /** + * The minimum number the y axis should display. Typically this should be + * `0`. + */ + yAxisMin: number, + /** + * The maximum number the y axis should display. This should almost always + * be the largest number from your dataset. + */ + yAxisMax: number, + /** + * An optional function used to format your y axis data. If you don't + * provide anything, we use the default D3 formatter. + */ + yAxisFormatter: func, + /** + * Stack the y axis data. This is only useful if you have multiple + * `yAxisFields`. Stacking will cause the chart to be aggregated by sum. + */ + yAxisIsStacked: bool, + /** + * Display points along with the y axis lines. + */ + yAxisHasPoints: bool, + /** + * There are some cases where you need to only show a "sampling" of ticks + * on the y axis. This number will control that. + */ + yAxisTickCount: number, + /** + * Set a title for the y axis. + */ + yAxisTitle: string, + /** + * Set a color for the y axis title. This takes any number 0 or greater and + * it converts it to a color in our color palette. + */ + yAxisTitleColor: number, + + + /** + * An array of your y2 axis fields. Typically this will just be a single + * item unless you need to display multiple lines. + */ + y2AxisFields: arrayOf(string), + /** + * The minimum number the y2 axis should display. Typically this should be + * `0`. + */ + y2AxisMin: number, + /** + * The maximum number the y2 axis should display. This should almost always + * be the largest number from your dataset. + */ + y2AxisMax: number, + /** + * An optional function used to format your y2 axis data. If you don't + * provide anything, we use the default D3 formatter. + */ + y2AxisFormatter: func, + /** + * Stack the y2 axis data. This is only useful if you have multiple + * `y2AxisFields`. Stacking will cause the chart to be aggregated by sum. + */ + y2AxisIsStacked: bool, + /** + * Display points along with the y2 axis lines. + */ + y2AxisHasPoints: bool, + /** + * There are some cases where you need to only show a "sampling" of ticks + * on the y2 axis. This number will control that. + */ + y2AxisTickCount: number, + /** + * Set a title for the y2 axis. + */ + y2AxisTitle: string, + /** + * Set a color for the y2 axis title. This takes any number 0 or greater and + * it converts it to a color in our color palette. + */ + y2AxisTitleColor: number, + }, + + getDefaultProps() { + return { + height: 400, + width: 1000, + margin: { + top: 10, + right: 80, + bottom: 65, + left: 80, + }, + + xAxisField: 'x', + xAxisTickCount: null, + xAxisFormatter: undefined, // purposefully done, see Axis.jsx + xAxisTitle: null, + xAxisTitleColor: -1, + + yAxisFields: ['y'], + yAxisIsStacked: false, + yAxisMin: 0, + yAxisHasPoints: true, + yAxisTickCount: null, + yAxisFormatter: undefined, // purposefully done, see Axis.jsx + yAxisTitle: null, + yAxisTitleColor: -1, + + y2AxisFields: null, + y2AxisIsStacked: false, + y2AxisHasPoints: true, + y2AxisMin: 0, + y2AxisTickCount: null, + y2AxisFormatter: undefined, // purposefully done, see Axis.jsx + y2AxisTitle: null, + y2AxisTitleColor: -1, + }; + }, + + render() { + const { + className, + height, + width, + margin, + data, + + xAxisField, + xAxisTickCount, + xAxisTitle, + xAxisTitleColor, + xAxisFormatter = formatDate, + xAxisMin = minByFields(data, xAxisField), + xAxisMax = maxByFields(data, xAxisField), + + yAxisFields, + yAxisFormatter, + yAxisHasPoints, + yAxisIsStacked, + yAxisTickCount, + yAxisTitle, + yAxisTitleColor, + yAxisMin, + yAxisMax = yAxisIsStacked + ? maxByFieldsStacked(data, yAxisFields) + : maxByFields(data, yAxisFields), + + y2AxisFields, + y2AxisFormatter, + y2AxisHasPoints, + y2AxisIsStacked, + y2AxisTickCount, + y2AxisTitle, + y2AxisTitleColor, + y2AxisMin, + y2AxisMax = y2AxisFields && y2AxisIsStacked + ? maxByFieldsStacked(data, y2AxisFields) + : maxByFields(data, y2AxisFields), + + ...passThroughs, + } = this.props; + + // TODO: Consider displaying something specific when there is no data, + // perhaps a loading indicator. + if (_.isEmpty(data)) { + return null; + } + + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const xScale = d3Scale.scaleTime() + .domain([xAxisMin, xAxisMax]) + .range([0, innerWidth]); + + const yScale = d3Scale.scaleLinear() + .domain([yAxisMin, yAxisMax]) + .range([innerHeight, 0]); + + const y2Scale = y2AxisFields + ? d3Scale.scaleLinear().domain([y2AxisMin, y2AxisMax]).range([innerHeight, 0]) + : null; + + return ( + + {/* x axis */} + + + + + {/* x axis title */} + + {xAxisTitle ? ( + + ) : null} + + + {/* y axis */} + + + + + {/* y axis title */} + + {yAxisTitle ? ( + + ) : null} + + + {/* y2 axis */} + {y2AxisFields ? + + + + : null} + + {/* y2 axis title */} + + {y2AxisTitle ? ( + + ) : null} + + + {/* y axis lines */} + + + {/* y axis points */} + {yAxisHasPoints ? + + : null} + + {/* y2 axis lines */} + {y2AxisFields ? + + : null} + + {/* y2 axis points */} + {y2AxisFields && y2AxisHasPoints ? + + : null} + + ); + } +}); + +export default LineChart; + diff --git a/src/components/LineChart/LineChart.spec.jsx b/src/components/LineChart/LineChart.spec.jsx new file mode 100644 index 000000000..4411b77ea --- /dev/null +++ b/src/components/LineChart/LineChart.spec.jsx @@ -0,0 +1,120 @@ +// Note: these tests are basically pin tests, given that we're rendering svgs, +// these tests serve to ensure that the rendered output is exactly at the +// author inteded. As a consequence, you may need to re-pin these tests if you +// change things. + +import React from 'react'; +import { mount } from 'enzyme'; +import { common } from '../../util/generic-tests'; +import describeWithDOM from '../../util/describe-with-dom'; +import assert from 'assert'; + +import LineChart from './LineChart'; + +describeWithDOM('LineChart', () => { + let wrapper; + + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); + + common(LineChart, { + exemptFunctionProps: [ + 'xAxisFormatter', + 'yAxisFormatter', + 'y2AxisFormatter', + ], + getDefaultProps: () => ({ + data: [ + {x: new Date('2015-01-01T00:00:00Z'), y: 1}, + {x: new Date('2015-01-01T00:00:00Z'), y: 2}, + ] + }) + }); + + describe('render', () => { + it('should render a single axis chart', () => { + wrapper = mount( + + ); + + assert.equal(wrapper.find('.lucid-Point').length, 3, 'did not find the correct number of points'); + }); + + it('should render a single axis chart with multiple series', () => { + wrapper = mount( + + ); + + assert.equal(wrapper.find('.lucid-Line').length, 2, 'did not find the correct number of lines'); + }); + + it('should render a dual axis chart', () => { + wrapper = mount( + + ); + + assert.equal(wrapper.find('.lucid-Axis').length, 3, 'did not find the correct number of axes'); + }); + + it('should have the correct html', () => { + wrapper = mount( + 'x axis tick'} + + yAxisFields={['ctr']} + yAxisTitle='Click Through Rate' + yAxisTickCount={5} + yAxisFormatter={() => 'y axis tick'} + + y2AxisFields={['imps']} + y2AxisTitle='Impressions' + y2AxisTickCount={2} + y2AxisFormatter={() => 'y2 axis tick'} + /> + ); + + assert.equal(wrapper.html(), 'x axis tickx axis tickx axis tickx axis tickx axis tickx axis tickx axis tickx axis tickx axis tickDatey axis ticky axis ticky axis ticky axis ticky axis tickClick Through Ratey2 axis ticky2 axis tickImpressions'); + }); + }); +}); diff --git a/src/components/LineChart/examples/1.basic.jsx b/src/components/LineChart/examples/1.basic.jsx new file mode 100644 index 000000000..bb23b1c84 --- /dev/null +++ b/src/components/LineChart/examples/1.basic.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import LineChart from '../LineChart'; + +const data = [ + { x: new Date('2015-01-01T00:00:00-08:00'), y: 1 }, + { x: new Date('2015-01-02T00:00:00-08:00'), y: 2 }, + { x: new Date('2015-01-03T00:00:00-08:00'), y: 3 }, + { x: new Date('2015-01-04T00:00:00-08:00'), y: 5 }, +]; + +export default React.createClass({ + render() { + return ( + + ); + } +}); diff --git a/src/components/LineChart/examples/2.multi.jsx b/src/components/LineChart/examples/2.multi.jsx new file mode 100644 index 000000000..b0754fede --- /dev/null +++ b/src/components/LineChart/examples/2.multi.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import LineChart from '../LineChart'; + +const data = [ + { x: new Date('2015-01-01T00:00:00-08:00'), apples: 2, oranges: 3, pears: 1 }, + { x: new Date('2015-01-02T00:00:00-08:00'), apples: 2, oranges: 5, pears: 6 }, + { x: new Date('2015-01-03T00:00:00-08:00'), apples: 3, oranges: 2, pears: 4 }, + { x: new Date('2015-01-04T00:00:00-08:00'), apples: 5, oranges: 6, pears: 1 }, +]; + +export default React.createClass({ + render() { + return ( + + ); + } +}); diff --git a/src/components/LineChart/examples/3.dual-axis.jsx b/src/components/LineChart/examples/3.dual-axis.jsx new file mode 100644 index 000000000..435c12637 --- /dev/null +++ b/src/components/LineChart/examples/3.dual-axis.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import LineChart from '../LineChart'; + +const data = [ + { x: new Date('2015-01-01T00:00:00-08:00'), apples: 2, oranges: 8}, + { x: new Date('2015-03-02T00:00:00-08:00'), apples: 2, oranges: 5}, + { x: new Date('2015-05-03T00:00:00-08:00'), apples: 3, oranges: 5}, + { x: new Date('2015-07-04T00:00:00-08:00'), apples: 5, oranges: 6}, +]; + +export default React.createClass({ + render() { + return ( + + ); + } +}); diff --git a/src/components/LineChart/examples/4.stacked.jsx b/src/components/LineChart/examples/4.stacked.jsx new file mode 100644 index 000000000..0454f4fb1 --- /dev/null +++ b/src/components/LineChart/examples/4.stacked.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import LineChart from '../LineChart'; + +const data = [ + { x: new Date('2015-01-01T00:00:00-08:00'), apples: 2, oranges: 3, pears: 1 }, + { x: new Date('2015-01-02T00:00:00-08:00'), apples: 2, oranges: 5, pears: 6 }, + { x: new Date('2015-01-03T00:00:00-08:00'), apples: 3, oranges: 2, pears: 4 }, + { x: new Date('2015-01-04T00:00:00-08:00'), apples: 5, oranges: 6, pears: 1 }, + { x: new Date('2015-01-05T00:00:00-08:00'), apples: 4, oranges: 3, pears: 2 }, + { x: new Date('2015-01-06T00:00:00-08:00'), apples: 3, oranges: 4, pears: 4 }, +]; + +export default React.createClass({ + render() { + return ( + + ); + } +}); diff --git a/src/components/LineChart/examples/5.all-the-things.jsx b/src/components/LineChart/examples/5.all-the-things.jsx new file mode 100644 index 000000000..c9c8bff39 --- /dev/null +++ b/src/components/LineChart/examples/5.all-the-things.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import LineChart from '../LineChart'; + +const data = [ + { date: new Date('2015-01-01T00:00:00-08:00'), apples: 2000, oranges: 3000 }, + { date: new Date('2015-01-02T00:00:00-08:00'), apples: 2000, oranges: 5000 }, + { date: new Date('2015-01-03T00:00:00-08:00'), apples: 3000, oranges: 2000 }, + { date: new Date('2015-01-04T00:00:00-08:00'), apples: 5000, }, + { date: new Date('2015-01-05T00:00:00-08:00'), apples: 2500, oranges: 6300 }, + { date: new Date('2015-01-06T00:00:00-08:00'), apples: 1500, oranges: 6100 }, +]; +const yFormatter = (d) => `${d / 1000}k`; +const xFormatter = (d) => `${d.getMonth() + 1}-${d.getDate()}`; + +export default React.createClass({ + render() { + return ( + + ); + } +}); diff --git a/src/components/Lines/Lines.jsx b/src/components/Lines/Lines.jsx new file mode 100644 index 000000000..bd13c178d --- /dev/null +++ b/src/components/Lines/Lines.jsx @@ -0,0 +1,179 @@ +import _ from 'lodash'; +import React from 'react'; +import { lucidClassNames } from '../../util/style-helpers'; +import { createClass } from '../../util/component-types'; +import { groupByFields } from '../../util/chart-helpers'; +import d3Shape from 'd3-shape'; + +import Line from '../Line/Line'; + +const cx = lucidClassNames.bind('&-Lines'); + +const { + any, + arrayOf, + func, + number, + object, + bool, + string, +} = React.PropTypes; + +/** + * {"categories": ["visualizations", "chart primitives"]} + * + * Such lines. Much wow. + */ +const Lines = createClass({ + displayName: 'Lines', + + statics: { + _lucidIsPrivate: true, + }, + + propTypes: { + /** + * Classes are appended to root element along with existing classes using + * the `classnames` library. + */ + className: any, + /** + * Top + */ + top: number, + /** + * Left + */ + left: number, + /** + * De-normalized data, e.g. + * + * + * [ + * { x: 'one' , y: 1 }, + * { x: 'two' , y: 2 }, + * { x: 'three' , y: 2 }, + * { x: 'four' , y: 3 }, + * { x: 'five' , y: 4 }, + * ] + * + * + * Or (be sure to set `yFields` to `['y0', 'y1', 'y2', 'y3']`) + * + * [ + * { x: 'one' , y0: 1 , y1: 2 , y2: 3 , y3: 5 }, + * { x: 'two' , y0: 2 , y1: 3 , y2: 4 , y3: 6 }, + * { x: 'three' , y0: 2 , y1: 4 , y2: 5 , y3: 6 }, + * { x: 'four' , y0: 3 , y1: 6 , y2: 7 , y3: 7 }, + * { x: 'five' , y0: 4 , y1: 8 , y2: 9 , y3: 8 }, + * { x: 'six' , y0: 20 , y1: 8 , y2: 9 , y3: 1 }, + * ] + * + */ + data: arrayOf(object).isRequired, + /** + * The scale for the x axis. This must be a d3-scale scale. + */ + xScale: func.isRequired, + /** + * The scale for the y axis. This must be a d3-scale scale. + */ + yScale: func.isRequired, + /** + * The field we should look up your x data by. + */ + xField: string, + /** + * The field(s) we should look up your y data by. Each entry represents a + * series. Your actual y data should be numeric. + */ + yFields: arrayOf(string), + /** + * This will stack the data instead of grouping it. In order to stack the + * data we have to calculate a new domain for the y scale that is based on + * the `sum` of the data. + */ + isStacked: bool, + /** + * Sometimes you might not want the colors to start rotating at the blue + * color, this number will be added the line index in determining which + * color the lines are. + */ + colorOffset: number, + }, + + getDefaultProps() { + return { + top: 0, + left: 0, + xField: 'x', + yFields: ['y'], + isStacked: false, + colorOffset: 0, + }; + }, + + render() { + const { + className, + data, + isStacked, + left, + top, + colorOffset, + xScale, + xField, + yFields, + yScale: yScaleOriginal, + ...passThroughs, + } = this.props; + + // Copy the original so we can mutate it + let yScale = yScaleOriginal.copy(); + + // If we are stacked, we need to calculate a new domain based on the sum of + // the various series' y data. One row per series. + const transformedData = isStacked + ? d3Shape.stack().keys(yFields)(data) + : groupByFields(data, yFields); + + const area = isStacked + ? d3Shape.area() + .defined((a) => _.isFinite(a[0]) && _.isFinite(a[1])) + .x((a, i) => xScale(data[i][xField])) + .y0((a) => yScale(a[1])) + .y1((a) => yScale(a[0])) + : d3Shape.area() + .defined(_.isFinite) + .x((a, i) => xScale(data[i][xField])) + .y((a) => yScale(a)); + + // If we are stacked, we need to calculate a new domain based on the sum of + // the various group's y data + if (isStacked) { + yScale.domain([ + yScale.domain()[0], // only stacks well if this is `0` + _.chain(transformedData).last().flatten().max().value() + ]); + } + + return ( + + {_.map(transformedData, (d, dIndex) => ( + + + + ))} + + ); + } +}); + +export default Lines; diff --git a/src/components/Lines/Lines.spec.js b/src/components/Lines/Lines.spec.js new file mode 100644 index 000000000..e44345723 --- /dev/null +++ b/src/components/Lines/Lines.spec.js @@ -0,0 +1,18 @@ +import d3Scale from 'd3-scale'; +import { common } from '../../util/generic-tests'; + +import Lines from './Lines'; + +describe('Lines', () => { + common(Lines, { + exemptFunctionProps: [ + 'xScale', + 'yScale', + ], + getDefaultProps: () => ({ + data: [{x: new Date('2015-01-01T00:00:00Z'), y: 1}], + xScale: d3Scale.scaleTime(), + yScale: d3Scale.scaleLinear(), + }), + }); +}); diff --git a/src/components/Lines/examples/stacked.jsx b/src/components/Lines/examples/stacked.jsx new file mode 100644 index 000000000..8c2a4b988 --- /dev/null +++ b/src/components/Lines/examples/stacked.jsx @@ -0,0 +1,56 @@ +import _ from 'lodash'; +import React from 'react'; +import Lines from '../Lines'; +import d3Scale from 'd3-scale'; + +const width = 1000; +const height = 400; + +const data = [ + { x: 'one', y0: 1, y1: 2, y2: 3, y3: 5 }, + { x: 'two', y0: 2, y1: 3, y2: 4, y3: 6 }, + { x: 'three', y0: 2, y1: 4, y2: 5, y3: 6 }, + { x: 'four', y0: 3, y1: 6, y2: 7, y3: 7 }, + { x: 'five', y0: 4, y1: 8, y2: 9, y3: 8 }, + { x: 'six', y0: 20, y1: 8, y2: 9, y3: 1 }, +]; + +const yFields = ['y0', 'y1', 'y2', 'y3']; +const yMax = _.max(_.reduce(yFields, (acc, field) => { + return acc.concat(_.map(data, field)); +}, [])); + +const xScale = d3Scale.scalePoint() + .domain(_.map(data, 'x')) + .range([0, width]); + +const yScale = d3Scale.scaleLinear() + .domain([0, yMax]) + .range([height, 0]); + +export default React.createClass({ + render() { + return ( +
+ + + + + + + +
+ ); + } +}); diff --git a/src/components/Point/Point.jsx b/src/components/Point/Point.jsx new file mode 100644 index 000000000..26c1f5f46 --- /dev/null +++ b/src/components/Point/Point.jsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { lucidClassNames } from '../../util/style-helpers'; +import { createClass } from '../../util/component-types'; +import { transformFromCenter } from '../../util/chart-helpers'; + +const cx = lucidClassNames.bind('&-Point'); + +const { + number, + any, + bool, +} = React.PropTypes; + +// These were originally built in a 12x12 grid, except triangles which were +// 14x12 cause triangles are poo. +const PATHS = [ + 'M6,12 C2.686,12 0,9.314 0,6 C0,2.686 2.686,0 6,0 C9.314,-0 12,2.686 12,6 C12,9.314 9.314,12 6,12 z', + 'M6,12 C0,12 0,12 0,6 C0,0 -0,0 6,0 C12,0 12,0 12,6 C12,12 12,12 6,12 z', + 'M6.034,1.656 C7,0 7,0 7.966,1.656 L13.034,10.344 C14,12 13,12 12,12 L2,12 C1,12 0,12 0.966,10.344 L6.034,1.656 z', + 'M7.966,10.344 C7,12 7,12 6.034,10.344 L0.966,1.656 C-0,0 1,0 2,0 L12,0 C13,0 14,0 13.034,1.656 L7.966,10.344 z', + 'M2.594,9.406 C-0.812,6 -0.812,6 2.594,2.594 C6,-0.812 6,-0.812 9.406,2.594 C12.812,6 12.812,6 9.406,9.406 C6,12.812 6,12.812 2.594,9.406 z', +]; + +/** + * {"categories": ["visualizations", "geoms"]} + * + * Points are typically used for scatter plots. Did I get the point across? + * + */ +const Point = createClass({ + displayName: 'Point', + + statics: { + _lucidIsPrivate: true, + }, + + propTypes: { + /** + * Classes are appended to root element along with existing classes using + * the `classnames` library. + */ + className: any, + /** + * Determines if the point has a white stroke around it. + */ + hasStroke: bool, + /** + * x coordinate + */ + x: number, + /** + * y coordinate + */ + y: number, + /** + * Zero-based set of shapes. It's recommended that you pass the index of + * your array for shapes. + */ + kind: number, + /** + * Zero-based set of colors. It's recommended that you pass the index of + * your array for colors. + */ + color: number, + /** + * Scale up the size of the symbol. 2 would be double the original size. + */ + scale: number, + }, + + getDefaultProps() { + return { + x: 0, + y: 0, + kind: 0, + color: 0, + hasStroke: false, + scale: 1, + }; + }, + + render() { + const { + className, + color, + hasStroke, + kind, + x, + y, + scale, + ...passThroughs, + } = this.props; + + const kindIndex = kind % 5; + const colorIndex = color % 6; + + const classes = cx(className, '&', `&-color-${colorIndex}`, { + '&-has-stroke': hasStroke, + }); + + // These transforms are used to center the icon on the x y coordinate + // provided. + const transforms = [ + transformFromCenter(x, y, 6, 6, scale), + transformFromCenter(x, y, 6, 6, scale), + transformFromCenter(x, y, 7, 6, scale), // triangle + transformFromCenter(x, y, 7, 6, scale), // triangle + transformFromCenter(x, y, 6, 6, scale), + ]; + + return ( + + ); + } +}); + +export default Point; diff --git a/src/components/Point/Point.less b/src/components/Point/Point.less new file mode 100644 index 000000000..ab5b5635e --- /dev/null +++ b/src/components/Point/Point.less @@ -0,0 +1,15 @@ +.lucid-Point { + + &-has-stroke { + stroke: @color-white; + stroke-width: 2; + } + + &-color-0 { fill: @color-chart-0; } + &-color-1 { fill: @color-chart-1; } + &-color-2 { fill: @color-chart-2; } + &-color-3 { fill: @color-chart-3; } + &-color-4 { fill: @color-chart-4; } + &-color-5 { fill: @color-chart-5; } +} + diff --git a/src/components/Point/Point.spec.js b/src/components/Point/Point.spec.js new file mode 100644 index 000000000..3a265ed27 --- /dev/null +++ b/src/components/Point/Point.spec.js @@ -0,0 +1,7 @@ +import { common } from '../../util/generic-tests'; + +import Point from './Point'; + +describe('Point', () => { + common(Point); +}); diff --git a/src/components/Point/examples/basic.jsx b/src/components/Point/examples/basic.jsx new file mode 100644 index 000000000..689980cfa --- /dev/null +++ b/src/components/Point/examples/basic.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import Point from '../Point'; + +const svgProps = { + width: 20, + height: 20 +}; + +const pointProps = { + x: 10, + y: 10, +}; + +export default React.createClass({ + render() { + return ( +
+ + + + + + + + + + + + + + + + + + + +
+ ); + } +}); diff --git a/src/components/Point/examples/with-stroke.jsx b/src/components/Point/examples/with-stroke.jsx new file mode 100644 index 000000000..d3fbb9f5c --- /dev/null +++ b/src/components/Point/examples/with-stroke.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import Point from '../Point'; + +const svgProps = { + width: 20, + height: 20, + style: { + backgroundColor: 'black' + } +}; + +const pointProps = { + x: 10, + y: 10, + hasStroke: true, +}; + +export default React.createClass({ + render() { + return ( +
+ + + + + + + + + + + + + + + + + + + +
+ ); + } +}); diff --git a/src/components/Points/Points.jsx b/src/components/Points/Points.jsx new file mode 100644 index 000000000..24ecc6f25 --- /dev/null +++ b/src/components/Points/Points.jsx @@ -0,0 +1,187 @@ +import _ from 'lodash'; +import React from 'react'; +import { lucidClassNames } from '../../util/style-helpers'; +import { createClass } from '../../util/component-types'; +import { groupByFields } from '../../util/chart-helpers'; +import d3Shape from 'd3-shape'; + +import Point from '../Point/Point'; + +const cx = lucidClassNames.bind('&-Points'); + +const { + any, + arrayOf, + func, + number, + object, + bool, + string, +} = React.PropTypes; + +function isValidSeries(series) { + if (_.isArray(series)) { + return _.isFinite(_.last(series)); + } + + return _.isFinite(series); +} + +/** + * {"categories": ["visualizations", "chart primitives"]} + * + * Put some points on that data. + */ +const Points = createClass({ + displayName: 'Points', + + statics: { + _lucidIsPrivate: true, + }, + + propTypes: { + /** + * Classes are appended to root element along with existing classes using + * the `classnames` library. + */ + className: any, + /** + * Top + */ + top: number, + /** + * Left + */ + left: number, + /** + * De-normalized data, e.g. + * + * + * [ + * { x: 'one' , y: 1 }, + * { x: 'two' , y: 2 }, + * { x: 'three' , y: 2 }, + * { x: 'four' , y: 3 }, + * { x: 'five' , y: 4 }, + * ] + * + * + * Or (be sure to set `yFields` to `['y0', 'y1', 'y2', 'y3']`) + * + * [ + * { x: 'one' , y0: 1 , y1: 2 , y2: 3 , y3: 5 }, + * { x: 'two' , y0: 2 , y1: 3 , y2: 4 , y3: 6 }, + * { x: 'three' , y0: 2 , y1: 4 , y2: 5 , y3: 6 }, + * { x: 'four' , y0: 3 , y1: 6 , y2: 7 , y3: 7 }, + * { x: 'five' , y0: 4 , y1: 8 , y2: 9 , y3: 8 }, + * { x: 'six' , y0: 20 , y1: 8 , y2: 9 , y3: 1 }, + * ] + * + */ + data: arrayOf(object).isRequired, + /** + * The scale for the x axis. This must be a d3-scale scale. + */ + xScale: func.isRequired, + /** + * The scale for the y axis. This must be a d3-scale scale. + */ + yScale: func.isRequired, + /** + * The field we should look up your x data by. + */ + xField: string, + /** + * The field(s) we should look up your y data by. Each entry represents a + * series. Your actual y data should be numeric. + */ + yFields: arrayOf(string), + /** + * Sometimes you might not want the colors to start rotating at the blue + * color, this number will be added the line index in determining which + * color the lines are. + */ + colorOffset: number, + /** + * Display a stroke around each of the points. + */ + hasStroke: bool, + /** + * This will stack the data. In order to stack the data we have to + * calculate a new domain for the y scale that is based on the `sum` of the + * data. + */ + isStacked: bool, + }, + + getDefaultProps() { + return { + top: 0, + left: 0, + xField: 'x', + yFields: ['y'], + colorOffset: 0, + hasStroke: true, + isStacked: false, + }; + }, + + render() { + const { + className, + data, + left, + top, + colorOffset, + xField, + hasStroke, + xScale, + yFields, + isStacked, + yScale: yScaleOriginal, + ...passThroughs, + } = this.props; + + // Copy the original so we can mutate it + let yScale = yScaleOriginal.copy(); + + // If we are stacked, we need to calculate a new domain based on the sum of + // the various series' y data. One row per series. + const transformedData = isStacked + ? d3Shape.stack().keys(yFields)(data) + : groupByFields(data, yFields); + + // If we are stacked, we need to calculate a new domain based on the sum of + // the various group's y data + if (isStacked) { + yScale.domain([ + yScale.domain()[0], + _.chain(transformedData).last().flatten().max().value() + ]); + } + + return ( + + {_.map(transformedData, (d, dIndex) => ( + _.map(d, (series, seriesIndex) => ( + isValidSeries(series) ? + + : null + )) + ))} + + ); + } +}); + +export default Points; diff --git a/src/components/Points/Points.less b/src/components/Points/Points.less new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/Points/Points.spec.js b/src/components/Points/Points.spec.js new file mode 100644 index 000000000..617a69337 --- /dev/null +++ b/src/components/Points/Points.spec.js @@ -0,0 +1,18 @@ +import d3Scale from 'd3-scale'; +import { common } from '../../util/generic-tests'; + +import Points from './Points'; + +describe('Points', () => { + common(Points, { + exemptFunctionProps: [ + 'xScale', + 'yScale', + ], + getDefaultProps: () => ({ + data: [{x: new Date('2015-01-01T00:00:00Z'), y: 1}], + xScale: d3Scale.scaleTime(), + yScale: d3Scale.scaleLinear(), + }), + }); +}); diff --git a/src/components/Points/examples/basic.jsx b/src/components/Points/examples/basic.jsx new file mode 100644 index 000000000..e643e514f --- /dev/null +++ b/src/components/Points/examples/basic.jsx @@ -0,0 +1,51 @@ +import _ from 'lodash'; +import React from 'react'; +import Points from '../Points'; +import d3Scale from 'd3-scale'; + +const width = 1000; +const height = 400; +const margin = {top: 10, right: 10, bottom: 10, left: 10}; + +const innerWidth = width - margin.left - margin.right; +const innerHeight = height - margin.top - margin.bottom; + +const data = [ + { x: 'one' , y0: 4 , y1: 5 , y2: 6 , y3: 7 } , + { x: 'two' , y0: 3 , y1: 4 , y2: 5 , y3: 6 } , + { x: 'three' , y0: 2 , y1: 3 , y2: 4 , y3: 5 } , + { x: 'four' , y0: 1 , y1: 2 , y2: 3 , y3: 4 } , + { x: 'five' , y0: 2 , y1: 3 , y2: 4 , y3: 5 } , + { x: 'six' , y0: 3 , y1: 4 , y2: 5 , y3: 6 } , + { x: 'seven' , y0: 4 , y1: 5 , y2: 6 , y3: 7 } , +]; + +const yFields = ['y0', 'y1', 'y2', 'y3']; +const yMax = _.max(_.reduce(yFields, (acc, field) => { + return acc.concat(_.map(data, field)); +}, [])); + +const xScale = d3Scale.scalePoint() + .domain(_.map(data, 'x')) + .range([0, innerWidth]); + +const yScale = d3Scale.scaleLinear() + .domain([0, yMax]) + .range([innerHeight, 0]); + +export default React.createClass({ + render() { + return ( + + + + ); + } +}); diff --git a/src/docs/containers/colors.jsx b/src/docs/containers/colors.jsx index b1cd9d816..2f0fb6804 100644 --- a/src/docs/containers/colors.jsx +++ b/src/docs/containers/colors.jsx @@ -10,212 +10,213 @@ const colorList = [ { category: 'Basic', description: 'Basic colors', - colors: [ - { - varName: 'white', - hex: '#fff' - }, - { - varName: 'black', - hex: '#000' - }, + variables: [ + 'color-white', + 'color-black', ] }, { category: 'Primary', description: 'Primary Colors come in three states primary, primaryMedium and primaryLight', - colors: [ - { - varName: 'primary', - hex: '#2abbb0' - }, - { - varName: 'primaryMedium', - hex: 'tint(@color-primary, 65%)' - }, - { - varName: 'primaryLight', - hex: 'tint(@color-primary, 85%)' - }, + variables: [ + 'color-primary', + 'color-primaryMedium', + 'color-primaryLight', ] }, { category: 'Container Colors', description: 'Colors used to define colors of containers within components.', - colors: [ - { - varName: 'backgroundColor', - hex: '@color-lightGray' - }, - { - varName: 'borderColor', - hex: '@color-mediumGray' - }, + variables: [ + 'color-backgroundColor', + 'color-borderColor', ] }, { category: 'Text Colors', description: 'Used to define the color of text within a component.', - colors: [ - { - varName: 'textColor', - hex: '@color-darkGray' - }, - { - varName: 'disabledText', - hex: 'tint(@color-textColor, 50%)' - }, - { - varName: 'linkColor', - hex: '@color-primary' - }, - { - varName: 'linkColorHover', - hex: 'darken(@color-linkColor, 20%)' - }, - ] - }, - { - category: 'Transparent Gray', - description: 'Gray with an opacity.', - colors: [ - { - varName: 'gray-5', - hex: 'fade(@color-black, 5%)' - }, - { - varName: 'gray-10', - hex: 'fade(@color-black, 10%)' - }, - { - varName: 'gray-25', - hex: 'fade(@color-black, 25%)' - }, - { - varName: 'gray-30', - hex: 'fade(@color-black, 30%)' - }, + variables: [ + 'color-textColor', + 'color-disabledText', + 'color-linkColor', + 'color-linkColorHover', ] }, { category: 'Grays', description: 'Defined gray colors to be used with a component. Do not use ' + 'if a variable has been created that is more descriptive.', - colors: [ - { - varName: 'lightGray', - hex: '#f4f4f4' - }, - { - varName: 'gray', - hex: '#e3e3e3' - }, - { - varName: 'mediumGray', - hex: '#c5c5c5' - }, - { - varName: 'darkGray', - hex: '#333333' - }, - ] - }, - { - category: 'Featured colors', + variables: [ + 'color-lightGray', + 'color-gray', + 'color-mediumGray', + 'color-darkGray', + ] + }, + { + category: 'Transparent Grays', + description: 'Gray with an opacity.', + variables: [ + 'color-gray-5', + 'color-gray-10', + 'color-gray-25', + 'color-gray-30', + ] + }, + { + category: 'Featured Colors', description: 'A featured color should only be used for a component that has ' + - 'multible states like banners or buttons or button like components ' + - '(drop select). Featured colors should not be consumed by most components ' + + 'multiple states like banners or buttons or button like components ' + + '(single select). Featured colors should not be consumed by most components ' + 'instead use the color variables defined above.', - colors: [ - { - varName: 'default', - hex: '#f3f3f3', - featured: 'featured-' - }, - { - varName: 'primary', - hex: '#2abbb0', - featured: 'featured-' - }, - { - varName: 'success', - hex: '#3fa516', - featured: 'featured-' - }, - { - varName:'info', - hex: '#0089c4', - featured: 'featured-' - }, - { - varName: 'warning', - hex: '#feb209', - featured: 'featured-' - }, - { - varName: 'danger', - hex: '#f7403a', - featured: 'featured-' - }, - ] - }, - { - category: 'Featured default', - colors: [ - {varName: 'default-borderColor', hex: '#c5c5c5', featured: 'featured-'}, - {varName: 'default-backgroundColor', hex: '#ededed', featured: 'featured-'}, - {varName: 'default-gradientStartColor', hex: '#f3f3f3', featured: 'featured-'}, - {varName: 'default-gradientEndColor', hex: '#e2e2e2', featured: 'featured-'}, - ] - }, - { - category: 'Featured primary', - colors: [ - {varName: 'primary-borderColor', hex: '@color-primary', featured: 'featured-'}, - {varName: 'primary-backgroundColor', hex: 'tint(@color-primary, 70%)', featured: 'featured-'}, - {varName: 'primary-borderColorLite', hex: 'tint(@color-primary, 60%)', featured: 'featured-'}, - {varName: 'primary-gradientStartColor', hex: 'tint(@color-primary, 30%)', featured: 'featured-'}, - {varName: 'primary-gradientEndColor', hex: '@color-primary', featured: 'featured-'}, - ] - }, - { - category: 'Featured success', - colors: [ - {varName: 'success-borderColor', hex: '@color-success', featured: 'featured-'}, - {varName: 'success-backgroundColor', hex: 'tint(@color-success, 70%)', featured: 'featured-'}, - {varName: 'success-borderColorLite', hex: 'tint(@color-success, 60%)', featured: 'featured-'}, - {varName: 'success-gradientStartColor', hex: 'tint(@color-success, 30%)', featured: 'featured-'}, - {varName: 'success-gradientEndColor', hex: '@color-success', featured: 'featured-'}, - ] - }, - { - category: 'Featured info', - colors: [ - {varName: 'info-borderColor', hex: '@color-info', featured: 'featured-'}, - {varName: 'info-backgroundColor', hex: 'tint(@color-info, 70%)', featured: 'featured-'}, - {varName: 'info-borderColorLite', hex: 'tint(@color-info, 60%)', featured: 'featured-'}, - {varName: 'info-gradientStartColor', hex: 'tint(@color-info, 30%)', featured: 'featured-'}, - {varName: 'info-gradientEndColor', hex: '@color-info', featured: 'featured-'}, - ] - }, - { - category: 'Featured warning', - colors: [ - {varName: 'warning-borderColor', hex: '@color-warning', featured: 'featured-'}, - {varName: 'warning-backgroundColor', hex: 'tint(@color-warning, 70%)', featured: 'featured-'}, - {varName: 'warning-borderColorLite', hex: 'tint(@color-warning, 60%)', featured: 'featured-'}, - {varName: 'warning-gradientStartColor', hex: 'tint(@color-warning, 30%)', featured: 'featured-'}, - {varName: 'warning-gradientEndColor', hex: '@color-warning', featured: 'featured-'}, - ] - }, - { - category: 'Featured danger', - colors: [ - {varName: 'danger-borderColor', hex: '@color-danger'}, - {varName: 'danger-backgroundColor', hex: 'tint(@color-danger, 70%)', featured: 'featured-'}, - {varName: 'danger-borderColorLite', hex: 'tint(@color-danger, 60%)', featured: 'featured-'}, - {varName: 'danger-gradientStartColor', hex: 'tint(@color-danger, 30%)', featured: 'featured-'}, - {varName: 'danger-gradientEndColor', hex: '@color-danger', featured: 'featured-'}, + variables: [ + 'featured-color-default', + 'featured-color-primary', + 'featured-color-success', + 'featured-color-info', + 'featured-color-warning', + 'featured-color-danger', + ] + }, + { + category: 'Featured Default', + variables: [ + 'featured-color-default-borderColor', + 'featured-color-default-backgroundColor', + 'featured-color-default-gradientStartColor', + 'featured-color-default-gradientEndColor', + ] + }, + { + category: 'Featured Primary', + variables: [ + 'featured-color-primary-borderColor', + 'featured-color-primary-backgroundColor', + 'featured-color-primary-borderColorLite', + 'featured-color-primary-gradientStartColor', + 'featured-color-primary-gradientEndColor', + ] + }, + { + category: 'Featured Success', + variables: [ + 'featured-color-success-borderColor', + 'featured-color-success-backgroundColor', + 'featured-color-success-borderColorLite', + 'featured-color-success-gradientStartColor', + 'featured-color-success-gradientEndColor', + ] + }, + { + category: 'Featured Info', + variables: [ + 'featured-color-info-borderColor', + 'featured-color-info-backgroundColor', + 'featured-color-info-borderColorLite', + 'featured-color-info-gradientStartColor', + 'featured-color-info-gradientEndColor', + ] + }, + { + category: 'Featured Warning', + variables: [ + 'featured-color-warning-borderColor', + 'featured-color-warning-backgroundColor', + 'featured-color-warning-borderColorLite', + 'featured-color-warning-gradientStartColor', + 'featured-color-warning-gradientEndColor', + ] + }, + { + category: 'Featured Danger', + variables: [ + 'featured-color-danger-borderColor', + 'featured-color-danger-backgroundColor', + 'featured-color-danger-borderColorLite', + 'featured-color-danger-gradientStartColor', + 'featured-color-danger-gradientEndColor', + ] + }, + { + category: 'Chart 0', + variables: [ + 'color-chart-0-lightest', + 'color-chart-0-light', + 'color-chart-0', + 'color-chart-0-dark', + 'color-chart-0-darkest', + ] + }, + { + category: 'Chart 1', + variables: [ + 'color-chart-1-lightest', + 'color-chart-1-light', + 'color-chart-1', + 'color-chart-1-dark', + 'color-chart-1-darkest', + ] + }, + { + category: 'Chart 2', + variables: [ + 'color-chart-2-lightest', + 'color-chart-2-light', + 'color-chart-2', + 'color-chart-2-dark', + 'color-chart-2-darkest', + ] + }, + { + category: 'Chart 3', + variables: [ + 'color-chart-3-lightest', + 'color-chart-3-light', + 'color-chart-3', + 'color-chart-3-dark', + 'color-chart-3-darkest', + ] + }, + { + category: 'Chart 4', + variables: [ + 'color-chart-4-lightest', + 'color-chart-4-light', + 'color-chart-4', + 'color-chart-4-dark', + 'color-chart-4-darkest', + ] + }, + { + category: 'Chart 5', + variables: [ + 'color-chart-5-lightest', + 'color-chart-5-light', + 'color-chart-5', + 'color-chart-5-dark', + 'color-chart-5-darkest', + ] + }, + { + category: 'Chart Semantic Good', + variables: [ + 'color-chart-good-light', + 'color-chart-good', + 'color-chart-good-dark', + ] + }, + { + category: 'Chart Semantic Bad', + variables: [ + 'color-chart-bad-light', + 'color-chart-bad', + 'color-chart-bad-dark', + ] + }, + { + category: 'Chart Other', + variables: [ + 'color-chart-neutral', ] }, ]; @@ -233,11 +234,10 @@ const ColorPalette = React.createClass({ {group.description ?

{group.description}

: null} - {_.map(group.colors, (color, j) => ( + {_.map(group.variables, (variable, j) => (
-
-

{`@${color.featured || ''}color-${color.varName};`}

-

{color.hex}

+
+

{`@${variable};`}

))} diff --git a/src/docs/containers/colors.less b/src/docs/containers/colors.less index ef65c0a8f..dbc5f9383 100644 --- a/src/docs/containers/colors.less +++ b/src/docs/containers/colors.less @@ -1,4 +1,107 @@ +@ColorPalette-variables: + color-primary, + color-primaryMedium, + color-primaryLight, + color-white, + color-textColor, + color-disabledText, + color-linkColor, + color-linkColorHover, + color-gray-5, + color-gray-10, + color-gray-25, + color-gray-30, + color-lightGray, + color-gray, + color-mediumGray, + color-darkGray, + color-borderColor, + color-backgroundColor, + featured-color-default, + featured-color-primary, + featured-color-success, + featured-color-info, + featured-color-warning, + featured-color-danger, + featured-color-default-borderColor, + featured-color-default-backgroundColor, + featured-color-default-gradientStartColor, + featured-color-default-gradientEndColor, + featured-color-primary-borderColor, + featured-color-primary-borderColorLite, + featured-color-primary-backgroundColor, + featured-color-primary-gradientStartColor, + featured-color-primary-gradientEndColor, + featured-color-success-borderColor, + featured-color-success-borderColorLite, + featured-color-success-backgroundColor, + featured-color-success-gradientStartColor, + featured-color-success-gradientEndColor, + featured-color-info-borderColor, + featured-color-info-borderColorLite, + featured-color-info-backgroundColor, + featured-color-info-gradientStartColor, + featured-color-info-gradientEndColor, + featured-color-warning-borderColor, + featured-color-warning-borderColorLite, + featured-color-warning-backgroundColor, + featured-color-warning-gradientStartColor, + featured-color-warning-gradientEndColor, + featured-color-danger-borderColor, + featured-color-danger-borderColorLite, + featured-color-danger-backgroundColor, + featured-color-danger-gradientStartColor, + featured-color-danger-gradientEndColor, + color-chart-0-lightest, + color-chart-0-light, + color-chart-0, + color-chart-0-dark, + color-chart-0-darkest, + color-chart-1-lightest, + color-chart-1-light, + color-chart-1, + color-chart-1-dark, + color-chart-1-darkest, + color-chart-2-lightest, + color-chart-2-light, + color-chart-2, + color-chart-2-dark, + color-chart-2-darkest, + color-chart-3-lightest, + color-chart-3-light, + color-chart-3, + color-chart-3-dark, + color-chart-3-darkest, + color-chart-4-lightest, + color-chart-4-light, + color-chart-4, + color-chart-4-dark, + color-chart-4-darkest, + color-chart-5-lightest, + color-chart-5-light, + color-chart-5, + color-chart-5-dark, + color-chart-5-darkest, + color-chart-good, + color-chart-good-dark, + color-chart-good-light, + color-chart-bad, + color-chart-bad-dark, + color-chart-bad-light, + color-black; + +.ColorPalette-loop(@i: length(@ColorPalette-variables)) when (@i > 0) { + @name: extract(@ColorPalette-variables, @i); + + &-@{name} { background-color: @@name } + + // Recurse + .ColorPalette-loop(@i - 1); +} + .lucid-ColorPalette { + // Invoke the loop + .ColorPalette-loop(); border: 1px solid @color-borderColor; background-color: @color-backgroundColor; width: 200px; @@ -6,12 +109,10 @@ margin-right: @size-standard; margin-bottom: @size-standard; border-radius: @size-borderRadius; - &:hover { - background-color: @color-primary; - } + & > div { height: 100px; - border-radius: 2px; + border-radius: @size-borderRadius; border: 1px solid @color-borderColor; } @@ -20,226 +121,4 @@ font-size: .8em; margin: 0; } - - &-color-white { - background-color: @color-white; - } - - &-color-black { - background-color: @color-black; - } - - &-color-textColor { - background-color: @color-textColor; - } - - &-color-disabledText { - background-color: @color-disabledText; - } - - &-color-linkColor { - background-color: @color-linkColor; - } - - &-color-linkColorHover { - background-color: @color-linkColorHover; - } - - &-color-primary { - background-color: @color-primary; - } - - &-color-primaryMedium { - background-color: @color-primaryMedium; - } - - &-color-primaryLight { - background-color: @color-primaryLight; - } - - &-color-gray-5 { - background-color: @color-gray-5; - } - - &-color-gray-10 { - background-color: @color-gray-10; - } - - &-color-gray-25 { - background-color: @color-gray-25; - } - - &-color-gray-30 { - background-color: @color-gray-30; - } - - &-color-lightGray { - background-color: @color-lightGray; - } - - &-color-gray { - background-color: @color-gray; - } - - &-color-mediumGray { - background-color: @color-mediumGray; - } - - &-color-darkGray { - background-color: @color-darkGray; - } - - &-color-default { - background-color: @featured-color-default; - } - - &-color-primary { - background-color: @featured-color-primary; - } - - &-color-success { - background-color: @featured-color-success; - } - - &-color-info { - background-color: @featured-color-info; - } - - &-color-warning { - background-color: @featured-color-warning; - } - - &-color-danger { - background-color: @featured-color-danger; - } - - &-color-borderColor { - background-color: @color-borderColor; - } - - &-color-backgroundColor { - background-color: @color-backgroundColor; - } - - // default - &-color-default-borderColor { - background-color: @featured-color-default-borderColor; - } - - &-color-default-backgroundColor { - background-color: @featured-color-default-backgroundColor; - } - - &-color-default-gradientStartColor { - background-color: @featured-color-default-gradientStartColor; - } - - &-color-default-gradientEndColor { - background-color: @featured-color-default-gradientEndColor; - } - - // primary - &-color-primary-borderColor { - background-color: @featured-color-primary-borderColor; - } - - &-color-primary-borderColorLite { - background-color: @featured-color-primary-borderColorLite; - } - - &-color-primary-backgroundColor { - background-color: @featured-color-primary-backgroundColor; - } - - &-color-primary-gradientStartColor { - background-color: @featured-color-primary-gradientStartColor; - } - - &-color-primary-gradientEndColor { - background-color: @featured-color-primary-gradientEndColor; - } - - // success - &-color-success-borderColor { - background-color: @featured-color-success-borderColor; - } - - &-color-success-borderColorLite { - background-color: @featured-color-success-borderColorLite; - } - - &-color-success-backgroundColor { - background-color: @featured-color-success-backgroundColor; - } - - &-color-success-gradientStartColor { - background-color: @featured-color-success-gradientStartColor; - } - - &-color-success-gradientEndColor { - background-color: @featured-color-success-gradientEndColor; - } - - // info - &-color-info-borderColor { - background-color: @featured-color-info-borderColor; - } - - &-color-info-borderColorLite { - background-color: @featured-color-info-borderColorLite; - } - - &-color-info-backgroundColor { - background-color: @featured-color-info-backgroundColor; - } - - &-color-info-gradientStartColor { - background-color: @featured-color-info-gradientStartColor; - } - - &-color-info-gradientEndColor { - background-color: @featured-color-info-gradientEndColor; - } - - // warning - &-color-warning-borderColor { - background-color: @featured-color-warning-borderColor; - } - - &-color-warning-borderColorLite { - background-color: @featured-color-warning-borderColorLite; - } - - &-color-warning-backgroundColor { - background-color: @featured-color-warning-backgroundColor; - } - - &-color-warning-gradientStartColor { - background-color: @featured-color-warning-gradientStartColor; - } - - &-color-warning-gradientEndColor { - background-color: @featured-color-warning-gradientEndColor; - } - - // danger - &-color-danger-borderColor { - background-color: @featured-color-danger-borderColor; - } - - &-color-danger-borderColorLite { - background-color: @featured-color-danger-borderColorLite; - } - - &-color-danger-backgroundColor { - background-color: @featured-color-danger-backgroundColor; - } - - &-color-danger-gradientStartColor { - background-color: @featured-color-danger-gradientStartColor; - } - - &-color-danger-gradientEndColor { - background-color: @featured-color-danger-gradientEndColor; - } } diff --git a/src/docs/index.html b/src/docs/index.html index a41c8fa8a..f61834338 100644 --- a/src/docs/index.html +++ b/src/docs/index.html @@ -1,6 +1,7 @@ + Lucid diff --git a/src/docs/index.jsx b/src/docs/index.jsx index 0c4558979..21ac7d34b 100644 --- a/src/docs/index.jsx +++ b/src/docs/index.jsx @@ -49,8 +49,8 @@ const docgenMap = _.mapValues(docgenMapRaw, (value, componentName) => { * file extensions then capitalizes each word and separates each with a space. */ function getExampleTitleFromFilename (filename) { - const words = _.words(_.startCase(basename(filename, '.jsx'))); - return (/^\d+$/.test(_.head(words)) ? _.tail(words) : words).join(' '); + const words = _.words(_.startCase(basename(filename, '.jsx'))); + return (/^\d+$/.test(_.head(words)) ? _.tail(words) : words).join(' '); } const examplesByComponent = _.chain(reqExamples.keys()) @@ -67,13 +67,6 @@ const examplesByComponent = _.chain(reqExamples.keys()) .groupBy('componentName') .value(); -const docgenGroups = _.reduce(docgenMap, (acc, value, key) => { - const path = value.customData.categories.join('.'); - const newGroup = _.get(acc, path, []); - newGroup.push(key); - return _.set(acc, path, newGroup); -}, {}); - function handleHighlightCode() { if (window.hljs) { //eslint-disable-line _.each(document.querySelectorAll('pre code'), (block) => { @@ -201,6 +194,8 @@ const Component = React.createClass({ const descriptionAsHTML = getDescriptionAsHtml(_.get(docgenMap, `${componentName}.description`)); + const privateString = _.get(docgenMap, `${componentName}.isPrivateComponent`) ? '(private)' : ''; + const composesComponents = _.chain(docgenMap) .get(`${componentName}.customData.madeFrom`, null) .thru((componentNames) => { @@ -228,7 +223,7 @@ const Component = React.createClass({ return (
-

{componentName} {composesComponents}

+

{componentName} {privateString} {composesComponents}

Props

@@ -324,10 +319,10 @@ const Component = React.createClass({ ) : null}

Examples

-