Skip to content

Commit

Permalink
feat(scatterplot): add ability to format x/y values
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte authored and Raphaël Benitte committed Jun 8, 2019
1 parent 501ee0f commit 7a80184
Show file tree
Hide file tree
Showing 13 changed files with 102 additions and 78 deletions.
29 changes: 19 additions & 10 deletions packages/scatterplot/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as React from 'react'
import { Dimensions, Box, Theme, MotionProps, CartesianMarkerProps } from '@nivo/core'
import {
Dimensions,
Box,
Theme,
MotionProps,
CartesianMarkerProps,
CssMixBlendMode,
} from '@nivo/core'
import { OrdinalColorsInstruction } from '@nivo/colors'
import { LegendProps } from '@nivo/legends'
import { AxisProps } from '@nivo/axes'
Expand Down Expand Up @@ -29,6 +36,8 @@ declare module '@nivo/scatterplot' {
sizes: [number, number]
}

type ValueFormatter = (value: number | string | Date) => string | number

export interface ScatterPlotProps {
data: Array<{
id: string
Expand All @@ -39,25 +48,26 @@ declare module '@nivo/scatterplot' {
}>

xScale?: Scale
xFormat?: string | ValueFormatter
yScale?: Scale

colors?: OrdinalColorsInstruction
theme?: Theme
yFormat?: string | ValueFormatter

margin?: Box

layers: any[]

theme?: Theme
colors?: OrdinalColorsInstruction
blendMode?: CssMixBlendMode

enableGridX?: boolean
enableGridY?: boolean
axisTop?: AxisProps | null
axisRight?: AxisProps | null
axisBottom?: AxisProps | null
axisLeft?: AxisProps | null

enableGridX?: boolean
enableGridY?: boolean

symbolSize?: number | DatumAccessor<number> | DynamicSizeSpec
symbolShape?: 'circle' | 'square'
nodeSize?: number | DatumAccessor<number> | DynamicSizeSpec

isInteractive?: boolean
useMesh?: boolean
Expand All @@ -67,7 +77,6 @@ declare module '@nivo/scatterplot' {
onMouseLeave?: ScatterPlotMouseHandler
onClick?: ScatterPlotMouseHandler

tooltipFormat?: TooltipFormatter
tooltip?: (data: ScatterPlotDatum) => React.ReactNode

legends?: LegendProps[]
Expand Down
10 changes: 2 additions & 8 deletions packages/scatterplot/src/AnimatedNodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,15 @@ const AnimatedNodes = ({

AnimatedNodes.propTypes = {
nodes: PropTypes.arrayOf(NodePropType).isRequired,
renderNode: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object,
]).isRequired,
renderNode: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,

isInteractive: PropTypes.bool.isRequired,
onMouseEnter: PropTypes.func,
onMouseMove: PropTypes.func,
onMouseLeave: PropTypes.func,
onClick: PropTypes.func,

tooltip: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object
]).isRequired,
tooltip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,

blendMode: blendModePropType.isRequired,
}
Expand Down
7 changes: 2 additions & 5 deletions packages/scatterplot/src/NodeWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const NodeWrapper = ({
onMouseEnter: isInteractive ? handleMouseEnter : undefined,
onMouseMove: isInteractive ? handleMouseMove : undefined,
onMouseLeave: isInteractive ? handleMouseLeave : undefined,
onClick: (isInteractive && onClick) ? handleClick : undefined,
onClick: isInteractive && onClick ? handleClick : undefined,
})
}

Expand All @@ -88,10 +88,7 @@ NodeWrapper.propTypes = {
onMouseLeave: PropTypes.func,
onClick: PropTypes.func,

tooltip: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object
]).isRequired,
tooltip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,

