Merge pull request #1013 from chaotixkilla/fix-change-dashboard-ui

Dashboard UI changes
This commit is contained in:
Rafael Taranto 2021-12-23 11:02:58 +00:00 committed by GitHub
commit dd09ae5a7e
12 changed files with 614 additions and 424 deletions

View file

@ -7,8 +7,8 @@ import { useHistory } from 'react-router-dom'
import { P } from 'src/components/typography/index' import { P } from 'src/components/typography/index'
import { ReactComponent as Wrench } from 'src/styling/icons/action/wrench/zodiac.svg' import { ReactComponent as Wrench } from 'src/styling/icons/action/wrench/zodiac.svg'
import { ReactComponent as LinkIcon } from 'src/styling/icons/button/link/zodiac.svg'
import { ReactComponent as CashBoxEmpty } from 'src/styling/icons/cassettes/cashbox-empty.svg' import { ReactComponent as CashBoxEmpty } from 'src/styling/icons/cassettes/cashbox-empty.svg'
import { ReactComponent as AlertLinkIcon } from 'src/styling/icons/month arrows/right.svg'
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg' import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg'
import styles from './Alerts.styles' import styles from './Alerts.styles'
@ -49,7 +49,7 @@ const AlertsTable = ({ numToRender, alerts, machines }) => {
<Wrench style={{ height: 23, width: 23, marginRight: 8 }} /> <Wrench style={{ height: 23, width: 23, marginRight: 8 }} />
)} )}
<P className={classes.listItemText}>{alertMessage(alert)}</P> <P className={classes.listItemText}>{alertMessage(alert)}</P>
<LinkIcon <AlertLinkIcon
className={classes.linkIcon} className={classes.linkIcon}
onClick={() => history.push(links[alert.type] || '/dashboard')} onClick={() => history.push(links[alert.type] || '/dashboard')}
/> />

View file

