├── .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 | Screen Shot 2021-01-18 at 10 59 08 PM 16 | 17 |
18 | 19 | ### 1. Bubble Map 20 | 21 | ![Kapture 2021-01-11 at 20 10 59](https://user-images.githubusercontent.com/55128990/104173607-6643d780-5449-11eb-91b4-51f53c32b22f.gif) 22 | 23 |
24 | 25 | ### 2. Multi Line Chart 26 | 27 | ![Kapture 2021-01-11 at 20 16 26](https://user-images.githubusercontent.com/55128990/104175137-0bf74680-544a-11eb-85a0-337184de9d5c.gif) 28 | 29 |
30 | 31 | ### 3. Bar Chart 32 | 33 | ![Kapture 2021-01-12 at 19 31 10](https://user-images.githubusercontent.com/55128990/104303017-dff0c980-550c-11eb-8196-bd05f0fa2d16.gif) 34 | 35 |
36 | 37 | ### 4. Scatter Plot 38 | 39 | ![Kapture 2021-01-18 at 22 50 30](https://user-images.githubusercontent.com/55128990/104923751-d7523480-59df-11eb-96f7-64559b7eb1d3.gif) 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 Visualization<span>with D3.js and React</span> 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 | 125 | 126 | 131 | 140 | 141 | 142 | 143 | Millions of People 144 | 145 | 146 | 147 | 148 | 149 | 150 | 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 | 109 | 110 | 111 | 112 | 113 | 114 | {countries.features.map((country, i) => ( 115 | 116 | {handleTooltip(country)} 117 | 118 | ))} 119 | 120 | 129 | 130 | 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 |