저번 포스팅 때, Legend에 hover했을 때, 해당 데이터만 highlight 되도록 구현을 했다.

이번에는 Legend를 커스터마이징하여 색상도 바꿔보도록 하려고한다.

1. CustomLegend 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const CustomLegend = (props: any) => {
const { payload, onMouseEnter, onMouseLeave } = props;

return (
<ul>
{payload.map((entry: any, index: any) => (
<li
key={`item-${index}`}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={{ listStyle: "none", color: colors.GRAPH[`${index + 1}`] }}
>
{entry.value}
</li>
))}
</ul>
);
};
  • 예시를 위해 타입은 any로 설정하였다.
  • Legend의 각 li에 mouse 이벤트를 할당하였다.
  • 마우스 이벤트는 호버된 데이터를 제외한 데이터들의 opacity를 줄여서 해당 데이터만 highlight 되도록 한다.

2. CustomLegend의 props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const handleMouseEnter = (o: any) => {
const dataKey = o.target.innerHTML;

const entries = Object.entries(opacity).map(([key, value]) =>
key === dataKey ? [key, 1] : [key, 0.2]
);

const mappedObj: any = entries.reduce((prev, curr) => {
const [key, value] = curr;
prev = { ...prev, [key]: value };
return prev;
}, {});

setOpacity(mappedObj);
};

그런데, 해당 데이터에 호버를 해도 모든 데이터의 opacity가 줄어드는 문제가 발생했다.

그 이유는 Legend에서의 props와 customLegend의 props가 달라서 mouse 이벤트가 잘못 동작했기 때문이다.

  • 이전 mouse 이벤트에서는 props안에 dataKey 속성으로 호버된 데이터 값을 가져올 수 있었다.
  • 하지만 customLegend에서는 props에 너무나도 많은 속성이 있었고 이 중 나는 target속성의 innerHTML 속성으로 호버된 데이터의 dataKey를 확인하는 로직을 구성하였다.