@ -1,7 +1,6 @@
import { useQuery } from '@apollo/react-hooks' import { useQuery } from '@apollo/react-hooks'
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
@ -13,6 +12,7 @@ import { H1, Info2, TL2, Label1 } from 'src/components/typography'
import AddMachine from 'src/pages/AddMachine' import AddMachine from 'src/pages/AddMachine'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { errorColor } from 'src/styling/variables'
import styles from './Dashboard.styles' import styles from './Dashboard.styles'
import Footer from './Footer' import Footer from './Footer'
@ -46,20 +46,20 @@ const Dashboard = () => {
<> <>
<TitleSection title="Dashboard"> <TitleSection title="Dashboard">
<div className={classes.headerLabels}> <div className={classes.headerLabels}>
<> <div>
<div
className={classnames(
classes.headerLabelContainer,
classes.headerLabelContainerMargin
)}>
<TxOutIcon />
<span className={classes.headerLabelSpan}>Cash-out</span>
</div>
<div className={classes.headerLabelContainer}>
<TxInIcon /> <TxInIcon />
<span className={classes.headerLabelSpan}>Cash-in</span> <span>Cash-in</span>
</div>
<div>
<TxOutIcon />
<span>Cash-out</span>
</div>
<div>
<svg width={12} height={12}>
<rect width={12} height={12} rx={3} fill={errorColor} />
</svg>
<span>Action Required</span>
</div> </div>
</>
</div> </div>
</TitleSection> </TitleSection>
<div className={classes.root}> <div className={classes.root}>

View file

@ -12,18 +12,26 @@ const { label1 } = typographyStyles
const styles = { const styles = {
headerLabels: { headerLabels: {
display: 'flex', display: 'flex',
flexDirection: 'row' flexDirection: 'row',
}, '& > div:first-child': {
headerLabelContainerMargin: {
marginRight: 24
},
headerLabelContainer: {
display: 'flex', display: 'flex',
alignItems: 'center' alignItems: 'center',
marginLeft: 0
}, },
headerLabelSpan: { '& > div': {
display: 'flex',
alignItems: 'center',
marginLeft: 25
},
'& > div:last-child': {
display: 'flex',
alignItems: 'center',
marginLeft: 64
},
'& > div > span': {
extend: label1, extend: label1,
marginLeft: 6 marginLeft: 7
}
}, },
root: { root: {
flexGrow: 1, flexGrow: 1,

View file

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import { useQuery } from '@apollo/react-hooks' import { useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
@ -27,19 +28,15 @@ const GET_DATA = gql`
} }
} }
` `
BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP }) BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const Footer = () => { const Footer = () => {
const { data } = useQuery(GET_DATA) const { data } = useQuery(GET_DATA)
const [expanded, setExpanded] = useState(false)
const [delayedExpand, setDelayedExpand] = useState(null)
const withCommissions = R.path(['cryptoRates', 'withCommissions'])(data) ?? {} const withCommissions = R.path(['cryptoRates', 'withCommissions'])(data) ?? {}
const classes = useStyles({ const classes = useStyles()
bigFooter: R.keys(withCommissions).length > 8,
expanded
})
const config = R.path(['config'])(data) ?? {} const config = R.path(['config'])(data) ?? {}
const canExpand = R.keys(withCommissions).length > 4 const canExpand = R.keys(withCommissions).length > 4
@ -99,31 +96,16 @@ const Footer = () => {
) )
} }
const handleMouseEnter = () => {
setDelayedExpand(setTimeout(() => canExpand && setExpanded(true), 300))
}
const handleMouseLeave = () => {
clearTimeout(delayedExpand)
setExpanded(false)
}
return ( return (
<> <div className={classes.footer1}>
<div <div className={classes.content1}>
className={classes.mouseWatcher} <Grid container>
onMouseLeave={handleMouseLeave} <Grid container className={classes.footerContainer1}>
onMouseEnter={handleMouseEnter}
/>
<div className={classes.content}>
<Grid container spacing={1}>
<Grid container className={classes.footerContainer}>
{R.keys(withCommissions).map(key => renderFooterItem(key))} {R.keys(withCommissions).map(key => renderFooterItem(key))}
</Grid> </Grid>
</Grid> </Grid>
</div> </div>
<div className={classes.footer} /> </div>
</>
) )
} }

View file

@ -17,52 +17,34 @@ const styles = {
txOutMargin: { txOutMargin: {
marginLeft: spacer * 3 marginLeft: spacer * 3
}, },
footer: ({ expanded, bigFooter }) => ({ tickerLabel: {
height: color: offColor,
expanded && bigFooter marginTop: -5
? spacer * 12 * 3 + spacer * 3 },
: expanded footer1: {
? spacer * 12 * 2 + spacer * 2
: spacer * 12,
left: 0, left: 0,
bottom: 0, bottom: 0,
position: 'fixed', position: 'fixed',
width: '100vw', width: '100vw',
backgroundColor: white, backgroundColor: white,
textAlign: 'left', textAlign: 'left',
boxShadow: '0px -1px 10px 0px rgba(50, 50, 50, 0.1)'
}),
tickerLabel: {
color: offColor,
marginTop: -5
},
content: {
width: 1200,
backgroundColor: white,
zIndex: 1, zIndex: 1,
position: 'fixed', boxShadow: '0px -1px 10px 0px rgba(50, 50, 50, 0.1)',
bottom: -spacer, minHeight: spacer * 12,
transform: 'translateY(-100%)' transition: 'min-height 0.5s ease-out',
'&:hover': {
transition: 'min-height 0.5s ease-in',
minHeight: 200
}
}, },
footerContainer: ({ expanded, bigFooter }) => ({ content1: {
marginLeft: spacer * 5, width: 1200,
height: 100, maxHeight: 100,
marginTop: expanded && bigFooter ? -300 : expanded ? -200 : -100, backgroundColor: white,
overflow: !expanded && 'hidden' zIndex: 2,
}), bottom: -spacer,
mouseWatcher: ({ expanded, bigFooter }) => ({ margin: '0 auto'
position: 'fixed', }
bottom: 0,
left: 0,
width: '100vw',
height:
expanded && bigFooter
? spacer * 12 * 3 + spacer * 3
: expanded
? spacer * 12 * 2 + spacer * 2
: spacer * 12,
zIndex: 2
})
} }
export default styles export default styles

View file

@ -8,11 +8,10 @@ import { java, neon, white } from 'src/styling/variables'
const styles = { const styles = {
wrapper: { wrapper: {
display: 'flex', display: 'flex',
height: 130, height: 142
marginTop: -8
}, },
percentageBox: { percentageBox: {
height: 130, height: 142,
borderRadius: 4, borderRadius: 4,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -33,11 +32,11 @@ const styles = {
borderRadius: 2 borderRadius: 2
}, },
inWidth: { inWidth: {
width: value => `${value}%` width: value => `${value}%`,
marginRight: value => (value === 100 ? 0 : 4)
}, },
outWidth: { outWidth: {
width: value => `${100 - value}%`, width: value => `${100 - value}%`
marginRight: 4
} }
} }
@ -59,14 +58,6 @@ const PercentageChart = ({ cashIn, cashOut }) => {
return ( return (
<div className={classes.wrapper}> <div className={classes.wrapper}>
<div
className={classnames(
percentageClasses,
classes.outColor,
classes.outWidth
)}>
{buildPercentageView(100 - value, 'cashOut')}
</div>
<div <div
className={classnames( className={classnames(
percentageClasses, percentageClasses,
@ -75,6 +66,14 @@ const PercentageChart = ({ cashIn, cashOut }) => {
)}> )}>
{buildPercentageView(value, 'cashIn')} {buildPercentageView(value, 'cashIn')}
</div> </div>
<div
className={classnames(
percentageClasses,
classes.outColor,
classes.outWidth
)}>
{buildPercentageView(100 - value, 'cashOut')}
</div>
</div> </div>
) )
} }

View file

@ -45,7 +45,7 @@ const RefLineChart = ({
const svg = d3.select(svgRef.current) const svg = d3.select(svgRef.current)
const margin = { top: 0, right: 0, bottom: 0, left: 0 } const margin = { top: 0, right: 0, bottom: 0, left: 0 }
const width = 336 - margin.left - margin.right const width = 336 - margin.left - margin.right
const height = 128 - margin.top - margin.bottom const height = 140 - margin.top - margin.bottom
const massageData = () => { const massageData = () => {
// if we're viewing transactions for the past day, then we group by hour. If not, we group by day // if we're viewing transactions for the past day, then we group by hour. If not, we group by day
@ -148,7 +148,7 @@ const RefLineChart = ({
const y = d3 const y = d3
.scaleLinear() .scaleLinear()
// 30 is a margin so that the labels and the percentage change label can fit and not overlay the line path // 30 is a margin so that the labels and the percentage change label can fit and not overlay the line path
.range([height, 30]) .range([height, 40])
.domain([0, yDomain[1]]) .domain([0, yDomain[1]])
const x = d3 const x = d3
.scaleTime() .scaleTime()

View file

@ -1,197 +1,357 @@
import BigNumber from 'bignumber.js'
import * as d3 from 'd3' import * as d3 from 'd3'
import { add } from 'date-fns/fp' import { getTimezoneOffset } from 'date-fns-tz'
import React, { useEffect, useRef, useCallback } from 'react' import { add, format, startOfWeek, startOfYear } from 'date-fns/fp'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import { backgroundColor, java, neon } from 'src/styling/variables' import {
import { formatDate, toUtc } from 'src/utils/timezones' java,
neon,
subheaderDarkColor,
offColor,
fontSecondary,
backgroundColor
} from 'src/styling/variables'
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
const RefScatterplot = ({ data: realData, timeFrame, timezone }) => { const Graph = ({ data, timeFrame, timezone }) => {
const svgRef = useRef() const ref = useRef(null)
const drawGraph = useCallback(() => {
const svg = d3.select(svgRef.current) const GRAPH_HEIGHT = 250
const margin = { top: 25, right: 0, bottom: 25, left: 15 } const GRAPH_WIDTH = 555
const width = 555 - margin.left - margin.right const GRAPH_MARGIN = useMemo(
const height = 150 - margin.top - margin.bottom () => ({
// finds maximum value for the Y axis. Minimum value is 100. If value is multiple of 1000, add 100 top: 20,
// (this is because the Y axis looks best with multiples of 100) right: 0.5,
const findMaxY = () => { bottom: 27,
if (realData.length === 0) return 100 left: 43.5
const maxvalueTx = }),
100 * Math.ceil(d3.max(realData, t => parseFloat(t.fiat)) / 100) []
const maxY = Math.max(100, maxvalueTx) )
if (maxY % 1000 === 0) return maxY + 100
return maxY const offset = getTimezoneOffset(timezone)
const NOW = Date.now() + offset
const periodDomains = {
Day: [NOW - DAY, NOW],
Week: [NOW - WEEK, NOW],
Month: [NOW - MONTH, NOW]
} }
const timeFormat = v => { const dataPoints = useMemo(
switch (timeFrame) { () => ({
case 'Week': Day: {
return d3.timeFormat('%a %d')(v) freq: 24,
case 'Month': step: 60 * 60 * 1000,
return d3.timeFormat('%b %d')(v) tick: d3.utcHour.every(4),
default: labelFormat: '%H:%M'
return formatDate(v, timezone, 'HH:mm') },
} Week: {
freq: 7,
step: 24 * 60 * 60 * 1000,
tick: d3.utcDay.every(1),
labelFormat: '%a %d'
},
Month: {
freq: 30,
step: 24 * 60 * 60 * 1000,
tick: d3.utcDay.every(2),
labelFormat: '%d'
} }
}),
[]
)
const filterDay = useMemo(
x => (timeFrame === 'day' ? x.getUTCHours() === 0 : x.getUTCDate() === 1),
[timeFrame]
)
const getPastAndCurrentDayLabels = useCallback(d => {
const currentDate = new Date(d)
const currentDateDay = currentDate.getUTCDate()
const currentDateWeekday = currentDate.getUTCDay()
const currentDateMonth = currentDate.getUTCMonth()
const previousDate = new Date(currentDate.getTime())
previousDate.setUTCDate(currentDateDay - 1)
const previousDateDay = previousDate.getUTCDate()
const previousDateWeekday = previousDate.getUTCDay()
const previousDateMonth = previousDate.getUTCMonth()
const daysOfWeek = Array.from(Array(7)).map((_, i) =>
format('EEE', add({ days: i }, startOfWeek(new Date())))
)
const months = Array.from(Array(12)).map((_, i) =>
format('LLL', add({ months: i }, startOfYear(new Date())))
)
// changes values of arguments in some d3 function calls to make the graph labels look good according to the selected time frame
const findXAxisSettings = () => {
switch (timeFrame) {
case 'Week':
return { return {
nice: 7, previous:
ticks: 7, currentDateMonth !== previousDateMonth
subtractDays: 7, ? months[previousDateMonth]
timeRange: [50, 500] : `${daysOfWeek[previousDateWeekday]} ${previousDateDay}`,
} current:
case 'Month': currentDateMonth !== previousDateMonth
return { ? months[currentDateMonth]
nice: 6, : `${daysOfWeek[currentDateWeekday]} ${currentDateDay}`
ticks: 6,
subtractDays: 30,
timeRange: [50, 500]
}
default:
return {
nice: null,
ticks: 4,
subtractDays: 1,
timeRange: [50, 500]
}
} }
}, [])
const buildTicks = useCallback(
domain => {
const points = []
const roundDate = d => {
const step = dataPoints[timeFrame].step
return new Date(Math.ceil(d.valueOf() / step) * step)
} }
// sets width of the graph for (let i = 0; i <= dataPoints[timeFrame].freq; i++) {
svg.attr('width', width) const stepDate = new Date(NOW - i * dataPoints[timeFrame].step)
if (roundDate(stepDate) > domain[1]) continue
if (stepDate < domain[0]) continue
points.push(roundDate(stepDate))
}
// background color for the graph return points
svg },
.append('rect') [NOW, dataPoints, timeFrame]
.attr('x', 0) )
.attr('y', 0)
.attr('width', width)
.attr('height', height + margin.top)
.attr('fill', backgroundColor)
// declare g variable where more svg components will be attached const x = d3
const g = svg .scaleUtc()
.append('g') .domain(periodDomains[timeFrame])
.attr('transform', `translate(${margin.left},${margin.top})`) .range([GRAPH_MARGIN.left, GRAPH_WIDTH - GRAPH_MARGIN.right])
// y axis range: round up to 100 highest data value, if rounds up to 1000, add 100.
// this keeps the vertical axis nice looking
const maxY = findMaxY()
const xAxisSettings = findXAxisSettings()
// y and x scales
const y = d3 const y = d3
.scaleLinear() .scaleLinear()
.range([height, 0])
.domain([0, maxY])
.nice(3)
const x = d3
.scaleTime()
.domain([ .domain([
add({ days: -xAxisSettings.subtractDays }, new Date()).valueOf(), 0,
new Date().valueOf() (d3.max(data, d => new BigNumber(d.fiat).toNumber()) ?? 1000) * 1.05
]) ])
.range(xAxisSettings.timeRange) .nice()
.nice(xAxisSettings.nice) .range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
const timeValue = s => { const buildBackground = useCallback(
const date = toUtc(s) g => {
return x(date.valueOf()) g.append('rect')
} .attr('x', 0)
.attr('y', GRAPH_MARGIN.top)
// horizontal gridlines .attr('width', GRAPH_WIDTH)
const makeYGridlines = () => { .attr('height', GRAPH_HEIGHT - GRAPH_MARGIN.top - GRAPH_MARGIN.bottom)
return d3.axisLeft(y).ticks(4) .attr('fill', backgroundColor)
} },
g.append('g') [GRAPH_MARGIN]
.style('color', '#eef1ff')
.call(
makeYGridlines()
.tickSize(-width)
.tickFormat('')
) )
const buildXAxis = useCallback(
g =>
g
.attr(
'transform',
`translate(0, ${GRAPH_HEIGHT - GRAPH_MARGIN.bottom})`
)
.call(
d3
.axisBottom(x)
.ticks(dataPoints[timeFrame].tick)
.tickFormat(d => {
return d3.timeFormat(dataPoints[timeFrame].labelFormat)(
d.getTime() + d.getTimezoneOffset() * MINUTE
)
})
)
.call(g => g.select('.domain').remove()),
[GRAPH_MARGIN, dataPoints, timeFrame, x]
)
const buildYAxis = useCallback(
g =>
g
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
.call(d3.axisLeft(y).ticks(5))
.call(g => g.select('.domain').remove()) .call(g => g.select('.domain').remove())
/* X AXIS */
// this one is for the labels at the bottom
g.append('g')
.attr('transform', 'translate(0,' + height + ')')
.style('font-size', '13px')
.style('color', '#5f668a')
.style('font-family', 'MuseoSans')
.style('margin-top', '11px')
.call(
d3
.axisBottom(x)
.ticks(xAxisSettings.ticks)
.tickSize(0)
.tickFormat(timeFormat)
)
.selectAll('text') .selectAll('text')
.attr('dy', '1.5em') .attr('dy', '-0.25rem'),
// this is for the x axis line. It is the same color as the horizontal grid lines [GRAPH_MARGIN, y]
g.append('g')
.attr('transform', 'translate(0,' + height + ')')
.style('color', '#eef1ff')
.call(
d3
.axisBottom(x)
.ticks(6)
.tickSize(0)
.tickFormat('')
) )
.selectAll('text')
.attr('dy', '1.5em')
// Y axis const buildGrid = useCallback(
g.append('g') g => {
.style('font-size', '13px') g.attr('stroke', subheaderDarkColor)
.style('color', '#5f668a') .attr('fill', subheaderDarkColor)
.style('font-family', 'MuseoSans') // Vertical lines
.style('margin-top', '11px') .call(g =>
.call( g
.append('g')
.selectAll('line')
.data(buildTicks(x.domain()))
.join('line')
.attr('x1', d => 0.5 + x(d))
.attr('x2', d => 0.5 + x(d))
.attr('y1', GRAPH_MARGIN.top)
.attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
.attr('stroke-width', 1)
)
// Horizontal lines
.call(g =>
g
.append('g')
.selectAll('line')
.data(
d3 d3
.axisLeft(y) .axisLeft(y)
.ticks(4) .scale()
.tickSize(0) .ticks(5)
) )
.call(g => g.select('.domain').remove()) .join('line')
.selectAll('text') .attr('y1', d => 0.5 + y(d))
.attr('dy', '-0.40em') .attr('y2', d => 0.5 + y(d))
.attr('dx', '3em') .attr('x1', GRAPH_MARGIN.left)
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.right)
// Append dots )
const dots = svg // Thick vertical lines
.call(g =>
g
.append('g') .append('g')
.attr('transform', `translate(${margin.left},${margin.top})`) .selectAll('line')
.data(buildTicks(x.domain()).filter(filterDay))
.join('line')
.attr('class', 'dateSeparator')
.attr('x1', d => 0.5 + x(d))
.attr('x2', d => 0.5 + x(d))
.attr('y1', GRAPH_MARGIN.top - 10)
.attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
.attr('stroke-width', 2)
.join('text')
)
// Left side breakpoint label
.call(g => {
const separator = d3
?.select('.dateSeparator')
?.node()
?.getBBox()
dots if (!separator) return
.selectAll('circle')
.data(realData) const breakpoint = buildTicks(x.domain()).filter(filterDay)
.enter()
.append('circle') const labels = getPastAndCurrentDayLabels(breakpoint)
.attr('cx', d => timeValue(d.created))
.attr('cy', d => y(d.fiat)) return g
.attr('r', 4) .append('text')
.style('fill', d => (d.txClass === 'cashIn' ? java : neon)) .attr('x', separator.x - 7)
}, [realData, timeFrame, timezone]) .attr('y', separator.y)
.attr('text-anchor', 'end')
.attr('dy', '.25em')
.text(labels.previous)
})
// Right side breakpoint label
.call(g => {
const separator = d3
?.select('.dateSeparator')
?.node()
?.getBBox()
if (!separator) return
const breakpoint = buildTicks(x.domain()).filter(filterDay)
const labels = getPastAndCurrentDayLabels(breakpoint)
return g
.append('text')
.attr('x', separator.x + 7)
.attr('y', separator.y)
.attr('text-anchor', 'start')
.attr('dy', '.25em')
.text(labels.current)
})
},
[GRAPH_MARGIN, buildTicks, getPastAndCurrentDayLabels, x, y, filterDay]
)
const formatTicksText = useCallback(
() =>
d3
.selectAll('.tick text')
.style('stroke', offColor)
.style('fill', offColor)
.style('stroke-width', 0)
.style('font-family', fontSecondary),
[]
)
const formatText = useCallback(
() =>
d3
.selectAll('text')
.style('stroke', offColor)
.style('fill', offColor)
.style('stroke-width', 0)
.style('font-family', fontSecondary),
[]
)
const formatTicks = useCallback(() => {
d3.selectAll('.tick line')
.style('stroke', 'transparent')
.style('fill', 'transparent')
}, [])
const drawData = useCallback(
g => {
g.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => {
const created = new Date(d.created)
return x(created.setTime(created.getTime() + offset))
})
.attr('cy', d => y(new BigNumber(d.fiat).toNumber()))
.attr('fill', d => (d.txClass === 'cashIn' ? java : neon))
.attr('r', 3.5)
},
[data, offset, x, y]
)
const drawChart = useCallback(() => {
const svg = d3
.select(ref.current)
.attr('viewBox', [0, 0, GRAPH_WIDTH, GRAPH_HEIGHT])
svg.append('g').call(buildBackground)
svg.append('g').call(buildGrid)
svg.append('g').call(buildXAxis)
svg.append('g').call(buildYAxis)
svg.append('g').call(formatTicksText)
svg.append('g').call(formatText)
svg.append('g').call(formatTicks)
svg.append('g').call(drawData)
return svg.node()
}, [
buildBackground,
buildGrid,
buildXAxis,
buildYAxis,
drawData,
formatText,
formatTicks,
formatTicksText
])
useEffect(() => { useEffect(() => {
// first we clear old chart DOM elements on component update d3.select(ref.current)
d3.select(svgRef.current)
.selectAll('*') .selectAll('*')
.remove() .remove()
drawGraph() drawChart()
}, [drawGraph]) }, [drawChart])
return ( return <svg ref={ref} />
<>
<svg ref={svgRef} />
</>
)
} }
export default RefScatterplot
export default Graph

View file

@ -9,11 +9,13 @@ import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
import { EmptyTable } from 'src/components/table' import { EmptyTable } from 'src/components/table'
import { Label1, Label2 } from 'src/components/typography/index' import { Label1, Label2, P } from 'src/components/typography/index'
import { ReactComponent as PercentDownIcon } from 'src/styling/icons/dashboard/down.svg' import { ReactComponent as PercentDownIcon } from 'src/styling/icons/dashboard/down.svg'
import { ReactComponent as PercentNeutralIcon } from 'src/styling/icons/dashboard/equal.svg' import { ReactComponent as PercentNeutralIcon } from 'src/styling/icons/dashboard/equal.svg'
import { ReactComponent as PercentUpIcon } from 'src/styling/icons/dashboard/up.svg' import { ReactComponent as PercentUpIcon } from 'src/styling/icons/dashboard/up.svg'
import { java, neon } from 'src/styling/variables'
import { fromNamespace } from 'src/utils/config' import { fromNamespace } from 'src/utils/config'
import { timezones } from 'src/utils/timezone-list'
import { toTimezone } from 'src/utils/timezones' import { toTimezone } from 'src/utils/timezones'
import PercentageChart from './Graphs/PercentageChart' import PercentageChart from './Graphs/PercentageChart'
@ -199,9 +201,30 @@ const SystemPerformance = () => {
</Grid> </Grid>
{/* todo new customers */} {/* todo new customers */}
</Grid> </Grid>
<Grid container className={classes.gridContainer}> <Grid container className={classes.txGraphContainer}>
<Grid item xs={12}> <Grid item xs={12}>
<Label2>Transactions</Label2> <div className={classes.graphHeader}>
<Label2 noMargin>Transactions</Label2>
<div className={classes.labelWrapper}>
<P noMargin>
{timezones[timezone].short ?? timezones[timezone].long}{' '}
timezone
</P>
<span className={classes.verticalLine} />
<div>
<svg width={8} height={8}>
<rect width={8} height={8} rx={4} fill={java} />
</svg>
<Label1 noMargin>In</Label1>
</div>
<div>
<svg width={8} height={8}>
<rect width={8} height={8} rx={4} fill={neon} />
</svg>
<Label1 noMargin>Out</Label1>
</div>
</div>
</div>
<Scatterplot <Scatterplot
timeFrame={selectedRange} timeFrame={selectedRange}
data={transactionsToShow} data={transactionsToShow}
@ -209,9 +232,9 @@ const SystemPerformance = () => {
/> />
</Grid> </Grid>
</Grid> </Grid>
<Grid container className={classes.gridContainer}> <Grid container className={classes.commissionGraphContainer}>
<Grid item xs={8}> <Grid item xs={8}>
<Label2 className={classes.labelMargin}> <Label2 noMargin className={classes.commissionProfitTitle}>
Profit from commissions Profit from commissions
</Label2> </Label2>
<div className={classes.profitContainer}> <div className={classes.profitContainer}>
@ -233,23 +256,22 @@ const SystemPerformance = () => {
/> />
</Grid> </Grid>
<Grid item xs={4}> <Grid item xs={4}>
<Grid container> <Grid container className={classes.graphHeader}>
<Grid item> <Label2 noMargin>Direction</Label2>
<Label2 className={classes.labelMargin}>Direction</Label2> <div className={classes.labelWrapper}>
</Grid> <div>
<Grid <svg width={8} height={8}>
item <rect width={8} height={8} rx={2} fill={java} />
className={classnames( </svg>
classes.directionLabelContainer, <Label1 noMargin>In</Label1>
classes.dirLabContMargin </div>
)}> <div>
<div className={classes.outSquare} /> <svg width={8} height={8}>
<Label1 className={classes.directionLabel}>Out</Label1> <rect width={8} height={8} rx={2} fill={neon} />
</Grid> </svg>
<Grid item className={classes.directionLabelContainer}> <Label1 noMargin>Out</Label1>
<div className={classes.inSquare} /> </div>
<Label1 className={classes.directionLabel}>In</Label1> </div>
</Grid>
</Grid> </Grid>
<Grid item xs> <Grid item xs>
<PercentageChart <PercentageChart

View file

@ -1,5 +1,6 @@
import { import {
offColor, offColor,
offDarkColor,
spacer, spacer,
primaryColor, primaryColor,
fontSize3, fontSize3,
@ -7,8 +8,6 @@ import {
fontColor, fontColor,
spring4, spring4,
tomato, tomato,
java,
neon,
comet comet
} from 'src/styling/variables' } from 'src/styling/variables'
@ -67,12 +66,6 @@ const styles = {
navContainer: { navContainer: {
display: 'flex' display: 'flex'
}, },
profitLabel: {
fontSize: fontSize3,
fontFamily: fontSecondary,
fontWeight: 700,
color: fontColor
},
percentUp: { percentUp: {
fontSize: fontSize3, fontSize: fontSize3,
fontFamily: fontSecondary, fontFamily: fontSecondary,
@ -96,34 +89,14 @@ const styles = {
profitContainer: { profitContainer: {
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
margin: '0 26px -30px 16px', margin: '23px 26px -30px 16px',
position: 'relative' position: 'relative'
}, },
gridContainer: { profitLabel: {
marginTop: 30, fontSize: fontSize3,
height: 225 fontFamily: fontSecondary,
}, fontWeight: 700,
inSquare: { color: fontColor
width: 8,
height: 8,
borderRadius: 2,
marginTop: 18,
marginRight: 4,
backgroundColor: java
},
outSquare: {
width: 8,
height: 8,
borderRadius: 2,
marginTop: 18,
marginRight: 4,
backgroundColor: neon
},
directionLabelContainer: {
display: 'flex'
},
dirLabContMargin: {
marginRight: 20
}, },
directionIcon: { directionIcon: {
width: 16, width: 16,
@ -131,12 +104,50 @@ const styles = {
marginBottom: -2, marginBottom: -2,
marginRight: 4 marginRight: 4
}, },
labelMargin: {
marginBottom: 20,
marginRight: 32
},
emptyTransactions: { emptyTransactions: {
paddingTop: 40 paddingTop: 40
},
commissionProfitTitle: {
marginBottom: 16
},
graphHeader: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 16
},
labelWrapper: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
'& > div': {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginLeft: 15,
'&:first-child': {
marginLeft: 0
},
'& > p': {
marginLeft: 8
}
}
},
txGraphContainer: {
height: 300,
marginTop: 30
},
commissionsGraphContainer: {
height: 250,
marginTop: 30
},
verticalLine: {
height: 15,
width: 1,
backgroundColor: offDarkColor,
marginLeft: 31,
marginRight: 16
} }
} }

View file

@ -2,7 +2,7 @@ import * as R from 'ramda'
import * as Yup from 'yup' import * as Yup from 'yup'
import Autocomplete from 'src/components/inputs/formik/Autocomplete.js' import Autocomplete from 'src/components/inputs/formik/Autocomplete.js'
import timezoneList from 'src/utils/timezone-list' import { labels as timezoneList } from 'src/utils/timezone-list'
const getFields = (getData, names, onChange, auxElements = []) => { const getFields = (getData, names, onChange, auxElements = []) => {
return R.filter( return R.filter(

View file

@ -3,85 +3,109 @@ import { getTimezoneOffset } from 'date-fns-tz'
import * as R from 'ramda' import * as R from 'ramda'
const timezones = { const timezones = {
'Pacific/Midway': 'Midway Island, Samoa', 'Pacific/Midway': { short: 'SST', long: 'Midway Island, Samoa' },
'Pacific/Honolulu': 'Hawaii', 'Pacific/Honolulu': { short: 'HAST', long: 'Hawaii' },
'America/Juneau': 'Alaska', 'America/Juneau': { short: 'AKST', long: 'Alaska' },
'America/Boise': 'Mountain Time', 'America/Boise': { short: 'MST', long: 'Mountain Time' },
'America/Dawson': 'Dawson, Yukon', 'America/Dawson': { short: 'MST', long: 'Dawson, Yukon' },
'America/Chihuahua': 'Chihuahua, La Paz, Mazatlan', 'America/Chihuahua': { short: null, long: 'Chihuahua, La Paz, Mazatlan' },
'America/Phoenix': 'Arizona', 'America/Phoenix': { short: 'MST', long: 'Arizona' },
'America/Chicago': 'Central Time', 'America/Chicago': { short: 'CST', long: 'Central Time' },
'America/Regina': 'Saskatchewan', 'America/Regina': { short: 'CST', long: 'Saskatchewan' },
'America/Mexico_City': 'Guadalajara, Mexico City, Monterrey', 'America/Mexico_City': {
'America/Belize': 'Central America', short: 'CST',
'America/Detroit': 'Eastern Time', long: 'Guadalajara, Mexico City, Monterrey'
'America/Bogota': 'Bogota, Lima, Quito', },
'America/Caracas': 'Caracas, La Paz', 'America/Belize': { short: 'CST', long: 'Central America' },
'America/Santiago': 'Santiago', 'America/Detroit': { short: 'EST', long: 'Eastern Time' },
'America/St_Johns': 'Newfoundland and Labrador', 'America/Bogota': { short: 'COT', long: 'Bogota, Lima, Quito' },
'America/Sao_Paulo': 'Brasilia', 'America/Caracas': { short: 'VET', long: 'Caracas, La Paz' },
'America/Tijuana': 'Tijuana', 'America/Santiago': { short: 'CLST', long: 'Santiago' },
'America/Montevideo': 'Montevideo', 'America/St_Johns': { short: 'HNTN', long: 'Newfoundland and Labrador' },
'America/Argentina/Buenos_Aires': 'Buenos Aires, Georgetown', 'America/Sao_Paulo': { short: 'BRT', long: 'Brasilia' },
'America/Godthab': 'Greenland', 'America/Tijuana': { short: 'PST', long: 'Tijuana' },
'America/Los_Angeles': 'Pacific Time', 'America/Montevideo': { short: 'UYT', long: 'Montevideo' },
'Atlantic/Azores': 'Azores', 'America/Argentina/Buenos_Aires': {
'Atlantic/Cape_Verde': 'Cape Verde Islands', short: null,
GMT: 'UTC', long: 'Buenos Aires, Georgetown'
'Europe/London': 'Edinburgh, London', },
'Europe/Dublin': 'Dublin', 'America/Godthab': { short: null, long: 'Greenland' },
'Europe/Lisbon': 'Lisbon', 'America/Los_Angeles': { short: 'PST', long: 'Pacific Time' },
'Africa/Casablanca': 'Casablanca, Monrovia', 'Atlantic/Azores': { short: 'AZOT', long: 'Azores' },
'Atlantic/Canary': 'Canary Islands', 'Atlantic/Cape_Verde': { short: 'CVT', long: 'Cape Verde Islands' },
'Europe/Belgrade': 'Belgrade, Bratislava, Budapest, Ljubljana, Prague', GMT: { short: 'GMT', long: 'UTC' },
'Europe/Sarajevo': 'Sarajevo, Skopje, Warsaw, Zagreb', 'Europe/London': { short: 'GMT', long: 'Edinburgh, London' },
'Europe/Brussels': 'Brussels, Copenhagen, Madrid, Paris', 'Europe/Dublin': { short: 'GMT', long: 'Dublin' },
'Europe/Amsterdam': 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna', 'Europe/Lisbon': { short: 'WET', long: 'Lisbon' },
'Africa/Algiers': 'West Central Africa', 'Africa/Casablanca': { short: 'WET', long: 'Casablanca, Monrovia' },
'Europe/Bucharest': 'Bucharest', 'Atlantic/Canary': { short: 'WET', long: 'Canary Islands' },
'Africa/Cairo': 'Cairo', 'Europe/Belgrade': {
'Europe/Helsinki': 'Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius', short: 'CET',
'Europe/Athens': 'Athens, Istanbul, Minsk', long: 'Belgrade, Bratislava, Budapest, Ljubljana, Prague'
'Asia/Jerusalem': 'Jerusalem', },
'Africa/Harare': 'Harare, Pretoria', 'Europe/Sarajevo': { short: 'CET', long: 'Sarajevo, Skopje, Warsaw, Zagreb' },
'Europe/Moscow': 'Moscow, St. Petersburg, Volgograd', 'Europe/Brussels': {
'Asia/Kuwait': 'Kuwait, Riyadh', short: 'CET',
'Africa/Nairobi': 'Nairobi', long: 'Brussels, Copenhagen, Madrid, Paris'
'Asia/Baghdad': 'Baghdad', },
'Asia/Tehran': 'Tehran', 'Europe/Amsterdam': {
'Asia/Dubai': 'Abu Dhabi, Muscat', short: 'CET',
'Asia/Baku': 'Baku, Tbilisi, Yerevan', long: 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna'
'Asia/Kabul': 'Kabul', },
'Asia/Yekaterinburg': 'Ekaterinburg', 'Africa/Algiers': { short: 'CET', long: 'West Central Africa' },
'Asia/Karachi': 'Islamabad, Karachi, Tashkent', 'Europe/Bucharest': { short: 'EET', long: 'Bucharest' },
'Asia/Kolkata': 'Chennai, Kolkata, Mumbai, New Delhi', 'Africa/Cairo': { short: 'EET', long: 'Cairo' },
'Asia/Kathmandu': 'Kathmandu', 'Europe/Helsinki': {
'Asia/Dhaka': 'Astana, Dhaka', short: 'EET',
'Asia/Colombo': 'Sri Jayawardenepura', long: 'Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius'
'Asia/Almaty': 'Almaty, Novosibirsk', },
'Asia/Rangoon': 'Yangon Rangoon', 'Europe/Athens': { short: 'EET', long: 'Athens, Istanbul, Minsk' },
'Asia/Bangkok': 'Bangkok, Hanoi, Jakarta', 'Asia/Jerusalem': { short: 'IST', long: 'Jerusalem' },
'Asia/Krasnoyarsk': 'Krasnoyarsk', 'Africa/Harare': { short: 'CAT', long: 'Harare, Pretoria' },
'Asia/Shanghai': 'Beijing, Chongqing, Hong Kong SAR, Urumqi', 'Europe/Moscow': { short: 'MSK', long: 'Moscow, St. Petersburg, Volgograd' },
'Asia/Kuala_Lumpur': 'Kuala Lumpur, Singapore', 'Asia/Kuwait': { short: 'AST', long: 'Kuwait, Riyadh' },
'Asia/Taipei': 'Taipei', 'Africa/Nairobi': { short: 'EAT', long: 'Nairobi' },
'Australia/Perth': 'Perth', 'Asia/Baghdad': { short: 'AST', long: 'Baghdad' },
'Asia/Irkutsk': 'Irkutsk, Ulaanbaatar', 'Asia/Tehran': { short: 'IRST', long: 'Tehran' },
'Asia/Seoul': 'Seoul', 'Asia/Dubai': { short: 'GST', long: 'Abu Dhabi, Muscat' },
'Asia/Tokyo': 'Osaka, Sapporo, Tokyo', 'Asia/Baku': { short: 'AZT', long: 'Baku, Tbilisi, Yerevan' },
'Asia/Yakutsk': 'Yakutsk', 'Asia/Kabul': { short: 'AFT', long: 'Kabul' },
'Australia/Darwin': 'Darwin', 'Asia/Yekaterinburg': { short: 'YEKT', long: 'Ekaterinburg' },
'Australia/Adelaide': 'Adelaide', 'Asia/Karachi': { short: 'PKT', long: 'Islamabad, Karachi, Tashkent' },
'Australia/Sydney': 'Canberra, Melbourne, Sydney', 'Asia/Kolkata': { short: 'IST', long: 'Chennai, Kolkata, Mumbai, New Delhi' },
'Australia/Brisbane': 'Brisbane', 'Asia/Kathmandu': { short: null, long: 'Kathmandu' },
'Australia/Hobart': 'Hobart', 'Asia/Dhaka': { short: 'BST', long: 'Astana, Dhaka' },
'Asia/Vladivostok': 'Vladivostok', 'Asia/Colombo': { short: 'IST', long: 'Sri Jayawardenepura' },
'Pacific/Guam': 'Guam, Port Moresby', 'Asia/Almaty': { short: 'ALMT', long: 'Almaty, Novosibirsk' },
'Asia/Magadan': 'Magadan, Solomon Islands, New Caledonia', 'Asia/Rangoon': { short: null, long: 'Yangon Rangoon' },
'Asia/Kamchatka': 'Kamchatka, Marshall Islands', 'Asia/Bangkok': { short: 'ICT', long: 'Bangkok, Hanoi, Jakarta' },
'Pacific/Fiji': 'Fiji Islands', 'Asia/Krasnoyarsk': { short: 'KRAT', long: 'Krasnoyarsk' },
'Pacific/Auckland': 'Auckland, Wellington', 'Asia/Shanghai': {
'Pacific/Tongatapu': "Nuku'alofa" short: 'CST',
long: 'Beijing, Chongqing, Hong Kong SAR, Urumqi'
},
'Asia/Kuala_Lumpur': { short: 'MYT', long: 'Kuala Lumpur, Singapore' },
'Asia/Taipei': { short: 'CST', long: 'Taipei' },
'Australia/Perth': { short: 'AWST', long: 'Perth' },
'Asia/Irkutsk': { short: 'IRKT', long: 'Irkutsk, Ulaanbaatar' },
'Asia/Seoul': { short: 'KST', long: 'Seoul' },
'Asia/Tokyo': { short: 'JST', long: 'Osaka, Sapporo, Tokyo' },
'Asia/Yakutsk': { short: 'YAKT', long: 'Yakutsk' },
'Australia/Darwin': { short: 'ACST', long: 'Darwin' },
'Australia/Adelaide': { short: 'ACDT', long: 'Adelaide' },
'Australia/Sydney': { short: 'AEDT', long: 'Canberra, Melbourne, Sydney' },
'Australia/Brisbane': { short: 'AEST', long: 'Brisbane' },
'Australia/Hobart': { short: 'AEDT', long: 'Hobart' },
'Asia/Vladivostok': { short: 'VLAT', long: 'Vladivostok' },
'Pacific/Guam': { short: 'ChST', long: 'Guam, Port Moresby' },
'Asia/Magadan': {
short: 'MAGT',
long: 'Magadan, Solomon Islands, New Caledonia'
},
'Asia/Kamchatka': { short: 'PETT', long: 'Kamchatka, Marshall Islands' },
'Pacific/Fiji': { short: 'FJT', long: 'Fiji Islands' },
'Pacific/Auckland': { short: 'NZDT', long: 'Auckland, Wellington' },
'Pacific/Tongatapu': { short: null, long: "Nuku'alofa" }
} }
const buildTzLabels = timezoneList => { const buildTzLabels = timezoneList => {
@ -106,7 +130,7 @@ const buildTzLabels = timezoneList => {
const prefix = `(GMT${isNegative ? `-` : `+`}${hours}:${minutes})` const prefix = `(GMT${isNegative ? `-` : `+`}${hours}:${minutes})`
acc.push({ acc.push({
label: `${prefix} - ${value[1]}`, label: `${prefix} - ${value[1].long}`,
code: value[0] code: value[0]
}) })
@ -117,4 +141,6 @@ const buildTzLabels = timezoneList => {
) )
} }
export default buildTzLabels(timezones) const labels = buildTzLabels(timezones)
export { labels, timezones }