From 2120a7b4db12a0c856b6fa1664a7380c18c06931 Mon Sep 17 00:00:00 2001 From: ricale Date: Thu, 13 Apr 2023 09:46:16 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[C02]=20C02LineChartWithOptionsScreen=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/navigation/RootNavigation.tsx | 7 ++ src/navigation/types.ts | 1 + .../comps/LineChart/LineChartBody/Grid.tsx | 27 +++++++ .../comps/LineChart/LineChartBody/Lines.tsx | 25 +++++++ .../comps/LineChart/LineChartBody/XAxis.tsx | 51 +++++++++++++ .../comps/LineChart/LineChartBody/YAxis.tsx | 51 +++++++++++++ .../comps/LineChart/LineChartBody/index.tsx | 33 +++++++++ .../comps/LineChart/constants.ts | 2 + .../comps/LineChart/getLinearScale.ts | 32 +++++++++ .../comps/LineChart/getTimeScale.ts | 25 +++++++ .../comps/LineChart/index.tsx | 71 +++++++++++++++++++ .../comps/LineChart/types.ts | 4 ++ .../C02LineChartWithOptionsScreen/dummy.ts | 18 +++++ .../C02LineChartWithOptionsScreen/index.tsx | 29 ++++++++ src/screens/HomeScreen/index.tsx | 26 +++++-- 15 files changed, 398 insertions(+), 4 deletions(-) create mode 100644 src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/Grid.tsx create mode 100644 src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/Lines.tsx create mode 100644 src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/XAxis.tsx create mode 100644 src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/YAxis.tsx create mode 100644 src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/index.tsx create mode 100644 src/screens/C02LineChartWithOptionsScreen/comps/LineChart/constants.ts create mode 100644 src/screens/C02LineChartWithOptionsScreen/comps/LineChart/getLinearScale.ts create mode 100644 src/screens/C02LineChartWithOptionsScreen/comps/LineChart/getTimeScale.ts create mode 100644 src/screens/C02LineChartWithOptionsScreen/comps/LineChart/index.tsx create mode 100644 src/screens/C02LineChartWithOptionsScreen/comps/LineChart/types.ts create mode 100644 src/screens/C02LineChartWithOptionsScreen/dummy.ts create mode 100644 src/screens/C02LineChartWithOptionsScreen/index.tsx diff --git a/src/navigation/RootNavigation.tsx b/src/navigation/RootNavigation.tsx index 80b4309..908bae5 100644 --- a/src/navigation/RootNavigation.tsx +++ b/src/navigation/RootNavigation.tsx @@ -2,7 +2,9 @@ import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import C01LineChartScreen from '../screens/C01LineChartScreen'; +import C02LineChartWithOptionsScreen from '../screens/C02LineChartWithOptionsScreen'; import HomeScreen from '../screens/HomeScreen'; + import { RootStackParamsList } from './types'; const Stack = createNativeStackNavigator(); @@ -17,6 +19,11 @@ function RootNavigation() { component={C01LineChartScreen} options={{ animation: 'none' }} /> + ); diff --git a/src/navigation/types.ts b/src/navigation/types.ts index 70b3c35..e3f470b 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -1,4 +1,5 @@ export type RootStackParamsList = { C01LineChart: undefined; + C02LineChartWithOptions: undefined; Home: undefined; }; diff --git a/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/Grid.tsx b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/Grid.tsx new file mode 100644 index 0000000..57db7a0 --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/Grid.tsx @@ -0,0 +1,27 @@ +import { G, Line } from 'react-native-svg'; + +type GridProps = { + yScale: d3.ScaleLinear; + x1: number; + x2: number; +}; +function Grid({ yScale, x1, x2 }: GridProps) { + const ticks = yScale.ticks(); + return ( + + {ticks.map(tick => ( + + ))} + + ); +} + +export default Grid; diff --git a/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/Lines.tsx b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/Lines.tsx new file mode 100644 index 0000000..72dc651 --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/Lines.tsx @@ -0,0 +1,25 @@ +import { G, Path } from 'react-native-svg'; +import { TimeSeries } from '../types'; + +type LinesProps = { + series: TimeSeries[]; + lineFunc: d3.Line; +}; +function Lines({ series, lineFunc }: LinesProps) { + return ( + + {series.map((sr, i) => ( + + ))} + + ); +} + +export default Lines; diff --git a/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/XAxis.tsx b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/XAxis.tsx new file mode 100644 index 0000000..ffddabc --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/XAxis.tsx @@ -0,0 +1,51 @@ +import * as d3 from 'd3'; +import { G, Line, Text } from 'react-native-svg'; +import dateFormat from '../../../../../utils/dateFormat'; + +const TICK_HEIGHT = 6; + +type XAxisProps = { + scale: d3.ScaleTime; + x?: number; + y?: number; +}; +function XAxis({ scale, x = 0, y = 0 }: XAxisProps) { + const range = scale.range(); + const ticks = scale.ticks(); + return ( + + + {ticks.map(tick => ( + + ))} + {ticks.map(tick => ( + + {`${dateFormat(tick)}`} + + ))} + + ); +} + +export default XAxis; diff --git a/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/YAxis.tsx b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/YAxis.tsx new file mode 100644 index 0000000..398c279 --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/YAxis.tsx @@ -0,0 +1,51 @@ +import { G, Line, Text } from 'react-native-svg'; + +const TICK_WIDTH = 6; + +type YAxisProps = { + scale: d3.ScaleLinear; + x: number; + y: number; +}; +function YAxis({ scale, x, y }: YAxisProps) { + const range = scale.range(); + const ticks = scale.ticks(); + + return ( + + + {ticks.map(tick => ( + + ))} + {ticks.map(tick => ( + + {`${tick}`} + + ))} + + ); +} + +export default YAxis; diff --git a/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/index.tsx b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/index.tsx new file mode 100644 index 0000000..96df617 --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/LineChartBody/index.tsx @@ -0,0 +1,33 @@ +import { Fragment } from 'react'; +import XAxis from './XAxis'; +import YAxis from './YAxis'; +import Grid from './Grid'; +import { TimeSeries } from '../types'; +import Lines from './Lines'; +import PaneBoundary from '../../../../../utils/PaneBoundary'; + +type LineChartBodyProps = { + series: TimeSeries[]; + xScale: d3.ScaleTime; + yScale: d3.ScaleLinear; + lineFunc: d3.Line; + paneBoundary: PaneBoundary; +}; +function LineChartBody({ + series, + xScale, + yScale, + lineFunc, + paneBoundary, +}: LineChartBodyProps) { + return ( + + + + + + + ); +} + +export default LineChartBody; diff --git a/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/constants.ts b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/constants.ts new file mode 100644 index 0000000..76564cc --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/constants.ts @@ -0,0 +1,2 @@ +export const X_AXIS_HEIGHT = 25; +export const Y_AXIS_WIDTH = 40; diff --git a/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/getLinearScale.ts b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/getLinearScale.ts new file mode 100644 index 0000000..cb02f05 --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/getLinearScale.ts @@ -0,0 +1,32 @@ +import * as d3 from 'd3'; +import { TimeSeries } from './types'; + +function getLinearScale(series: TimeSeries[], range: [number, number]) { + if (!series?.length) { + return null; + } + if (range[0] === 0 && range[1] === 0) { + return null; + } + + const allData = series.reduce( + (acc, sr) => [...acc, ...sr.data], + [] as TimeSeries['data'] + ); + if (!allData.length) { + return null; + } + + const domain = d3.extent(allData, dt => dt.value) as [number, number]; + + const offsetDelta = 0.2; + const offset = (domain[1] - domain[0]) * offsetDelta; + domain[0] -= offset; + domain[1] += offset; + + const scale = d3.scaleLinear().domain(domain).range(range); + + return scale; +} + +export default getLinearScale; diff --git a/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/getTimeScale.ts b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/getTimeScale.ts new file mode 100644 index 0000000..75d9dca --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/getTimeScale.ts @@ -0,0 +1,25 @@ +import * as d3 from 'd3'; +import { TimeSeries } from './types'; + +function getTimeScale( + series: TimeSeries[] | undefined, + range: [number, number] +) { + if (!series?.length) { + return null; + } + if (range[0] === 0 && range[1] === 0) { + return null; + } + + const allData = series.reduce( + (acc, sr) => [...acc, ...sr.data], + [] as TimeSeries['data'] + ); + const domain = d3.extent(allData, dt => dt.date) as [Date, Date]; + const scale = d3.scaleTime().domain(domain).range(range); + + return scale; +} + +export default getTimeScale; diff --git a/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/index.tsx b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/index.tsx new file mode 100644 index 0000000..694e60d --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/index.tsx @@ -0,0 +1,71 @@ +import * as d3 from 'd3'; +import { Svg, SvgProps } from 'react-native-svg'; +import { useImmer } from 'use-immer'; +import { TimeSeries } from './types'; +import getTimeScale from './getTimeScale'; +import getLinearScale from './getLinearScale'; +import LineChartBody from './LineChartBody'; +import { X_AXIS_HEIGHT, Y_AXIS_WIDTH } from './constants'; +import PaneBoundary from '../../../../utils/PaneBoundary'; + +type LineChartProps = { + series: TimeSeries[]; + width?: string | number; + height?: string | number; +}; +function LineChart({ series, width = '100%', height = 200 }: LineChartProps) { + const [state, setState] = useImmer({ + width: 0, + height: 0, + paneBoundary: new PaneBoundary({ x1: 0, x2: 0, y1: 0, y2: 0 }), + }); + + const onLayout: SvgProps['onLayout'] = evt => { + const { layout } = evt.nativeEvent; + setState(dr => { + dr.width = Math.round(layout.width); + dr.height = Math.round(layout.height); + + const marginTop = 10; + const marginLeft = 10; + const marginRight = 10; + const marginBottom = 10; + + dr.paneBoundary = new PaneBoundary({ + x1: marginLeft + Y_AXIS_WIDTH, + x2: dr.width - marginRight, + y1: dr.height - marginBottom - X_AXIS_HEIGHT, + y2: marginTop, + }); + }); + }; + + const xScale = getTimeScale(series, state.paneBoundary.xs); + const yScale = getLinearScale(series, state.paneBoundary.ys); + const lineFunc = + !xScale || !yScale + ? null + : d3 + .line() + .defined(dt => !isNaN(dt.value)) + .x(dt => xScale(dt.date)) + .y(dt => yScale(dt.value)); + + const loaded = xScale !== null && yScale !== null && lineFunc !== null; + + return ( + + {loaded && ( + + )} + + ); +} + +export default LineChart; diff --git a/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/types.ts b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/types.ts new file mode 100644 index 0000000..5653f1a --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/comps/LineChart/types.ts @@ -0,0 +1,4 @@ +export type TimeSeriesDatum = { date: Date; value: number }; +export type TimeSeries = { + data: TimeSeriesDatum[]; +}; diff --git a/src/screens/C02LineChartWithOptionsScreen/dummy.ts b/src/screens/C02LineChartWithOptionsScreen/dummy.ts new file mode 100644 index 0000000..4aee2b9 --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/dummy.ts @@ -0,0 +1,18 @@ +const dummyValues = [ + 990810, 921890, 1080000, 857030, 865140, 780000, 978650, 1136757, 1258378, + 1002973, 1286757, 1477297, 1671892, 1469189, 1440811, 1400270, 1538108, + 1371892, 1363784, 1542162, 1497568, 1570541, 1631351, 1542162, 1667838, + 1757027, 1813784, 1854324, 1931352, 1732703, 1744865, 1680000, 1643514, + 1558378, 1400270, 1602973, 1611081, 1546216, 1615135, 1651622, 1700270, + 1838108, 1915136, 1980000, 1793514, 1740811, 1748919, 1882703, 1821892, + 1728649, 1761081, 1631351, 1594865, 1448919, 1388108, 1448919, +]; +const dummyDate = new Date('2022-01-01'); + +const dummy = dummyValues.map((value, i) => { + const date = new Date(dummyDate); + date.setDate(date.getDate() + i); + return { date, value: Math.floor(value / 1000) }; +}); + +export default dummy; diff --git a/src/screens/C02LineChartWithOptionsScreen/index.tsx b/src/screens/C02LineChartWithOptionsScreen/index.tsx new file mode 100644 index 0000000..e4db3fd --- /dev/null +++ b/src/screens/C02LineChartWithOptionsScreen/index.tsx @@ -0,0 +1,29 @@ +import { StyleSheet, View } from 'react-native'; +import ScreenContainer from '../../components/ScreenContainer'; +import Text from '../../components/Text'; +import LineChart from './comps/LineChart'; +import dummy from './dummy'; + +function C02LineChartWithOptionsScreen() { + return ( + + + C02LineChartWithOptionsScreen + - 라인 차트 옵션 기능 구현 + + + + + ); +} + +const styles = StyleSheet.create({ + header: { + padding: 16, + }, + title: { + fontSize: 20, + }, + subtitle: {}, +}); +export default C02LineChartWithOptionsScreen; diff --git a/src/screens/HomeScreen/index.tsx b/src/screens/HomeScreen/index.tsx index b4fbd8b..dc1e920 100644 --- a/src/screens/HomeScreen/index.tsx +++ b/src/screens/HomeScreen/index.tsx @@ -3,6 +3,16 @@ import Button from '../../components/Button'; import ScreenContainer from '../../components/ScreenContainer'; import Text from '../../components/Text'; import useNavigation from '../../navigation/useNavigation'; +import { RootStackParamsList } from '../../navigation/types'; +import { Fragment } from 'react'; + +const buttons: { title: string; screenName: keyof RootStackParamsList }[] = [ + { title: 'C01 - 라인차트 기본 기능 구현', screenName: 'C01LineChart' }, + { + title: 'C02 - 라인차트 옵션 기능 구현', + screenName: 'C02LineChartWithOptions', + }, +]; function HomeScreen() { const nav = useNavigation(); @@ -11,10 +21,15 @@ function HomeScreen() { 화면 목록 -