Browse Source

Merge pull request #356 from meriadec/update-chart

Update chart
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
688455b200
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 34
      src/components/BalanceSummary/index.js
  2. 22
      src/components/CalculateBalance.js
  3. 4
      src/components/CounterValue/index.js
  4. 4
      src/components/DashboardPage/AccountCard.js
  5. 20
      src/components/DevTools.js
  6. 6
      src/components/TopBar/ItemContainer.js
  7. 4
      src/components/TopBar/index.js
  8. 96
      src/components/base/Chart/Tooltip.js
  9. 35
      src/components/base/Chart/handleMouseEvents.js
  10. 2
      src/components/base/Chart/helpers.js
  11. 15
      src/components/base/Chart/index.js
  12. 12
      src/components/base/Chart/refreshDraw.js
  13. 23
      src/components/base/Chart/refreshNodes.js
  14. 44
      src/components/base/Chart/stories.js
  15. 1
      src/components/base/Chart/types.js

34
src/components/BalanceSummary/index.js

@ -35,7 +35,8 @@ const BalanceSummary = ({
renderHeader,
selectedTime,
}: Props) => {
const unit = getFiatCurrencyByTicker(counterValue).units[0]
const currency = getFiatCurrencyByTicker(counterValue)
const account = accounts.length === 1 ? accounts[0] : undefined
return (
<Card p={0} py={6}>
<CalculateBalance
@ -61,21 +62,30 @@ const BalanceSummary = ({
<Box ff="Open Sans" fontSize={4} color="graphite" pt={6}>
<Chart
id={chartId}
account={account}
color={chartColor}
data={balanceHistory}
height={250}
unit={unit}
currency={currency}
tickXScale={selectedTime}
renderTooltip={d =>
isAvailable ? (
<FormattedVal
alwaysShowSign={false}
color="white"
showCode
fiat={counterValue}
val={d.value}
/>
) : null
renderTooltip={
isAvailable && !account
? d => (
<Fragment>
<FormattedVal
alwaysShowSign={false}
fontSize={5}
color="dark"
showCode
fiat={counterValue}
val={d.value}
/>
<Box ff="Open Sans|Regular" color="grey" fontSize={3} mt={2}>
{d.date.toISOString().substr(0, 10)}
</Box>
</Fragment>
)
: undefined
}
/>
</Box>

22
src/components/CalculateBalance.js

@ -4,7 +4,7 @@
import { PureComponent } from 'react'
import { connect } from 'react-redux'
import type { Account, BalanceHistory } from '@ledgerhq/live-common/lib/types'
import type { Account } from '@ledgerhq/live-common/lib/types'
import { getBalanceHistorySum } from '@ledgerhq/live-common/lib/helpers/account'
import CounterValues from 'helpers/countervalues'
import { exchangeSettingsForAccountSelector, counterValueCurrencySelector } from 'reducers/settings'
@ -16,8 +16,14 @@ type OwnProps = {
children: Props => *,
}
type Item = {
date: Date,
value: number,
originalValue: number,
}
type Props = OwnProps & {
balanceHistory: BalanceHistory,
balanceHistory: Item[],
balanceStart: number,
balanceEnd: number,
isAvailable: boolean,
@ -26,10 +32,18 @@ type Props = OwnProps & {
const mapStateToProps = (state: State, props: OwnProps) => {
const counterValueCurrency = counterValueCurrencySelector(state)
let isAvailable = true
// create array of original values, used to reconciliate
// with counter values after calculation
const originalValues = []
const balanceHistory = getBalanceHistorySum(
props.accounts,
props.daysCount,
(account, value, date) => {
// keep track of original value
originalValues.push(value)
const cv = CounterValues.calculateSelector(state, {
value,
date,
@ -43,7 +57,11 @@ const mapStateToProps = (state: State, props: OwnProps) => {
}
return cv
},
).map((item, i) =>
// reconciliate balance history with original values
({ ...item, originalValue: originalValues[i] || 0 }),
)
return {
isAvailable,
balanceHistory,

4
src/components/CounterValue/index.js

@ -49,7 +49,9 @@ const mapStateToProps = (state: State, props: OwnProps) => {
class CounterValue extends PureComponent<Props> {
render() {
const { value, counterValueCurrency, date, ...props } = this.props
if (!value && value !== 0) return null
if (!value && value !== 0) {
return null
}
return (
<FormattedVal
val={value}

4
src/components/DashboardPage/AccountCard.js

@ -86,9 +86,9 @@ const AccountCard = ({
color={account.currency.color}
height={52}
hideAxis
interactive={false}
isInteractive={false}
id={`account-chart-${account.id}`}
unit={account.unit}
account={account}
/>
</Box>
)}

20
src/components/DevTools.js

@ -254,26 +254,8 @@ class DevTools extends PureComponent<any, State> {
color="#8884d8"
height={50}
hideAxis
interactive={false}
isInteractive={false}
/>
{/* <WrapperChart
height={50}
render={({ height, width }) => (
<VictoryArea
data={cpuUsage[k]}
y="value"
style={{
data: {
stroke: '#8884d8',
fill: '#8884d8',
},
}}
height={height}
width={width}
padding={{ top: 10, right: 0, left: 0, bottom: 0 }}
/>
)}
/> */}
</Box>
</Box>
))}

6
src/components/TopBar/ItemContainer.js

@ -8,13 +8,13 @@ export default styled(Box).attrs({
px: 2,
ml: 0,
justifyContent: 'center',
cursor: p => (p.interactive ? 'pointer' : 'default'),
cursor: p => (p.isInteractive ? 'pointer' : 'default'),
})`
opacity: 0.7;
&:hover {
opacity: ${p => (p.interactive ? 0.85 : 0.7)};
opacity: ${p => (p.isInteractive ? 0.85 : 0.7)};
}
&:active {
opacity: ${p => (p.interactive ? 1 : 0.7)};
opacity: ${p => (p.isInteractive ? 1 : 0.7)};
}
`

4
src/components/TopBar/index.js

@ -93,7 +93,7 @@ class TopBar extends PureComponent<Props> {
<Box justifyContent="center">
<Bar />
</Box>
<ItemContainer interactive onClick={this.navigateToSettings}>
<ItemContainer isInteractive onClick={this.navigateToSettings}>
<IconSettings size={16} />
</ItemContainer>
{hasPassword && ( // FIXME this should be a dedicated component. therefore this component don't need to connect()
@ -101,7 +101,7 @@ class TopBar extends PureComponent<Props> {
<Box justifyContent="center">
<Bar />
</Box>
<ItemContainer interactive justifyContent="center" onClick={this.handleLock}>
<ItemContainer isInteractive justifyContent="center" onClick={this.handleLock}>
<IconLock size={16} />
</ItemContainer>
</Fragment>

96
src/components/base/Chart/Tooltip.js

@ -1,47 +1,35 @@
// @flow
import React from 'react'
import React, { Fragment } from 'react'
import styled from 'styled-components'
import type { Unit } from '@ledgerhq/live-common/lib/types'
import type { Account } from '@ledgerhq/live-common/lib/types'
import { colors as themeColors } from 'styles/theme'
import { TooltipContainer } from 'components/base/Tooltip'
import FormattedVal from 'components/base/FormattedVal'
import Box from 'components/base/Box'
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 Container = styled(Box).attrs({
px: 4,
py: 3,
align: 'center',
})`
background: white;
border: 1px solid #d8d8d8;
border-radius: 4px;
width: 150px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.03);
`
const Tooltip = ({
d,
item,
renderTooltip,
fiat,
unit,
account,
}: {
d: Item,
item: Item,
renderTooltip?: Function,
fiat?: string,
unit?: Unit,
account?: Account,
}) => (
<div style={{ position: 'relative' }}>
<div
@ -51,32 +39,40 @@ const Tooltip = ({
left: 0,
transform: `translate3d(-50%, 0, 0)`,
whiteSpace: 'nowrap',
marginBottom: 5,
marginBottom: -5,
}}
>
<TooltipContainer style={{ textAlign: 'center' }}>
<Container style={{ textAlign: 'center' }}>
{renderTooltip ? (
renderTooltip(d)
renderTooltip(item)
) : (
<FormattedVal
alwaysShowSign={false}
color="white"
showCode
fiat={fiat}
unit={unit}
val={d.value}
/>
<Fragment>
<FormattedVal
color="dark"
fontSize={5}
alwaysShowSign={false}
showCode
fiat="USD"
val={item.value}
/>
{account && (
<FormattedVal
color="grey"
fontSize={3}
alwaysShowSign={false}
showCode
unit={account.unit}
val={item.originalValue}
/>
)}
<Box ff="Open Sans|Regular" color="grey" fontSize={3} mt={2}>
{item.date.toISOString().substr(0, 10)}
</Box>
</Fragment>
)}
</TooltipContainer>
<Arrow />
</Container>
</div>
</div>
)
Tooltip.defaultProps = {
renderTooltip: undefined,
fiat: undefined,
unit: undefined,
}
export default Tooltip

35
src/components/base/Chart/handleMouseEvents.js

@ -29,7 +29,7 @@ export default function handleMouseEvents({
renderTooltip?: Function,
}) {
const { MARGINS, HEIGHT, WIDTH, NODES, DATA, x, y } = ctx
const { hideAxis, unit } = props
const { account } = props
const bisectDate = d3.bisector(d => d.parsedDate).left
@ -65,21 +65,15 @@ export default function handleMouseEvents({
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)`)
.style('transform', `translate3d(${MARGINS.left + x(d.parsedDate)}px, 0, 0)`)
NODES.focus.style('opacity', 1)
if (!hideAxis) {
NODES.xBar.style('opacity', 1)
NODES.yBar.style('opacity', 1)
}
NODES.xBar.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)
}
NODES.xBar.style('opacity', 0)
}
function mouseMove() {
@ -97,24 +91,17 @@ export default function handleMouseEvents({
renderToString(
<Provider store={createStore({})}>
<ThemeProvider theme={theme}>
<Tooltip unit={unit} renderTooltip={renderTooltip} d={d.ref} />
<Tooltip account={account} renderTooltip={renderTooltip} item={d.ref} />
</ThemeProvider>
</Provider>,
),
)
.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))
}
.style('transform', `translate3d(${MARGINS.left + x(d.parsedDate)}px, 0, 0)`)
NODES.xBar
.attr('x1', x(d.parsedDate))
.attr('x2', x(d.parsedDate))
.attr('y1', -30) // ensure that xbar is covered
.attr('y2', HEIGHT)
}
return node

2
src/components/base/Chart/helpers.js

@ -16,7 +16,7 @@ export function generateColors(color) {
focus: color,
gradientStart: cColor.fade(0.7),
gradientStop: cColor.fade(1),
focusBar: cColor.fade(0.5),
focusBar: '#d8d8d8',
}
}

15
src/components/base/Chart/index.js

@ -18,7 +18,7 @@
*
* <Chart
* data={data}
* interactive // Handle mouse events, display tooltip etc.
* isInteractive // Handle mouse events, display tooltip etc.
* color="#5f8ced" // Main color for line, gradient, etc.
* height={300} // Fix height. Width is responsive to container.
* />
@ -37,7 +37,7 @@ import React, { PureComponent } from 'react'
import * as d3 from 'd3'
import noop from 'lodash/noop'
import type { Unit } from '@ledgerhq/live-common/lib/types'
import type { Account } from '@ledgerhq/live-common/lib/types'
import refreshNodes from './refreshNodes'
import refreshDraw from './refreshDraw'
@ -48,7 +48,7 @@ import type { Data } from './types'
export type Props = {
data: Data, // eslint-disable-line react/no-unused-prop-types
unit?: Unit, // eslint-disable-line react/no-unused-prop-types
account?: Account, // eslint-disable-line react/no-unused-prop-types
id?: string, // eslint-disable-line react/no-unused-prop-types
height?: number,
@ -56,7 +56,7 @@ export type Props = {
color?: string, // eslint-disable-line react/no-unused-prop-types
hideAxis?: boolean, // eslint-disable-line react/no-unused-prop-types
dateFormat?: string, // eslint-disable-line react/no-unused-prop-types
interactive?: boolean, // eslint-disable-line react/no-unused-prop-types
isInteractive?: boolean, // eslint-disable-line react/no-unused-prop-types
renderTooltip?: Function, // eslint-disable-line react/no-unused-prop-types
}
@ -67,9 +67,8 @@ class Chart extends PureComponent<Props> {
height: 400,
hideAxis: false,
id: 'chart',
interactive: true,
isInteractive: true,
tickXScale: 'month',
unit: undefined,
}
componentDidMount() {
@ -113,7 +112,7 @@ class Chart extends PureComponent<Props> {
this.refreshChart = prevProps => {
const { _node: node, props } = this
const { data: raw, color, height, hideAxis, interactive, renderTooltip } = props
const { data: raw, color, height, hideAxis, isInteractive, renderTooltip } = props
ctx.DATA = enrichData(raw)
@ -157,7 +156,7 @@ class Chart extends PureComponent<Props> {
// Mouse handler
mouseHandler && mouseHandler.remove() // eslint-disable-line no-unused-expressions
if (interactive) {
if (isInteractive) {
mouseHandler = handleMouseEvents({
ctx,
props,

12
src/components/base/Chart/refreshDraw.js

@ -31,11 +31,11 @@ function getRenderTickX(selectedTime) {
export default function refreshDraw({ ctx, props }: { ctx: CTX, props: Props }) {
const { NODES, WIDTH, HEIGHT, MARGINS, COLORS, INVALIDATED, DATA, x, y } = ctx
const { hideAxis, interactive, tickXScale, unit } = props
const { hideAxis, isInteractive, tickXScale, account } = props
const nbTicksX = getTickXCount(tickXScale)
const renderTickX = getRenderTickX(tickXScale)
const renderTickY = t => (unit ? formatShort(unit, t) : t)
const renderTickY = t => (account ? formatShort(account.unit, t) : t)
const area = d3
.area()
@ -62,12 +62,11 @@ export default function refreshDraw({ ctx, props }: { ctx: CTX, props: Props })
}
if (INVALIDATED.color) {
if (interactive) {
if (isInteractive) {
// 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)
NODES.focus.attr('stroke', COLORS.focus)
}
// Update gradient color
NODES.gradientStart.attr('stop-color', COLORS.gradientStart)
@ -78,11 +77,10 @@ export default function refreshDraw({ ctx, props }: { ctx: CTX, props: Props })
}
// Hide interactive things
if (interactive) {
if (isInteractive) {
NODES.focus.style('opacity', 0)
NODES.tooltip.style('opacity', 0)
NODES.xBar.style('opacity', 0)
NODES.yBar.style('opacity', 0)
}
// Draw axis

23
src/components/base/Chart/refreshNodes.js

@ -10,7 +10,7 @@ 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
const { hideAxis, isInteractive, id } = props
// Container
@ -26,20 +26,11 @@ export default function refreshNodes({ ctx, node, props }: { ctx: CTX, node: any
// Focus bars
ensure({ onlyIf: interactive, NODES, key: 'xBar' }, () =>
ensure({ onlyIf: isInteractive, 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'),
.attr('stroke-width', '1px'),
)
// Gradient
@ -84,7 +75,7 @@ export default function refreshNodes({ ctx, node, props }: { ctx: CTX, node: any
// Tooltip & focus point
ensure({ onlyIf: interactive, NODES, key: 'tooltip' }, () =>
ensure({ onlyIf: isInteractive, NODES, key: 'tooltip' }, () =>
d3
.select(node)
.append('div')
@ -94,11 +85,13 @@ export default function refreshNodes({ ctx, node, props }: { ctx: CTX, node: any
.style('pointer-events', 'none'),
)
ensure({ onlyIf: interactive, NODES, key: 'focus' }, () =>
ensure({ onlyIf: isInteractive, NODES, key: 'focus' }, () =>
NODES.wrapper
.append('g')
.append('circle')
.attr('fill', COLORS.focus)
.attr('fill', 'white')
.attr('stroke', COLORS.focus)
.attr('stroke-width', 2)
.attr('r', 4),
)

44
src/components/base/Chart/stories.js

@ -9,11 +9,17 @@ import { boolean, number } from '@storybook/addon-knobs'
import { color } from '@storybook/addon-knobs/react'
import Chart from 'components/base/Chart'
import Box from 'components/base/Box'
const stories = storiesOf('Components/base', module)
const data = generateRandomData(365)
const unit = getCryptoCurrencyById('bitcoin').units[0]
const currency = getCryptoCurrencyById('bitcoin')
// $FlowFixMe
const fakeAccount = {
currency,
}
type State = {
start: number,
@ -32,23 +38,31 @@ class Wrapper extends Component<any, State> {
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}
/>
<Box mb={8} horizontal>
<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}
/>
</Box>
<Chart
interactive={boolean('interactive', true)}
hideAxis={boolean('hideAxis', false)}
isInteractive={boolean('isInteractive', true)}
hideAxis={boolean('hideAxis', true)}
color={color('color', '#5f8ced')}
data={data.slice(start, stop)}
height={number('height', 300)}
unit={unit}
account={fakeAccount}
/>
</Fragment>
)
@ -63,9 +77,11 @@ function generateRandomData(n) {
const data = []
const chance = new Chance()
while (!day.isSame(today)) {
const value = chance.integer({ min: 0.5e8, max: 1e8 })
data.push({
date: day.toDate(),
value: chance.integer({ min: 0.5e8, max: 1e8 }),
value,
originalValue: value,
})
day.add(1, 'day')
}

1
src/components/base/Chart/types.js

@ -3,6 +3,7 @@
export type Item = {
date: Date,
value: number,
originalValue: number,
}
type EnrichedItem = {

Loading…
Cancel
Save