displayFn 차트 활용 예제

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,

관련자료