Browse Source

Merge pull request #212 from meriadec/master

Rewrite Chart component
master
Loëck Vézien 7 years ago
committed by GitHub
parent
commit
61d8abcb16
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      electron-builder.yml
  2. 1
      package.json
  3. 27
      src/components/BalanceSummary/index.js
  4. 9
      src/components/DashboardPage/AccountCard.js
  5. 10
      src/components/TransactionsList/ConfirmationCheck.js
  6. 6
      src/components/TransactionsList/index.js
  7. 67
      src/components/base/NewChart/Tooltip.js
  8. 116
      src/components/base/NewChart/handleMouseEvents.js
  9. 49
      src/components/base/NewChart/helpers.js
  10. 184
      src/components/base/NewChart/index.js
  11. 116
      src/components/base/NewChart/refreshDraw.js
  12. 129
      src/components/base/NewChart/refreshNodes.js
  13. 70
      src/components/base/NewChart/stories.js
  14. 28
      src/components/base/NewChart/types.js
  15. 41
      src/components/base/Tooltip/index.js
  16. 2
      src/helpers/balance.js
  17. 2
      src/index.ejs
  18. 2
      src/styles/reset.js
  19. 2
      static/i18n/en/transactionsList.yml
  20. 195
      yarn.lock

2
electron-builder.yml

@ -48,7 +48,7 @@ files:
# Exclude files
- "!report-*.html"
- "!node_modules/**/{CONTRIBUTORS,License,CNAME,AUTHOR,TODO,CONTRIBUTING,COPYING,INSTALL,NEWS,PORTING,Makefile,htdocs,CHANGELOG,ChangeLog,changelog,README,Readme,readme,test,sample,example,demo,composer.json,tsconfig.json,jsdoc.json,tslint.json,typings.json,gulpfile,bower.json,package-lock,Gruntfile,CMakeLists,karma.conf,yarn.lock}*"
- "!node_modules/**/{man,flow-typed,benchmark,node_modules,spec,cmake,browser,vagrant,doxy*,bin,obj,obj.target,example,examples,test,tests,__tests__,mocks,__mocks__,doc,docs,msvc,Xcode,CVS,RCS,SCCS,.storybook}{,/**/*}"
- "!node_modules/**/{man,flow-typed,benchmark,spec,cmake,browser,vagrant,doxy*,bin,obj,obj.target,example,examples,test,tests,__tests__,mocks,__mocks__,doc,docs,msvc,Xcode,CVS,RCS,SCCS,.storybook}{,/**/*}"
- "!node_modules/**/*.{conf,png,pc,coffee,txt,spec.js,ts,js.flow,html,def,jst,xml,ico,in,ac,sln,dsp,dsw,cmd,vcproj,vcxproj,vcxproj.filters,pdb,exp,obj,lib,map,md,sh,gypi,gyp,h,cpp,yml,log,tlog,Makefile,mk,c,cc,rc,xcodeproj,xcconfig,d.ts,yaml,hpp}"
# Exclude modules

1
package.json

@ -55,6 +55,7 @@
"bs58check": "^2.1.1",
"color": "^3.0.0",
"cross-env": "^5.1.3",
"d3": "^4.13.0",
"debug": "^3.1.0",
"downshift": "^1.30.0",
"electron-store": "^1.3.0",

27
src/components/BalanceSummary/index.js

@ -7,9 +7,7 @@ import { formatShort, getFiatUnit } from '@ledgerhq/currencies'
import type { Accounts } from 'types/common'
import { space } from 'styles/theme'
import { AreaChart } from 'components/base/Chart'
import Chart from 'components/base/NewChart'
import Box, { Card } from 'components/base/Box'
import CalculateBalance from 'components/CalculateBalance'
import FormattedVal from 'components/base/FormattedVal'
@ -76,33 +74,24 @@ const BalanceSummary = ({
})}
</Box>
)}
<Box ff="Open Sans" fontSize={4} color="graphite">
<AreaChart
<Box ff="Open Sans" fontSize={4} color="graphite" pt={6}>
<Chart
id={chartId}
color={chartColor}
data={allBalances}
height={250}
id={chartId}
padding={{
top: space[6],
bottom: space[6],
left: space[6] * 2,
right: space[6],
}}
strokeWidth={2}
renderLabels={d => d.y}
nbTicksX={getTickCountX(selectedTime)}
renderTickX={renderTickX(selectedTime)}
renderTickY={t => formatShort(unit, t)}
renderTooltip={d => (
<FormattedVal
alwaysShowSign={false}
color="white"
showCode
fiat={counterValue}
val={d}
val={d.value}
/>
)}
renderTickX={renderTickX(selectedTime)}
renderTickY={t => formatShort(unit, t)}
tickCountX={getTickCountX(selectedTime)}
tickCountY={4}
/>
</Box>
</Fragment>

