diff --git a/pages/components/docs/Donut.md b/pages/components/docs/Donut.md new file mode 100644 index 00000000000..3f15650a2d0 --- /dev/null +++ b/pages/components/docs/Donut.md @@ -0,0 +1,43 @@ +# Donut +The Donut component is a circular chart that shows the relative share of commit status states for a pull request. + +## The `data` prop +The `data` prop is the simplest way to define the share of states. It takes an object literal with states as keys and the number of statuses with that state as values. Slices are always rendered clockwise in descending order by size. + +```.jsx + +``` + +When using the `data` prop, the fill of each slice comes from the corresponding value in the theme's `colors.state` object. In other words, if `theme.colors.state.error = "red"`, then the `error` slice will get `fill="red"`. You can customize the slice colors by either passing a custom `theme` prop or using the `Donut.Slice` component described below. + +## System props + +Donut components get `space` system props. Read our [System Props](/system-props) doc page for a full list of available props. + +## Component props + +| Prop name | Type | Description | +| :- | :- | :- | +| data | Object | Use the keys `error`, `pending`, and `success` to set values used to generate slices in the chart | +| size | Number | Used to set the width and height of the component | + +# Donut.Slice +If you need to customize the color of your slices, you can use the `Donut.Slice` component as a child of `Donut`. + +```.jsx + + + + + +``` + +## `Donut.Slice` component props + +| Prop name | Type | Description | +| :- | :- | :- | +| state | String | The commit status state which this slice represents | +| value | Number | The number of statuses with this slice's state | +| fill | String | The fill color of the slice, which overrides the color determined by the `state` prop | + +export const meta = {displayName: 'Donut'} diff --git a/pages/components/docs/DonutChart.md b/pages/components/docs/DonutChart.md deleted file mode 100644 index 30fffcc18f8..00000000000 --- a/pages/components/docs/DonutChart.md +++ /dev/null @@ -1,26 +0,0 @@ -# DonutChart - -The DonutChart component is used to represent build status in a pull request. - -## Default example -```.jsx - - - - - - -``` - -## System props - -DonutChart components get `space` system props. Read our [System Props](/system-props) doc page for a full list of available props. - -## Component props - -| Name | Type | Default | Description | -| :- | :- | :-: | :- | -| data | Object | | Use the keys `error`, `pending`, and `success` to set values used to generate slices in the chart | -| size | Number | 30 | Used to set the width and height of the component - -export const meta = {displayName: 'DonutChart'} diff --git a/pages/components/docs/index.js b/pages/components/docs/index.js index 83681033338..a20e2cff3eb 100644 --- a/pages/components/docs/index.js +++ b/pages/components/docs/index.js @@ -7,7 +7,7 @@ export {meta as CircleBadge} from './CircleBadge.md' export {meta as CircleOcticon} from './CircleOcticon.md' export {meta as CounterLabel} from './CounterLabel.md' export {meta as Details} from './Details.md' -export {meta as DonutChart} from './DonutChart.md' +export {meta as Donut} from './Donut.md' export {meta as Dropdown} from './Dropdown.md' export {meta as FilterList} from './FilterList.md' export {meta as Flash} from './Flash.md' diff --git a/src/Donut.js b/src/Donut.js new file mode 100644 index 00000000000..27d2ade0cab --- /dev/null +++ b/src/Donut.js @@ -0,0 +1,80 @@ +import React from 'react' +import PropTypes from 'prop-types' +import {arc as Arc, pie as Pie} from 'd3-shape' +import {themeGet} from 'styled-system' +import {withSystemProps, withDefaultTheme} from './system-props' + +const defaultColor = '#666' +const getStateColors = themeGet('colors.state', {}) + +function Donut(props) { + const {className, data, children = mapData(data), size} = props + + const radius = size / 2 + const innerRadius = radius - 6 + + const pie = Pie().value(child => child.props.value) + + // coerce the children into an array + const childList = React.Children.toArray(children) + const arcData = pie(childList) + const arc = Arc() + .innerRadius(innerRadius) + .outerRadius(radius) + + const slices = childList.map((child, i) => { + return React.cloneElement(child, {d: arc(arcData[i])}) + }) + + return ( + + {slices} + + ) +} + +function mapData(data) { + return Object.keys(data).map(key => ) +} + +Donut.defaultProps = { + size: 30 +} + +Donut.propTypes = { + // require elements, not mixed content: , , etc. + children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]), + data: PropTypes.objectOf(PropTypes.number), + size: PropTypes.number +} + +const Slice = withDefaultTheme(props => { + const {children, d, fill, state, value} = props + const stateColors = getStateColors(props) + const color = fill || stateColors[state] || stateColors.unknown || defaultColor + return ( + <path d={d} fill={color} data-value={value}> + {children} + </path> + ) +}) + +Slice.propTypes = { + // <title> is really the only thing that should be acceptable here + children: PropTypes.shape({type: 'title'}), + d: PropTypes.string, + fill: PropTypes.string, + state: PropTypes.string, + /* eslint-disable react/no-unused-prop-types */ + theme: PropTypes.shape({ + colors: PropTypes.shape({ + state: PropTypes.objectOf(PropTypes.string) + }) + }), + /* eslint-enable */ + value: PropTypes.number +} + +Donut.Slice = Slice + +export default withSystemProps(Donut, ['space']) diff --git a/src/DonutChart.js b/src/DonutChart.js deleted file mode 100644 index 3933d85071d..00000000000 --- a/src/DonutChart.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import {arc as Arc, pie as Pie} from 'd3-shape' -import DonutSlice from './DonutSlice' -import {withSystemProps} from './system-props' - -function DonutChart(props) { - const {className, data, children = mapData(data), size} = props - - const radius = size / 2 - const innerRadius = radius - 6 - - const pie = Pie().value(child => child.props.value) - - // coerce the children into an array - const childList = React.Children.toArray(children) - const arcData = pie(childList) - const arc = Arc() - .innerRadius(innerRadius) - .outerRadius(radius) - - const slices = childList.map((child, i) => { - return React.cloneElement(child, {d: arc(arcData[i])}) - }) - - return ( - <svg width={size} height={size} className={className}> - <g transform={`translate(${radius},${radius})`}>{slices}</g> - </svg> - ) -} - -function mapData(data) { - return Object.keys(data).map(key => <DonutSlice key={key} state={key} value={data[key]} />) -} - -DonutChart.defaultProps = { - size: 30 -} - -DonutChart.propTypes = { - // require elements, not mixed content: <DonutSlice>, <title>, etc. - children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]), - data: PropTypes.objectOf(PropTypes.number), - size: PropTypes.number -} - -export default withSystemProps(DonutChart, ['space']) diff --git a/src/DonutSlice.js b/src/DonutSlice.js deleted file mode 100644 index a06b6a5c0c8..00000000000 --- a/src/DonutSlice.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import {themeGet} from 'styled-system' -import {withDefaultTheme} from './system-props' - -const defaultColor = '#666' -const getStateColors = themeGet('colors.state', {}) - -function DonutSlice(props) { - const {children, d, fill, state, value} = props - const stateColors = getStateColors(props) - const color = fill || stateColors[state] || stateColors.unknown || defaultColor - return ( - <path d={d} fill={color} data-value={value}> - {children} - </path> - ) -} - -DonutSlice.propTypes = { - // <title> is really the only thing that should be acceptable here - children: PropTypes.shape({type: 'title'}), - d: PropTypes.string, - fill: PropTypes.string, - state: PropTypes.string, - /* eslint-disable react/no-unused-prop-types */ - theme: PropTypes.shape({ - colors: PropTypes.shape({ - state: PropTypes.objectOf(PropTypes.string) - }) - }), - /* eslint-enable */ - value: PropTypes.number -} - -export default withDefaultTheme(DonutSlice) diff --git a/src/__tests__/Donut.js b/src/__tests__/Donut.js new file mode 100644 index 00000000000..e93731bea87 --- /dev/null +++ b/src/__tests__/Donut.js @@ -0,0 +1,85 @@ +import React from 'react' +import Donut from '../Donut' +import theme, {colors} from '../theme' +import {render} from '../utils/testing' + +const {state} = colors + +describe('Donut', () => { + it('is a system component', () => { + expect(Donut.systemComponent).toEqual(true) + }) + + it('renders the data prop', () => { + const donut = render(<Donut data={{error: 1}} />) + expect(donut).toMatchSnapshot() + + expect(donut.type).toEqual('svg') + expect(donut.props.width).toEqual(30) + expect(donut.props.height).toEqual(30) + expect(donut.children).toHaveLength(1) + + const [g] = donut.children + expect(g.type).toEqual('g') + expect(g.children).toHaveLength(1) + + const [slice] = g.children + expect(slice.type).toEqual('path') + // expect(slice.props.fill).toEqual(colors.state.error) + }) + + it('renders Donut.Slice children', () => { + const donut = render( + <Donut> + <Donut.Slice state="success" value={1} /> + <Donut.Slice state="failure" value={1} /> + </Donut> + ) + expect(donut).toMatchSnapshot() + expect(donut.children).toHaveLength(1) + const slices = donut.children[0].children + expect(slices).toHaveLength(2) + expect(slices.map(slice => slice.type)).toEqual(['path', 'path']) + expect(slices[0].props.fill).toEqual(colors.state.success) + expect(slices[1].props.fill).toEqual(colors.state.failure) + }) + + it('renders a single Donut.Slice child', () => { + const donut = render( + <Donut> + <Donut.Slice state="success" value={1} /> + </Donut> + ) + expect(donut).toMatchSnapshot() + expect(donut.type).toEqual('svg') + }) + + it('respects margin utility prop', () => { + expect(render(<Donut m={4} data={{error: 1}} />)).toHaveStyleRule('margin', `${theme.space[4]}px`) + }) + + it('respects padding utility prop', () => { + expect(render(<Donut p={4} data={{error: 1}} />)).toHaveStyleRule('padding', `${theme.space[4]}px`) + }) + + describe('Donut.Slice', () => { + it('renders known states as colors', () => { + expect(render(<Donut.Slice state="error" />).props.fill).toEqual(state.error) + expect(render(<Donut.Slice state="pending" />).props.fill).toEqual(state.pending) + expect(render(<Donut.Slice state="success" />).props.fill).toEqual(state.success) + expect(render(<Donut.Slice state="unknown" />).props.fill).toEqual(state.unknown) + }) + + it('renders unknown states with theme.colors.state.unknown', () => { + expect(render(<Donut.Slice state="xyz" />).props.fill).toEqual(state.unknown) + }) + + it('renders the fallback color when no state color is found in the theme', () => { + expect(render(<Donut.Slice state="error" theme={{}} />).props.fill).toEqual('#666') + }) + + it('respects the fill attribute', () => { + expect(render(<Donut.Slice fill="pink" />).props.fill).toEqual('pink') + }) + }) +}) diff --git a/src/__tests__/DonutChart.js b/src/__tests__/DonutChart.js deleted file mode 100644 index d98af748820..00000000000 --- a/src/__tests__/DonutChart.js +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react' -import DonutChart from '../DonutChart' -import DonutSlice from '../DonutSlice' -import theme, {colors} from '../theme' -import {render} from '../utils/testing' - -describe('DonutChart', () => { - it('is a system component', () => { - expect(DonutChart.systemComponent).toEqual(true) - }) - - it('renders the data prop', () => { - const donut = render(<DonutChart data={{error: 1}} />) - expect(donut).toMatchSnapshot() - - expect(donut.type).toEqual('svg') - expect(donut.props.width).toEqual(30) - expect(donut.props.height).toEqual(30) - expect(donut.children).toHaveLength(1) - - const [g] = donut.children - expect(g.type).toEqual('g') - expect(g.children).toHaveLength(1) - - const [slice] = g.children - expect(slice.type).toEqual('path') - // expect(slice.props.fill).toEqual(colors.state.error) - }) - - it('renders DonutSlice children', () => { - const donut = render( - <DonutChart> - <DonutSlice state="success" value={1} /> - <DonutSlice state="failure" value={1} /> - </DonutChart> - ) - expect(donut).toMatchSnapshot() - expect(donut.children).toHaveLength(1) - const slices = donut.children[0].children - expect(slices).toHaveLength(2) - expect(slices.map(slice => slice.type)).toEqual(['path', 'path']) - expect(slices[0].props.fill).toEqual(colors.state.success) - expect(slices[1].props.fill).toEqual(colors.state.failure) - }) - - it('renders a single DonutSlice child', () => { - const donut = render( - <DonutChart> - <DonutSlice state="success" value={1} /> - </DonutChart> - ) - expect(donut).toMatchSnapshot() - expect(donut.type).toEqual('svg') - }) - - it('respects margin utility prop', () => { - expect(render(<DonutChart m={4} data={{error: 1}} />)).toHaveStyleRule('margin', `${theme.space[4]}px`) - }) - - it('respects padding utility prop', () => { - expect(render(<DonutChart p={4} data={{error: 1}} />)).toHaveStyleRule('padding', `${theme.space[4]}px`) - }) -}) diff --git a/src/__tests__/DonutSlice.js b/src/__tests__/DonutSlice.js deleted file mode 100644 index 46ee60e4607..00000000000 --- a/src/__tests__/DonutSlice.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react' -import DonutSlice from '../DonutSlice' -import {colors} from '../theme' -import {renderWithTheme as render} from '../utils/testing' - -const {state} = colors - -describe('DonutSlice', () => { - it('renders known states as colors', () => { - expect(render(<DonutSlice state="error" />).props.fill).toEqual(state.error) - expect(render(<DonutSlice state="pending" />).props.fill).toEqual(state.pending) - expect(render(<DonutSlice state="success" />).props.fill).toEqual(state.success) - expect(render(<DonutSlice state="unknown" />).props.fill).toEqual(state.unknown) - }) - - it('renders unknown states with theme.colors.state.unknown', () => { - expect(render(<DonutSlice state="xyz" />).props.fill).toEqual(state.unknown) - }) - - it('renders the fallback color when no state color is found in the theme', () => { - expect(render(<DonutSlice state="error" theme={{}} />).props.fill).toEqual('#666') - }) - - it('respects the fill attribute', () => { - expect(render(<DonutSlice fill="pink" />).props.fill).toEqual('pink') - }) -}) diff --git a/src/__tests__/UtilitySystemProps.js b/src/__tests__/UtilitySystemProps.js index 5b1c02c1066..c293b3896dc 100644 --- a/src/__tests__/UtilitySystemProps.js +++ b/src/__tests__/UtilitySystemProps.js @@ -6,7 +6,8 @@ import {renderStyles} from '../utils/testing' const testProps = { OcticonButton: {icon: X, label: 'button'}, - DonutChart: {data: {pending: 1}} + Donut: {data: {pending: 1}}, + Avatar: {alt: ''} } describe('UtilitySystemProps', () => { diff --git a/src/__tests__/__snapshots__/DonutChart.js.snap b/src/__tests__/__snapshots__/Donut.js.snap similarity index 88% rename from src/__tests__/__snapshots__/DonutChart.js.snap rename to src/__tests__/__snapshots__/Donut.js.snap index 4f8a874a51e..bc430a1166e 100644 --- a/src/__tests__/__snapshots__/DonutChart.js.snap +++ b/src/__tests__/__snapshots__/Donut.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DonutChart renders DonutSlice children 1`] = ` +exports[`Donut renders Donut.Slice children 1`] = ` <svg className="emotion-0" height={30} @@ -23,7 +23,7 @@ exports[`DonutChart renders DonutSlice children 1`] = ` </svg> `; -exports[`DonutChart renders a single DonutSlice child 1`] = ` +exports[`Donut renders a single Donut.Slice child 1`] = ` <svg className="emotion-0" height={30} @@ -41,7 +41,7 @@ exports[`DonutChart renders a single DonutSlice child 1`] = ` </svg> `; -exports[`DonutChart renders the data prop 1`] = ` +exports[`Donut renders the data prop 1`] = ` <svg className="emotion-0" height={30} diff --git a/src/index.js b/src/index.js index f9ec2acdf45..0dca744241c 100644 --- a/src/index.js +++ b/src/index.js @@ -33,8 +33,7 @@ export {default as CircleBadge} from './CircleBadge' export {default as Details} from './Details' export {default as Dropdown} from './Dropdown' -export {default as DonutChart} from './DonutChart' -export {default as DonutSlice} from './DonutSlice' +export {default as Donut} from './Donut' export {default as FilterList} from './FilterList' export {default as FilterListItem} from './FilterListItem' export {default as FlexContainer} from './FlexContainer'