Loëck Vézien
7 years ago
committed by
GitHub
20 changed files with 994 additions and 64 deletions
@ -0,0 +1,67 @@ |
|||
// @flow
|
|||
|
|||
import React, { Fragment } from 'react' |
|||
|
|||
import { colors as themeColors } from 'styles/theme' |
|||
import { TooltipContainer } from 'components/base/Tooltip' |
|||
|
|||
import type { Item } from './types' |
|||
|
|||
/** |
|||
* we use inline style for more perfs, as tooltip may re-render numerous times |
|||
*/ |
|||
|
|||
const Arrow = () => ( |
|||
<svg |
|||
style={{ |
|||
display: 'block', |
|||
position: 'absolute', |
|||
left: '50%', |
|||
bottom: 0, |
|||
marginBottom: -10, |
|||
transform: 'translate(-50%, 0)', |
|||
}} |
|||
viewBox="0 0 14 6.2" |
|||
width={16} |
|||
height={16} |
|||
> |
|||
<path fill={themeColors.dark} d="m14 0-5.5 5.6c-0.8 0.8-2 0.8-2.8 0l-5.7-5.6" /> |
|||
</svg> |
|||
) |
|||
|
|||
const Tooltip = ({ d, renderTooltip }: { d: Item, renderTooltip?: Function }) => ( |
|||
<div style={{ position: 'relative' }}> |
|||
<div |
|||
style={{ |
|||
position: 'absolute', |
|||
bottom: '100%', |
|||
left: 0, |
|||
transform: `translate3d(-50%, 0, 0)`, |
|||
whiteSpace: 'nowrap', |
|||
marginBottom: 10, |
|||
}} |
|||
> |
|||
<TooltipContainer style={{ textAlign: 'center' }}> |
|||
{renderTooltip ? ( |
|||
renderTooltip(d) |
|||
) : ( |
|||
<Fragment> |
|||
<div style={{ fontSize: 14 }}> |
|||
<b>{Math.round(d.value)}</b> |
|||
</div> |
|||
<span>{d.date}</span> |
|||
</Fragment> |
|||
)} |
|||
</TooltipContainer> |
|||
<div style={{ background: 'red' }}> |
|||
<Arrow /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
) |
|||
|
|||
Tooltip.defaultProps = { |
|||
renderTooltip: undefined, |
|||
} |
|||
|
|||
export default Tooltip |
@ -0,0 +1,116 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
import * as d3 from 'd3' |
|||
import { renderToString } from 'react-dom/server' |
|||
import { ThemeProvider } from 'styled-components' |
|||
|
|||
import theme from 'styles/theme' |
|||
|
|||
import type { Props } from '.' |
|||
import type { CTX } from './types' |
|||
|
|||
import Tooltip from './Tooltip' |
|||
|
|||
export default function handleMouseEvents({ |
|||
ctx, |
|||
props, |
|||
shouldTooltipUpdate, |
|||
onTooltipUpdate, |
|||
renderTooltip, |
|||
}: { |
|||
ctx: CTX, |
|||
props: Props, |
|||
shouldTooltipUpdate: Function, |
|||
onTooltipUpdate: Function, |
|||
renderTooltip?: Function, |
|||
}) { |
|||
const { MARGINS, HEIGHT, WIDTH, NODES, DATA, x, y } = ctx |
|||
const { hideAxis } = props |
|||
|
|||
const bisectDate = d3.bisector(d => d.parsedDate).left |
|||
|
|||
const node = NODES.wrapper |
|||
.append('rect') |
|||
.attr('fill', 'none') |
|||
.attr('pointer-events', 'all') |
|||
.attr('class', 'overlay') |
|||
.attr('width', WIDTH) |
|||
.attr('height', HEIGHT) |
|||
|
|||
node |
|||
.on('mousemove', mouseMove) |
|||
.on('mouseenter', mouseEnter) |
|||
.on('mouseout', mouseOut) |
|||
|
|||
function getStep() { |
|||
const x0 = x.invert(d3.mouse(this)[0]) |
|||
const i = bisectDate(DATA, x0, 1) |
|||
const d0 = DATA[i - 1] |
|||
const d1 = DATA[i] |
|||
if (!d0 || !d1) { |
|||
return null |
|||
} |
|||
return x0 - d0.parsedDate > d1.parsedDate - x0 ? d1 : d0 |
|||
} |
|||
|
|||
function mouseEnter() { |
|||
const d = getStep.call(this) |
|||
if (!d) { |
|||
return |
|||
} |
|||
NODES.tooltip |
|||
.style('transition', '100ms cubic-bezier(.61,1,.53,1) opacity') |
|||
.style('opacity', 1) |
|||
.style('transform', `translate3d(${MARGINS.left + x(d.parsedDate)}px, ${y(d.value)}px, 0)`) |
|||
NODES.focus.style('opacity', 1) |
|||
if (!hideAxis) { |
|||
NODES.xBar.style('opacity', 1) |
|||
NODES.yBar.style('opacity', 1) |
|||
} |
|||
} |
|||
|
|||
function mouseOut() { |
|||
NODES.tooltip.style('opacity', 0).style('transition', '100ms linear opacity') |
|||
NODES.focus.style('opacity', 0) |
|||
if (!hideAxis) { |
|||
NODES.xBar.style('opacity', 0) |
|||
NODES.yBar.style('opacity', 0) |
|||
} |
|||
} |
|||
|
|||
function mouseMove() { |
|||
const d = getStep.call(this) |
|||
if (!d) { |
|||
return |
|||
} |
|||
if (!shouldTooltipUpdate(d)) { |
|||
return |
|||
} |
|||
onTooltipUpdate(d) |
|||
NODES.focus.attr('transform', `translate(${x(d.parsedDate)},${y(d.value)})`) |
|||
NODES.tooltip |
|||
.html( |
|||
renderToString( |
|||
<ThemeProvider theme={theme}> |
|||
<Tooltip renderTooltip={renderTooltip} d={d.ref} /> |
|||
</ThemeProvider>, |
|||
), |
|||
) |
|||
.style('transform', `translate3d(${MARGINS.left + x(d.parsedDate)}px, ${y(d.value)}px, 0)`) |
|||
if (!hideAxis) { |
|||
NODES.xBar |
|||
.attr('x1', x(d.parsedDate)) |
|||
.attr('x2', x(d.parsedDate)) |
|||
.attr('y1', HEIGHT) |
|||
.attr('y2', y(d.value)) |
|||
NODES.yBar |
|||
.attr('x1', 0) |
|||
.attr('x2', x(d.parsedDate)) |
|||
.attr('y1', y(d.value)) |
|||
.attr('y2', y(d.value)) |
|||
} |
|||
} |
|||
|
|||
return node |
|||
} |
@ -0,0 +1,49 @@ |
|||
import c from 'color' |
|||
|
|||
export function enrichData(data, parseTime) { |
|||
return data.map((d, i) => ({ |
|||
...d, |
|||
ref: d, |
|||
index: i, |
|||
parsedDate: parseTime(d.date), |
|||
})) |
|||
} |
|||
|
|||
export function generateColors(color) { |
|||
const cColor = c(color) |
|||
return { |
|||
line: color, |
|||
focus: color, |
|||
gradientStart: cColor.fade(0.7), |
|||
gradientStop: cColor.fade(1), |
|||
focusBar: cColor.fade(0.5), |
|||
} |
|||
} |
|||
|
|||
export function generateMargins(hideAxis) { |
|||
const margins = { |
|||
top: hideAxis ? 5 : 10, |
|||
bottom: hideAxis ? 5 : 30, |
|||
right: hideAxis ? 5 : 40, |
|||
left: hideAxis ? 5 : 80, |
|||
} |
|||
|
|||
// FIXME: Forced to "use" margins here to prevent babel/uglify to believe
|
|||
// there is a constant variable re-assignment. I don't get it, but it
|
|||
// works, so, eh.
|
|||
void margins |
|||
|
|||
return margins |
|||
} |
|||
|
|||
export function observeResize(node, cb) { |
|||
const ro = new ResizeObserver(() => { |
|||
if (!node) { |
|||
return |
|||
} |
|||
const { width } = node.getBoundingClientRect() |
|||
cb(width) |
|||
}) |
|||
|
|||
ro.observe(node) |
|||
} |
@ -0,0 +1,184 @@ |
|||
// @flow
|
|||
|
|||
/** |
|||
* Chart |
|||
* ----- |
|||
* |
|||
* XX |
|||
* XXXX |
|||
* X XX X |
|||
* XXXX XX X |
|||
* XX X XX X |
|||
* X XXXX X XX X |
|||
* XX XX X XX XX XX |
|||
* XX XX XX XXXX |
|||
* XX |
|||
* XX |
|||
* Usage: |
|||
* |
|||
* <Chart |
|||
* data={data} |
|||
* interactive // Handle mouse events, display tooltip etc.
|
|||
* color="#5f8ced" // Main color for line, gradient, etc.
|
|||
* height={300} // Fix height. Width is responsive to container.
|
|||
* /> |
|||
* |
|||
* `data` looks like: |
|||
* |
|||
* [ |
|||
* { date: '2018-01-01', value: 10 }, |
|||
* { date: '2018-01-02', value: 25 }, |
|||
* { date: '2018-01-03', value: 50 }, |
|||
* ] |
|||
* |
|||
*/ |
|||
|
|||
import React, { PureComponent } from 'react' |
|||
import * as d3 from 'd3' |
|||
import noop from 'lodash/noop' |
|||
|
|||
import refreshNodes from './refreshNodes' |
|||
import refreshDraw from './refreshDraw' |
|||
import handleMouseEvents from './handleMouseEvents' |
|||
import { enrichData, generateColors, generateMargins, observeResize } from './helpers' |
|||
|
|||
import type { Data } from './types' |
|||
|
|||
export type Props = { |
|||
data: Data, // eslint-disable-line react/no-unused-prop-types
|
|||
color?: string, // eslint-disable-line react/no-unused-prop-types
|
|||
hideAxis?: boolean, // eslint-disable-line react/no-unused-prop-types
|
|||
interactive?: boolean, // eslint-disable-line react/no-unused-prop-types
|
|||
height: number, |
|||
dateFormat?: string, // eslint-disable-line react/no-unused-prop-types
|
|||
id?: string, // eslint-disable-line react/no-unused-prop-types
|
|||
nbTicksX: number, // eslint-disable-line react/no-unused-prop-types
|
|||
renderTooltip?: Function, // eslint-disable-line react/no-unused-prop-types
|
|||
renderTickX?: Function, // eslint-disable-line react/no-unused-prop-types
|
|||
renderTickY?: Function, // eslint-disable-line react/no-unused-prop-types
|
|||
} |
|||
|
|||
class Chart extends PureComponent<Props> { |
|||
static defaultProps = { |
|||
color: '#000', |
|||
hideAxis: false, |
|||
interactive: true, |
|||
height: 400, |
|||
dateFormat: '%Y-%m-%d', |
|||
id: 'chart', |
|||
nbTicksX: 5, |
|||
} |
|||
|
|||
componentDidMount() { |
|||
const { width } = this._ruler.getBoundingClientRect() |
|||
this._width = width |
|||
this.createChart() |
|||
observeResize(this._ruler, width => { |
|||
if (width !== this._width) { |
|||
this._width = width |
|||
this.refreshChart(this.props) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
componentDidUpdate(prevProps: Props) { |
|||
this.refreshChart(prevProps) |
|||
} |
|||
|
|||
_ruler: any |
|||
_node: any |
|||
_width: number |
|||
refreshChart: Function |
|||
|
|||
createChart() { |
|||
const ctx = { |
|||
NODES: {}, |
|||
INVALIDATED: {}, |
|||
MARGINS: {}, |
|||
COLORS: {}, |
|||
DATA: [], |
|||
WIDTH: 0, |
|||
HEIGHT: 0, |
|||
x: noop, |
|||
y: noop, |
|||
} |
|||
|
|||
let firstRender = true |
|||
|
|||
// Keep reference to mouse handler to allow destroy when refresh
|
|||
let mouseHandler = null |
|||
|
|||
this.refreshChart = prevProps => { |
|||
const { _node: node, props } = this |
|||
const { data: raw, color, dateFormat, height, hideAxis, interactive, renderTooltip } = props |
|||
|
|||
ctx.DATA = enrichData(raw, d3.timeParse(dateFormat)) |
|||
|
|||
// Detect what needs to be updated
|
|||
ctx.INVALIDATED = { |
|||
color: firstRender || (prevProps && color !== prevProps.color), |
|||
margin: firstRender || (prevProps && hideAxis !== prevProps.hideAxis), |
|||
} |
|||
firstRender = false |
|||
|
|||
// Reset color if needed
|
|||
if (ctx.INVALIDATED.color) { |
|||
ctx.COLORS = generateColors(color) |
|||
} |
|||
|
|||
// Reset margins if needed
|
|||
if (ctx.INVALIDATED.margin) { |
|||
ctx.MARGINS = generateMargins(hideAxis) |
|||
} |
|||
|
|||
// Derived draw variables
|
|||
ctx.HEIGHT = Math.max(0, height - ctx.MARGINS.top - ctx.MARGINS.bottom) |
|||
ctx.WIDTH = Math.max(0, this._width - ctx.MARGINS.left - ctx.MARGINS.right) |
|||
|
|||
// Scales and areas
|
|||
const x = d3.scaleTime().range([0, ctx.WIDTH]) |
|||
const y = d3.scaleLinear().range([ctx.HEIGHT, 0]) |
|||
x.domain(d3.extent(ctx.DATA, d => d.parsedDate)) |
|||
y.domain([0, d3.max(ctx.DATA, d => d.value)]) |
|||
ctx.x = x |
|||
ctx.y = y |
|||
|
|||
// Reference to last tooltip, to prevent un-necessary re-render
|
|||
let lastDisplayedTooltip = null |
|||
|
|||
// Add/remove nodes depending on props
|
|||
refreshNodes({ ctx, node, props }) |
|||
|
|||
// Redraw
|
|||
refreshDraw({ ctx, props }) |
|||
|
|||
// Mouse handler
|
|||
mouseHandler && mouseHandler.remove() // eslint-disable-line no-unused-expressions
|
|||
if (interactive) { |
|||
mouseHandler = handleMouseEvents({ |
|||
ctx, |
|||
props, |
|||
shouldTooltipUpdate: d => d !== lastDisplayedTooltip, |
|||
onTooltipUpdate: d => (lastDisplayedTooltip = d), |
|||
renderTooltip, |
|||
}) |
|||
} |
|||
} |
|||
|
|||
this.refreshChart() |
|||
} |
|||
|
|||
render() { |
|||
const { height } = this.props |
|||
return ( |
|||
<div style={{ position: 'relative', height }} ref={n => (this._ruler = n)}> |
|||
<div |
|||
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }} |
|||
ref={n => (this._node = n)} |
|||
/> |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default Chart |
@ -0,0 +1,116 @@ |
|||
// @flow
|
|||
|
|||
import * as d3 from 'd3' |
|||
|
|||
import { colors as themeColors } from 'styles/theme' |
|||
|
|||
import type { Props } from '.' |
|||
import type { CTX } from './types' |
|||
|
|||
export default function refreshDraw({ ctx, props }: { ctx: CTX, props: Props }) { |
|||
const { NODES, WIDTH, HEIGHT, MARGINS, COLORS, INVALIDATED, DATA, x, y } = ctx |
|||
const { hideAxis, interactive, renderTickX, renderTickY, nbTicksX } = props |
|||
|
|||
const area = d3 |
|||
.area() |
|||
.x(d => x(d.parsedDate)) |
|||
.y0(HEIGHT) |
|||
.y1(d => y(d.value)) |
|||
|
|||
const valueline = d3 |
|||
.line() |
|||
.x(d => x(d.parsedDate)) |
|||
.y(d => y(d.value)) |
|||
|
|||
// Resize container
|
|||
NODES.svg |
|||
.attr('width', WIDTH + MARGINS.left + MARGINS.right) |
|||
.attr('height', HEIGHT + MARGINS.top + MARGINS.bottom) |
|||
|
|||
// Resize wrapper & axis
|
|||
NODES.wrapper.attr('transform', `translate(${MARGINS.left},${MARGINS.top})`) |
|||
|
|||
// Resize axis
|
|||
if (!hideAxis) { |
|||
NODES.axisBot.attr('transform', `translate(0,${HEIGHT + 10})`) |
|||
} |
|||
|
|||
if (INVALIDATED.color) { |
|||
if (interactive) { |
|||
// Update focus bar colors
|
|||
NODES.xBar.attr('stroke', COLORS.focusBar) |
|||
NODES.yBar.attr('stroke', COLORS.focusBar) |
|||
// Update dot color
|
|||
NODES.focus.attr('fill', COLORS.focus) |
|||
} |
|||
// Update gradient color
|
|||
NODES.gradientStart.attr('stop-color', COLORS.gradientStart) |
|||
NODES.gradientStop.attr('stop-color', COLORS.gradientStop) |
|||
|
|||
// Update line color
|
|||
NODES.line.attr('stroke', COLORS.line) |
|||
} |
|||
|
|||
// Hide interactive things
|
|||
if (interactive) { |
|||
NODES.focus.style('opacity', 0) |
|||
NODES.tooltip.style('opacity', 0) |
|||
NODES.xBar.style('opacity', 0) |
|||
NODES.yBar.style('opacity', 0) |
|||
} |
|||
|
|||
// Draw axis
|
|||
if (!hideAxis) { |
|||
NODES.axisLeft.call( |
|||
d3 |
|||
.axisLeft(y) |
|||
.ticks(3) |
|||
.tickFormat(val => (renderTickY ? renderTickY(val) : val)), |
|||
) |
|||
NODES.axisBot.call( |
|||
d3 |
|||
.axisBottom(x) |
|||
.ticks(nbTicksX) |
|||
.tickFormat(val => (renderTickX ? renderTickX(val) : val)), |
|||
) |
|||
stylizeAxis(NODES.axisLeft) |
|||
stylizeAxis(NODES.axisBot) |
|||
} |
|||
|
|||
// Draw ticks
|
|||
if (!hideAxis) { |
|||
const yTicks = d3 |
|||
.axisLeft(y) |
|||
.ticks(3) |
|||
.tickSize(-WIDTH) |
|||
.tickFormat('') |
|||
|
|||
NODES.yTicks.call(yTicks) |
|||
NODES.yTicks.select('.domain').remove() |
|||
|
|||
NODES.yTicks |
|||
.selectAll('.tick line') |
|||
.attr('stroke', 'rgba(0, 0, 0, 0.1)') |
|||
.attr('stroke-dasharray', '5, 5') |
|||
|
|||
NODES.yTicks |
|||
.selectAll('.tick:first-of-type line') |
|||
.attr('stroke-width', '2px') |
|||
.attr('stroke-dasharray', 'none') |
|||
} |
|||
|
|||
// Draw line and gradient
|
|||
NODES.fillArea.data([DATA]).attr('d', area) |
|||
NODES.line.data([DATA]).attr('d', valueline) |
|||
} |
|||
|
|||
function stylizeAxis(axis) { |
|||
axis.selectAll('.tick line').attr('stroke', 'none') |
|||
axis.selectAll('path').attr('stroke', 'none') |
|||
axis |
|||
.selectAll('text') |
|||
.attr('stroke', themeColors.grey) |
|||
.style('font-size', '12px') |
|||
.style('font-family', 'Open Sans') |
|||
.style('font-weight', 300) |
|||
} |
@ -0,0 +1,129 @@ |
|||
// @flow
|
|||
|
|||
import * as d3 from 'd3' |
|||
import d from 'debug' |
|||
|
|||
import type { Props } from '.' |
|||
import type { CTX } from './types' |
|||
|
|||
const debug = d('Chart') |
|||
|
|||
export default function refreshNodes({ ctx, node, props }: { ctx: CTX, node: any, props: Props }) { |
|||
const { NODES, COLORS } = ctx |
|||
const { hideAxis, interactive, id } = props |
|||
|
|||
// Container
|
|||
|
|||
ensure({ NODES, key: 'svg' }, () => d3.select(node).append('svg')) |
|||
ensure({ NODES, key: 'wrapper' }, () => NODES.svg.append('g')) |
|||
ensure({ NODES, key: 'defs' }, () => NODES.wrapper.append('defs')) |
|||
|
|||
// Axis & ticks
|
|||
|
|||
ensure({ onlyIf: !hideAxis, NODES, key: 'axisBot' }, () => NODES.wrapper.append('g')) |
|||
ensure({ onlyIf: !hideAxis, NODES, key: 'axisLeft' }, () => NODES.wrapper.append('g')) |
|||
ensure({ onlyIf: !hideAxis, NODES, key: 'yTicks' }, () => NODES.wrapper.append('g')) |
|||
|
|||
// Focus bars
|
|||
|
|||
ensure({ onlyIf: interactive, NODES, key: 'xBar' }, () => |
|||
NODES.wrapper |
|||
.append('line') |
|||
.attr('stroke', COLORS.focusBar) |
|||
.attr('stroke-width', '1px') |
|||
.attr('stroke-dasharray', '3, 2'), |
|||
) |
|||
|
|||
ensure({ onlyIf: interactive, NODES, key: 'yBar' }, () => |
|||
NODES.wrapper |
|||
.append('line') |
|||
.attr('stroke', COLORS.focusBar) |
|||
.attr('stroke-width', '1px') |
|||
.attr('stroke-dasharray', '3, 2'), |
|||
) |
|||
|
|||
// Gradient
|
|||
|
|||
ensure({ NODES, key: 'gradient' }, () => |
|||
NODES.defs |
|||
.append('linearGradient') |
|||
.attr('id', `gradient-${id || ''}`) |
|||
.attr('x1', '0%') |
|||
.attr('x2', '0%') |
|||
.attr('y1', '0%') |
|||
.attr('y2', '100%'), |
|||
) |
|||
|
|||
ensure({ NODES, key: 'gradientStart' }, () => |
|||
NODES.gradient |
|||
.append('stop') |
|||
.attr('stop-color', COLORS.gradientStart) |
|||
.attr('offset', '0'), |
|||
) |
|||
|
|||
ensure({ NODES, key: 'gradientStop' }, () => |
|||
NODES.gradient |
|||
.append('stop') |
|||
.attr('stop-color', COLORS.gradientStop) |
|||
.attr('offset', '1'), |
|||
) |
|||
|
|||
ensure({ NODES, key: 'fillArea' }, () => |
|||
NODES.wrapper.append('path').attr('fill', `url(#gradient-${id || ''})`), |
|||
) |
|||
|
|||
// Line
|
|||
|
|||
ensure({ NODES, key: 'line' }, () => |
|||
NODES.wrapper |
|||
.append('path') |
|||
.attr('fill', 'none') |
|||
.attr('stroke', COLORS.line) |
|||
.attr('stroke-width', '2px'), |
|||
) |
|||
|
|||
// Tooltip & focus point
|
|||
|
|||
ensure({ onlyIf: interactive, NODES, key: 'tooltip' }, () => |
|||
d3 |
|||
.select(node) |
|||
.append('div') |
|||
.style('position', 'absolute') |
|||
.style('top', '0') |
|||
.style('left', '0') |
|||
.style('pointer-events', 'none'), |
|||
) |
|||
|
|||
ensure({ onlyIf: interactive, NODES, key: 'focus' }, () => |
|||
NODES.wrapper |
|||
.append('g') |
|||
.append('circle') |
|||
.attr('fill', COLORS.focus) |
|||
.attr('r', 4), |
|||
) |
|||
|
|||
ctx.NODES = NODES |
|||
} |
|||
|
|||
function ensure( |
|||
{ onlyIf: condition = true, NODES, key }: { onlyIf?: boolean, NODES: Object, key: string }, |
|||
create, |
|||
) { |
|||
if (!condition && NODES[key]) { |
|||
remove(NODES, key) |
|||
} |
|||
if (condition && !NODES[key]) { |
|||
append(NODES, key, create()) |
|||
} |
|||
} |
|||
|
|||
function remove(NODES, key) { |
|||
debug(`Destroying ${key}`) |
|||
NODES[key].remove() |
|||
NODES[key] = null |
|||
} |
|||
|
|||
function append(NODES, key, node) { |
|||
debug(`Appending ${key}`) |
|||
NODES[key] = node |
|||
} |
@ -0,0 +1,70 @@ |
|||
// @flow
|
|||
|
|||
import React, { Component, Fragment } from 'react' |
|||
import Chance from 'chance' |
|||
import moment from 'moment' |
|||
import { storiesOf } from '@storybook/react' |
|||
import { boolean, number } from '@storybook/addon-knobs' |
|||
import { color } from '@storybook/addon-knobs/react' |
|||
|
|||
import Chart from 'components/base/NewChart' |
|||
|
|||
const stories = storiesOf('Components/Chart', module) |
|||
|
|||
const data = generateRandomData(365) |
|||
|
|||
type State = { |
|||
start: number, |
|||
stop: number, |
|||
} |
|||
|
|||
class Wrapper extends Component<any, State> { |
|||
state = { |
|||
start: 0, |
|||
stop: 365, |
|||
} |
|||
|
|||
handleChange = key => e => this.setState({ [key]: Number(e.target.value) }) |
|||
|
|||
render() { |
|||
const { start, stop } = this.state |
|||
return ( |
|||
<Fragment> |
|||
<input type="range" value={start} onChange={this.handleChange('start')} min={0} max={365} /> |
|||
<input |
|||
type="range" |
|||
value={stop} |
|||
style={{ marginLeft: 10 }} |
|||
onChange={this.handleChange('stop')} |
|||
min={0} |
|||
max={365} |
|||
/> |
|||
|
|||
<Chart |
|||
interactive={boolean('interactive', true)} |
|||
hideAxis={boolean('hideAxis', false)} |
|||
color={color('color', '#5f8ced')} |
|||
data={data.slice(start, stop)} |
|||
height={number('height', 300, { min: 100, max: 900 })} |
|||
/> |
|||
</Fragment> |
|||
) |
|||
} |
|||
} |
|||
|
|||
stories.add('default', () => <Wrapper />) |
|||
|
|||
function generateRandomData(n) { |
|||
const today = moment() |
|||
const day = moment(today).subtract(n, 'days') |
|||
const data = [] |
|||
const chance = new Chance() |
|||
while (!day.isSame(today)) { |
|||
data.push({ |
|||
date: day.format('YYYY-MM-DD'), |
|||
value: chance.integer({ min: 0, max: 50e3 }), |
|||
}) |
|||
day.add(1, 'day') |
|||
} |
|||
return data |
|||
} |
@ -0,0 +1,28 @@ |
|||
// @flow
|
|||
|
|||
export type Item = { |
|||
date: string, |
|||
value: number, |
|||
} |
|||
|
|||
type EnrichedItem = { |
|||
date: string, |
|||
value: number, |
|||
parsedDate: Date, |
|||
ref: Item, |
|||
} |
|||
|
|||
export type Data = Array<Item> |
|||
export type EnrichedData = Array<EnrichedItem> |
|||
|
|||
export type CTX = { |
|||
NODES: Object, |
|||
MARGINS: Object, |
|||
COLORS: Object, |
|||
INVALIDATED: Object, |
|||
HEIGHT: number, |
|||
WIDTH: number, |
|||
DATA: EnrichedData, |
|||
x: Function, |
|||
y: Function, |
|||
} |
Loading…
Reference in new issue