Skip to content

Conversation

@shawnbot
Copy link
Contributor

@shawnbot shawnbot commented Jun 5, 2018

The DonutChart renders a little chart that looks like this, from the Merge Box:

The React component takes either a data prop, which is an object literal in the form {[state]: value}, e.g.

<DonutChart data={{success: 3, error: 2}} />
<DonutChart data={{pending: 1, success: 4}} />

Or, if you need more control, you can pass the exported DonutSlice component as children with value and either state or fill props:

<DonutChart>
  <DonutSlice value={4} state='pending' key='pending' />
  <DonutSlice value={1} state='success' key='success' />
  <DonutSlice value={3} fill={someCustomColor} key='other' />
</DonutChart>

Under the hood, this uses the pie generator from d3-shape, which has a really nice API. ✨

TODO

  • Get sizing and inner radius to match the existing component
  • Check that we're using the correct colors
  • Get DonutSlice test passing
  • Review

@shawnbot
Copy link
Contributor Author

shawnbot commented Jun 5, 2018

As of 8288ef1 it looks like this on Pages:

image

const DonutChart = props => {
const {
data,
children = mapData(data),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the children prop isn't passed, generate slices from the data prop.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌


const radius = size / 2
const {
innerRadius = radius / 2
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dunno if we want the inner radius of the donut (the size of the "hole") to be fixed or relative. The way it's written write now, it can be passed via the innerRadius prop, which is probably not what we want.

} = props

const pie = Pie()
.value(child => child.props.value)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Translation: create a pie slice generator with a value accessor that gets props.value from each child.

const pie = Pie()
.value(child => child.props.value)

const arcData = pie(children)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Translation: generate the pie slice data from the children array.

const arcData = pie(children)
const arc = Arc()
.innerRadius(innerRadius)
.outerRadius(radius)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use a custom arc generator here because we need to set the inner and outer radii to fixed values.

const arcs = React.Children.map(children, (child, i) => {
const {
state,
fill = fillForState[state]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fill prop can be passed as a string; if it's not passed, we get the color from the corresponding key of fillForState. We should probably have a default here, e.g. theme.colors.gray[5].

fill = fillForState[state]
} = child.props
return React.cloneElement(child, {
d: arc(arcData[i]),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The d attribute is the SVG path's actual shape, which arc() generates for us from the corresponding entry of the pie slice data.


return (
<svg width={size} height={size}>
<g transform={`translate(${radius},${radius})`}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pie paths are always centered on (0, 0), so we need to translate to the center.

}

const DonutSlice = ({children, d, fill}) => {
return <path d={d} fill={fill}>{children}</path>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we pass through children here so that we can add an SVG <title>.

}

DonutChart.propTypes = {
children: PropTypes.arrayOf(DonutSlice)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only allow DonutSlice elements as children.

}

DonutChart.propTypes = {
children: PropTypes.arrayOf(DonutSlice),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only allow DonutSlice elements as children.


DonutChart.propTypes = {
children: PropTypes.arrayOf(DonutSlice),
data: PropTypes.objectOf(PropTypes.number),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data should have values that are numbers.

DonutSlice.propTypes = {
d: PropTypes.string,
fill: PropTypes.string,
state: PropTypes.oneOf(Object.keys(fillForState)),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate allowed values for the state prop based on the keys of our fillForState object.

@shawnbot shawnbot changed the title [WIP] Add DonutChart component Add DonutChart component Jun 5, 2018
@shawnbot
Copy link
Contributor Author

shawnbot commented Jun 5, 2018

Okay, this is ready for a final review. Note that in 44f5a9d I had to employ a variant of this hack to make DonutChart's propTypes allow one or more DonutSlice children. (TIL: when you only have one child, children is not an array!)

Copy link

@emplums emplums left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just left a few comments, but overall this is looking great! 🎉

const DonutChart = props => {
const {
data,
children = mapData(data),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌

)
}

const DonutSlice = ({children, d, fill}) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although DonutChart and DonutSlice are fairly tied together, I still think it makes sense to put DonutSlice in a separate file and import it. Makes it easier to find in the file system if we need to make changes, simplifies this file a bit, etc. How do we feel about making that a general best practice (keep each component in a separate file)? cc @shawnbot @broccolini @jonrohan

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a "one export per file" rule?

state,
fill = fillForState[state] || DEFAULT_FILL
} = child.props
return React.cloneElement(child, {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cloning a DonutSlice correct? Wonder if it would be more declarative here to instead do:

return <DonutSlice d={arc(arcData[i]} fill={fill} key={i}/>

since we don't need access to the refs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, good call!

}

DonutChart.propTypes = {
children: PropTypes.arrayOf(DonutSlice),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General component API thoughts:

  • I like using PropTypes to check types here and including it at the bottom of every component file, should we start doing this for every component?
  • Should we start setting isRequired in our prop types declarations? I think in this component, the component would break if data is not passed in, correct?

@shawnbot
Copy link
Contributor Author

shawnbot commented Jun 5, 2018

Thanks, @emplums! I moved DonutSlice into its own file, which nicely simplified DonutChart and compartmentalized all of the color logic to DonutSlice so that DonutChart doesn't have to know anything about it. It'd be nice to be able to move all of the geometry stuff into DonutSlice eventually, but I think this is good enough for now. ✌️

@shawnbot shawnbot changed the base branch from master to release-0.0.3-beta June 5, 2018 20:18
@shawnbot shawnbot merged commit bd03f04 into release-0.0.3-beta Jun 5, 2018
@shawnbot shawnbot deleted the donut-chart branch June 5, 2018 20:18
@shawnbot shawnbot mentioned this pull request Jun 5, 2018
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants