Skip to content

Commit

Permalink
feat(markers): add support for markers on Line & Bar charts
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte committed Aug 21, 2017
1 parent da49e15 commit e36a7a2
Show file tree
Hide file tree
Showing 8 changed files with 419 additions and 2 deletions.
6 changes: 6 additions & 0 deletions src/Nivo.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export const defaultTheme = {
strokeWidth: 1,
strokeDasharray: '',
},
markers: {
lineColor: '#000',
lineStrokeWidth: 1,
textColor: '#000',
fontSize: '11px',
},
dots: {
textColor: '#000',
fontSize: '11px',
Expand Down
58 changes: 58 additions & 0 deletions src/components/cartesian/markers/CartesianMarkers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* This file is part of the nivo project.
*
* Copyright 2016-present, Raphaël Benitte.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React from 'react'
import PropTypes from 'prop-types'
import pure from 'recompose/pure'
import CartesianMarkersItem from './CartesianMarkersItem'

const CartesianMarkers = ({ markers, width, height, xScale, yScale, theme }) => {
if (!markers || markers.length === 0) return null

return (
<g>
{markers.map((marker, i) =>
<CartesianMarkersItem
key={i}
{...marker}
width={width}
height={height}
scale={marker.axis === 'y' ? yScale : xScale}
theme={theme}
/>
)}
</g>
)
}

CartesianMarkers.propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,

xScale: PropTypes.func.isRequired,
yScale: PropTypes.func.isRequired,

theme: PropTypes.shape({
markers: PropTypes.shape({
lineColor: PropTypes.string.isRequired,
lineStrokeWidth: PropTypes.number.isRequired,
textColor: PropTypes.string.isRequired,
fontSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
}).isRequired,
}).isRequired,

markers: PropTypes.arrayOf(
PropTypes.shape({
axis: PropTypes.oneOf(['x', 'y']).isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
style: PropTypes.object,
})
),
}

export default pure(CartesianMarkers)
269 changes: 269 additions & 0 deletions src/components/cartesian/markers/CartesianMarkersItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
/*
* This file is part of the nivo project.
*
* Copyright 2016-present, Raphaël Benitte.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React from 'react'
import PropTypes from 'prop-types'
import pure from 'recompose/pure'

/**
*
* @param {string} axis
* @param {number} width
* @param {number} height
* @param {string} position
* @param {number} offsetX
* @param {number} offsetY
* @param {string} orientation
* @return {{ x: number, y: number, textAnchor: string }}
*/
const computeLabel = ({ axis, width, height, position, offsetX, offsetY, orientation }) => {
let x = 0
let y = 0
const rotation = orientation === 'vertical' ? -90 : 0
let textAnchor = 'start'

if (axis === 'x') {
switch (position) {
case 'top-left':
x = -offsetX
y = offsetY
textAnchor = 'end'
break
case 'top':
y = -offsetY
if (orientation === 'horizontal') {
textAnchor = 'middle'
} else {
textAnchor = 'start'
}
break
case 'top-right':
x = offsetX
y = offsetY
if (orientation === 'horizontal') {
textAnchor = 'start'
} else {
textAnchor = 'end'
}
break
case 'right':
x = offsetX
y = height / 2
if (orientation === 'horizontal') {
textAnchor = 'start'
} else {
textAnchor = 'middle'
}
break
case 'bottom-right':
x = offsetX
y = height - offsetY
textAnchor = 'start'
break
case 'bottom':
y = height + offsetY
if (orientation === 'horizontal') {
textAnchor = 'middle'
} else {
textAnchor = 'end'
}
break
case 'bottom-left':
y = height - offsetY
x = -offsetX
if (orientation === 'horizontal') {
textAnchor = 'end'
} else {
textAnchor = 'start'
}
break
case 'left':
x = -offsetX
y = height / 2
if (orientation === 'horizontal') {
textAnchor = 'end'
} else {
textAnchor = 'middle'
}
break
}
} else {
switch (position) {
case 'top-left':
x = offsetX
y = -offsetY
textAnchor = 'start'
break
case 'top':
x = width / 2
y = -offsetY
if (orientation === 'horizontal') {
textAnchor = 'middle'
} else {
textAnchor = 'start'
}
break
case 'top-right':
x = width - offsetX
y = -offsetY
if (orientation === 'horizontal') {
textAnchor = 'end'
} else {
textAnchor = 'start'
}
break
case 'right':
x = width + offsetX
if (orientation === 'horizontal') {
textAnchor = 'start'
} else {
textAnchor = 'middle'
}
break
case 'bottom-right':
x = width - offsetX
y = offsetY
textAnchor = 'end'
break
case 'bottom':
x = width / 2
y = offsetY
if (orientation === 'horizontal') {
textAnchor = 'middle'
} else {
textAnchor = 'end'
}
break
case 'bottom-left':
x = offsetX
y = offsetY
if (orientation === 'horizontal') {
textAnchor = 'start'
} else {
textAnchor = 'end'
}
break
case 'left':
x = -offsetX
if (orientation === 'horizontal') {
textAnchor = 'end'
} else {
textAnchor = 'middle'
}
break
}
}

return { x, y, rotation, textAnchor }
}

