From f079a2926b3a528a453f3ec7ea31475ce643c423 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Wed, 10 Aug 2022 23:20:45 +0200 Subject: [PATCH 1/6] feat: add period of three days --- new-lamassu-admin/src/pages/Analytics/Analytics.js | 2 ++ .../src/pages/Analytics/graphs/OverTimeDotGraph.js | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/new-lamassu-admin/src/pages/Analytics/Analytics.js b/new-lamassu-admin/src/pages/Analytics/Analytics.js index b7eed8fa..2e0afc7f 100644 --- a/new-lamassu-admin/src/pages/Analytics/Analytics.js +++ b/new-lamassu-admin/src/pages/Analytics/Analytics.js @@ -34,11 +34,13 @@ const REPRESENTING_OPTIONS = [ ] const PERIOD_OPTIONS = [ { code: 'day', display: 'Last 24 hours' }, + { code: 'threeDays', display: 'Last 3 days' }, { code: 'week', display: 'Last 7 days' }, { code: 'month', display: 'Last 30 days' } ] const TIME_OPTIONS = { day: DAY, + threeDays: 3 * DAY, week: WEEK, month: MONTH } diff --git a/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js index acd7a181..2dad847c 100644 --- a/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js +++ b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js @@ -46,6 +46,7 @@ const Graph = ({ const periodDomains = { day: [NOW - DAY, NOW], + threeDays: [NOW - 3 * DAY, NOW], week: [NOW - WEEK, NOW], month: [NOW - MONTH, NOW] } @@ -58,6 +59,12 @@ const Graph = ({ tick: d3.utcHour.every(1), labelFormat: '%H:%M' }, + threeDays: { + freq: 12, + step: 6 * 60 * 60 * 1000, + tick: d3.utcDay.every(1), + labelFormat: '%a %d' + }, week: { freq: 7, step: 24 * 60 * 60 * 1000, From 80715259b1af76e06fe59c1ba1d1e8ef556ecb20 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Thu, 11 Aug 2022 20:19:59 +0200 Subject: [PATCH 2/6] feat: add y log scale --- .../src/pages/Analytics/Analytics.styles.js | 24 ++++++- .../components/wrappers/OverTimeWrapper.js | 11 ++- .../src/pages/Analytics/graphs/Graph.js | 4 +- .../Analytics/graphs/OverTimeDotGraph.js | 69 +++++++++++-------- 4 files changed, 75 insertions(+), 33 deletions(-) diff --git a/new-lamassu-admin/src/pages/Analytics/Analytics.styles.js b/new-lamassu-admin/src/pages/Analytics/Analytics.styles.js index 089547f8..8274d929 100644 --- a/new-lamassu-admin/src/pages/Analytics/Analytics.styles.js +++ b/new-lamassu-admin/src/pages/Analytics/Analytics.styles.js @@ -1,4 +1,14 @@ -import { offDarkColor, tomato, neon, java } from 'src/styling/variables' +import { + offColor, + offDarkColor, + tomato, + neon, + java +} from 'src/styling/variables' + +import typographyStyles from '../../components/typography/styles' + +const { label1 } = typographyStyles const styles = { overviewLegend: { @@ -135,6 +145,18 @@ const styles = { topMachinesRadio: { display: 'flex', flexDirection: 'row' + }, + graphHeaderSwitchBox: { + display: 'flex', + flexDirection: 'column', + '& > *': { + margin: 0 + }, + '& > :first-child': { + marginBottom: 2, + extend: label1, + color: offColor + } } } diff --git a/new-lamassu-admin/src/pages/Analytics/components/wrappers/OverTimeWrapper.js b/new-lamassu-admin/src/pages/Analytics/components/wrappers/OverTimeWrapper.js index 502d817a..def8fe90 100644 --- a/new-lamassu-admin/src/pages/Analytics/components/wrappers/OverTimeWrapper.js +++ b/new-lamassu-admin/src/pages/Analytics/components/wrappers/OverTimeWrapper.js @@ -1,8 +1,8 @@ import { Box } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' -import React from 'react' +import React, { useState } from 'react' -import { Select } from 'src/components/inputs' +import { Select, Switch } from 'src/components/inputs' import { H2 } from 'src/components/typography' import { primaryColor } from 'src/styling/variables' @@ -25,6 +25,8 @@ const OverTimeDotGraphHeader = ({ }) => { const classes = useStyles() + const [logarithmic, setLogarithmic] = useState() + const legend = { cashIn:
, cashOut:
, @@ -57,6 +59,10 @@ const OverTimeDotGraphHeader = ({
+
+ Log. scale + setLogarithmic(event.target.checked)} /> +
+
+ + + + ) +} + +export default VolumeOverTimeGraphHeader diff --git a/new-lamassu-admin/src/pages/Analytics/graphs/Graph.js b/new-lamassu-admin/src/pages/Analytics/graphs/Graph.js index 720ee687..18ac8fdf 100644 --- a/new-lamassu-admin/src/pages/Analytics/graphs/Graph.js +++ b/new-lamassu-admin/src/pages/Analytics/graphs/Graph.js @@ -5,6 +5,7 @@ import GraphTooltip from '../components/tooltips/GraphTooltip' import HourOfDayBarGraph from './HourOfDayBarGraph' import OverTimeDotGraph from './OverTimeDotGraph' +import OverTimeLineGraph from './OverTimeLineGraph' import TopMachinesBarGraph from './TopMachinesBarGraph' const GraphWrapper = ({ @@ -37,6 +38,19 @@ const GraphWrapper = ({ log={log} /> ) + case 'volumeOverTime': + return ( + + ) case 'topMachinesVolume': return ( { + const ref = useRef(null) + + const GRAPH_POPOVER_WIDTH = 150 + const GRAPH_POPOVER_MARGIN = 25 + const GRAPH_HEIGHT = 401 + const GRAPH_WIDTH = 1163 + const GRAPH_MARGIN = useMemo( + () => ({ + top: 25, + right: 3.5, + bottom: 27, + left: 36.5 + }), + [] + ) + + const offset = getTimezoneOffset(timezone) + const NOW = Date.now() + offset + + const periodDomains = { + day: [NOW - DAY, NOW], + threeDays: [NOW - 3 * DAY, NOW], + week: [NOW - WEEK, NOW], + month: [NOW - MONTH, NOW] + } + + const dataPoints = useMemo( + () => ({ + day: { + freq: 24, + step: 60 * 60 * 1000, + tick: d3.utcHour.every(1), + labelFormat: '%H:%M' + }, + threeDays: { + freq: 12, + step: 6 * 60 * 60 * 1000, + tick: d3.utcDay.every(1), + labelFormat: '%a %d' + }, + 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(1), + labelFormat: '%d' + } + }), + [] + ) + + 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()))) + ) + + return { + previous: + currentDateMonth !== previousDateMonth + ? months[previousDateMonth] + : `${daysOfWeek[previousDateWeekday]} ${previousDateDay}`, + current: + currentDateMonth !== previousDateMonth + ? months[currentDateMonth] + : `${daysOfWeek[currentDateWeekday]} ${currentDateDay}` + } + }, []) + + const buildTicks = useCallback( + domain => { + const points = [] + + const roundDate = d => { + const step = dataPoints[period.code].step + return new Date(Math.ceil(d.valueOf() / step) * step) + } + + for (let i = 0; i <= dataPoints[period.code].freq; i++) { + const stepDate = new Date(NOW - i * dataPoints[period.code].step) + if (roundDate(stepDate) > domain[1]) continue + if (stepDate < domain[0]) continue + points.push(roundDate(stepDate)) + } + + return points + }, + [NOW, dataPoints, period.code] + ) + + const buildAreas = useCallback( + domain => { + const points = [] + + points.push(domain[1]) + + const roundDate = d => { + const step = dataPoints[period.code].step + return new Date(Math.ceil(d.valueOf() / step) * step) + } + + for (let i = 0; i <= dataPoints[period.code].freq; i++) { + const stepDate = new Date(NOW - i * dataPoints[period.code].step) + if (roundDate(stepDate) > new Date(domain[1])) continue + if (stepDate < new Date(domain[0])) continue + points.push(roundDate(stepDate)) + } + + points.push(domain[0]) + + return points + }, + [NOW, dataPoints, period.code] + ) + + const x = d3 + .scaleUtc() + .domain(periodDomains[period.code]) + .range([GRAPH_MARGIN.left, GRAPH_WIDTH - GRAPH_MARGIN.right]) + + // Create a second X axis for mouseover events to be placed correctly across the entire graph width and not limited by X's domain + const x2 = d3 + .scaleUtc() + .domain(periodDomains[period.code]) + .range([GRAPH_MARGIN.left, GRAPH_WIDTH]) + + const bins = buildAreas(x.domain()) + .sort((a, b) => compareDesc(a.date, b.date)) + .map(addMilliseconds(-dataPoints[period.code].step)) + .map((date, i, dates) => { + // move first and last bin in such way + // that all bin have uniform width + if (i === 0) + return addMilliseconds(dataPoints[period.code].step, dates[1]) + else if (i === dates.length - 1) + return addMilliseconds( + -dataPoints[period.code].step, + dates[dates.length - 2] + ) + else return date + }) + .map(date => { + const middleOfBin = addMilliseconds( + dataPoints[period.code].step / 2, + date + ) + + const txs = data.filter(tx => { + const txCreated = new Date(tx.created) + const shift = new Date(txCreated.getTime() + offset) + + return ( + Math.abs(differenceInMilliseconds(shift, middleOfBin)) < + dataPoints[period.code].step / 2 + ) + }) + + const cashIn = txs + .filter(tx => tx.txClass === 'cashIn') + .reduce((sum, tx) => sum + new BigNumber(tx.fiat).toNumber(), 0) + + const cashOut = txs + .filter(tx => tx.txClass === 'cashOut') + .reduce((sum, tx) => sum + new BigNumber(tx.fiat).toNumber(), 0) + + return { date: middleOfBin, cashIn, cashOut } + }) + + const min = d3.min(bins, d => Math.min(d.cashIn, d.cashOut)) ?? 0 + const max = d3.max(bins, d => Math.max(d.cashIn, d.cashOut)) ?? 1000 + + const yLin = d3 + .scaleLinear() + .domain([0, (max === min ? min + 1000 : max) * 1.03]) + .nice() + .range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top]) + + const yLog = d3 + .scaleLog() + .domain([ + min === 0 ? 1 : min * 0.9, + (max === min ? min + Math.pow(10, 2 * min + 1) : max) * 2 + ]) + .clamp(true) + .range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top]) + + const y = log ? yLog : yLin + + const getAreaInterval = (breakpoints, dataLimits, graphLimits) => { + const fullBreakpoints = [ + graphLimits[1], + ...R.filter(it => it > dataLimits[0] && it < dataLimits[1], breakpoints), + dataLimits[0] + ] + + const intervals = [] + for (let i = 0; i < fullBreakpoints.length - 1; i++) { + intervals.push([fullBreakpoints[i], fullBreakpoints[i + 1]]) + } + + return intervals + } + + const getAreaIntervalByX = (intervals, xValue) => { + return R.find(it => xValue <= it[0] && xValue >= it[1], intervals) ?? [0, 0] + } + + const getDateIntervalByX = (areas, intervals, xValue) => { + const flattenIntervals = R.uniq(R.flatten(intervals)) + + // flattenIntervals and areas should have the same number of elements + for (let i = intervals.length - 1; i >= 0; i--) { + if (xValue < flattenIntervals[i]) { + return [areas[i], areas[i + 1]] + } + } + } + + const buildXAxis = useCallback( + g => + g + .attr( + 'transform', + `translate(0, ${GRAPH_HEIGHT - GRAPH_MARGIN.bottom})` + ) + .call( + d3 + .axisBottom(x) + .ticks(dataPoints[period.code].tick) + .tickFormat(d => { + return d3.timeFormat(dataPoints[period.code].labelFormat)( + d.getTime() + d.getTimezoneOffset() * MINUTE + ) + }) + .tickSizeOuter(0) + ) + .call(g => + g + .select('.domain') + .attr('stroke', primaryColor) + .attr('stroke-width', 1) + ), + [GRAPH_MARGIN, dataPoints, period.code, x] + ) + + const buildYAxis = useCallback( + g => + g + .attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`) + .call( + d3 + .axisLeft(y) + .ticks(GRAPH_HEIGHT / 100) + .tickSizeOuter(0) + .tickFormat(d => { + if (log && !['1', '2', '5'].includes(d.toString()[0])) return '' + + if (d > 999) return Math.floor(d / 1000) + 'k' + else return d + }) + ) + .select('.domain') + .attr('stroke', primaryColor) + .attr('stroke-width', 1), + [GRAPH_MARGIN, y, log] + ) + + const buildGrid = useCallback( + g => { + g.attr('stroke', subheaderDarkColor) + .attr('fill', subheaderDarkColor) + // Vertical lines + .call(g => + 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) + ) + // Horizontal lines + .call(g => + g + .append('g') + .selectAll('line') + .data( + d3 + .axisLeft(y) + .scale() + .ticks(GRAPH_HEIGHT / 100) + ) + .join('line') + .attr('y1', d => 0.5 + y(d)) + .attr('y2', d => 0.5 + y(d)) + .attr('x1', GRAPH_MARGIN.left) + .attr('x2', GRAPH_WIDTH) + ) + // Vertical transparent rectangles for events + .call(g => + g + .append('g') + .selectAll('line') + .data(buildAreas(x.domain())) + .join('rect') + .attr('x', d => x(d)) + .attr('y', GRAPH_MARGIN.top) + .attr('width', d => { + const xValue = Math.round(x(d) * 100) / 100 + const intervals = getAreaInterval( + buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100), + x.range(), + x2.range() + ) + const interval = getAreaIntervalByX(intervals, xValue) + return Math.round((interval[0] - interval[1]) * 100) / 100 + }) + .attr( + 'height', + GRAPH_HEIGHT - GRAPH_MARGIN.bottom - GRAPH_MARGIN.top + ) + .attr('stroke', 'transparent') + .attr('fill', 'transparent') + .on('mouseover', d => { + const xValue = Math.round(d.target.x.baseVal.value * 100) / 100 + const areas = buildAreas(x.domain()) + const intervals = getAreaInterval( + buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100), + x.range(), + x2.range() + ) + + const dateInterval = getDateIntervalByX(areas, intervals, xValue) + if (!dateInterval) return + const filteredData = data.filter(it => { + const created = new Date(it.created) + const tzCreated = created.setTime(created.getTime() + offset) + return ( + tzCreated > new Date(dateInterval[1]) && + tzCreated <= new Date(dateInterval[0]) + ) + }) + + const rectXCoords = { + left: R.clone(d.target.getBoundingClientRect().x), + right: R.clone( + d.target.getBoundingClientRect().x + + d.target.getBoundingClientRect().width + ) + } + + const xCoord = + d.target.x.baseVal.value < 0.75 * GRAPH_WIDTH + ? rectXCoords.right + GRAPH_POPOVER_MARGIN + : rectXCoords.left - + GRAPH_POPOVER_WIDTH - + GRAPH_POPOVER_MARGIN + const yCoord = R.clone(d.target.getBoundingClientRect().y) + + setSelectionDateInterval(dateInterval) + setSelectionData(filteredData) + setSelectionCoords({ + x: Math.round(xCoord), + y: Math.round(yCoord) + }) + + d3.select(d.target).attr('fill', subheaderColor) + }) + .on('mouseleave', d => { + d3.select(d.target).attr('fill', 'transparent') + setSelectionDateInterval(null) + setSelectionData(null) + setSelectionCoords(null) + }) + ) + // Thick vertical lines + .call(g => + g + .append('g') + .selectAll('line') + .data( + buildTicks(x.domain()).filter(x => { + if (period.code === 'day') return x.getUTCHours() === 0 + return x.getUTCDate() === 1 + }) + ) + .join('line') + .attr('class', 'dateSeparator') + .attr('x1', d => 0.5 + x(d)) + .attr('x2', d => 0.5 + x(d)) + .attr('y1', GRAPH_MARGIN.top - 50) + .attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom) + .attr('stroke-width', 5) + .join('text') + ) + // Left side breakpoint label + .call(g => { + const separator = d3 + ?.select('.dateSeparator') + ?.node() + ?.getBBox() + + if (!separator) return + + const breakpoint = buildTicks(x.domain()).filter(x => { + if (period.code === 'day') return x.getUTCHours() === 0 + return x.getUTCDate() === 1 + }) + + const labels = getPastAndCurrentDayLabels(breakpoint) + + return g + .append('text') + .attr('x', separator.x - 10) + .attr('y', separator.y + 33) + .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(x => { + if (period.code === 'day') return x.getUTCHours() === 0 + return x.getUTCDate() === 1 + }) + + const labels = getPastAndCurrentDayLabels(breakpoint) + + return g + .append('text') + .attr('x', separator.x + 10) + .attr('y', separator.y + 33) + .attr('text-anchor', 'start') + .attr('dy', '.25em') + .text(labels.current) + }) + }, + [ + GRAPH_MARGIN, + buildTicks, + getPastAndCurrentDayLabels, + x, + x2, + y, + period, + buildAreas, + data, + offset, + setSelectionCoords, + setSelectionData, + setSelectionDateInterval + ] + ) + + const formatTicksText = useCallback( + () => + d3 + .selectAll('.tick text') + .style('stroke', fontColor) + .style('fill', fontColor) + .style('stroke-width', 0.5) + .style('font-family', fontSecondary), + [] + ) + + const formatText = useCallback( + () => + d3 + .selectAll('text') + .style('stroke', offColor) + .style('fill', offColor) + .style('stroke-width', 0.5) + .style('font-family', fontSecondary), + [] + ) + + const formatTicks = useCallback(() => { + d3.selectAll('.tick line') + .style('stroke', primaryColor) + .style('fill', primaryColor) + }, []) + + const drawData = useCallback( + g => { + g.append('clipPath') + .attr('id', 'clip-path') + .append('rect') + .attr('x', GRAPH_MARGIN.left) + .attr('y', GRAPH_MARGIN.top) + .attr('width', GRAPH_WIDTH) + .attr('height', GRAPH_HEIGHT - GRAPH_MARGIN.bottom - GRAPH_MARGIN.top) + .attr('fill', java) + + g.append('g') + .attr('clip-path', 'url(#clip-path)') + .selectAll('circle .cashIn') + .data(bins) + .join('circle') + .attr('cx', d => x(d.date)) + .attr('cy', d => y(d.cashIn)) + .attr('fill', java) + .attr('r', 3.5) + + g.append('g') + .attr('clip-path', 'url(#clip-path)') + .selectAll('circle .cashIn') + .data(bins) + .join('circle') + .attr('cx', d => x(d.date)) + .attr('cy', d => y(d.cashOut)) + .attr('fill', neon) + .attr('r', 3.5) + + g.append('path') + .datum(bins) + .attr('fill', 'none') + .attr('stroke', java) + .attr('stroke-width', 3) + .attr('clip-path', 'url(#clip-path)') + .attr( + 'd', + d3 + .line() + .curve(d3.curveMonotoneX) + .x(d => x(d.date)) + .y(d => y(d.cashIn)) + ) + + g.append('path') + .datum(bins) + .attr('fill', 'none') + .attr('stroke', neon) + .attr('stroke-width', 3) + .attr('clip-path', 'url(#clip-path)') + .attr( + 'd', + d3 + .line() + .curve(d3.curveMonotoneX) + .x(d => x(d.date)) + .y(d => y(d.cashOut)) + ) + }, + [x, y, bins, GRAPH_MARGIN] + ) + + const drawChart = useCallback(() => { + const svg = d3 + .select(ref.current) + .attr('viewBox', [0, 0, GRAPH_WIDTH, GRAPH_HEIGHT]) + + svg.append('g').call(buildGrid) + svg.append('g').call(drawData) + 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) + + return svg.node() + }, [ + buildGrid, + buildXAxis, + buildYAxis, + drawData, + formatText, + formatTicks, + formatTicksText + ]) + + useEffect(() => { + d3.select(ref.current) + .selectAll('*') + .remove() + drawChart() + }, [drawChart]) + + return +} + +export default memo( + Graph, + (prev, next) => + R.equals(prev.period, next.period) && + R.equals(prev.selectedMachine, next.selectedMachine) && + R.equals(prev.log, next.log) +) From b5e798339be008144a3b04c65391965c5ceed76a Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Mon, 15 Aug 2022 14:33:54 +0200 Subject: [PATCH 4/6] fix: use `k` for thousands fix: graph tooltip for `threeDays` interval fix: y domain for log graph fix: line graph drawing order fix: line graph dots --- .../components/tooltips/GraphTooltip.js | 9 ++++-- .../Analytics/graphs/OverTimeDotGraph.js | 8 +++-- .../Analytics/graphs/OverTimeLineGraph.js | 32 ++++++++++--------- .../Graphs/RefScatterplot.js | 12 ++++++- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/new-lamassu-admin/src/pages/Analytics/components/tooltips/GraphTooltip.js b/new-lamassu-admin/src/pages/Analytics/components/tooltips/GraphTooltip.js index 89208a4a..02b1ab19 100644 --- a/new-lamassu-admin/src/pages/Analytics/components/tooltips/GraphTooltip.js +++ b/new-lamassu-admin/src/pages/Analytics/components/tooltips/GraphTooltip.js @@ -29,12 +29,14 @@ const GraphTooltip = ({ formatDate( dateInterval[1], null, - period.code === 'day' ? 'MMM d, HH:mm' : 'MMM d' + R.includes(period.code, ['day', 'threeDays']) + ? 'MMM d, HH:mm' + : 'MMM d' ), formatDate( dateInterval[0], null, - period.code === 'day' ? 'HH:mm' : 'MMM d' + R.includes(period.code, ['day', 'threeDays']) ? 'HH:mm' : 'MMM d' ) ] : [ @@ -56,7 +58,8 @@ const GraphTooltip = ({ return ( - {period.code === 'day' || R.includes('hourOfDay', representing.code) + {R.includes(period.code, ['day', 'threeDays']) || + R.includes('hourOfDay', representing.code) ? `${formattedDateInterval[0]} - ${formattedDateInterval[1]}` : `${formattedDateInterval[0]}`} diff --git a/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js index 1b86af23..bf705a09 100644 --- a/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js +++ b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js @@ -15,6 +15,7 @@ import { fontSecondary, subheaderColor } from 'src/styling/variables' +import { numberToFiatAmount } from 'src/utils/number' import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time' const Graph = ({ @@ -37,7 +38,7 @@ const Graph = ({ top: 25, right: 3.5, bottom: 27, - left: 36.5 + left: 38 }), [] ) @@ -260,8 +261,9 @@ const Graph = ({ .tickFormat(d => { if (log && !['1', '2', '5'].includes(d.toString()[0])) return '' - if (d > 1000) return Math.floor(d / 1000) + 'k' - else return d + if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k' + + return numberToFiatAmount(d) }) ) .select('.domain') diff --git a/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeLineGraph.js b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeLineGraph.js index f2ac39fa..6c5613a1 100644 --- a/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeLineGraph.js +++ b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeLineGraph.js @@ -23,6 +23,7 @@ import { fontSecondary, subheaderColor } from 'src/styling/variables' +import { numberToFiatAmount } from 'src/utils/number' import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time' const Graph = ({ @@ -45,7 +46,7 @@ const Graph = ({ top: 25, right: 3.5, bottom: 27, - left: 36.5 + left: 38 }), [] ) @@ -234,7 +235,7 @@ const Graph = ({ const yLog = d3 .scaleLog() .domain([ - min === 0 ? 1 : min * 0.9, + min === 0 ? 0.9 : min * 0.9, (max === min ? min + Math.pow(10, 2 * min + 1) : max) * 2 ]) .clamp(true) @@ -311,8 +312,9 @@ const Graph = ({ .tickFormat(d => { if (log && !['1', '2', '5'].includes(d.toString()[0])) return '' - if (d > 999) return Math.floor(d / 1000) + 'k' - else return d + if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k' + + return numberToFiatAmount(d) }) ) .select('.domain') @@ -564,17 +566,7 @@ const Graph = ({ .attr('cx', d => x(d.date)) .attr('cy', d => y(d.cashIn)) .attr('fill', java) - .attr('r', 3.5) - - g.append('g') - .attr('clip-path', 'url(#clip-path)') - .selectAll('circle .cashIn') - .data(bins) - .join('circle') - .attr('cx', d => x(d.date)) - .attr('cy', d => y(d.cashOut)) - .attr('fill', neon) - .attr('r', 3.5) + .attr('r', d => (d.cashIn === 0 ? 0 : 3.5)) g.append('path') .datum(bins) @@ -591,6 +583,16 @@ const Graph = ({ .y(d => y(d.cashIn)) ) + g.append('g') + .attr('clip-path', 'url(#clip-path)') + .selectAll('circle .cashIn') + .data(bins) + .join('circle') + .attr('cx', d => x(d.date)) + .attr('cy', d => y(d.cashOut)) + .attr('fill', neon) + .attr('r', d => (d.cashOut === 0 ? 0 : 3.5)) + g.append('path') .datum(bins) .attr('fill', 'none') diff --git a/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js b/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js index 9d3d288d..6148c966 100644 --- a/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js +++ b/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js @@ -12,6 +12,7 @@ import { fontSecondary, backgroundColor } from 'src/styling/variables' +import { numberToFiatAmount } from 'src/utils/number' import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time' const Graph = ({ data, timeFrame, timezone }) => { @@ -172,7 +173,16 @@ const Graph = ({ data, timeFrame, timezone }) => { g => g .attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`) - .call(d3.axisLeft(y).ticks(5)) + .call( + d3 + .axisLeft(y) + .ticks(5) + .tickFormat(d => { + if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k' + + return numberToFiatAmount(d) + }) + ) .call(g => g.select('.domain').remove()) .selectAll('text') .attr('dy', '-0.25rem'), From 1d37608a191dfd0e565278e3917e8890607195ba Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:13:25 +0200 Subject: [PATCH 5/6] feat: change average to median --- .../src/pages/Analytics/Analytics.js | 18 ++++++++---------- .../components/wrappers/OverTimeWrapper.js | 4 ++-- .../pages/Analytics/graphs/OverTimeDotGraph.js | 8 ++++---- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/new-lamassu-admin/src/pages/Analytics/Analytics.js b/new-lamassu-admin/src/pages/Analytics/Analytics.js index 5e8d94bd..2bf8e278 100644 --- a/new-lamassu-admin/src/pages/Analytics/Analytics.js +++ b/new-lamassu-admin/src/pages/Analytics/Analytics.js @@ -227,13 +227,11 @@ const Analytics = () => { previous: filteredData(period.code).previous.length } - const avgAmount = { - current: - R.sum(R.map(d => d.fiat, filteredData(period.code).current)) / - (txs.current === 0 ? 1 : txs.current), - previous: - R.sum(R.map(d => d.fiat, filteredData(period.code).previous)) / - (txs.previous === 0 ? 1 : txs.previous) + const median = values => (values.length === 0 ? 0 : R.median(values)) + + const medianAmount = { + current: median(R.map(d => d.fiat, filteredData(period.code).current)), + previous: median(R.map(d => d.fiat, filteredData(period.code).previous)) } const txVolume = { @@ -365,9 +363,9 @@ const Analytics = () => { />
diff --git a/new-lamassu-admin/src/pages/Analytics/components/wrappers/OverTimeWrapper.js b/new-lamassu-admin/src/pages/Analytics/components/wrappers/OverTimeWrapper.js index def8fe90..6efaaf40 100644 --- a/new-lamassu-admin/src/pages/Analytics/components/wrappers/OverTimeWrapper.js +++ b/new-lamassu-admin/src/pages/Analytics/components/wrappers/OverTimeWrapper.js @@ -31,7 +31,7 @@ const OverTimeDotGraphHeader = ({ cashIn:
, cashOut:
, transaction:
, - average: ( + median: ( - +
diff --git a/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js index bf705a09..9da30a97 100644 --- a/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js +++ b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js @@ -498,9 +498,9 @@ const Graph = ({ const buildAvg = useCallback( g => { - const mean = d3.mean(data, d => new BigNumber(d.fiat).toNumber()) ?? 0 + const median = d3.median(data, d => new BigNumber(d.fiat).toNumber()) ?? 0 - if (log && mean === 0) return + if (log && median === 0) return g.attr('stroke', primaryColor) .attr('stroke-width', 3) @@ -508,8 +508,8 @@ const Graph = ({ .call(g => g .append('line') - .attr('y1', 0.5 + y(mean)) - .attr('y2', 0.5 + y(mean)) + .attr('y1', 0.5 + y(median)) + .attr('y2', 0.5 + y(median)) .attr('x1', GRAPH_MARGIN.left) .attr('x2', GRAPH_WIDTH) ) From 909fbb7ae21635160920ac5705c0a391833ed158 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:42:56 +0200 Subject: [PATCH 6/6] fix: tooltip format --- .../components/tooltips/GraphTooltip.js | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/new-lamassu-admin/src/pages/Analytics/components/tooltips/GraphTooltip.js b/new-lamassu-admin/src/pages/Analytics/components/tooltips/GraphTooltip.js index 02b1ab19..ff32a290 100644 --- a/new-lamassu-admin/src/pages/Analytics/components/tooltips/GraphTooltip.js +++ b/new-lamassu-admin/src/pages/Analytics/components/tooltips/GraphTooltip.js @@ -26,20 +26,12 @@ const GraphTooltip = ({ const formattedDateInterval = !R.includes('hourOfDay', representing.code) ? [ - formatDate( - dateInterval[1], - null, - R.includes(period.code, ['day', 'threeDays']) - ? 'MMM d, HH:mm' - : 'MMM d' - ), - formatDate( - dateInterval[0], - null, - R.includes(period.code, ['day', 'threeDays']) ? 'HH:mm' : 'MMM d' - ) + formatDate(dateInterval[1], null, 'MMM d'), + formatDate(dateInterval[1], null, 'HH:mm'), + formatDate(dateInterval[0], null, 'HH:mm') ] : [ + formatDate(dateInterval[1], null, 'MMM d'), formatDateNonUtc(dateInterval[1], 'HH:mm'), formatDateNonUtc(dateInterval[0], 'HH:mm') ] @@ -57,11 +49,11 @@ const GraphTooltip = ({ return ( + {!R.includes('hourOfDay', representing.code) && ( + {`${formattedDateInterval[0]}`} + )} - {R.includes(period.code, ['day', 'threeDays']) || - R.includes('hourOfDay', representing.code) - ? `${formattedDateInterval[0]} - ${formattedDateInterval[1]}` - : `${formattedDateInterval[0]}`} + {`${formattedDateInterval[1]} - ${formattedDateInterval[2]}`}

{R.length(data)}{' '}