25
loading...
This website collects cookies to deliver better user experience
export const data =
[{"country":"DENMARK","currency":"RDDUSD","type":"RENEWABLE","year":1975,"amount":0.804},
{"country":"DENMARK","currency":"RDDUSD","type":"RENEWABLE","year":1976,"amount":1.350},
{"country":"DENMARK","currency":"RDDUSD","type":"RENEWABLE","year":1977,"amount":7.928},
{"country":"DENMARK","currency":"RDDUSD","type":"RENEWABLE","year":1978,"amount":15.357}]
npx create-next-app@latest visx-demo
cd visx-demo
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4
npm install @visx/axis @visx/event @visx/glyph @visx/gradient @visx/grid @visx/group @visx/responsive @visx/scale @visx/shape @visx/tooltip
pages/_app.js
component, to include chakra styling.import { ChakraProvider, CSSReset } from "@chakra-ui/react";
import Head from "next/head";
const GlobalStyle = ({ children }) => {
return (
<>
<Head>
<meta content="width=device-width, initial-scale=1"
name="viewport" />
</Head>
<CSSReset />
{children}
</>
);
};
function MyApp({ Component, pageProps }) {
return (
<ChakraProvider>
<GlobalStyle />
<Component {...pageProps} />
</ChakraProvider>
)
}
export default MyApp
components/Header.js
:import { Box, Flex, Heading } from "@chakra-ui/react";
const Header = () => {
return (
<Box
pos="fixed"
as="header"
top="0"
bg='#242730'
left="0"
right="0"
borderBottomWidth="1px"
width="full"
height="4rem"
>
<Box width="full" mx="auto" px={6} pr={[1, 6]} height="100%" >
<Flex size="100%" p={[0, 3]} pl={[0, 4]} align="center" justify="space-between">
<Box as="a" d="block" href="/" aria-label="VisX Area Chart">
<Heading color="gray.100" as="h4" size="md">VizX Area Chart</Heading>
</Box>
</Flex>
</Box>
</Box>
);
}
export default Header;
pages/index.js
file and build simple layout with chakra ui components.import Header from "../components/Header"
import { Container, Heading, Box, Text, Link } from '@chakra-ui/react'
import ParentSize from '@visx/responsive/lib/components/ParentSize';
import LineChart from '../components/LineChart';
import { data } from '../data/stats_for_Denmark'
export default function Home() {
return (
<>
<Header />
<Box height='100vh' bg="#242730">
<Container maxW='4xl' height='85vh' mt="4rem" >
<Heading ml='40px' as='i' size='md' color={'gray.100'}>Denmark R&D Spend on Renewable Energy vs Total</Heading>
// Chart will go here
<Link ml='40px' fontSize='sm' color={'gray.100'} href='https://www.iea.org/data-and-statistics/data-product/energy-technology-rd-and-d-budget-database-2' isExternal>
Data by IEA, 2021
</Link></Container>
</Box>
</>
)
}
<ParentSize />
component, which could be imported from @visx/responsive/lib/components/ParentSize
. We will wrap our chart component in <ParentSize />
and will get width and height as props.<ParentSize>
{({ width, height }) =>
<LineChart data={data} width={width} height={height} />}</ParentSize>
<Group />
and visualise margins
. VisX <Group />
components is just a container for all children <g />
elements, it allows to simplify the code and pass the margins. components/LineChart.js
:import { Group } from "@visx/group";
function LineChart({ data, width, height }) {
// define margins from where to start drawing the chart
const margin = { top: 40, right: 40, bottom: 50, left: 40 };
// defining inner measurements
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
return (
<svg width={width} height={height} >
<rect x={0} y={0} width={width} height={height} fill={'#718096'} rx={14} />
<Group left={margin.left} top={margin.top}>
<rect x={0} y={0} width={innerWidth} height={innerHeight} fill={'#A0AEC0'} />
</Group>
</svg>
)
}
export default LineChart
<svg />
elements in a coordinate system from top left corner (0,0), and the margins are defined clockwise with this initial coordinate, i.e. point of origin. d3.extent()
function, which returns a min and max values from the array. const formatDate = (year) => year.toString()
components/Chart.js
:// Defining selector functions
const getRD = (d) => d.amount;
const getDate = (d) => d.year;
// Defining scales
// horizontal, x scale
const timeScale = scaleLinear({
range: [0, innerWidth],
domain: extent(data, getDate),
nice: true
})
// vertical, y scale
const rdScale = scaleLinear({
range: [innerHeight, 0],
domain: extent(data, getRD),
nice: true,
});
<svg/>
container:<AxisLeft
tickTextFill={'#EDF2F7'}
stroke={'#EDF2F7'}
tickStroke={'#EDF2F7'}
scale={rdScale}
tickLabelProps={() => ({
fill: '#EDF2F7',
fontSize: 11,
textAnchor: 'end',
})}
/>
<text x="-125" y="20" transform="rotate(-90)" fontSize={12} fill='#EDF2F7'>
R&D Spend, RDDUSD
</text>
<AxisBottom
scale={timeScale}
stroke={'#EDF2F7'}
tickFormat={formatDate}
tickStroke={'#EDF2F7'}
tickTextFill={'#EDF2F7'}
top={innerHeight}
tickLabelProps={() => ({
fill: '#EDF2F7',
fontSize: 11,
textAnchor: 'middle',
})}
/>
<GridRows />
and GridColumns />
for our chart, they will use the same scales as the axes.<GridRows
scale={rdScale}
width={innerWidth}
height={innerHeight - margin.top}
stroke='#EDF2F7'
strokeOpacity={0.2}
/>
<GridColumns
scale={timeScale}
width={innerWidth}
height={innerHeight}
stroke='#EDF2F7'
strokeOpacity={0.2}
/>
<LinePath />
component. I want to build two lines to compare Denmark Total R&D investments versus investment in renewable energy. For that, I will filter the data from the original array and will define a series, which will contain both outputs to build lines. //colours for lines
const colors = ['#43b284', '#fab255']
// data for lines
const data1 = data.filter(function (el) {
return el.type === "RENEWABLE"
});
const data2 = data.filter(function (el) {
return el.type === "TOTAL"
});
const series = [data1, data2]
<LinePath />
inside the<svg />
container:{series.map((sData, i) => (
<LinePath
key={i}
stroke={colors[i]}
strokeWidth={3}
data={sData}
x={(d) => timeScale(getDate(d)) ?? 0}
y={(d) => rdScale(getRD(d)) ?? 0}
/>
))}
<div position = 'relative'>
/// Your whole component ///
</div>
// tooltip parameters
const {
tooltipData,
tooltipLeft = 0,
tooltipTop = 0,
showTooltip,
hideTooltip
} = useTooltip();
// function get data from a year
const getD = (year) => {
const output = data.filter(function (el) {
return el.year === year
})
return output
}
const handleTooltip = useCallback((event) => {
const { x } = localPoint(event) || { x: 0 };
const x0 = timeScale.invert(x - margin.left); // get Date from the scale
const index = bisectDate(data, x0, 1); // get index of this date from the array
const d0 = data[index - 1];
const d1 = data[index];
let d = d0;
// is previous data point available?
if (d1 && getDate(d1)) {
d = x0.valueOf() - getDate(d0).valueOf() >
getDate(d1).valueOf() - x0.valueOf() ? d1 : d0;
}
showTooltip({
tooltipData: getD(d.year),
tooltipLeft: x,
tooltipTop: rdScale(getRD(d))
})
})
[
{
"country": "DENMARK",
"currency": "RDDUSD",
"type": "RENEWABLE",
"year": 2006,
"amount": 41.657
},
{
"country": "DENMARK",
"currency": "RDDUSD",
"type": "TOTAL",
"year": 2006,
"amount": 112.857
}
]
{/* render a tooltip */}
{tooltipData ? (
<TooltipWithBounds
key={Math.random()}
top={tooltipTop}
left={tooltipLeft}
style={tooltipStyles}
>
<p>{`Total Spend: $${getRD(tooltipData[1])}`}</p>
<p>{`Renewable Spend: $${getRD(tooltipData[0])}`}</p>
<p>{`Year: ${getDate(tooltipData[1])}`}</p>
</TooltipWithBounds>
)
: null}
<g/>
elements must go inside the<svg />
:{tooltipData && (
<g>
<Line
from={{ x: tooltipLeft - margin.left, y: 0 }}
to={{ x: tooltipLeft - margin.left, y:innerHeight
}}
stroke={'#EDF2F7'}
strokeWidth={2}
pointerEvents="none"
strokeDasharray="4,2"
/>
</g>
)}
@visx/glyph
:{tooltipData && tooltipData.map((d, i) => (
<g>
<GlyphCircle
left={tooltipLeft - margin.left}
top={rdScale(d.amount) + 2}
size={110}
fill={colors[i]}
stroke={'white'}
strokeWidth={2} />
</g>
))}
handleTooltip
on any user interaction:<rect
x={0}
y={0}
width={innerWidth}
height={innerHeight}
fill={'transparent'}
onTouchStart={handleTooltip}
onTouchMove={handleTooltip}
onMouseMove={handleTooltip}
onMouseLeave={() => hideTooltip()}
/>
<rect />
after all my elements, because they are stacked one onto the other, and making this the top element will enable interactivity for all of chart.