From e36a7a2bd2616bc4e4c2a4a927dfb14299ee9d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Benitte?= Date: Mon, 21 Aug 2017 16:42:47 +0900 Subject: [PATCH] feat(markers): add support for markers on Line & Bar charts --- src/Nivo.js | 6 + .../cartesian/markers/CartesianMarkers.js | 58 ++++ .../cartesian/markers/CartesianMarkersItem.js | 269 ++++++++++++++++++ src/components/charts/bar/Bar.js | 14 +- src/components/charts/line/Line.js | 21 ++ stories/charts/bar.stories.js | 16 ++ stories/charts/line.stories.js | 35 ++- stories/style.css | 2 + 8 files changed, 419 insertions(+), 2 deletions(-) create mode 100644 src/components/cartesian/markers/CartesianMarkers.js create mode 100644 src/components/cartesian/markers/CartesianMarkersItem.js diff --git a/src/Nivo.js b/src/Nivo.js index 91d3c24a1..1d70be5c8 100644 --- a/src/Nivo.js +++ b/src/Nivo.js @@ -37,6 +37,12 @@ export const defaultTheme = { strokeWidth: 1, strokeDasharray: '', }, + markers: { + lineColor: '#000', + lineStrokeWidth: 1, + textColor: '#000', + fontSize: '11px', + }, dots: { textColor: '#000', fontSize: '11px', diff --git a/src/components/cartesian/markers/CartesianMarkers.js b/src/components/cartesian/markers/CartesianMarkers.js new file mode 100644 index 000000000..972ebc54d --- /dev/null +++ b/src/components/cartesian/markers/CartesianMarkers.js @@ -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 ( + + {markers.map((marker, i) => + + )} + + ) +} + +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) diff --git a/src/components/cartesian/markers/CartesianMarkersItem.js b/src/components/cartesian/markers/CartesianMarkersItem.js new file mode 100644 index 000000000..b00f1cab9 --- /dev/null +++ b/src/components/cartesian/markers/CartesianMarkersItem.js @@ -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 = ( + + {legend} + + ) + } + + return ( + + + {legendNode} + + ) +} + +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) diff --git a/src/components/charts/bar/Bar.js b/src/components/charts/bar/Bar.js index 714cba923..9e2151794 100644 --- a/src/components/charts/bar/Bar.js +++ b/src/components/charts/bar/Bar.js @@ -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' @@ -53,6 +54,9 @@ const Bar = ({ getLabelsLinkColor, getLabelsTextColor, + // markers + markers, + // theming theme, getColor, @@ -137,6 +141,14 @@ const Bar = ({ yScale={enableGridY ? result.yScale : null} {...motionProps} /> + + /> ) +stories.add('with marker', () => + +) + stories.add('using custom colorBy', () => data[`${id}Color`]} /> ) diff --git a/stories/charts/line.stories.js b/stories/charts/line.stories.js index 69ab0cc2d..6df09ccbe 100644 --- a/stories/charts/line.stories.js +++ b/stories/charts/line.stories.js @@ -5,11 +5,12 @@ import { generateDrinkStats } from 'nivo-generators' import '../style.css' import { Line } from '../../src' +const data = generateDrinkStats(18) const commonProperties = { width: 900, height: 360, margin: { top: 60, right: 80, bottom: 60, left: 80 }, - data: generateDrinkStats(18), + data, animate: true, } @@ -102,9 +103,41 @@ stories.add('using data colors', () => /> ) +stories.add('with markers', () => + +) + stories.add('with custom min/max Y', () =>