const CartesianMarkersItem = ({
width,
height,
axis,
scale,
value,
theme,
style,
legend,
legendPosition,
legendOffsetX,
legendOffsetY,
legendOrientation,
}) => {
let x = 0
let x2 = 0
let y = 0
let y2 = 0

if (axis === 'y') {
y = scale(value)
x2 = width
} else {
x = scale(value)
y2 = height
}

let legendNode = null
if (legend) {
const legendProps = computeLabel({
axis,
width,
height,
position: legendPosition,
offsetX: legendOffsetX,
offsetY: legendOffsetY,
orientation: legendOrientation,
})
legendNode = (
<text
transform={`translate(${legendProps.x}, ${legendProps.y}) rotate(${legendProps.rotation})`}
textAnchor={legendProps.textAnchor}
alignmentBaseline="central"
>
{legend}
</text>
)
}

return (
<g transform={`translate(${x}, ${y})`}>
<line
x1={0}
x2={x2}
y1={0}
y2={y2}
stroke={theme.markers.lineColor}
strokeWidth={theme.markers.lineStrokeWidth}
style={style}
/>
{legendNode}
</g>
)
}

CartesianMarkersItem.propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,

axis: PropTypes.oneOf(['x', 'y']).isRequired,
scale: PropTypes.func.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
style: PropTypes.object,

legend: PropTypes.string,
legendPosition: PropTypes.oneOf([
'top-left',
'top',
'top-right',
'right',
'bottom-right',
'bottom',
'bottom-left',
'left',
]),
legendOffsetX: PropTypes.number.isRequired,
legendOffsetY: PropTypes.number.isRequired,
legendOrientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,

theme: PropTypes.shape({
markers: PropTypes.shape({
textColor: PropTypes.string.isRequired,
fontSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
}).isRequired,
}).isRequired,
}

CartesianMarkersItem.defaultProps = {
legendPosition: 'top-right',
legendOffsetX: 14,
legendOffsetY: 14,
legendOrientation: 'horizontal',
}

export default pure(CartesianMarkersItem)
14 changes: 13 additions & 1 deletion src/components/charts/bar/Bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import { generateGroupedBars, generateStackedBars } from '../../../lib/charts/ba
import { getAccessorFor } from '../../../lib/propertiesConverters'
import Container from '../Container'
import SvgWrapper from '../SvgWrapper'
import Axes from '../../axes/Axes'
import Grid from '../../axes/Grid'
import CartesianMarkers from '../../cartesian/markers/CartesianMarkers'
import Axes from '../../axes/Axes'
import BarItem from './BarItem'
import BarItemLabel from './BarItemLabel'

Expand Down Expand Up @@ -53,6 +54,9 @@ const Bar = ({
getLabelsLinkColor,
getLabelsTextColor,

// markers
markers,

// theming
theme,
getColor,
Expand Down Expand Up @@ -137,6 +141,14 @@ const Bar = ({
yScale={enableGridY ? result.yScale : null}
{...motionProps}
/>
<CartesianMarkers
markers={markers}
width={width}
height={height}
xScale={result.xScale}
yScale={result.yScale}
theme={theme}
/>
<Axes
xScale={result.xScale}
yScale={result.yScale}
Expand Down
Loading

0 comments on commit e36a7a2

Please sign in to comment.