Skip to content

Commit

Permalink
Abacus BO: use bar charts for homepage analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
mrtnzlml authored and kodiakhq[bot] committed Jul 26, 2021
1 parent e258293 commit 5fa952f
Show file tree
Hide file tree
Showing 12 changed files with 591 additions and 75 deletions.
5 changes: 5 additions & 0 deletions src/abacus-backoffice/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,10 @@ module.exports = (withPlugins(
});
return nextConfig;
},
experimental: {
// https://github.com/vercel/next.js/issues/23725
// https://github.com/vercel/next.js/pull/27069
esmExternals: true,
},
},
) /*: $FlowFixMe */);
3 changes: 2 additions & 1 deletion src/abacus-backoffice/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
"@adeira/relay": "^3.2.4",
"@adeira/sx": "^0.27.0",
"@adeira/sx-design": "^0.14.0",
"d3": "^7.0.0",
"fbt": "^0.16.6",
"graphql": "^15.5.1",
"immutable": "^4.0.0-rc.14",
"next": "^11.0.1",
"next": "^11.0.2-canary.21",
"next-plugin-custom-babel-config": "^1.0.4",
"next-transpile-modules": "^8.0.0",
"react": "^17.0.2",
Expand Down
2 changes: 2 additions & 0 deletions src/abacus-backoffice/src/LayoutHeading.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ export default function LayoutHeading(props: Props): Node {
return (
<div className={styles('headingWrapper')}>
<StatusBar />

{props.heading ?? null}
{props.description ? (
<p className={styles('description')}>
<small>{props.description}</small>
</p>
) : null}

<LayoutInline>{props.children ?? null}</LayoutInline>
</div>
);
Expand Down
115 changes: 115 additions & 0 deletions src/abacus-backoffice/src/d3/BarChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// @flow

import React, { useCallback, useState, type Node } from 'react';
import * as d3 from 'd3';

function drawSvgChart(svgRef, width, unsortedData, config) {
const data = d3.sort(unsortedData, (a, b) => {
if (config.sort === 'DESC') {
return d3.descending(a.value, b.value);
}
return d3.ascending(a.value, b.value);
});

const margin = { top: 20, right: 10, bottom: 10, left: 10 };
const barHeight = 16;
const height = Math.ceil((data.length + 0.1) * barHeight) + margin.top + margin.bottom;
const svg = d3.select(svgRef.current).attr('viewBox', [0, 0, width, height]);

const x = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value)])
.range([margin.left, width - margin.right]);

const y = d3
.scaleBand()
.domain(d3.range(data.length))
.rangeRound([margin.top, height - margin.bottom])
.padding(0.1);

// Bars
svg
.append('g')
.attr('fill', 'rgba(var(--sx-success))')
.selectAll('rect')
.data(data)
.join('rect')
.attr('x', x(0))
.attr('y', (d, i) => y(i))
.attr('width', (d) => x(d.value) - x(0))
.attr('height', y.bandwidth());

// Text labels
svg
.append('g')
.attr('fill', 'white')
.attr('text-anchor', 'end')
.attr('font-family', 'sans-serif')
.attr('font-size', 12)
.selectAll('text')
.data(data)
.join('text')
.attr('x', (d) => x(d.value))
.attr('y', (d, i) => y(i) + y.bandwidth() / 2)
.attr('dy', '0.35em')
.attr('dx', -4)
.text((d) => d.label)
.call((text) =>
text
.filter((d) => {
// short bars
const textLength = text.node().getComputedTextLength();
return x(d.value) - x(0) < textLength;
})
.attr('dx', +4)
.attr('fill', 'rgba(var(--sx-foreground))')
.attr('text-anchor', 'start'),
);

// X axis
svg.append('g').call((g) =>
g
.attr('transform', `translate(0,${margin.top})`)
.call(d3.axisTop(x).ticks(width / 100))
.call((g) => g.select('.domain').remove()),
);

// Y axis
svg
.append('g')
.call((g) =>
g
.attr('transform', `translate(${margin.left},0)`)
.call(d3.axisLeft(y).tickValues([]).tickSizeOuter(0)),
);
}

type Props = {
+sort: 'ASC' | 'DESC',
+data: $ReadOnlyArray<{ +label: Fbt, +value: number }>,
};

