Added world map component.

This commit is contained in:
Mike Cao 2020-08-01 03:34:56 -07:00
parent e4e7f5b05c
commit a65f637df2
10 changed files with 545 additions and 79 deletions

View file

@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react';
import { useSpring, animated } from 'react-spring';
import classNames from 'classnames';
import { get } from 'lib/web';
import { percentFilter } from 'lib/filters';
import styles from './RankingsChart.module.css';
export default function RankingsChart({
@ -13,6 +14,7 @@ export default function RankingsChart({
heading,
className,
dataFilter,
onDataLoad = () => {},
}) {
const [data, setData] = useState();
@ -23,16 +25,17 @@ export default function RankingsChart({
return [];
}, [data]);
const total = useMemo(() => rankings?.reduce((n, { y }) => n + y, 0) || 0, [rankings]);
async function loadData() {
setData(
await get(`/api/website/${websiteId}/rankings`, {
start_at: +startDate,
end_at: +endDate,
type,
}),
);
const data = await get(`/api/website/${websiteId}/rankings`, {
start_at: +startDate,
end_at: +endDate,
type,
});
const updated = percentFilter(data);
setData(updated);
onDataLoad(updated);
}
useEffect(() => {
@ -51,26 +54,25 @@ export default function RankingsChart({
<div className={styles.title}>{title}</div>
<div className={styles.heading}>{heading}</div>
</div>
{rankings.map(({ x, y }) => (
<Row key={x} label={x} value={y} total={total} />
{rankings.map(({ x, y, z }) => (
<Row key={x} label={x} value={y} percent={z} />
))}
</div>
);
}
const Row = ({ label, value, total }) => {
const pct = total ? (value / total) * 100 : 0;
const props = useSpring({ width: pct, from: { width: 0 } });
const Row = ({ label, value, percent }) => {
const props = useSpring({ width: percent, from: { width: 0 } });
const valueProps = useSpring({ y: value, from: { y: 0 } });
return (
<div className={styles.row}>
<div className={styles.label}>{label}</div>
<animated.div className={styles.value}>
{valueProps.y.interpolate(y => y.toFixed(0))}
{valueProps.y.interpolate(n => n.toFixed(0))}
</animated.div>
<div className={styles.percent}>
<animated.div>{props.width.interpolate(y => `${y.toFixed(0)}%`)}</animated.div>
<animated.div>{props.width.interpolate(n => `${n.toFixed(0)}%`)}</animated.div>
<animated.div className={styles.bar} style={{ ...props }} />
</div>
</div>

View file

@ -1,74 +1,19 @@
import React, { useEffect, useState } from 'react';
import classNames from 'classnames';
import { get } from 'lib/web';
import WebsiteChart from './WebsiteChart';
import RankingsChart from './RankingsChart';
import { getDateRange } from '../lib/date';
import WorldMap from './WorldMap';
import { getDateRange } from 'lib/date';
import { get } from 'lib/web';
import { browserFilter, urlFilter, refFilter, deviceFilter, countryFilter } from 'lib/filters';
import styles from './WebsiteDetails.module.css';
const pageviewClasses = 'col-md-12 col-lg-6';
const sessionClasses = 'col-12 col-lg-4';
const urlFilter = data => data.filter(({ x }) => x !== '' && !x.startsWith('#'));
const refFilter = data =>
data.filter(({ x }) => x !== '' && !x.startsWith('/') && !x.startsWith('#'));
const deviceFilter = data => {
const devices = data.reduce(
(obj, { x, y }) => {
const [width] = x.split('x');
if (width >= 1920) {
obj.Desktop += +y;
} else if (width >= 1024) {
obj.Laptop += +y;
} else if (width >= 767) {
obj.Tablet += +y;
} else {
obj.Mobile += +y;
}
return obj;
},
{ Desktop: 0, Laptop: 0, Tablet: 0, Mobile: 0 },
);
return Object.keys(devices).map(key => ({ x: key, y: devices[key] }));
};
const browsers = {
aol: 'AOL',
edge: 'Edge',
'edge-ios': 'Edge (iOS)',
yandexbrowser: 'Yandex',
kakaotalk: 'KKaoTalk',
samsung: 'Samsung',
silk: 'Silk',
miui: 'MIUI',
beaker: 'Beaker',
'edge-chromium': 'Edge (Chromium)',
chrome: 'Chrome',
'chromium-webview': 'Chrome (webview)',
phantomjs: 'PhantomJS',
crios: 'Chrome (iOS)',
firefox: 'Firefox',
fxios: 'Firefox (iOS)',
'opera-mini': 'Opera Mini',
opera: 'Opera',
ie: 'IE',
bb10: 'BlackBerry 10',
android: 'Android',
ios: 'iOS',
safari: 'Safari',
facebook: 'Facebook',
instagram: 'Instagram',
'ios-webview': 'iOS (webview)',
searchbot: 'Searchbot',
};
const browserFilter = data => data.map(({ x, y }) => ({ x: browsers[x] || x, y }));
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
const [data, setData] = useState();
const [countryData, setCountryData] = useState();
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const { startDate, endDate } = dateRange;
@ -151,6 +96,20 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
dataFilter={deviceFilter}
/>
</div>
<div className="row">
<WorldMap data={countryData} className="col-12 col-md-12 col-lg-8" />
<RankingsChart
title="Countries"
type="country"
heading="Visitors"
className="col-12 col-md-12 col-lg-4"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={countryFilter}
onDataLoad={data => setCountryData(data)}
/>
</div>
</>
);
}

71
components/WorldMap.js Normal file
View file

@ -0,0 +1,71 @@
import React, { useState } from 'react';
import ReactTooltip from 'react-tooltip';
import classNames from 'classnames';
import tinycolor from 'tinycolor2';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import styles from './WorldMap.module.css';
const geoUrl = '/world-110m.json';
export default function WorldMap({
data,
className,
baseColor = '#e9f3fd',
fillColor = '#f5f5f5',
strokeColor = '#2680eb',
hoverColor = '#2680eb',
}) {
const [tooltip, setTooltip] = useState();
function getFillColor(code) {
if (code === 'AQ') return '#ffffff';
const country = data?.find(({ x }) => x === code);
return country ? tinycolor(baseColor).darken(country.z) : fillColor;
}
function getStrokeColor(code) {
return code === 'AQ' ? '#ffffff' : strokeColor;
}
function getHoverColor(code) {
return code === 'AQ' ? '#ffffff' : hoverColor;
}
function handleHover({ ISO_A2: code, NAME: name }) {
const country = data?.find(({ x }) => x === code);
setTooltip(`${name}: ${country?.y || 0} visitors`);
}
return (
<div className={classNames(styles.container, className)}>
<ComposableMap data-tip="" projection="geoMercator">
<ZoomableGroup zoom={0.8} minZoom={0.7} center={[0, 40]}>
<Geographies geography={geoUrl}>
{({ geographies }) => {
return geographies.map(geo => {
const code = geo.properties.ISO_A2;
return (
<Geography
key={geo.rsmKey}
geography={geo}
fill={getFillColor(code)}
stroke={getStrokeColor(code)}
style={{
default: { outline: 'none' },
hover: { outline: 'none', fill: getHoverColor(code) },
pressed: { outline: 'none' },
}}
onMouseOver={() => handleHover(geo.properties)}
onMouseOut={() => setTooltip(null)}
/>
);
});
}}
</Geographies>
</ZoomableGroup>
</ComposableMap>
<ReactTooltip>{tooltip}</ReactTooltip>
</div>
);
}

View file

@ -0,0 +1,5 @@
.container {
overflow: hidden;
position: relative;
border-top: 1px solid #e1e1e1;
}