blendMode: blendModePropType.isRequired,
}
Expand Down
17 changes: 13 additions & 4 deletions packages/scatterplot/src/ScatterPlot.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
*/
import React, { memo, Fragment, useMemo } from 'react'
import { TransitionMotion, spring } from 'react-motion'
import { SvgWrapper, withContainer, useDimensions, useTheme, useMotionConfig, CartesianMarkers } from '@nivo/core'
import {
SvgWrapper,
withContainer,
useDimensions,
useTheme,
useMotionConfig,
CartesianMarkers,
} from '@nivo/core'
import { Axes, Grid } from '@nivo/axes'
import { BoxLegendSvg } from '@nivo/legends'
import { Mesh } from '@nivo/voronoi'
Expand All @@ -21,7 +28,9 @@ const ScatterPlot = props => {
const {
data,
xScale: xScaleSpec,
xFormat,
yScale: yScaleSpec,
yFormat,

width,
height,
Expand Down Expand Up @@ -62,7 +71,9 @@ const ScatterPlot = props => {
const { xScale, yScale, nodes } = useScatterPlot({
data,
xScaleSpec,
xFormat,
yScaleSpec,
yFormat,
width: innerWidth,
height: innerHeight,
nodeSize,
Expand Down Expand Up @@ -154,9 +165,7 @@ const ScatterPlot = props => {

if (typeof layer === 'function') {
return (
<Fragment key={i}>
{React.createElement(layer, customLayerProps)}
</Fragment>
<Fragment key={i}>{React.createElement(layer, customLayerProps)}</Fragment>
)
}

Expand Down
10 changes: 2 additions & 8 deletions packages/scatterplot/src/StaticNodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,15 @@ const StaticNodes = ({

StaticNodes.propTypes = {
nodes: PropTypes.arrayOf(NodePropType).isRequired,
renderNode: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object,
]).isRequired,
renderNode: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,

isInteractive: PropTypes.bool.isRequired,
onMouseEnter: PropTypes.func,
onMouseMove: PropTypes.func,
onMouseLeave: PropTypes.func,
onClick: PropTypes.func,

tooltip: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object
]).isRequired,
tooltip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,

blendMode: blendModePropType.isRequired,
}
Expand Down
2 changes: 1 addition & 1 deletion packages/scatterplot/src/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const Tooltip = ({ node }) => {
return (
<BasicTooltip
id={node.serieId}
value={`x: ${node.data.x}, y: ${node.data.y}`}
value={`x: ${node.data.formattedX}, y: ${node.data.formattedY}`}
enableChip={true}
color={node.style.color}
/>
Expand Down
18 changes: 10 additions & 8 deletions packages/scatterplot/src/compute.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ export const getNodeSizeGenerator = size => {
throw new Error('symbolSize is invalid, it should be either a function, a number or an object')
}

export const computePoints = ({
series
}) => {
export const computePoints = ({ series, formatX, formatY }) => {
return series.reduce(
(agg, serie) => [
...agg,
Expand All @@ -53,19 +51,23 @@ export const computePoints = ({
serieId: serie.id,
x: d.position.x,
y: d.position.y,
data: { ...d.data, serie, id: `${serie.id}.${i}` },
data: {
...d.data,
id: `${serie.id}.${i}`,
formattedX: formatX(d.data.x),
formattedY: formatY(d.data.y),
serie,
},
})),
],
[]
)
}

export const computeLegendData = ({
series,
}) => {
export const computeLegendData = ({ series }) => {
return series.map(serie => ({
id: serie.id,
label: serie.id,
//color: getColor({ serie }),
}))
}
}
38 changes: 20 additions & 18 deletions packages/scatterplot/src/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* file that was distributed with this source code.
*/
import { useMemo } from 'react'
import { useTheme } from '@nivo/core'
import { useTheme, useValueFormatter } from '@nivo/core'
import { useOrdinalColorScale } from '@nivo/colors'
import { computeXYScalesForSeries } from '@nivo/scales'
import { computePoints, getNodeSizeGenerator } from './compute'
Expand All @@ -17,41 +17,43 @@ const useNodeSize = size => useMemo(() => getNodeSizeGenerator(size), [size])
export const useScatterPlot = ({
data,
xScaleSpec,
xFormat,
yScaleSpec,
yFormat,
width,
height,
nodeSize,
colors,
}) => {
const {
series,
xScale,
yScale,
} = useMemo(
const { series, xScale, yScale } = useMemo(
() => computeXYScalesForSeries(data, xScaleSpec, yScaleSpec, width, height),
[data, xScaleSpec, yScaleSpec, width, height]
)

const rawNodes = useMemo(
() => computePoints({ series }),
[series]
)
const formatX = useValueFormatter(xFormat)
const formatY = useValueFormatter(yFormat)
const rawNodes = useMemo(() => computePoints({ series, formatX, formatY }), [
series,
formatX,
formatY,
])

const getNodeSize = useNodeSize(nodeSize)

const theme = useTheme()
const getColor = useOrdinalColorScale(colors, 'serie.id')

const nodes = useMemo(
() => rawNodes.map(rawNode => {
return {
...rawNode,
size: getNodeSize(rawNode.data),
style: {
color: getColor(rawNode.data)
() =>
rawNodes.map(rawNode => {
return {
...rawNode,
size: getNodeSize(rawNode.data),
style: {
color: getColor(rawNode.data),
},
}
}
}),
}),
[rawNodes, getNodeSize, getColor]
)

Expand Down
1 change: 1 addition & 0 deletions packages/scatterplot/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { default as ResponsiveScatterPlot } from './ResponsiveScatterPlot'
export { default as ScatterPlotCanvas } from './ScatterPlotCanvas'
export { default as ResponsiveScatterPlotCanvas } from './ResponsiveScatterPlotCanvas'
export * from './props'
export * from './hooks'
22 changes: 13 additions & 9 deletions packages/scatterplot/src/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ const commonPropTypes = {
})
).isRequired,
xScale: scalePropType.isRequired,
xFormat: PropTypes.any,
yScale: scalePropType.isRequired,
yFormat: PropTypes.any,

layers: PropTypes.arrayOf(
PropTypes.oneOfType([
Expand All @@ -61,10 +63,7 @@ const commonPropTypes = {
}),
PropTypes.func,
]).isRequired,
renderNode: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object,
]).isRequired,
renderNode: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,

markers: PropTypes.arrayOf(
PropTypes.shape({
Expand All @@ -85,10 +84,7 @@ const commonPropTypes = {
onMouseLeave: PropTypes.func,
onClick: PropTypes.func,

tooltip: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object
]).isRequired,
tooltip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,

legends: PropTypes.arrayOf(PropTypes.shape(LegendPropShape)).isRequired,
}
Expand Down Expand Up @@ -154,7 +150,15 @@ export const NodePropType = PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
data: PropTypes.shape({
x: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.instanceOf(Date)])
.isRequired,
formattedX: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
y: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.instanceOf(Date)])
.isRequired,
formattedY: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
}).isRequired,
style: PropTypes.shape({
color: PropTypes.string.isRequired,
}).isRequired,
})
})
2 changes: 1 addition & 1 deletion website/src/data/components/scatterplot/meta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ ScatterPlot:
It lets you plot data on 2 dimensions, **x** & **y**, and can optionally
show a third quantitative dimension if you enable **dynamic node size**,
please have a look at the `size` property for further information.
please have a look at the `nodeSize` property for further information.
The responsive alternative of this component is
`ResponsiveScatterPlot`, it also offers another
Expand Down
Loading

0 comments on commit 7a80184

Please sign in to comment.