├── .babelrc
├── .gitignore
├── README.md
├── jsconfig.json
├── package-lock.json
├── package.json
├── pages
├── _app.js
├── _document.js
├── covid19-dashboard.js
├── index.js
├── korea-crop-production.js
├── literacy-rates-by-country.js
└── most-populated-countries.js
├── public
└── images
│ ├── bar-chart.png
│ ├── bubble-map.png
│ ├── line-chart.png
│ └── scatter-plot.png
└── src
├── components
├── Axes
│ └── Axes.js
├── AxisLabel
│ └── AxisLabel.js
├── BarChart
│ └── BarChart.js
├── BubbleMap
│ ├── BubbleMap.js
│ └── SizeLegend.js
├── Card
│ └── Card.js
├── ColorLegend
│ └── ColorLegend.js
├── Layout
│ ├── Footer.js
│ ├── Header.js
│ └── Layout.js
├── LineChart
│ ├── Indicator.js
│ ├── LineChart.js
│ └── Tooltip.js
└── ScatterPlot
│ ├── ScatterPlot.js
│ └── Tooltip.js
├── style
├── reset.css
├── style.css
└── style.js
└── utility
└── utility.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": ["babel-plugin-styled-components"]
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Data Visualization with D3.js and React
2 |
3 | > Data visualization is an interdisciplinary field that deals with the graphic representation of data. It is a particularly efficient way of communicating when the data is numerous as for example a Time Series.
4 |
5 |
6 |
7 | ## Live Demo
8 |
9 | You can check demo [here](https://data-visualization-f7b1yckzn.vercel.app/).
10 |
11 |
12 |
13 | ## Preview
14 |
15 |
16 |
17 |
18 |
19 | ### 1. Bubble Map
20 |
21 | 
22 |
23 |
24 |
25 | ### 2. Multi Line Chart
26 |
27 | 
28 |
29 |
30 |
31 | ### 3. Bar Chart
32 |
33 | 
34 |
35 |
36 |
37 | ### 4. Scatter Plot
38 |
39 | 
40 |
41 |
42 |
43 | ### ✨ Features
44 |
45 | - Interactive graph based on user selected option.
46 | - Animated graph
47 | - Sort graph ascending or descending.
48 | - Zoomable World Map.
49 | - Tooltip
50 |
51 |
52 |
53 | ### 🛠 Tech and libraries
54 |
55 | - Next.js
56 | - D3.js
57 | - styled-components
58 | - Topojson
59 | - PropTypes
60 |
61 |
62 |
63 | ### Getting started
64 |
65 | Install npm packages
66 |
67 |
68 | `npm install`
69 |
70 | Run dev server
71 |
72 |
73 | `npm run dev`
74 |
75 |
76 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "components/*": ["src/components/*"],
6 | "utility/*": ["src/utility/*"],
7 | "hooks/*": ["src/hooks/*"],
8 | "style/*": ["src/style/*"],
9 | },
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-data-visualization",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "d3": "^6.3.1",
12 | "next": "10.0.4",
13 | "react": "17.0.1",
14 | "react-dom": "17.0.1",
15 | "styled-components": "^5.2.1",
16 | "topojson": "^3.0.2"
17 | },
18 | "devDependencies": {
19 | "babel-plugin-styled-components": "^1.12.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import 'src/style/style.css';
2 | import 'src/style/reset.css';
3 |
4 | function MyApp({ Component, pageProps }) {
5 | return ;
6 | }
7 |
8 | export default MyApp;
9 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Document, { Html, Head, Main, NextScript } from 'next/document';
3 | import { ServerStyleSheet } from 'styled-components';
4 |
5 | export default class MyDocument extends Document {
6 | static async getInitialProps(ctx) {
7 | const sheet = new ServerStyleSheet();
8 | const originalRenderPage = ctx.renderPage;
9 |
10 | try {
11 | ctx.renderPage = () =>
12 | originalRenderPage({
13 | enhanceApp: App => props => sheet.collectStyles(),
14 | });
15 |
16 | const initialProps = await Document.getInitialProps(ctx);
17 | return {
18 | ...initialProps,
19 | styles: (
20 | <>
21 | {initialProps.styles}
22 | {sheet.getStyleElement()}
23 | >
24 | ),
25 | };
26 | } finally {
27 | sheet.seal();
28 | }
29 | }
30 |
31 | render() {
32 | return (
33 |
34 |
35 |
39 |
43 |
44 |
48 |
49 |
50 |
54 | {/* */}
55 | {/* */}
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/pages/covid19-dashboard.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { csv, json } from 'd3';
3 | import { feature } from 'topojson';
4 | import PropsTypes from 'prop-types';
5 |
6 | import BubbleMap from 'components/BubbleMap/BubbleMap';
7 | import Layout from 'components/Layout/Layout';
8 |
9 | function Covid19Dashboard({ countries }) {
10 | return (
11 |
18 |
19 |
20 | );
21 | }
22 |
23 | export async function getStaticProps() {
24 | const TOPO_JSON_DATA =
25 | 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json';
26 | const COVID_DATA =
27 | 'https://gist.githubusercontent.com/suyeonme/d9cce30a620249ef17bd39e120c8fa30/raw/639c1aa9ce1fd4dbb2f80b1babf81b721eaabf16/gistfile2.txt';
28 |
29 | const covidData = await csv(COVID_DATA);
30 | const topoData = await json(TOPO_JSON_DATA);
31 | const countries = await feature(topoData, topoData.objects.countries);
32 |
33 | const rowByName = await covidData.reduce((accumulator, d) => {
34 | accumulator[d['country']] = d;
35 | return accumulator;
36 | }, {});
37 |
38 | await countries.features.forEach(d => {
39 | Object.assign(d.properties, rowByName[d.properties.name]);
40 | });
41 |
42 | return {
43 | props: {
44 | countries,
45 | },
46 | };
47 | }
48 |
49 | Covid19Dashboard.propTypes = {
50 | countries: PropsTypes.object,
51 | };
52 |
53 | export default Covid19Dashboard;
54 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | import Layout from 'components/Layout/Layout';
4 | import Card from 'components/Card/Card';
5 | import { CardsWrapper } from 'components/Card/Card';
6 |
7 | const Wrapper = styled.div`
8 | padding: 5rem;
9 | `;
10 |
11 | const Title = styled.h1`
12 | font-size: 3.5rem;
13 | font-weight: 700;
14 | text-align: center;
15 |
16 | span {
17 | display: block;
18 | font-size: 2rem;
19 | font-weight: 400;
20 | color: #696969;
21 | margin-top: 1rem;
22 | }
23 | `;
24 |
25 | function Home() {
26 | const cards = [
27 | {
28 | bgUrl: '/images/bubble-map.png',
29 | title: 'WHO Covid-19 World Dashboard',
30 | chartType: 'Bubble Map',
31 | link: '/covid19-dashboard',
32 | },
33 | {
34 | bgUrl: '/images/line-chart.png',
35 | title: 'Korea Crop Production',
36 | chartType: 'Line Chart',
37 | link: '/korea-crop-production',
38 | },
39 | {
40 | bgUrl: '/images/bar-chart.png',
41 | title: 'Top 20 Countries by Population 2020',
42 | chartType: 'Bar Chart',
43 | link: '/most-populated-countries',
44 | },
45 | {
46 | bgUrl: '/images/scatter-plot.png',
47 | title: 'Literacy Rates by Country',
48 | chartType: 'Scatter Plot',
49 | link: '/literacy-rates-by-country',
50 | },
51 | ];
52 |
53 | return (
54 |
55 |
56 |
57 | 📊 Data Visualizationwith D3.js and React
58 |
59 |
60 | {cards.map((card, i) => (
61 |
68 | ))}
69 |
70 |
71 |
72 | );
73 | }
74 |
75 | export default Home;
76 |
--------------------------------------------------------------------------------
/pages/korea-crop-production.js:
--------------------------------------------------------------------------------
1 | import { csv } from 'd3';
2 | import PropsTypes from 'prop-types';
3 |
4 | import LineChart from 'components/LineChart/LineChart';
5 | import Layout from 'components/Layout/Layout';
6 |
7 | function KoreaCropProduction({ data }) {
8 | return (
9 |
16 |
17 |
18 | );
19 | }
20 |
21 | export async function getStaticProps() {
22 | const data = await csv(
23 | 'https://gist.githubusercontent.com/suyeonme/7595c37f1cbc51023ba1d5f6ba767b37/raw/e855d60b0719b3c20ec26842994b5bd89a7d5a2c/korea_crop_producction.csv'
24 | );
25 |
26 | data.forEach(d => {
27 | d.time = +d.time;
28 | d.value = +d.value;
29 | });
30 |
31 | return {
32 | props: {
33 | data,
34 | },
35 | };
36 | }
37 |
38 | KoreaCropProduction.propTypes = {
39 | data: PropsTypes.array,
40 | };
41 |
42 | export default KoreaCropProduction;
43 |
--------------------------------------------------------------------------------
/pages/literacy-rates-by-country.js:
--------------------------------------------------------------------------------
1 | import { csv } from 'd3';
2 | import PropsTypes from 'prop-types';
3 |
4 | import Layout from 'components/Layout/Layout';
5 | import ScatterPlot from 'components/ScatterPlot/ScatterPlot';
6 |
7 | function LiteracyRatesByContry({ data }) {
8 | return (
9 |
16 |
17 |
18 | );
19 | }
20 |
21 | export async function getStaticProps() {
22 | const data = await csv(
23 | 'https://gist.githubusercontent.com/suyeonme/4d3c8ca10e33ca59a89d43b6cb8bf687/raw/c99b981242c76cbf1643409c4b760d889eea7e62/literacy-rates-of-the-the-younger-population-15-24-years-versus-literacy-rates-of-the-older-population-65.csv'
24 | );
25 |
26 | data.forEach(d => {
27 | d.youthRate = +d.youthRate;
28 | d.elderlyRate = +d.elderlyRate;
29 | });
30 |
31 | return {
32 | props: {
33 | data,
34 | },
35 | };
36 | }
37 |
38 | LiteracyRatesByContry.propTypes = {
39 | data: PropsTypes.array,
40 | };
41 |
42 | export default LiteracyRatesByContry;
43 |
--------------------------------------------------------------------------------
/pages/most-populated-countries.js:
--------------------------------------------------------------------------------
1 | import { csv } from 'd3';
2 | import PropsTypes from 'prop-types';
3 |
4 | import BarChart from 'components/BarChart/BarChart';
5 | import Layout from 'components/Layout/Layout';
6 |
7 | function MostPopulatedCountries({ data }) {
8 | return (
9 |
16 |
17 |
18 | );
19 | }
20 |
21 | export async function getStaticProps() {
22 | const data = await csv(
23 | 'https://gist.githubusercontent.com/suyeonme/da2138240874fa5f369700628505ddc8/raw/2176c06bed989a8befe0ed8f156953dac881a257/gistfile2.txt'
24 | );
25 |
26 | data.forEach(d => (d.population = +d.population));
27 |
28 | return {
29 | props: {
30 | data,
31 | },
32 | };
33 | }
34 |
35 | MostPopulatedCountries.propTypes = {
36 | data: PropsTypes.array,
37 | };
38 |
39 | export default MostPopulatedCountries;
40 |
--------------------------------------------------------------------------------
/public/images/bar-chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suyeonme/data-visualization/5230bdc11e3301e8e96a492e2e02f68f103cc644/public/images/bar-chart.png
--------------------------------------------------------------------------------
/public/images/bubble-map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suyeonme/data-visualization/5230bdc11e3301e8e96a492e2e02f68f103cc644/public/images/bubble-map.png
--------------------------------------------------------------------------------
/public/images/line-chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suyeonme/data-visualization/5230bdc11e3301e8e96a492e2e02f68f103cc644/public/images/line-chart.png
--------------------------------------------------------------------------------
/public/images/scatter-plot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suyeonme/data-visualization/5230bdc11e3301e8e96a492e2e02f68f103cc644/public/images/scatter-plot.png
--------------------------------------------------------------------------------
/src/components/Axes/Axes.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { axisLeft, axisBottom, select, format } from 'd3';
3 | import styled from 'styled-components';
4 | import PropsTypes from 'prop-types';
5 |
6 | const Tick = styled.g`
7 | transform: ${props =>
8 | props.axisType === 'yAxis' && `translate(0, ${props.innerHeight}px)`};
9 |
10 | path,
11 | line {
12 | stroke: #dcdbdb;
13 | }
14 |
15 | text {
16 | font-size: 1.4rem;
17 | }
18 | `;
19 |
20 | function Axes({
21 | xScale,
22 | yScale,
23 | innerHeight,
24 | yAxisTickFormat,
25 | xAixsTickFormat,
26 | yTickSize,
27 | xTickPadding,
28 | yTickPadding,
29 | }) {
30 | const xAxisRef = useRef(null);
31 | const yAxisRef = useRef(null);
32 |
33 | useEffect(() => {
34 | const xGroup = select(xAxisRef.current);
35 | const yGroup = select(yAxisRef.current);
36 | const xAxis = axisBottom(xScale)
37 | .tickSize(-innerHeight)
38 | .tickPadding(xTickPadding);
39 | const yAxis = axisLeft(yScale)
40 | .tickSize(yTickSize)
41 | .tickPadding(yTickPadding);
42 |
43 | if (yAxisTickFormat) yAxis.tickFormat(yAxisTickFormat); // Line Chart
44 | if (xAixsTickFormat) xAxis.tickFormat(xAixsTickFormat); // Bar Chart
45 |
46 | xGroup.call(yAxis);
47 | yGroup.call(xAxis);
48 | });
49 |
50 | return (
51 |
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | Axes.propTypes = {
59 | xScale: PropsTypes.func,
60 | yScale: PropsTypes.func,
61 | innerHeight: PropsTypes.number,
62 | yAxisTickFormat: PropsTypes.func,
63 | xAixsTickFormat: PropsTypes.func,
64 | yTickSize: PropsTypes.number,
65 | xTickPadding: PropsTypes.number,
66 | yTickPadding: PropsTypes.number,
67 | };
68 |
69 | export default React.memo(Axes);
70 |
--------------------------------------------------------------------------------
/src/components/AxisLabel/AxisLabel.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import PropsTypes from 'prop-types';
3 |
4 | const YAxis = styled.text`
5 | transform: rotate(-90deg);
6 | text-anchor: middle;
7 | font-size: 2rem;
8 | `;
9 |
10 | const XAxis = styled.text`
11 | font-size: 2rem;
12 | `;
13 |
14 | function AxisLabel({
15 | innerHeight,
16 | innerWidth,
17 | axisPadding,
18 | yLabel,
19 | xLabel,
20 | marginLeft,
21 | }) {
22 | return (
23 |
24 |
25 | {yLabel}
26 |
27 |
28 | {xLabel}
29 |
30 |
31 | );
32 | }
33 |
34 | AxisLabel.propTypes = {
35 | y: PropsTypes.number,
36 | yLabel: PropsTypes.string,
37 | xLabel: PropsTypes.string,
38 | marginLeft: PropsTypes.number,
39 | innerHeight: PropsTypes.number,
40 | innerWidth: PropsTypes.number,
41 | };
42 |
43 | export default AxisLabel;
44 |
--------------------------------------------------------------------------------
/src/components/BarChart/BarChart.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from 'react';
2 | import styled from 'styled-components';
3 | import {
4 | scaleLinear,
5 | max,
6 | scaleBand,
7 | format,
8 | select,
9 | ascending,
10 | descending,
11 | } from 'd3';
12 | import PropsTypes from 'prop-types';
13 |
14 | import Axes from 'components/Axes/Axes';
15 | import { Group, MapWrapper, Map, Dropdown } from 'src/style/style';
16 | import { width, formatNumber } from 'utility/utility';
17 |
18 | const XAxisLabel = styled.g`
19 | rect {
20 | width: 15px;
21 | height: 15px;
22 | fill: #ec008b;
23 | }
24 |
25 | text {
26 | font-size: 1.4rem;
27 | }
28 | `;
29 |
30 | const GroupedRect = styled.g`
31 | rect {
32 | fill: #ec008b;
33 | transition: all 0.2s;
34 |
35 | &:hover {
36 | fill: #e46aa7;
37 | }
38 | }
39 | `;
40 |
41 | const GroupedText = styled.g`
42 | text {
43 | font-size: 1.2rem;
44 | }
45 | `;
46 |
47 | function BarChart({ data }) {
48 | const [sortOpt, setSortOpt] = useState('highest');
49 | const rectRef = useRef();
50 | const textRef = useRef();
51 |
52 | useEffect(() => {
53 | // Animation rect with select
54 | const group = select(rectRef.current);
55 | const text = select(textRef.current);
56 |
57 | handleSelect();
58 | handleDrawRect(group);
59 | handleDrawText(text);
60 | }, [sortOpt, data]);
61 |
62 | const height = 700;
63 | const margin = { top: 50, right: 95, bottom: 60, left: 50 };
64 | const innerWidth = width - margin.right - margin.left;
65 | const innerHeight = height - margin.top - margin.bottom;
66 |
67 | const xValue = d => d.population;
68 | const yValue = d => d.country;
69 |
70 | const xScale = scaleLinear()
71 | .domain([0, max(data, xValue)])
72 | .range([0, innerWidth])
73 | .nice();
74 |
75 | const yScale = scaleBand()
76 | .domain(data.map(yValue))
77 | .range([0, innerHeight])
78 | .padding(0.2);
79 |
80 | const handleDrawRect = group => {
81 | group
82 | .selectAll('rect')
83 | .data(data)
84 | .join('rect')
85 | .attr('height', yScale.bandwidth())
86 | .attr('y', d => yScale(yValue(d)))
87 | .transition()
88 | .duration(750)
89 | .attr('width', d => xScale(xValue(d)));
90 | };
91 |
92 | const handleDrawText = group => {
93 | group
94 | .selectAll('text')
95 | .data(data)
96 | .join('text')
97 | .attr('y', d => yScale(yValue(d)) + yScale.bandwidth() / 1.5)
98 | .text(d => formatNumber(d.population))
99 | .attr('x', d => xScale(xValue(d)) + 5)
100 | .attr('fill-opacity', 0)
101 | .transition()
102 | .delay(1200)
103 | .duration(750)
104 | .attr('fill-opacity', 1);
105 | };
106 |
107 | const handleSelect = () => {
108 | if (sortOpt === 'highest') {
109 | data.sort((a, b) => ascending(a.population, b.population));
110 | } else if (sortOpt === 'lowest') {
111 | data.sort((a, b) => descending(a.population, b.population));
112 | }
113 | };
114 |
115 | const xAixsTickFormat = number => format('.2s')(number).replace('G', 'B');
116 | const handleChange = e => setSortOpt(e.target.value);
117 |
118 | return (
119 |
120 |
121 |
122 |
123 |
124 |
151 |
152 | );
153 | }
154 |
155 | Axes.propTypes = {
156 | data: PropsTypes.array,
157 | };
158 |
159 | export default BarChart;
160 |
--------------------------------------------------------------------------------
/src/components/BubbleMap/BubbleMap.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import {
3 | select,
4 | zoom,
5 | scaleSqrt,
6 | max,
7 | geoNaturalEarth1,
8 | geoCentroid,
9 | geoPath,
10 | } from 'd3';
11 | import styled from 'styled-components';
12 | import PropsTypes from 'prop-types';
13 |
14 | import { width, height, formatNumber } from 'utility/utility';
15 | import SizeLegend from 'components/BubbleMap/SizeLegend';
16 | import { MapWrapper, Map, Dropdown } from 'src/style/style';
17 |
18 | const CountryPath = styled.path`
19 | fill: #d2d2d2;
20 | stroke: white;
21 | stroke-width: 0.1px;
22 |
23 | &:hover {
24 | fill: #f7b0ad;
25 | }
26 | `;
27 |
28 | const SpherePath = styled.path`
29 | fill: #eaeaea;
30 | opacity: 0.3;
31 | `;
32 |
33 | function BubbleMap({ countries }) {
34 | const [value, setValue] = useState('death');
35 | const svgRef = useRef(null);
36 |
37 | const radiusScale = scaleSqrt();
38 | const radiusValue = country => country.properties[value];
39 | radiusScale
40 | .domain([0, max(countries.features, radiusValue)])
41 | .range([0, value === 'death' ? 6 : 3]);
42 |
43 | const ticksOfDeaths = [335000, 150000, 70000, 10000];
44 | const ticksOfCases = [20000000, 10000000, 2500000, 200000];
45 |
46 | useEffect(() => {
47 | const svg = select(svgRef.current);
48 |
49 | handleZoom(svg);
50 |
51 | // Transition of Circles
52 | svg
53 | .selectAll('.bubble')
54 | .data(countries.features)
55 | .join('circle')
56 | .attr('class', 'bubble')
57 | .attr('cx', d => d.properties.projected[0])
58 | .attr('cy', d => d.properties.projected[1])
59 | .transition()
60 | .attr('r', d => radiusScale(radiusValue(d)));
61 | });
62 |
63 | const handleChange = val => setValue(val);
64 |
65 | const handleZoom = svg => {
66 | svg.call(
67 | zoom().on('zoom', ({ transform }) => {
68 | svg.attr('transform', transform);
69 | })
70 | );
71 | };
72 |
73 | const insertProjectionToProperties = (features, projection) => {
74 | features.forEach(country => {
75 | country.properties.projected = projection(geoCentroid(country));
76 | });
77 | };
78 |
79 | const handleTooltip = country => {
80 | if (country.properties.case === undefined) {
81 | return 'No Reported Data';
82 | } else if (value === 'death') {
83 | return `${country.properties.country}: ${formatNumber(
84 | country.properties.death
85 | )}`;
86 | } else {
87 | return `${country.properties.country}: ${formatNumber(
88 | country.properties.case
89 | )}`;
90 | }
91 | };
92 |
93 | const projection = geoNaturalEarth1()
94 | .fitSize([width, height], countries)
95 | .precision(100);
96 |
97 | const pathGenerator = geoPath().projection(projection);
98 |
99 | insertProjectionToProperties(countries.features, projection);
100 |
101 | return (
102 |
103 | handleChange(e.target.value)}>
104 |
105 |
106 |
107 |
108 |
131 |
132 |
133 | );
134 | }
135 |
136 | BubbleMap.propTypes = {
137 | countries: PropsTypes.object,
138 | };
139 |
140 | export default BubbleMap;
141 |
--------------------------------------------------------------------------------
/src/components/BubbleMap/SizeLegend.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import PropsTypes from 'prop-types';
4 |
5 | const GroupWrapper = styled.g`
6 | transform: translate(3rem, 15rem);
7 | `;
8 |
9 | const Group = styled.g`
10 | transform: ${props => `translate(0, ${props.spacing}px)`};
11 |
12 | circle {
13 | opacity: 0.7;
14 | fill: #d31922;
15 | pointer-events: none;
16 | }
17 |
18 | text {
19 | font-size: 1.4rem;
20 | }
21 | `;
22 |
23 | const SizeLegend = ({
24 | ticks,
25 | xCircle,
26 | yCircle,
27 | formatNumber,
28 | radiusScale,
29 | xLabel,
30 | spacing,
31 | }) => {
32 | return (
33 |
34 | {ticks.map((tick, i) => (
35 |
36 |
41 |
42 | {formatNumber(tick)}
43 |
44 |
45 | ))}
46 |
47 | );
48 | };
49 |
50 | SizeLegend.propTypes = {
51 | ticks: PropsTypes.array,
52 | xCircle: PropsTypes.number,
53 | yCircle: PropsTypes.number,
54 | formatNumber: PropsTypes.func,
55 | radiusScale: PropsTypes.func,
56 | xLabel: PropsTypes.number,
57 | spacing: PropsTypes.number,
58 | };
59 |
60 | export default React.memo(SizeLegend);
61 |
--------------------------------------------------------------------------------
/src/components/Card/Card.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import Link from 'next/link';
4 | import PropsTypes from 'prop-types';
5 |
6 | export const CardsWrapper = styled.div`
7 | display: flex;
8 | flex-direction: row;
9 | flex-wrap: wrap;
10 | justify-content: space-around;
11 | margin-top: 10rem;
12 |
13 | @media (max-width: 568px) {
14 | margin-top: 7rem;
15 | }
16 | `;
17 |
18 | const Wrapper = styled.div`
19 | text-align: center;
20 | transition: all 0.3s;
21 | font-size: 1.6rem;
22 | width: 30rem;
23 |
24 | @media (max-width: 1200px) {
25 | margin-bottom: 5rem;
26 | }
27 |
28 | @media (max-width: 568px) {
29 | width: 35rem;
30 | }
31 |
32 | h2,
33 | p {
34 | text-transform: uppercase;
35 | }
36 |
37 | h2 {
38 | word-spacing: 4px;
39 | }
40 |
41 | p {
42 | color: #696969;
43 | margin-top: 1rem;
44 | font-size: 1.4rem;
45 | }
46 |
47 | &:hover {
48 | transform: translate(0, -10px);
49 | }
50 | `;
51 |
52 | const CardWrapper = styled.div`
53 | height: auto;
54 | border-radius: 0.5rem;
55 | padding: 1rem;
56 | margin-bottom: 1.5rem;
57 | box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2);
58 | `;
59 |
60 | const Image = styled.div`
61 | background: ${props => `url(${props.bgUrl})`};
62 | background-size: contain;
63 | background-repeat: no-repeat;
64 | background-position: center;
65 | height: 15rem;
66 | `;
67 |
68 | function Card({ bgUrl, title, chartType, link }) {
69 | return (
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | {title}
79 | {chartType}
80 |
81 | );
82 | }
83 |
84 | Card.propTypes = {
85 | bgUrl: PropsTypes.string,
86 | title: PropsTypes.string,
87 | chartType: PropsTypes.string,
88 | link: PropsTypes.string,
89 | };
90 |
91 | export default Card;
92 |
--------------------------------------------------------------------------------
/src/components/ColorLegend/ColorLegend.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import PropsTypes from 'prop-types';
3 |
4 | import { formatString } from 'utility/utility';
5 |
6 | const Wrapper = styled.g`
7 | transform: ${props =>
8 | props.align === 'row'
9 | ? `translate(${props.spacing}px, 0)`
10 | : `translate(${props.spacing}px, 60px)`};
11 | `;
12 |
13 | const ColorTick = styled.g`
14 | transform: ${props =>
15 | props.align === 'row'
16 | ? `translate(${props.spacing}px, 30px)`
17 | : `translate(200px, ${props.spacing}px)`};
18 |
19 | text {
20 | font-size: 1.4rem;
21 | }
22 | `;
23 |
24 | const ColorCircle = styled.circle`
25 | fill: ${props => props.color};
26 | `;
27 |
28 | function ColorLegend({
29 | moveX,
30 | colorScale,
31 | spacing,
32 | radius,
33 | textX,
34 | width,
35 | align,
36 | }) {
37 | return (
38 |
39 | {colorScale.domain().map((d, i) => (
40 |
41 |
42 |
43 | {formatString(d)}
44 |
45 |
46 | ))}
47 |
48 | );
49 | }
50 |
51 | ColorLegend.propTypes = {
52 | moveX: PropsTypes.number,
53 | colorScale: PropsTypes.func,
54 | spacing: PropsTypes.number,
55 | radius: PropsTypes.number,
56 | textX: PropsTypes.number,
57 | width: PropsTypes.number,
58 | };
59 |
60 | export default ColorLegend;
61 |
--------------------------------------------------------------------------------
/src/components/Layout/Footer.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Wrapper = styled.footer`
4 | padding: 1rem 0;
5 | text-align: center;
6 | `;
7 |
8 | function Footer() {
9 | return (
10 |
11 | 2020 © Suyeon Kang. All right reserved.
12 |
13 | );
14 | }
15 |
16 | export default Footer;
17 |
--------------------------------------------------------------------------------
/src/components/Layout/Header.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import PropsTypes from 'prop-types';
3 | import Link from 'next/link';
4 |
5 | const Wrapper = styled.header`
6 | padding: 5rem 5rem 0;
7 |
8 | p {
9 | margin-bottom: 0.5rem;
10 |
11 | @media (max-width: 568px) {
12 | line-height: 1.5;
13 | }
14 | }
15 |
16 | a {
17 | color: currentColor;
18 | text-decoration: underline;
19 | }
20 | `;
21 |
22 | const Title = styled.h1`
23 | font-size: 3rem;
24 | font-weight: 700;
25 | margin-bottom: 1rem;
26 | margin-top: 5rem;
27 |
28 | @media (max-width: 568px) {
29 | text-align: center;
30 | }
31 | `;
32 |
33 | const Logo = styled.div`
34 | font-size: 2.5rem;
35 | margin-bottom: 1.5rem;
36 | position: fixed;
37 | top: 5rem;
38 | left: 5rem;
39 |
40 | @media (max-width: 568px) {
41 | font-size: 3rem;
42 | }
43 | `;
44 |
45 | const SubTitle = styled.h3`
46 | font-size: 2rem;
47 | color: #696969;
48 | margin-bottom: 4.5rem;
49 |
50 | @media (max-width: 568px) {
51 | text-align: center;
52 | }
53 | `;
54 |
55 | function Header({ title, subTitle, chartType, dataSource, dataSourceUrl }) {
56 | if (title) {
57 | return (
58 |
59 |
60 |
61 | 📊
62 |
63 |
64 | {title}
65 | {subTitle}
66 | Chart Type: {chartType}
67 |
68 | Data Source:
69 |
70 | {dataSource}
71 |
72 |
73 |
74 | );
75 | }
76 |
77 | return null;
78 | }
79 |
80 | Header.propTypes = {
81 | title: PropsTypes.string,
82 | subTitle: PropsTypes.string,
83 | chartType: PropsTypes.string,
84 | dataSource: PropsTypes.string,
85 | dataSourceUrl: PropsTypes.string,
86 | };
87 |
88 | export default Header;
89 |
--------------------------------------------------------------------------------
/src/components/Layout/Layout.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import styled, { createGlobalStyle } from 'styled-components';
3 | import PropsTypes from 'prop-types';
4 |
5 | import Header from 'components/Layout/Header';
6 | import Footer from 'components/Layout/Footer';
7 |
8 | const GlobalStyle = createGlobalStyle`
9 | html {
10 | ${'' /* 10px = 1rem */}
11 | font-size: 62.5%;
12 |
13 | @media (max-width: 992px) {
14 | ${'' /* 9px = 1rem */}
15 | font-size: 56.25%;
16 | }
17 |
18 | @media (max-width: 768px) {
19 | ${'' /* 8px = 1rem */}
20 | font-size: 50%;
21 | }
22 |
23 | @media (max-width: 320px) {
24 | ${'' /* 7px = 1rem */}
25 | font-size: 43.75%;
26 | }
27 |
28 | scroll-behavior: smooth;
29 | }
30 |
31 | body {
32 | margin: 0;
33 | padding: 0;
34 | font-family: 'Lato', sans-serif;
35 | }
36 |
37 | a {
38 | color: currentColor;
39 | text-decoration: none;
40 | }
41 |
42 | p {
43 | font-size: 1.6rem;
44 | }
45 | `;
46 |
47 | const Wrapper = styled.div`
48 | display: flex;
49 | min-height: 100vh;
50 | flex-direction: column;
51 |
52 | main {
53 | flex: 1;
54 | }
55 | `;
56 |
57 | function Layout({
58 | children,
59 | title,
60 | subTitle,
61 | chartType,
62 | dataSource,
63 | dataSourceUrl,
64 | headTitle,
65 | }) {
66 | return (
67 |
68 |
69 | {headTitle}
70 |
71 |
72 |
79 | {children}
80 |
81 |
82 | );
83 | }
84 |
85 | Layout.propTypes = {
86 | title: PropsTypes.string,
87 | subTitle: PropsTypes.string,
88 | chartType: PropsTypes.string,
89 | dataSource: PropsTypes.string,
90 | dataSourceUrl: PropsTypes.string,
91 | headTitle: PropsTypes.string,
92 | };
93 |
94 | export default Layout;
95 |
--------------------------------------------------------------------------------
/src/components/LineChart/Indicator.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { timeParse, select, pointer } from 'd3';
3 | import PropsTypes from 'prop-types';
4 |
5 | import { Group } from 'src/style/style';
6 |
7 | function Indicator({
8 | right,
9 | top,
10 | innerHeight,
11 | innerWidth,
12 | selectedYear,
13 | setSelectedYear,
14 | xScale,
15 | isHover,
16 | onMouseEnter,
17 | onMouseLeave,
18 | setXPosition,
19 | }) {
20 | const gRef = useRef();
21 | const parseYear = timeParse('%Y');
22 | const selectedYearData = parseYear(selectedYear);
23 |
24 | useEffect(() => {
25 | const g = select(gRef.current);
26 |
27 | g.append('rect')
28 | .attr('fill', 'none')
29 | .attr('pointer-events', 'all')
30 | .merge(g.select('rect'))
31 | .attr('width', innerWidth)
32 | .attr('height', innerHeight)
33 | .on('mousemove', e => {
34 | const x = pointer(e, g.node())[0];
35 | const hoveredDate = xScale.invert(x);
36 |
37 | setSelectedYear(hoveredDate.getFullYear());
38 | setXPosition(x);
39 | onMouseEnter();
40 | });
41 | }, []);
42 |
43 | return (
44 |
50 | {isHover && (
51 |
58 | )}
59 |
60 | );
61 | }
62 |
63 | Indicator.propTypes = {
64 | right: PropsTypes.number,
65 | top: PropsTypes.number,
66 | innerHeight: PropsTypes.number,
67 | innerWidth: PropsTypes.number,
68 | selectedYear: PropsTypes.number,
69 | setSelectedYear: PropsTypes.func,
70 | xScale: PropsTypes.func,
71 | isHover: PropsTypes.bool,
72 | onMouseEnter: PropsTypes.func,
73 | onMouseLeave: PropsTypes.func,
74 | setXPosition: PropsTypes.func,
75 | };
76 |
77 | export default React.memo(Indicator);
78 |
--------------------------------------------------------------------------------
/src/components/LineChart/LineChart.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useRef, useEffect } from 'react';
2 | import styled from 'styled-components';
3 | import {
4 | scaleLinear,
5 | extent,
6 | scaleTime,
7 | line,
8 | group,
9 | scaleOrdinal,
10 | schemeCategory10,
11 | curveMonotoneX,
12 | format,
13 | } from 'd3';
14 | import PropsTypes from 'prop-types';
15 |
16 | import Axes from 'components/Axes/Axes';
17 | import AxisLabel from 'components/AxisLabel/AxisLabel';
18 | import ColorLegend from 'components/ColorLegend/ColorLegend';
19 | import Indicator from 'components/LineChart/Indicator';
20 | import Tooltip from 'components/LineChart/Tooltip';
21 | import { Group, drawDash, MapWrapper, Map } from 'src/style/style';
22 | import { width, height } from 'utility/utility';
23 |
24 | const LinePath = styled.path`
25 | fill: none;
26 | stroke-width: 4;
27 | stroke-dasharray: ${props => props.pathLength};
28 | stroke-dashoffset: ${props => props.pathLength};
29 | stroke-linejoin: round;
30 | stroke-linecap: round;
31 | mix-blend-mode: multiply;
32 | animation: ${drawDash} 4s forwards;
33 | `;
34 |
35 | const Circle = styled.circle`
36 | fill: ${props => (props.isFilled ? props.color : 'white')};
37 | stroke: ${props => props.color};
38 | `;
39 |
40 | function LineChart({ data }) {
41 | const [selectedYear, setSelectedYear] = useState();
42 | const [isHover, setIsHover] = useState(false);
43 | const [xPosition, setXPosition] = useState(0);
44 | const [pathLength, setPathLength] = useState();
45 | const [showCircles, setShowCircles] = useState(false);
46 | const lineRef = useRef();
47 |
48 | useEffect(() => {
49 | // Animation
50 | const totalLength = lineRef?.current?.getTotalLength();
51 | setPathLength(totalLength);
52 |
53 | setTimeout(() => {
54 | setShowCircles(true);
55 | }, 4000);
56 | }, []);
57 |
58 | const margin = { top: 50, right: 100, bottom: 60, left: 50 };
59 | const innerWidth = width - margin.right - margin.left;
60 | const innerHeight = height - margin.top - margin.bottom;
61 |
62 | const xValue = d => new Date(parseInt(d.time), 0);
63 | const yValue = d => d.value;
64 | const colorValue = d => d.subject;
65 |
66 | const xScale = scaleTime()
67 | .domain(extent(data, xValue))
68 | .range([0, innerWidth]);
69 |
70 | const yScale = scaleLinear()
71 | .domain(extent(data, yValue))
72 | .range([innerHeight, 0])
73 | .nice();
74 |
75 | const colorScale = scaleOrdinal(schemeCategory10);
76 |
77 | const lineGenerator = line()
78 | .x(d => xScale(xValue(d)))
79 | .y(d => yScale(yValue(d)))
80 | .curve(curveMonotoneX);
81 |
82 | const nested = Array.from(group(data, colorValue), ([key, value]) => ({
83 | key,
84 | value,
85 | }));
86 |
87 | colorScale.domain(nested.map(d => d.key));
88 |
89 | const handleMouseEnter = useCallback(() => setIsHover(true), []);
90 | const handleMouseLeave = useCallback(() => setIsHover(false), []);
91 | const getValueofSelectedYear = () => {
92 | const crops = data.filter(d => {
93 | return (
94 | (d.subject === 'RICE') |
95 | (d.subject === 'WHEAT') |
96 | (d.subject === 'MAIZE') && d.time === selectedYear
97 | );
98 | });
99 | return crops;
100 | };
101 |
102 | const yAxisTickFormat = useCallback(number => {
103 | return format('.3s')(number).replace('.', '') + 'T';
104 | }, []);
105 |
106 | return (
107 |
108 |
188 |
189 | );
190 | }
191 |
192 | LineChart.propTypes = {
193 | data: PropsTypes.array,
194 | };
195 |
196 | export default LineChart;
197 |
--------------------------------------------------------------------------------
/src/components/LineChart/Tooltip.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import PropsTypes from 'prop-types';
4 |
5 | import { formatString } from 'utility/utility';
6 | import { TooltipWrapper } from 'style/style';
7 |
8 | const Wrapper = styled(TooltipWrapper).attrs(({ xPosition }) => ({
9 | style: {
10 | left: xPosition,
11 | },
12 | }))`
13 | top: ${props => props.yPosition}px;
14 | `;
15 |
16 | function Tooltip({ selectedYear, xPosition, crops }) {
17 | return (
18 |
19 | {selectedYear}
20 |
21 | {crops.map((crop, i) => (
22 | -
23 | {formatString(crop.subject)}: {crop.value}
24 |
25 | ))}
26 |
27 |
28 | );
29 | }
30 |
31 | Tooltip.propTypes = {
32 | selectedYear: PropsTypes.number,
33 | xPosition: PropsTypes.number,
34 | crops: PropsTypes.array,
35 | };
36 |
37 | export default React.memo(Tooltip);
38 |
--------------------------------------------------------------------------------
/src/components/ScatterPlot/ScatterPlot.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useRef, useEffect, useState } from 'react';
2 | import {
3 | scaleLinear,
4 | extent,
5 | select,
6 | scaleOrdinal,
7 | schemeCategory10,
8 | group,
9 | } from 'd3';
10 | import PropsTypes from 'prop-types';
11 |
12 | import { Group, MapWrapper, Map } from 'src/style/style';
13 | import { height, width } from 'utility/utility';
14 | import AxisLabel from 'components/AxisLabel/AxisLabel';
15 | import Axes from 'components/Axes/Axes';
16 | import ColorLegend from 'components/ColorLegend/ColorLegend';
17 | import Tooltip from 'components/ScatterPlot/Tooltip';
18 |
19 | function ScatterPlot({ data }) {
20 | const [selectedCircle, setSelectedCircle] = useState(null);
21 | const [position, setPosition] = useState({ xPosition: 0, yPosition: 0 });
22 | const circleRef = useRef();
23 |
24 | const margin = { top: 50, right: 100, bottom: 70, left: 100 };
25 | const innerWidth = width - margin.right - margin.left;
26 | const innerHeight = height - margin.top - margin.bottom;
27 | const circleRadius = 10;
28 |
29 | const xValue = d => d.youthRate;
30 | const yValue = d => d.elderlyRate;
31 | const colorValue = d => d.continent;
32 |
33 | const xScale = scaleLinear()
34 | .domain(extent(data, xValue))
35 | .range([0, innerWidth])
36 | .nice();
37 |
38 | const yScale = scaleLinear()
39 | .domain(extent(data, yValue))
40 | .range([innerHeight, 0])
41 | .nice();
42 |
43 | const colorScale = scaleOrdinal(schemeCategory10);
44 |
45 | const nested = Array.from(group(data, colorValue), ([key, value]) => ({
46 | key,
47 | value,
48 | }));
49 |
50 | colorScale.domain(nested.map(d => d.key));
51 |
52 | const axesTickFormat = useCallback(number => {
53 | return number + '%';
54 | }, []);
55 |
56 | useEffect(() => {
57 | const circleGroup = select(circleRef.current);
58 |
59 | circleGroup
60 | .selectAll('.scatter-circle')
61 | .data(data)
62 | .join('circle')
63 | .attr('class', 'scatter-circle')
64 | .attr('cy', innerHeight / 2)
65 | .attr('cx', innerWidth / 2)
66 | .attr('r', 0)
67 | .on('mouseover', (e, d) => {
68 | setSelectedCircle(d);
69 | setPosition({ xPosition: e.pageX, yPosition: e.pageY });
70 | })
71 | .on('mouseout', () => setSelectedCircle(null))
72 | .transition()
73 | .duration(2000)
74 | .delay((_, i) => i * 10)
75 | .attr('cy', d => yScale(yValue(d)))
76 | .attr('cx', d => xScale(xValue(d)))
77 | .attr('r', circleRadius)
78 | .style('fill', d => colorScale(colorValue(d)));
79 | }, []);
80 |
81 | return (
82 |
83 |
129 |
130 | );
131 | }
132 |
133 | ScatterPlot.propTypes = {
134 | data: PropsTypes.array,
135 | };
136 |
137 | export default ScatterPlot;
138 |
--------------------------------------------------------------------------------
/src/components/ScatterPlot/Tooltip.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import PropsTypes from 'prop-types';
4 |
5 | import { formatPercentage } from 'utility/utility';
6 | import { TooltipWrapper } from 'style/style';
7 |
8 | const Wrapper = styled(TooltipWrapper)`
9 | top: ${props => props.yPosition}px;
10 | left: ${props => props.xPosition}px;
11 | `;
12 |
13 | function Tooltip({ selectedCircle, position, xLabel, yLabel }) {
14 | const { xPosition, yPosition } = position;
15 | const { youthRate, elderlyRate } = selectedCircle;
16 |
17 | return (
18 |
19 |
20 | {selectedCircle.country}
21 | -
22 | {xLabel}: {formatPercentage(youthRate)}
23 |
24 | -
25 | {yLabel}: {formatPercentage(elderlyRate)}
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | Tooltip.propTypes = {
33 | selectedCircle: PropsTypes.object,
34 | position: PropsTypes.object,
35 | xLabel: PropsTypes.string,
36 | yLabel: PropsTypes.string,
37 | };
38 |
39 | export default React.memo(Tooltip);
40 |
--------------------------------------------------------------------------------
/src/style/reset.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | div,
4 | span,
5 | applet,
6 | object,
7 | iframe,
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6,
14 | p,
15 | blockquote,
16 | pre,
17 | a,
18 | abbr,
19 | acronym,
20 | address,
21 | big,
22 | cite,
23 | code,
24 | del,
25 | dfn,
26 | em,
27 | img,
28 | ins,
29 | kbd,
30 | q,
31 | s,
32 | samp,
33 | small,
34 | strike,
35 | strong,
36 | sub,
37 | sup,
38 | tt,
39 | var,
40 | b,
41 | u,
42 | i,
43 | center,
44 | dl,
45 | dt,
46 | dd,
47 | ol,
48 | ul,
49 | li,
50 | fieldset,
51 | form,
52 | label,
53 | legend,
54 | table,
55 | caption,
56 | tbody,
57 | tfoot,
58 | thead,
59 | tr,
60 | th,
61 | td,
62 | article,
63 | aside,
64 | canvas,
65 | details,
66 | embed,
67 | figure,
68 | figcaption,
69 | footer,
70 | header,
71 | hgroup,
72 | menu,
73 | nav,
74 | output,
75 | ruby,
76 | section,
77 | summary,
78 | time,
79 | mark,
80 | audio,
81 | video {
82 | margin: 0;
83 | padding: 0;
84 | border: 0;
85 | font-size: 100%;
86 | font: inherit;
87 | vertical-align: baseline;
88 | }
89 | /* HTML5 display-role reset for older browsers */
90 | article,
91 | aside,
92 | details,
93 | figcaption,
94 | figure,
95 | footer,
96 | header,
97 | hgroup,
98 | menu,
99 | nav,
100 | section {
101 | display: block;
102 | }
103 | body {
104 | line-height: 1;
105 | }
106 | ol,
107 | ul {
108 | list-style: none;
109 | }
110 | blockquote,
111 | q {
112 | quotes: none;
113 | }
114 | blockquote:before,
115 | blockquote:after,
116 | q:before,
117 | q:after {
118 | content: '';
119 | content: none;
120 | }
121 | table {
122 | border-collapse: collapse;
123 | border-spacing: 0;
124 | }
125 |
--------------------------------------------------------------------------------
/src/style/style.css:
--------------------------------------------------------------------------------
1 | .bubble {
2 | opacity: 0.7;
3 | fill: #d31922;
4 | pointer-events: none;
5 | }
6 |
7 | .scatter-circle {
8 | opacity: 0.7;
9 | transition: opacity 0.2s ease-in;
10 | }
11 |
12 | .scatter-circle:hover {
13 | opacity: 1;
14 | }
15 |
--------------------------------------------------------------------------------
/src/style/style.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components';
2 |
3 | export const Group = styled.g`
4 | transform: ${props => `translate(${props.right}px, ${props.top}px)`};
5 | `;
6 |
7 | export const drawDash = keyframes`
8 | to {
9 | stroke-dashoffset: 0;
10 | }
11 | `;
12 |
13 | export const MapWrapper = styled.div`
14 | overflow-x: scroll;
15 | width: 100%;
16 | `;
17 |
18 | export const Map = styled.div`
19 | overflow: hidden;
20 | display: flex;
21 | justify-content: center;
22 | margin: 3rem 0;
23 |
24 | @media (max-width: 992px) {
25 | display: inline-flex;
26 | justify-content: initial;
27 | }
28 |
29 | svg {
30 | display: inline-table;
31 | }
32 | `;
33 |
34 | export const Dropdown = styled.select`
35 | font-size: 1.4rem;
36 | padding: 0.3rem 0.8rem;
37 | margin: 3rem 0 3rem 5rem;
38 |
39 | &:focus {
40 | outline: 0;
41 | }
42 | `;
43 |
44 | export const TooltipWrapper = styled.div`
45 | position: absolute;
46 | z-index: 4000;
47 | font-size: 1.2rem;
48 | padding: 1.5rem;
49 | box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2);
50 | background-color: white;
51 | pointer-events: none;
52 |
53 | h3,
54 | li:not(:last-child) {
55 | margin-bottom: 0.7rem;
56 | }
57 |
58 | h3 {
59 | font-size: 1.4rem;
60 | }
61 | `;
62 |
--------------------------------------------------------------------------------
/src/utility/utility.js:
--------------------------------------------------------------------------------
1 | import { format } from 'd3';
2 |
3 | export const formatNumber = format(',');
4 | export const formatPercentage = n => n.toFixed(2) + '%';
5 |
6 | export const formatString = str => {
7 | const lowerStr = str.toLowerCase();
8 | return lowerStr.charAt(0).toUpperCase() + lowerStr.slice(1);
9 | };
10 |
11 | export const width = 1000;
12 | export const height = 500;
13 |
--------------------------------------------------------------------------------