9
src/components/DashboardPage/AccountCard.js

@ -5,7 +5,7 @@ import { getIconByCoinType } from '@ledgerhq/currencies/react'
import type { Account } from 'types/common'
import { SimpleAreaChart } from 'components/base/Chart'
import Chart from 'components/base/NewChart'
import Bar from 'components/base/Bar'
import Box, { Card } from 'components/base/Box'
import CalculateBalance from 'components/CalculateBalance'
@ -84,14 +84,13 @@ const AccountCard = ({
/>
</Box>
</Box>
<SimpleAreaChart
<Chart
data={allBalances}
color={account.currency.color}
height={52}
hideAxis
interactive={false}
id={`account-chart-${account.id}`}
linearGradient={[[5, 0.2], [100, 0]]}
simple
strokeWidth={1.5}
/>
</Box>
)}

10
src/components/TransactionsList/ConfirmationCheck.js

@ -5,6 +5,8 @@ import styled from 'styled-components'
import { rgba } from 'styles/helpers'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import Tooltip from 'components/base/Tooltip'
import IconCheck from 'icons/Check'
@ -24,13 +26,19 @@ const Container = styled(Box).attrs({
const ConfirmationCheck = ({
confirmations,
minConfirmations,
t,
}: {
confirmations: number,
minConfirmations: number,
t: T,
}) => {
const isConfirmed = confirmations >= minConfirmations
return (
<Tooltip render={() => (isConfirmed ? 'Confirmed' : 'Not confirmed')}>
<Tooltip
render={() =>
isConfirmed ? t('transactionsList:confirmed') : t('transactionsList:notConfirmed')
}
>
<Container isConfirmed={isConfirmed}>
{isConfirmed ? <IconCheck width={12} /> : <IconClock width={12} />}
</Container>

6
src/components/TransactionsList/index.js

@ -175,7 +175,11 @@ const Transaction = ({
/>
</Cell>
<Cell size={CONFIRMATION_COL_SIZE} px={0} align="center" justify="flex-start">
<ConfirmationCheck minConfirmations={minConfirmations} confirmations={tx.confirmations} />
<ConfirmationCheck
minConfirmations={minConfirmations}
confirmations={tx.confirmations}
t={t}
/>
</Cell>
</TransactionRaw>
)

67
src/components/base/NewChart/Tooltip.js

@ -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

116
src/components/base/NewChart/handleMouseEvents.js

@ -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
}

49
src/components/base/NewChart/helpers.js

@ -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)
}

184
src/components/base/NewChart/index.js

@ -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

116
src/components/base/NewChart/refreshDraw.js

@ -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)
}

129
src/components/base/NewChart/refreshNodes.js

@ -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
}

70
src/components/base/NewChart/stories.js

@ -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
}

28
src/components/base/NewChart/types.js

@ -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,
}

41
src/components/base/Tooltip/index.js

@ -4,7 +4,7 @@ import React, { PureComponent } from 'react'
import styled from 'styled-components'
import tippy from 'tippy.js'
import { space } from 'styles/theme'
import { space, colors } from 'styles/theme'
import Box from 'components/base/Box'
@ -16,15 +16,36 @@ const Template = styled.div`
display: none;
`
export const TooltipContainer = styled(Box).attrs({
bg: 'dark',
borderRadius: 1,
color: 'white',
ff: 'Open Sans|SemiBold',
fontSize: 2,
px: 2,
py: 1,
})``
export const TooltipContainer = ({
children,
innerRef,
style,
}: {
children: any,
innerRef?: Function,
style?: Object,
}) => (
<div
ref={innerRef}
style={{
background: colors.dark,
borderRadius: 4,
color: 'white',
fontFamily: 'Open Sans',
fontWeight: 600,
fontSize: 10,
padding: '5px 10px 5px 10px',
...style,
}}
>
{children}
</div>
)
TooltipContainer.defaultProps = {
innerRef: undefined,
style: undefined,
}
type Props = {
offset?: Array<number>,

2
src/helpers/balance.js

@ -140,7 +140,7 @@ export default function calculateBalance(props: CalculateBalance) {
accounts: props.accounts,
counterValues: props.counterValues,
interval,
}).map(e => ({ name: e.date, value: e.balance }))
}).map(e => ({ date: e.date, value: e.balance }))
const firstNonEmptyDay = find(allBalances, e => e.value)
const refBalance = firstNonEmptyDay ? firstNonEmptyDay.value : 0

