displayFn 활용 예제
- block.id와 displayFn안에 차트를 그릴때 ID가 동일해야합니다.
- sqlMany 기능이 추가되었습니다. (여러개 쿼리 실행시 rows[0], rows[1] 안에 각각 결과가 담깁니다: 지난주/이번주 데이터 비교 등에 사용 가능)
- displayFn안에서 쓸수있는 변수: rows, id, openModal, openAction,
_
lodash 데이터 처리용
1. 카테고리별 매출 및 수익 분석 - 도넛 차트
- 목표: 카테고리별 매출 비중을 시각적으로 보여줍니다.
displayFn
주요 로직:rows
데이터를 순회하여 각 카테고리별 매출과 수익 합계를totalsByCategory
객체에 저장.- Chart.js의
doughnut
차트를 설정하여 데이터를 시각화. id
는 내부적으로 Chart.js에서ctx
로 매핑되어 HTML 컨텍스트와 연결됩니다.
const totalsByCategory = {};
rows.forEach(row => {
if (!totalsByCategory[row.category]) {
totalsByCategory[row.category] = { revenue: 0, profit: 0 };
}
totalsByCategory[row.category].revenue += row.revenue;
});
const data = {
labels: Object.keys(totalsByCategory),
datasets: [
{
data: Object.values(totalsByCategory).map(t => t.revenue),
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56'], // 색상
}
]
};
const config = {
type: 'doughnut',
data: data,
options: {
plugins: { title: { display: true, text: '카테고리별 매출 비중' } },
responsive: true
}
};
new Chart(ctx, config);
2. 고객 세그먼트별 구매 행동 분석 - 레이더 차트
- 목표: 고객 세그먼트별 평균 주문금액, 주문 수 등을 비교.
displayFn
주요 로직:rows.map()
을 통해 각 세그먼트의 데이터를 데이터셋으로 변환.- 데이터는 스케일 조정(단위 변환) 후 Chart.js의
radar
차트로 시각화.
const data = {
labels: ['평균 주문금액', '총 주문수', '고객수', '고객당 매출', '고객당 주문수'],
datasets: rows.map(row => ({
label: row.segment,
data: [
row.avg_order_value / 10000, // 스케일 변환
row.total_orders * 10,
row.unique_customers * 10,
row.revenue_per_customer / 10000,
row.orders_per_customer * 100
],
backgroundColor: 'rgba(255, 159, 64, 0.2)',
borderColor: 'rgb(255, 159, 64)',
}))
};
const config = {
type: 'radar',
data: data,
options: {
plugins: { title: { display: true, text: '세그먼트별 구매 행동 분석' } },
responsive: true
}
};
new Chart(ctx, config);
3. 마케팅 캠페인 효과 분석 - 버블 차트
- 목표: 캠페인 예산, 매출, 전환율의 관계를 시각적으로 분석.
displayFn
주요 로직:rows
데이터를 순회하며 각 캠페인의 버블 데이터를 구성.- X축은 예산, Y축은 매출, 버블 크기는 전환율에 비례.
const data = {
datasets: rows.map(row => ({
label: row.campaign_name,
data: [
{
x: row.campaign_budget / 1000000, // 예산 (단위: 백만원)
y: row.campaign_revenue / 1000000, // 매출 (단위: 백만원)
r: row.conversion_rate * 2 // 전환율
}
],
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgb(54, 162, 235)'
}))
};
const config = {
type: 'bubble',
data: data,
options: {
plugins: { title: { display: true, text: '캠페인 효과 분석' } },
responsive: true,
scales: {
x: { title: { display: true, text: '예산(백만원)' } },
y: { title: { display: true, text: '매출(백만원)' } }
}
}
};
new Chart(ctx, config);
4. 종합 성과 지표 분석 - 극좌표 레이더 차트
- 목표: 주요 KPI(매출, 평균 주문금액 등)를 날짜별로 비교.
displayFn
주요 로직:- 각 지표의 최대값으로 정규화하여 0~100% 범위로 스케일 조정.
rows.map()
으로 날짜별 데이터셋 생성.
const maxValues = {
revenue_mm: Math.max(...rows.map(r => r.revenue_mm)),
avg_order_value: Math.max(...rows.map(r => r.avg_order_value)),
unique_customers: Math.max(...rows.map(r => r.unique_customers)),
total_items: Math.max(...rows.map(r => r.total_items)),
conversion_rate: Math.max(...rows.map(r => r.conversion_rate))
};
const data = {
labels: ['매출(백만원)', '평균 주문금액', '구매고객수', '판매상품수', '전환율(%)'],
datasets: rows.map(row => ({
label: row.date,
data: [
(row.revenue_mm / maxValues.revenue_mm) * 100,
(row.avg_order_value / maxValues.avg_order_value) * 100,
(row.unique_customers / maxValues.unique_customers) * 100,
(row.total_items / maxValues.total_items) * 100,
(row.conversion_rate / maxValues.conversion_rate) * 100
],
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgb(75, 192, 192)',
}))
};
const config = {
type: 'radar',
data: data,
options: {
plugins: { title: { display: true, text: '일간 KPI 종합 분석' } },
responsive: true,
scales: { r: { min: 0, max: 100, ticks: { stepSize: 20 } } }
}
};
new Chart(ctx, config);
핵심 요약
displayFn
을 통해 SQL로 가져온rows
데이터를 처리하고, Chart.js로 원하는 차트를 생성.- 주요 변수:
rows
: SQL 결과 데이터.ctx
: HTML 컨텍스트(차트를 렌더링할 DOM 요소).lodash
: 데이터 처리를 위한 유틸리티 제공.
- 차트 설정은 JavaScript로 커스터마이징 가능하며, 다양한 데이터 구조와 시각화 유형을 지원.
전체 YAML 예제
menus:
- path: pages/ecommerce-analytics
name: E-commerce Analytics Dashboard
group: ecom-chart
- path: pages/ecommerce-analytics-1
group: ecom-chart
- path: pages/ecommerce-analytics-2
group: ecom-chart
- path: pages/ecommerce-analytics-3
group: ecom-chart
- path: pages/ecommerce-analytics-4
group: ecom-chart
pages:
- path: pages/ecommerce-analytics
title: E-commerce Analytics Dashboard
subtitle: Sales, Customer, and Marketing Performance Analytics
blocks:
- type: markdown
content: |
<div class="p-4 mb-4 bg-blue-50 border border-blue-200 rounded-lg">
<h3 class="text-lg font-semibold text-blue-700 mb-2">Dashboard Guide</h3>
<p class="text-blue-600">
각 분석 항목별 상세 내용은 좌측 메뉴에서 확인하실 수 있습니다:
<ul class="list-disc list-inside mt-2 ml-4">
<li>Category Analysis: 카테고리별 매출 및 수익 분석</li>
<li>Customer Segments: 고객 세그먼트별 구매 행동 분석</li>
<li>Campaign Analysis: 마케팅 캠페인 효과 분석</li>
<li>KPI Overview: 주요 성과 지표 종합 분석</li>
</ul>
</p>
</div>
- path: pages/ecommerce-analytics-1
blocks:
# 1. 카테고리별 매출 및 수익률 분석 - 스택형 도넛 차트 + 라인 차트 조합
- type: query
style:
minWidth: 500px
minHeight: 500px
resource: mysql
sqlType: select
sql: >
SELECT
pc.name as category,
DATE_FORMAT(o.order_date, '%Y-%m-%d') as date,
SUM(od.quantity * od.price) as revenue,
SUM(od.quantity * (od.price - p.cost)) as profit,
COUNT(DISTINCT o.customer_id) as unique_customers
FROM ecom_order_details od
JOIN ecom_orders o ON od.order_id = o.id
JOIN ecom_products p ON od.product_id = p.id
JOIN ecom_product_categories pc ON p.category_id = pc.id
GROUP BY pc.name, DATE_FORMAT(o.order_date, '%Y-%m-%d')
ORDER BY date
display: chartjs
displayFn: |
const totalsByCategory = {};
rows.forEach(row => {
if (!totalsByCategory[row.category]) {
totalsByCategory[row.category] = {
revenue: 0,
profit: 0
};
}
totalsByCategory[row.category].revenue += row.revenue;
totalsByCategory[row.category].profit += row.profit;
});
const categoryColors = {
'전자기기': '#FF6384',
'스마트폰': '#FF9F40',
'노트북': '#FFCD56',
'의류': '#4BC0C0',
'여성복': '#36A2EB',
'남성복': '#9966FF',
'식품': '#C9CBCF',
'신선식품': '#7BC8A4',
'가공식품': '#7B8EF0'
};
const data = {
datasets: [
{
data: Object.values(totalsByCategory).map(t => t.revenue),
backgroundColor: Object.keys(totalsByCategory).map(cat => categoryColors[cat]),
label: '카테고리별 매출'
}
],
labels: Object.keys(totalsByCategory)
};
const config = {
type: 'doughnut',
data: data,
options: {
responsive: true,
maintainAspectRatio: false, // 이 옵션을 추가
plugins: {
legend: {
position: 'right',
},
title: {
display: true,
text: '카테고리별 매출 비중'
}
}
}
};
const chart = new Chart(ctx, config);
- path: pages/ecommerce-analytics-2
blocks:
# 2. 고객 세그먼트별 구매 행동 분석 - 레이더 차트
- type: query
style:
minWidth: 500px
minHeight: 500px
resource: mysql
sqlType: select
sql: >
SELECT
cs.name as segment,
ROUND(AVG(o.total_amount), 0) as avg_order_value,
COUNT(DISTINCT o.id) as total_orders,
COUNT(DISTINCT o.customer_id) as unique_customers,
ROUND(SUM(o.total_amount) / COUNT(DISTINCT o.customer_id), 0) as revenue_per_customer,
ROUND(COUNT(o.id) / COUNT(DISTINCT o.customer_id), 1) as orders_per_customer
FROM ecom_orders o
JOIN ecom_customers c ON o.customer_id = c.id
JOIN ecom_customer_segments cs ON c.segment_id = cs.id
GROUP BY cs.name
display: chartjs
displayFn: |
const segmentColors = {
'VIP': {
background: 'rgba(255, 99, 132, 0.2)',
border: 'rgb(255, 99, 132)'
},
'GOLD': {
background: 'rgba(255, 159, 64, 0.2)',
border: 'rgb(255, 159, 64)'
},
'SILVER': {
background: 'rgba(75, 192, 192, 0.2)',
border: 'rgb(75, 192, 192)'
},
'BRONZE': {
background: 'rgba(153, 102, 255, 0.2)',
border: 'rgb(153, 102, 255)'
}
};
const data = {
labels: [
'평균 주문금액',
'총 주문수',
'고객수',
'고객당 매출',
'고객당 주문수'
],
datasets: rows.map(r => ({
label: r.segment,
data: [
r.avg_order_value / 10000, // 스케일 조정
r.total_orders * 10, // 스케일 조정
r.unique_customers * 10, // 스케일 조정
r.revenue_per_customer / 10000,
r.orders_per_customer * 100
],
fill: true,
backgroundColor: segmentColors[r.segment].background,
borderColor: segmentColors[r.segment].border,
pointBackgroundColor: segmentColors[r.segment].border,
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: segmentColors[r.segment].border
}))
};
const config = {
type: 'radar',
data: data,
options: {
responsive: true,
maintainAspectRatio: false, // 이걸 false로 설정
plugins: {
title: {
display: true,
text: '세그먼트별 구매 행동 분석'
}
}
}
};
const chart = new Chart(ctx, config);
- path: pages/ecommerce-analytics-3
blocks:
# 3. 마케팅 캠페인 효과 분석 - 버블 차트
- type: query
style:
minWidth: 500px
minHeight: 500px
resource: mysql
sqlType: select
sql: >
SELECT
mc.name as campaign_name,
SUM(CASE WHEN o.id IS NOT NULL THEN o.total_amount ELSE 0 END) as campaign_revenue,
COUNT(*) as total_visits,
ROUND(SUM(wt.is_conversion) / COUNT(*) * 100, 1) as conversion_rate,
mc.budget as campaign_budget
FROM ecom_website_traffic wt
LEFT JOIN ecom_orders o ON DATE(wt.visit_date) = DATE(o.order_date)
JOIN ecom_marketing_campaigns mc ON DATE(wt.visit_date) BETWEEN DATE(mc.start_date) AND DATE(mc.end_date)
GROUP BY mc.name, mc.budget
display: chartjs
displayFn: |
const bubbleColors = [
{
background: 'rgba(255, 99, 132, 0.5)',
border: 'rgb(255, 99, 132)'
},
{
background: 'rgba(54, 162, 235, 0.5)',
border: 'rgb(54, 162, 235)'
},
{
background: 'rgba(75, 192, 192, 0.5)',
border: 'rgb(75, 192, 192)'
}
];
const data = {
datasets: rows.map((r, i) => ({
label: r.campaign_name,
data: [{
x: r.campaign_budget / 1000000, // 예산 (단위: 백만원)
y: r.campaign_revenue / 1000000, // 매출 (단위: 백만원)
r: r.conversion_rate * 2 // 버블 크기는 전환율에 비례
}],
backgroundColor: bubbleColors[i].background,
borderColor: bubbleColors[i].border
}))
};
const config = {
type: 'bubble',
data: data,
options: {
responsive: true,
maintainAspectRatio: false, // 이걸 false로 설정
plugins: {
title: {
display: true,
text: '캠페인 효과 분석 (버블 크기 = 전환율)'
},
tooltip: {
callbacks: {
label: function(context) {
const campaign = rows[context.datasetIndex];
return [
`캠페인: ${campaign.campaign_name}`,
`예산: ${campaign.campaign_budget.toLocaleString()}원`,
`매출: ${campaign.campaign_revenue.toLocaleString()}원`,
`전환율: ${campaign.conversion_rate}%`
];
}
}
}
},
scales: {
x: {
title: {
display: true,
text: '캠페인 예산 (백만원)'
}
},
y: {
title: {
display: true,
text: '캠페인 매출 (백만원)'
}
}
}
}
};
const chart = new Chart(ctx, config);
- path: pages/ecommerce-analytics-4
blocks:
# 4. 종합 성과 지표 (KPI) 분석 - 극좌표 영역 차트
- type: query
style:
minWidth: 500px
minHeight: 500px
resource: mysql
sqlType: select
sql: >
SELECT
DATE_FORMAT(o.order_date, '%Y-%m-%d') as date,
ROUND(SUM(o.total_amount) / 1000000, 1) as revenue_mm,
ROUND(SUM(o.total_amount)/COUNT(DISTINCT o.id), 0) as avg_order_value,
COUNT(DISTINCT o.customer_id) as unique_customers,
SUM(od.quantity) as total_items,
ROUND(SUM(wt.is_conversion)/COUNT(*) * 100, 1) as conversion_rate
FROM ecom_orders o
JOIN ecom_order_details od ON o.id = od.order_id
JOIN ecom_website_traffic wt ON DATE(o.order_date) = DATE(wt.visit_date)
GROUP BY DATE_FORMAT(o.order_date, '%Y-%m-%d')
ORDER BY date
display: chartjs
displayFn: |
// 각 지표의 최대값을 구해서 정규화
const maxValues = {
revenue_mm: Math.max(...rows.map(r => r.revenue_mm)),
avg_order_value: Math.max(...rows.map(r => r.avg_order_value)),
unique_customers: Math.max(...rows.map(r => r.unique_customers)),
total_items: Math.max(...rows.map(r => r.total_items)),
conversion_rate: Math.max(...rows.map(r => r.conversion_rate))
};
const data = {
labels: ['매출(백만원)', '평균주문금액', '구매고객수', '판매상품수', '전환율(%)'],
datasets: rows.map(r => ({
label: r.date,
data: [
(r.revenue_mm / maxValues.revenue_mm) * 100,
(r.avg_order_value / maxValues.avg_order_value) * 100,
(r.unique_customers / maxValues.unique_customers) * 100,
(r.total_items / maxValues.total_items) * 100,
(r.conversion_rate / maxValues.conversion_rate) * 100
],
fill: true,
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgb(54, 162, 235)',
pointBackgroundColor: 'rgb(54, 162, 235)',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: 'rgb(54, 162, 235)'
}))
};
const config = {
type: 'radar',
data: data,
options: {
responsive: true,
maintainAspectRatio: false, // 이걸 false로 설정
plugins: {
title: {
display: true,
text: '일간 KPI 종합 분석 (일자별 최대값 대비 %)'
},
tooltip: {
callbacks: {
label: function(context) {
const row = rows[context.datasetIndex];
return [
`날짜: ${row.date}`,
`매출: ${row.revenue_mm}백만원`,
`평균주문금액: ${row.avg_order_value.toLocaleString()}원`,
`구매고객수: ${row.unique_customers}명`,
`판매상품수: ${row.total_items}개`,
`전환율: ${row.conversion_rate}%`
];
}
}
}
},
scales: {
r: {
min: 0,
max: 100,
ticks: {
stepSize: 20
}
}
}
}
};
const chart = new Chart(ctx, config);
차트 영역 설정
너비/높이 지정
- type: query
style:
minWidth: 500px
minHeight: 500px
차트 반응형, 비율 설정
options: {
responsive: true,
maintainAspectRatio: false,