export default function BarChart(props: Props): Node {
const svg = React.useRef(null);
const [width, setWidth] = useState(null);

React.useEffect(() => {
if (width != null) {
drawSvgChart(svg, width, props.data, {
sort: props.sort,
});
}
}, [props.data, props.sort, svg, width]);

const divRef = useCallback((node) => {
if (node !== null) {
setWidth(node.getBoundingClientRect().width);
}
}, []);

return (
<div id="chart" ref={divRef}>
<svg ref={svg} />
</div>
);
}
55 changes: 24 additions & 31 deletions src/abacus-backoffice/src/index/IndexPage.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// @flow

import * as React from 'react';
import { Heading, Note, Section } from '@adeira/sx-design';
import { Heading, Section } from '@adeira/sx-design';
import { graphql, useLazyLoadQuery } from '@adeira/relay';
import fbt from 'fbt';

import BarChart from '../d3/BarChart';
import type { IndexPageQuery } from './__generated__/IndexPageQuery.graphql';

export default function IndexPage(): React.Node {
Expand All @@ -13,12 +14,10 @@ export default function IndexPage(): React.Node {
query IndexPageQuery {
analytics {
mostSoldProducts {
productId
productName
productUnits
}
leastSoldProducts {
productId
productName
productUnits
}
Expand All @@ -28,38 +27,32 @@ export default function IndexPage(): React.Node {
);

return (
<>
<Section>
<Heading>
<fbt desc="quick analytics (stats) on the Abacus homepage">Quick analytics</fbt>
<fbt desc="most sold products heading">Most sold products</fbt>
</Heading>
<Note tint="warning">work in progress</Note>
<Section>
<Heading>
<fbt desc="most sold products heading">Most sold products</fbt>
</Heading>
<Section>
<ol>
{data.analytics.mostSoldProducts.map((info) => (
<li key={info.productId}>
{info.productName} ({info.productUnits} units)
</li>
))}
</ol>
</Section>
<BarChart
sort="DESC"
data={data.analytics.mostSoldProducts.map((info) => ({
label: info.productName,
value: info.productUnits,
}))}
/>
</Section>

<Heading>
<fbt desc="least sold products heading">Least sold products</fbt>
</Heading>
<Section>
<ol>
{data.analytics.leastSoldProducts.map((info) => (
<li key={info.productId}>
{info.productName} ({info.productUnits} units)
</li>
))}
</ol>
</Section>
<Heading>
<fbt desc="least sold products heading">Least sold products</fbt>
</Heading>
<Section>
<BarChart
sort="ASC"
data={data.analytics.leastSoldProducts.map((info) => ({
label: info.productName,
value: info.productUnits,
}))}
/>
</Section>
</>
</Section>
);
}
12 changes: 12 additions & 0 deletions src/abacus-backoffice/src/index/IndexPageLayout.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
// @flow

import { Heading } from '@adeira/sx-design';
import fbt from 'fbt';
import * as React from 'react';

import Layout from '../Layout';
import LayoutHeading from '../LayoutHeading';
import IndexPage from './IndexPage';

export default function IndexPageLayout(): React.Node {
return (
<Layout>
<LayoutHeading
heading={
<Heading>
<fbt desc="quick analytics (stats) on the Abacus homepage">Quick analytics</fbt>
</Heading>
}
description={<fbt desc="have a great day message">Have a great day!</fbt>}
/>

<IndexPage />
</Layout>
);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/abacus-backoffice/src/products/ProductsAddonsLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import fbt from 'fbt';
import React, { type Node } from 'react';
import sx from '@adeira/sx';
import { Heading } from '@adeira/sx-design';
import { Heading, Note } from '@adeira/sx-design';

import Layout from '../Layout';
import LayoutHeading from '../LayoutHeading';
Expand Down Expand Up @@ -31,7 +31,7 @@ export default function ProductsAddonsLayout(): Node {
</LayoutHeadingLink>
</LayoutHeading>

<div>TODO (print all add-ons, create, edit)</div>
<Note tint="warning">work in progress (print all add-ons, create, edit)</Note>
</Layout>
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/abacus-backoffice/translations/out/es-MX.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
"DyJ2E": "El campo \"{form field label}\" debe tener un valor mayor que {form field min value}.",
"1OZewv": "Por favor ingrese un valor válido. Los dos valores válidos más cercanos son {low boundary} y {high boundary}.",
"ZcMj7": "El campo \"{form field label}\" es inválido.",
"396VGn": "Quick analytics",
"3nkeCo": "Most sold products",
"3OLUNN": "Least sold products",
"396VGn": "Quick analytics",
"tKryu": "Have a great day!",
"WVQMe": "Ledger",
"3gmClg": "Inicio",
"2CyhH3": "Inventario de productos",
Expand Down
Loading

0 comments on commit 5fa952f

Please sign in to comment.