2
src/index.ejs

@ -53,8 +53,6 @@
const initApp = (options = {}) => {
const { force = false } = options
appEl.style.display = 'flex'
if (force) {
preloadEl.remove()
} else {

2
src/styles/reset.js

@ -21,7 +21,7 @@ body {
}
#app {
display: none;
display: flex;
flex-direction: column;
min-height: 100vh;
}

2
static/i18n/en/transactionsList.yml

@ -5,3 +5,5 @@ amount: Amount
from: From
to: To
showMore: Show more
confirmed: Confirmed
notConfirmed: Not confirmed

195
yarn.lock

@ -2655,6 +2655,10 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
commander@2, commander@^2.13.0:
version "2.15.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.0.tgz#ad2a23a1c3b036e392469b8012cec6b33b4c1322"
commander@2.14.x, commander@^2.11.0, commander@^2.12.2, commander@^2.14.1, commander@^2.9.0, commander@~2.14.1:
version "2.14.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa"
@ -2663,10 +2667,6 @@ commander@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d"
commander@^2.13.0:
version "2.15.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.0.tgz#ad2a23a1c3b036e392469b8012cec6b33b4c1322"
commander@~2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
@ -3095,37 +3095,121 @@ cyclist@~0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
d3-array@^1.2.0:
d3-array@1, d3-array@1.2.1, d3-array@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
d3-collection@1:
d3-axis@1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa"
d3-brush@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4"
dependencies:
d3-dispatch "1"
d3-drag "1"
d3-interpolate "1"
d3-selection "1"
d3-transition "1"
d3-chord@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.4.tgz#7dec4f0ba886f713fe111c45f763414f6f74ca2c"
dependencies:
d3-array "1"
d3-path "1"
d3-collection@1, d3-collection@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
d3-color@1:
d3-color@1, d3-color@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
d3-ease@^1.0.0:
d3-dispatch@1, d3-dispatch@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
d3-drag@1, d3-drag@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.1.tgz#df8dd4c502fb490fc7462046a8ad98a5c479282d"
dependencies:
d3-dispatch "1"
d3-selection "1"
d3-dsv@1, d3-dsv@1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.0.8.tgz#907e240d57b386618dc56468bacfe76bf19764ae"
dependencies:
commander "2"
iconv-lite "0.4"
rw "1"
d3-ease@1, d3-ease@1.0.3, d3-ease@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
d3-format@1:
d3-force@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.1.0.tgz#cebf3c694f1078fcc3d4daf8e567b2fbd70d4ea3"
dependencies:
d3-collection "1"
d3-dispatch "1"
d3-quadtree "1"
d3-timer "1"
d3-format@1, d3-format@1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a"
d3-interpolate@1, d3-interpolate@^1.1.1:
d3-geo@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.1.tgz#157e3b0f917379d0f73bebfff3be537f49fa7356"
dependencies:
d3-array "1"
d3-hierarchy@1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26"
d3-interpolate@1, d3-interpolate@1.1.6, d3-interpolate@^1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.6.tgz#2cf395ae2381804df08aa1bf766b7f97b5f68fb6"
dependencies:
d3-color "1"
d3-path@1:
d3-path@1, d3-path@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764"
d3-scale@^1.0.0:
d3-polygon@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.3.tgz#16888e9026460933f2b179652ad378224d382c62"
d3-quadtree@1, d3-quadtree@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.3.tgz#ac7987e3e23fe805a990f28e1b50d38fcb822438"
d3-queue@3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-3.0.7.tgz#c93a2e54b417c0959129d7d73f6cf7d4292e7618"
d3-random@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.0.tgz#6642e506c6fa3a648595d2b2469788a8d12529d3"
d3-request@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-request/-/d3-request-1.0.6.tgz#a1044a9ef4ec28c824171c9379fae6d79474b19f"
dependencies:
d3-collection "1"
d3-dispatch "1"
d3-dsv "1"
xmlhttprequest "1"
d3-scale@1.0.7, d3-scale@^1.0.0:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
dependencies:
@ -3137,30 +3221,90 @@ d3-scale@^1.0.0:
d3-time "1"
d3-time-format "2"
d3-shape@^1.0.0, d3-shape@^1.2.0:
d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d"
d3-shape@1.2.0, d3-shape@^1.0.0, d3-shape@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
dependencies:
d3-path "1"
d3-time-format@2:
d3-time-format@2, d3-time-format@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
dependencies:
d3-time "1"
d3-time@1:
d3-time@1, d3-time@1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
d3-timer@^1.0.0:
d3-timer@1, d3-timer@1.0.7, d3-timer@^1.0.0:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
d3-voronoi@^1.1.2:
d3-transition@1, d3-transition@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039"
dependencies:
d3-color "1"
d3-dispatch "1"
d3-ease "1"
d3-interpolate "1"
d3-selection "^1.1.0"
d3-timer "1"
d3-voronoi@1.1.2, d3-voronoi@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c"
d3-zoom@1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63"
dependencies:
d3-dispatch "1"
d3-drag "1"
d3-interpolate "1"
d3-selection "1"
d3-transition "1"
d3@^4.13.0:
version "4.13.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-4.13.0.tgz#ab236ff8cf0cfc27a81e69bf2fb7518bc9b4f33d"
dependencies:
d3-array "1.2.1"
d3-axis "1.0.8"
d3-brush "1.0.4"
d3-chord "1.0.4"
d3-collection "1.0.4"
d3-color "1.0.3"
d3-dispatch "1.0.3"
d3-drag "1.2.1"
d3-dsv "1.0.8"
d3-ease "1.0.3"
d3-force "1.1.0"
d3-format "1.2.2"
d3-geo "1.9.1"
d3-hierarchy "1.1.5"
d3-interpolate "1.1.6"
d3-path "1.0.5"
d3-polygon "1.0.3"
d3-quadtree "1.0.3"
d3-queue "3.0.7"
d3-random "1.1.0"
d3-request "1.0.6"
d3-scale "1.0.7"
d3-selection "1.3.0"
d3-shape "1.2.0"
d3-time "1.0.8"
d3-time-format "2.1.1"
d3-timer "1.0.7"
d3-transition "1.1.1"
d3-voronoi "1.1.2"
d3-zoom "1.7.1"
d@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
@ -3572,7 +3716,7 @@ electron-builder-lib@~20.2.0:
semver "^5.5.0"
temp-file "^3.1.1"
electron-builder@^20.0.4, electron-builder@^20.4.0:
electron-builder@^20.4.0:
version "20.4.0"
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-20.4.0.tgz#d1393719339c17dd7c2dd16d58b4e138ca6646ce"
dependencies:
@ -3736,7 +3880,7 @@ electron-webpack@1.13.0:
webpack-merge "^4.1.1"
yargs "^11.0.0"
electron@1.8.3, electron@^1.8.2:
electron@1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/electron/-/electron-1.8.3.tgz#001416ea3a25ce594e317cb5531bc41eadd22f7f"
dependencies:
@ -5269,7 +5413,7 @@ i18next@^10.5.0:
version "10.5.0"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-10.5.0.tgz#a2a90e67774fa85b8ff9cd0063e697e482440de7"
iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@^0.4.19, iconv-lite@~0.4.13:
iconv-lite@0.4, iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@^0.4.19, iconv-lite@~0.4.13:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
@ -6326,9 +6470,6 @@ ledger-test-library@KhalilBellakrid/ledger-test-library-nodejs#7d37482:
dependencies:
axios "^0.17.1"
bindings "^1.3.0"
electron "^1.8.2"
electron-builder "^20.0.4"
electron-rebuild "^1.7.3"
nan "^2.6.2"
prebuild-install "^2.2.2"
@ -8949,6 +9090,10 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
rw@1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
rx-lite-aggregates@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
@ -10686,6 +10831,10 @@ xmldom@0.1.x:
version "0.1.27"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
xmlhttprequest@1:
version "1.8.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
xpipe@*:
version "1.0.5"
resolved "https://registry.yarnpkg.com/xpipe/-/xpipe-1.0.5.tgz#8dd8bf45fc3f7f55f0e054b878f43a62614dafdf"

Loading…
Cancel
Save