3. 전체 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { useEffect, useState } from "react";
import {
LineChart,
Line,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
import colors from "styles/colors";

const CustomLegend = (props: any) => {
const { payload, onMouseEnter, onMouseLeave } = props;

return (
<ul>
{payload.map((entry: any, index: any) => (
<li
key={`item-${index}`}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={{ listStyle: "none", color: colors.GRAPH[`${index + 1}`] }}
>
{entry.value}
</li>
))}
</ul>
);
};

function NewPortChart() {
const [opacity, setOpacity] = useState<any>({});

const handleMouseEnter = (o: any) => {
const dataKey = o.target.innerHTML;

const entries = Object.entries(opacity).map(([key, value]) =>
key === dataKey ? [key, 1] : [key, 0.2]
);

const mappedObj: any = entries.reduce((prev, curr) => {
const [key, value] = curr;
prev = { ...prev, [key]: value };
return prev;
}, {});

setOpacity(mappedObj);
};

const handleMouseLeave = () => {
const entries = Object.entries(opacity).map(([key, value]) => [key, 1]);

const mappedObj: any = entries.reduce((prev, curr) => {
const [key, value] = curr;
prev = { ...prev, [key]: value };
return prev;
}, {});

setOpacity(mappedObj);
};

useEffect(() => {
const mappedOpacity = Object.keys(data[0]).reduce((prev, curr) => {
prev = { ...prev, [curr]: 1 };
return prev;
}, {});
setOpacity(mappedOpacity);
}, []);

return (
<ResponsiveContainer width="100%" height="100%">
<LineChart width={857} height={440} data={data}>
<Tooltip />
<Legend
align="right"
verticalAlign="middle"
layout="vertical"
content={
<CustomLegend
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
}
/>
{Object.keys(data[0]).map((key, index) => (
<Line
key={key}
type="linear"
dataKey={key}
strokeOpacity={opacity[key]}
strokeLinecap="round"
stroke={colors.GRAPH[`${index + 1}`]}
activeDot={false}
dot={false}
/>
))}
</LineChart>
</ResponsiveContainer>
);
}

export default NewPortChart;

기본 CustomLegend

기본 customlegend

Hover된 데이터만 highlight

hover1
hover2
hover3

추가로…

색상만 바꿀 것이였다면 왜 CustomLegend까지 쓰면서 복잡하게 시도를 했을까 궁금증이 들 수도 있다.

디자이너 요구사항이 Legend와 해당 Line 데이터 끝 부분을 선으로 연결해달라는 요청이 있었기 때문에 CustomLegend를 사용해보았다.

아직 해당 부분은 좀 더 고민이 필요하기 때문에 추후에 포스팅하도록 하겠다.

댓글 공유

서비스가 주식 관련 서비스이다 보니 차트를 사용할 일이 잦다.

차트를 직접 구현하자니 너무 공수가 많이 들 것 같아 Recharts 라이브러리를 자주 사용하고 있다.

하지만 공식문서에서 모든게 나와있지 않아서 ChatGPT의 도움도 많이 받고 있다. 그래도 꽤 쓸만한 라이브러리이다.

오늘은 Recharts 라이브러리로 legend에 hover했을 때, hover된 데이터만 highlight 되도록 구현해볼 것이다.

chart

우선 기본적인 Line 차트를 렌더링한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { useState } from "react";
import {
LineChart,
Line,
XAxis,
YAxis,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";

const data = [
{
name: "Page A",
uv: 4000,
pv: 2400,
amt: 2400,
},
{
name: "Page B",
uv: 3000,
pv: 1398,
amt: 2210,
},
{
name: "Page C",
uv: 2000,
pv: 9800,
amt: 2290,
},
{
name: "Page D",
uv: 2780,
pv: 3908,
amt: 2000,
},
{
name: "Page E",
uv: 1890,
pv: 4800,
amt: 2181,
},
{
name: "Page F",
uv: 2390,
pv: 3800,
amt: 2500,
},
{
name: "Page G",
uv: 3490,
pv: 4300,
amt: 2100,
},
];

function MultiLineCharts() {
const [opacity, setOpacity] = useState({ uv: 1, pv: 1 });

const handleMouseEnter = (o) => {
const { dataKey } = o;

const entries = Object.entries(opacity).map(([key, value]) =>
key === dataKey ? [key, 1] : [key, 0.2]
);

const mappedObj = entries.reduce((prev, curr) => {
const [key, value] = curr;
prev = { ...prev, [key]: value };
return prev;
}, {});

setOpacity(mappedObj);
};

const handleMouseLeave = (o) => {
const { dataKey } = o;

const entries = Object.entries(opacity).map(([key, value]) => [key, 1]);

const mappedObj = entries.reduce((prev, curr) => {
const [key, value] = curr;
prev = { ...prev, [key]: value };
return prev;
}, {});

setOpacity(mappedObj);
};
return (
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height={300}>
<LineChart
width={500}
height={300}
data={data}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
<Line
type="monotone"
dataKey="pv"
strokeOpacity={opacity.pv}
stroke="#8884d8"
activeDot={{ r: 8 }}
/>
<Line
type="monotone"
dataKey="uv"
strokeOpacity={opacity.uv}
stroke="#82ca9d"
/>
</LineChart>
</ResponsiveContainer>
</ResponsiveContainer>
);
}

export default MultiLineCharts;
  • Line 그래프의 opacityuseState로 관리한다.
  • Legend에 onMouseEnter, onMouseLeave 이벤트를 할당한다.
  • 여기서 유저가 이벤트를 발생시킨 요소만 opacity를 두고 나머지 데이터들의 opacity를 줄여주기 위해 opacity 객체를 재구성했다.
  • Object.entries()reduce()를 사용하여 편리하게 객체를 재구성할 수 있다.

결과

hover chart

  • 보라색 Legend Hover 시

hover_chart2

  • 연두색 Legend Hover 시

댓글 공유

input에 입력값을 입력하기 전까지 placeholder는 사라지지 않는다.

이는 input 창이 포커싱되어있는지 헷갈리게 할 수 있어 UX를 떨어뜨릴 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
input:focus::-webkit-input-placeholder,
textarea:focus::-webkit-input-placeholder {
/* WebKit browsers */
color: transparent;
}

input:focus:-moz-placeholder,
textarea:focus:-moz-placeholder {
/* Mozilla Firefox 4 to 18 */
color: transparent;
}

input:focus::-moz-placeholder,
textarea:focus::-moz-placeholder {
/* Mozilla Firefox 19+ */
color: transparent;
}

input:focus:-ms-input-placeholder,
textarea:focus:-ms-input-placeholder {
/* Internet Explorer 10+ */
color: transparent;
}
  • 크로스 브라우징을 고려하여 CSS를 추가해주었다.
  • 포커스가 되었을 때, placeholder 색상을 투명하게 설정하도록 해준다.

댓글 공유

객체의 key 값에 유니온 타입 선언하기

1
2
3
4
5
type userInfoType = "name" | "age" | "address";

interface User {
[key: userInfoType]: string;
}

유니온 타입 에러

  • “인덱스 시그니처 매개변수는 리터럴 타입이나 제네릭 타입이 될 수 없다.”는 에러가 발생했다.
  • 매핑된 객체를 대신 사용하라고 알려준다.

해결방법

1
2
3
4
5
6
7
8
9
10
11
type userInfoType = "name" | "age" | "address";

type userType = {
[key in userInfoType]: string;
};

let user: userType = {
name: "홍길동",
age: "20",
address: "서울",
};

댓글 공유

Queue 클래스로 구현하기

enqueue와 dequeue

  • Queue는 입구가 rear이고 출구가 front이다.
  • 입구쪽에서 데이터를 추가하는 것을 enqueue라고 한다.
  • 출구쪽에서 데이터를 제거하는 것을 dequeue라고 한다.

노드 삽입할 때,

  • 빈 Queue이면 front, rear가 모두 첫 노드를 가리킨다.
  • 빈 Queue가 아니면, rear의 next가 새 노드를 가리키고 rear를 새 노드로 옮긴다.

노드 꺼낼 때,

  • 빈 Queue가 되면, front, rear는 모두 None을 가리킨다.
  • Queue에 노드가 남아있으면 front를 front의 next로 옮긴다.

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Node:
def __init__(self,data):
self.data = data
self.next = None

class Queue:
def __init__(self):
self.front = None
self.rear = None

def enqueue(self,data):
node = Node(data)
if self.front == None:
self.front = node
self.rear = node
else:
self.rear.next = node
self.rear = node

def dequeue(self,data):
if self.front == None:
return None
node = self.front
if self.front == self.rear:
self.front = None
self.rear = None
else:
self.front = self.front.next
return node.data

def is_empty(self):
return self.front == None

댓글 공유

Queue(큐)란?

큐는 대기행렬(줄)이다.

우리가 무언가를 사거나 장소에 들어갈 때 줄을 선 순서를 생각하면 된다.

많은 양의 자료를 프린터로 출력한다 했을 때, 프린터 상태창을 보면 출력할 자료가 순서대로 들어가있고 들어간 순서대로 출력되는 것을 알 수 있다.

queue

스택이 한쪽입구가 막힌 상자에 자료를 차곡차곡 쌓는 것이라면, 큐는 입구와 출구가 따로 있는 통로로서 한쪽에서 밀어 넣으면 반대쪽에서 나오는 것이다.

  • 큐에서는 head 대신 front, tail 대신 rear(or back) 이라 한다.
  • enqueue: 가장 마지막에 자료를 넣는 것으로 위 그림에서 연결리스트의 append()와 같다.
  • dequeue: 가장 먼저 들어간 자료를 꺼내는 것으로 연결리스트의 popLeft()와 같다.

댓글 공유

내부 경계(border) 효과 주기

문제

문제사진

CSS로 작업을 하다가 경계가 검정색 바탕 부분과 border에 약간의 단차가 생기는 문제가 있었다.

마크업 구조는 다음과 같다.

1
2
3
4
5
6
<div>
<h2>나의 자산 목표 금액 계산기</h2>
<div>
<h3>목표 금액 계산기란?</h3>
</div>
</div>
  • h2 태그에 배경색이 검정색이다.
  • h2 태그와 형제태그인 div 태그에 보더를 주었더니 단차가 생겼다.

해결방법

1
box-shadow: 0 0 0 1px gray inset;
  • box-shadow 효과를 inset으로 주어서 보더 처럼 보이도록 구현했다.

댓글 공유

문제

참조: https://www.geeksforgeeks.org/next-greater-element/

음이 아닌 정수 배열이 주어졌을 때, 각 원소의 오른쪽에 있는 원소 중에서 현재 원소보다 큰 값을 출력하되, 가장 근접한 원소를 출력하라. 현재 원소보다 큰 값이 없으면 -1을 출력하라.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
예시 1

입력: [4, 5, 2, 25]
출력:
4 --> 5
5 --> 25
2 --> 25
25 --> -1
예시 2

입력: [13, 7, 6, 12]
출력:
13 --> -1
7 --> 12
6 --> 12
12 --> -1

풀이

이중반복문

1
2
3
4
5
6
7
8
9
def solution(int_arr):
for i in range(len(int_arr)):
int = int_arr[i]
result = -1
for j in range(i,len(int_arr)):
if int < int_arr[j]:
result = int_arr[j]
break
print(f"{int} --> {result}")

스택

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def solution*by_stack(int_arr):
n = len(int_arr)
s = []
res = [-1 for * in range(len(int_arr))]

for i in range(n-1,-1,-1):
while s:
if s[-1] > int_arr[i]:
res[i] = s[-1]
break
else:
s.pop()
s.append(int_arr[i])

for i in range(n):
print(f"{int_arr[i]} --> {res[i]}")
  • 현재 원소와 오른쪽 원소를 비교하므로, 오른쪽에서 왼쪽으로 이동하면서 비교하면 수월하다.
  • 문제에서 요구하는 것은 현재 원소의 오른쪽 값 중 가장 가까운 값이므로, 오른쪽 부터 왼쪽으로 가면서 원소를 저장했다면 꺼낼 때는 역순으로 꺼내서 비교한다.
  1. stack을 빈 상태로 초기화
  2. res 배열을 -1로 배열 길이 만큼 초기화
  3. 역순으로 순회를 하면서 stack이 비어있으면, 원소를 추가한다.
  4. 만약 stack에 값이 있다면, 스택의 값들과 현재 원소를 비교한다.
  5. 스택의 값이 크다면 해당 원소의 index에 위치하는 res 배열에 스택의 값을 할당하고 해당 원소를 stack 저장
  6. 다음 순회
  7. 만약 스택의 값이 작다면 stack에 마지막 값을 pop한다.

댓글 공유

후위 표기법 2

소괄호를 포함한 후위 표기법 바꾸기

소괄호()*보다 우선순위가 높다.

후위 표기법은 우선순위가 높은 것을 먼저 출력하므로, 열린 소괄호가 나오면 스택에 넣는다.

이후 닫힌 소괄호가 나오면 스택에 열린 소괄호 나올 때 까지 pop하여 연산자를 결과값에 추가한다.

  • 괄호는 변수에 추가하면 안되므로 스택에서 pop하여 제거한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def solution(str):
priority = {'+':2,'-':2,'*':1,'/':1}
result = ''
s = []

for x in str:
if x.isnumeric():
result += x
elif x == '(':
s.append(x)
elif x == ')':
while s[-1] != "(":
result += s.pop()
s.pop()
elif x in priority:
if s and s[-1] != '(' and priority[s[-1]] < priority[x]:
result += s.pop()
s.append(x)

while s:
result += s.pop()

return result
  • 코딩테스트를 본다고 생각하고 리스트를 활용하여 위 문제를 풀어보았다.
  • isnumeric() 함수는 문자열이 숫자인지 판단하는 메서드이다.
  • stack의 top을 의미하는 s[-1] 슬라이싱을 활용하였다.
  • 빈 리스트는 논리값이 False라는 점을 활용하여 while 반복문을 실행했다.

후위 표기법 계산하기

  1. 문자열을 순회하면서 해당 문자가 숫자면 정수형으로 변환하여 스택에 push
  2. 연산자이면 스택에서 두 수를 pop하여 계산
  3. 스택은 후입선출이므로, 처음 pop한 수를 n2, 두번째 pop한 수를 n1으로 두고 (n1 연산자 n2)로 계산한다.
  4. 계산결과를 스택에 push
  5. 스택에 마지막에 저장된 값이 결과값이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def eval_postfix(expression):
s = []
for exp in expression:
if exp.isnumeric():
s.append(int(exp))
elif exp != " ":
n2 = s.pop()
n1 = s.pop()
if exp == '+':
res = n1 + n2
elif exp == '-':
res = n1 - n2
elif exp == '*':
res = n1 * n2
else:
res = n1 / n2
s.append(res)
return s[0]

댓글 공유

후위표기법 1

연산자를 피연산자 뒤에 쓰는 연산기법

예를 들어, 3+5x2 를 중위 표기법이라 하고,

352x+를 후위 표기법이라고 한다.

계산 방법

3+5x2를 후위 표기법으로 적으면, 352x+이다.

352x+를 계산하기 위해서 다음과정을 따른다.

  1. 피연산자(숫자)는 스택에 담는다. [3,5,2]
  2. 연산자를 만나면, 스택에서 피연산자 2개를 꺼내 계산한다.
  3. 결과값을 다시 스택에 넣는다. [3,10]
  4. 다음 연산자는 +이므로 3+10을 계산한다.
  • 컴퓨터 입장에서는 후위 표기법이 연산의 우선순위가 명확하다는 장점이 있다.

중위 표기법을 후위 표기법으로 변환

3+5x2를 후위 표기법으로 바꾸는 과정을 알아보자.

  1. 피연산자 3을 결과값에 추가
    • 연산자 스택에 push
  2. 피연산자 5 결과값에 추가
  3. x 연산자와 스택의 마지막 값인 + 우선순위 비교
  4. x 연산자가 우선순위 높으므로 스택에 push
  5. 피연산자 2 결과값에 추가
  6. 스택이 빌 때까지 pop하여 결과값에 추가

3x5+2를 후위 표기법으로 바꿔보자.

  1. 피연산자 3을 결과값에 추가
  2. x 연산자 스택에 push
  3. 피연산자 5 결과값에 추가
    • 연산자와 스택의 마지막 값인 x 우선순위 비교
  4. 스택의 마지막 값인 x 연산자가 높으니 pop하여 결과값에 추가
    • 연산자는 스택에 push
  5. 피연산자 2을 결과값에 추가
  6. 스택이 빌 때까지 pop하여 결과값에 추가

문제 풀이

Stack 클래스를 이용한 풀이

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def solution(str):
priority = {'+':2,'-':2,'*':1,'/':1}
s = Stack()

result = ''
for x in str:
if x not in priority.keys():
result += x
else:
if s.top == None:
s.push(x)
else:
if priority[s.top.data] < priority[x]:
s_pop = s.pop()
result += s_pop
s.push(x)

while not s.is_empty():
s_pop = s.pop()
result += s_pop

return result

아래는 if 문으 조금 줄여보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def solution(str):
priority = {'+':2,'-':2,'*':1,'/':1}
s = Stack()

result = ''
for x in str:
if x not in priority.keys():
result += x
else:
if not s.is_empty() and priority[s.top.data] < priority[x]:
s_pop = s.pop()
result += s_pop
s.push(x)

while not s.is_empty():
s_pop = s.pop()
result += s_pop

return result

댓글 공유

loco9939

author.bio


author.job