본문으로 건너뛰기
Neumorphism UI
문서스타일링차트템플릿
—
KO
English한국어简体中文日本語

차트

매출, 전환율, 빌드 시간과 서비스 지연 시간을 비교하세요. 각 차트에서 원본 데이터 표도 확인할 수 있습니다.

제품 분석운영·개발설치 안내

제품 분석

같은 기간의 매출, 유입 경로, 전환율을 비교합니다.

예시 데이터 · 2026년 9월 · USD. 실제 서비스와 연결되지 않았습니다.

매출과 목표

일별 매출을 통화로 표시합니다. 점선은 목표입니다.

매출: US$60,644.00 · 목표: US$75,810.00

정확한 데이터 보기
매출과 목표
날짜매출목표
2026-09-17US$3,682.00US$5,064.00
2026-09-18US$3,842.00US$5,118.00
2026-09-19US$4,292.00US$5,172.00
2026-09-20US$4,542.00US$5,226.00
2026-09-21US$3,640.00US$5,280.00
2026-09-22US$4,040.00US$5,334.00
2026-09-23US$4,142.00US$5,388.00
2026-09-24US$4,642.00US$5,442.00
2026-09-25US$4,842.00US$5,496.00
2026-09-26US$3,890.00US$5,550.00
2026-09-27US$4,340.00US$5,604.00
2026-09-28US$4,550.00US$5,658.00
2026-09-29US$5,000.00US$5,712.00
2026-09-30US$5,200.00US$5,766.00

유입 경로 비교

선택 기간의 경로별 합계입니다. 막대는 0에서 시작합니다.

방문: 23,778

정확한 데이터 보기
유입 경로 비교
유입 경로방문전환 방문전환율
Organic9,4703864.08%
Direct8,0114205.24%
Referral6,2974046.42%

방문 전환율

전환 방문 수를 방문 수로 나눕니다. 세로축은 0–100%로 고정합니다.

5.09% · 1,210 / 23,778

정확한 데이터 보기
방문 전환율
날짜방문전환 방문전환율
2026-09-171,557734.69%
2026-09-181,547774.98%
2026-09-191,622865.3%
2026-09-201,612915.65%
2026-09-211,602724.49%
2026-09-221,677804.77%
2026-09-231,667834.98%
2026-09-241,742935.34%
2026-09-251,732975.6%
2026-09-261,722774.47%
2026-09-271,797864.79%
2026-09-281,787915.09%
2026-09-291,8621005.37%
2026-09-301,8521045.62%

누락 날짜는 0이 아닌 빈 구간입니다. 방문이 0인 전환율은 정의되지 않습니다(—).

운영·개발 차트

제품 분석과 분리해 빌드, 지연 시간, 릴리스, 전달량, 설치 단계 같은 운영 지표를 다룹니다.

빌드 시간

캐시를 사용하지 않은 빌드와 캐시를 사용한 빌드의 소요 시간(초)입니다.

캐시 미사용 평균 210.2 s · 캐시 사용 평균 103.7 s · 2/6 캐시 미사용 빌드가 목표 초과

빌드 데이터 보기
빌드 시간 데이터
빌드캐시 미사용 (s)캐시 사용 (s)단축 시간 (s)
B1210108102
B219510293
B3238114124
B4226109117
B520496108
B61889395

서비스 지연 시간

일별 p50·p95 응답 시간과 p95 목표값을 비교합니다.

최대 p95 470 ms · 3/7 기간에서 목표 초과

지연 시간 데이터 보기
서비스 지연 시간 데이터
기간p50 (ms)p95 (ms)목표
월80210이내
화76195이내
수95340초과
목130470초과
금100330초과
토82240이내
일79215이내

릴리스 활동

릴리스별 신규 설치와 업데이트 건수입니다. 고유 사용자 수가 아닌 이벤트 수입니다.

전체 활동 2,350 · 업데이트 비율 32.8%

릴리스 데이터 보기
릴리스 활동 데이터
릴리스신규 설치업데이트업데이트 비율
R11486229.5%
R223210431.0%
R31968831.0%
R428413231.7%
R534117634.0%
R637820935.6%

계획·완료 수량

계획한 항목과 완료한 항목의 수를 비교합니다. 생산성 점수가 아닙니다.

계획 140 · 완료 140 · 순차이 +0

계획·완료 데이터 보기
계획·완료 수량 데이터
기간계획완료차이
S13228-4
S23639+3
S33431-3
S43842+4

설치 단계 진단

한 번의 설치에서 순서대로 실행된 각 단계의 소요 시간입니다.

전체 시간 1260 ms · 가장 오래 걸린 단계 다운로드 (840 ms)

설치 데이터 보기
설치 단계 진단 데이터
단계소요 시간비율
의존성 확인120 ms9.5%
다운로드840 ms66.7%
변환210 ms16.7%
파일 쓰기90 ms7.1%

설치 안내

필요한 차트를 개별 설치하고 사용 예제와 설치되는 소스를 펼쳐 확인하세요.

제품 분석

매출

선택한 보고 기간의 일별 매출입니다.

@neumorphism-ui/chart-revenue
사용 예제
"use client";
import { RevenueChart } from "@/components/blocks/revenue-chart";
import { summarizeAnalytics, type AnalyticsRecord } from "@/lib/analytics-model";

export function Example({ records }: { records: AnalyticsRecord[] }) {
  return <RevenueChart data={summarizeAnalytics(records, 14)} locale="ko" />;
}
설치되는 소스
"use client";

import * as React from "react";
import { Area, CartesianGrid, ComposedChart, Line, XAxis, YAxis } from "recharts";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
import { Checkbox } from "@/components/ui/checkbox";
import type { AnalyticsSummary } from "@/lib/analytics-model";
import { analyticsCopy, analyticsFormats, type AnalyticsLocale } from "@/lib/analytics-copy";

export function RevenueChart({ data, locale = "en", currency = "USD" }: {
  data: AnalyticsSummary; locale?: AnalyticsLocale; currency?: string;
}) {
  const [showTarget, setShowTarget] = React.useState(true);
  const t = analyticsCopy[locale];
  const f = analyticsFormats(locale, currency);
  return <ChartContainer
    data-chart="revenue" title={t.revenueTitle} description={t.revenueBody}
    summary={data.observedDays ? <>{t.revenue}: {f.money(data.totals.revenueCents)} · {t.target}: {f.money(data.totals.targetCents)}</> : t.empty}
    actions={<label className="inline-flex items-center gap-2 text-xs"><Checkbox checked={showTarget} onCheckedChange={value => setShowTarget(value === true)} />{t.targetToggle}</label>}
    tableLabel={t.table} empty={data.observedDays === 0} emptyLabel={t.empty}
    table={<table><caption className="sr-only">{t.revenueTitle}</caption><thead><tr><th scope="col">{t.date}</th><th scope="col">{t.revenue}</th><th scope="col">{t.target}</th></tr></thead><tbody>{data.points.map(point => <tr key={point.date}><th scope="row">{point.date}</th><td>{f.money(point.revenueCents)}</td><td>{f.money(point.targetCents)}</td></tr>)}</tbody></table>}
  >
    <ComposedChart data={data.points} accessibilityLayer margin={{ top: 12, right: 12, bottom: 4, left: 0 }}>
      <CartesianGrid vertical={false} stroke="var(--border)" strokeDasharray="3 4" />
      <XAxis dataKey="date" tickFormatter={(value: string) => value.slice(5)} minTickGap={28} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
      <YAxis domain={[0, "auto"]} width={72} tickFormatter={(value: number) => f.compactMoney(value)} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
      <ChartTooltip formatter={value => f.money(typeof value === "number" ? value : null)} />
      <Area type="linear" dataKey="revenueCents" name={t.revenue} stroke="var(--primary)" fill="var(--primary)" fillOpacity={0.1} strokeWidth={2.5} connectNulls={false} isAnimationActive={false} />
      {showTarget && <Line type="linear" dataKey="targetCents" name={t.target} stroke="var(--muted-foreground)" strokeWidth={2} strokeDasharray="6 4" dot={false} connectNulls={false} isAnimationActive={false} />}
    </ComposedChart>
  </ChartContainer>;
}
제품 분석

유입 경로

경로별 방문 수와 전환 수를 비교합니다.

@neumorphism-ui/chart-channel
사용 예제
"use client";
import { ChannelChart } from "@/components/blocks/channel-chart";
import { summarizeAnalytics, type AnalyticsRecord } from "@/lib/analytics-model";

export function Example({ records }: { records: AnalyticsRecord[] }) {
  return <ChannelChart data={summarizeAnalytics(records, 14)} locale="ko" />;
}
설치되는 소스
"use client";

import * as React from "react";
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
import { Select, SelectItem } from "@/components/ui/select";
import type { AnalyticsSummary } from "@/lib/analytics-model";
import { analyticsCopy, analyticsFormats, type AnalyticsLocale } from "@/lib/analytics-copy";

export function ChannelChart({ data, locale = "en" }: { data: AnalyticsSummary; locale?: AnalyticsLocale }) {
  const [metric, setMetric] = React.useState<"visits" | "convertedVisits">("visits");
  const t = analyticsCopy[locale];
  const f = analyticsFormats(locale);
  const label = metric === "visits" ? t.visits : t.conversions;
  return <ChartContainer
    data-chart="channels" title={t.channelsTitle} description={t.channelsBody}
    summary={data.observedDays ? <>{label}: {f.number(data.totals[metric])}</> : t.empty}
    actions={<label className="grid max-w-full gap-1.5 text-xs">{t.metric}<Select value={metric} onChange={event => setMetric(event.target.value === "visits" ? "visits" : "convertedVisits")}><SelectItem value="visits">{t.visits}</SelectItem><SelectItem value="convertedVisits">{t.conversions}</SelectItem></Select></label>}
    tableLabel={t.table} empty={!data.channels.length} emptyLabel={t.empty}
    table={<table><caption className="sr-only">{t.channelsTitle}</caption><thead><tr><th scope="col">{t.channel}</th><th scope="col">{t.visits}</th><th scope="col">{t.conversions}</th><th scope="col">{t.conversion}</th></tr></thead><tbody>{data.channels.map(group => <tr key={group.channel}><th scope="row">{group.channel}</th><td>{f.number(group.visits)}</td><td>{f.number(group.convertedVisits)}</td><td>{f.percent(group.conversion)}</td></tr>)}</tbody></table>}
  >
    <BarChart data={data.channels} accessibilityLayer margin={{ top: 12, right: 12, bottom: 4, left: 0 }}>
      <CartesianGrid vertical={false} stroke="var(--border)" strokeDasharray="3 4" />
      <XAxis tickFormatter={(value: string) => value.length > 14 ? `${value.slice(0, 13)}…` : value} dataKey="channel" minTickGap={16} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
      <YAxis domain={[0, "auto"]} width={52} allowDecimals={false} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
      <ChartTooltip formatter={value => f.number(typeof value === "number" ? value : null)} />
      <Bar dataKey={metric} name={label} fill="var(--primary)" radius={[4, 4, 0, 0]} maxBarSize={56} isAnimationActive={false} />
    </BarChart>
  </ChartContainer>;
}
제품 분석

전환율

일별 전환율입니다. 누락된 값은 0으로 표시하지 않습니다.

@neumorphism-ui/chart-conversion
사용 예제
"use client";
import { ConversionChart } from "@/components/blocks/conversion-chart";
import { summarizeAnalytics, type AnalyticsRecord } from "@/lib/analytics-model";

export function Example({ records }: { records: AnalyticsRecord[] }) {
  return <ConversionChart data={summarizeAnalytics(records, 14)} locale="ko" />;
}
설치되는 소스
"use client";

import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
import type { AnalyticsSummary } from "@/lib/analytics-model";
import { analyticsCopy, analyticsFormats, type AnalyticsLocale } from "@/lib/analytics-copy";

export function ConversionChart({ data, locale = "en" }: { data: AnalyticsSummary; locale?: AnalyticsLocale }) {
  const t = analyticsCopy[locale];
  const f = analyticsFormats(locale);
  return <ChartContainer
    data-chart="conversion" title={t.conversionTitle} description={t.conversionBody}
    summary={data.observedDays ? <>{f.percent(data.conversion)} · {f.number(data.totals.convertedVisits)} / {f.number(data.totals.visits)}</> : t.empty}
    tableLabel={t.table} empty={!data.observedDays} emptyLabel={t.empty}
    table={<table><caption className="sr-only">{t.conversionTitle}</caption><thead><tr><th scope="col">{t.date}</th><th scope="col">{t.visits}</th><th scope="col">{t.conversions}</th><th scope="col">{t.conversion}</th></tr></thead><tbody>{data.points.map(point => <tr key={point.date}><th scope="row">{point.date}</th><td>{f.number(point.visits)}</td><td>{f.number(point.convertedVisits)}</td><td>{f.percent(point.conversion)}</td></tr>)}</tbody></table>}
  >
    <LineChart data={data.points} accessibilityLayer margin={{ top: 12, right: 12, bottom: 4, left: 0 }}>
      <CartesianGrid vertical={false} stroke="var(--border)" strokeDasharray="3 4" />
      <XAxis dataKey="date" tickFormatter={(value: string) => value.slice(5)} minTickGap={28} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
      <YAxis domain={[0, 1]} ticks={[0, 0.25, 0.5, 0.75, 1]} width={48} tickFormatter={(value: number) => `${value * 100}%`} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
      <ChartTooltip formatter={value => f.percent(typeof value === "number" ? value : null)} />
      <Line type="linear" dataKey="conversion" name={t.conversion} stroke="var(--primary)" strokeWidth={2.5} dot={{ r: 2 }} activeDot={{ r: 5 }} connectNulls={false} isAnimationActive={false} />
    </LineChart>
  </ChartContainer>;
}
운영·개발

빌드 시간

캐시 사용 여부에 따른 빌드 시간을 시간 기준선과 비교합니다.

@neumorphism-ui/chart-build-duration
사용 예제
"use client";
import { BuildDurationChart, type BuildDurationPoint } from "@/components/blocks/build-duration-chart";

const data: BuildDurationPoint[] = [
  { label: "B1", coldSeconds: 210, cachedSeconds: 108 },
];

export function Example() {
  return <BuildDurationChart data={data} locale="ko" />;
}
설치되는 소스
"use client";

import * as React from "react";
import { CartesianGrid, Line, LineChart, ReferenceLine, XAxis, YAxis } from "recharts";

import { ChartContainer, ChartTooltip, ChartLegend } from "@/components/ui/chart";
import { Select, SelectItem } from "@/components/ui/select";

const copy = {
  "en": {
    "title": "Build duration",
    "description": "Paired cold and cached build runs in seconds.",
    "meanCold": "Mean cold",
    "cached": "cached",
    "overBudget": "cold runs over budget",
    "empty": "No build runs.",
    "series": "Visible series",
    "both": "Both",
    "coldOnly": "Cold only",
    "cachedOnly": "Cached only",
    "table": "View build data",
    "caption": "Build duration data",
    "period": "Build",
    "cold": "Cold",
    "cachedHead": "Cached",
    "saved": "Saved",
    "coldBuild": "Cold build",
    "cachedBuild": "Cached build",
    "budget": "Budget"
  },
  "ko": {
    "title": "빌드 시간",
    "description": "캐시를 사용하지 않은 빌드와 캐시를 사용한 빌드의 소요 시간(초)입니다.",
    "meanCold": "캐시 미사용 평균",
    "cached": "캐시 사용 평균",
    "overBudget": "캐시 미사용 빌드가 목표 초과",
    "empty": "빌드 기록이 없습니다.",
    "series": "표시할 계열",
    "both": "모두",
    "coldOnly": "캐시 미사용",
    "cachedOnly": "캐시 사용",
    "table": "빌드 데이터 보기",
    "caption": "빌드 시간 데이터",
    "period": "빌드",
    "cold": "캐시 미사용",
    "cachedHead": "캐시 사용",
    "saved": "단축 시간",
    "coldBuild": "캐시 미사용",
    "cachedBuild": "캐시 사용",
    "budget": "목표"
  },
  "ja": {
    "title": "ビルド時間",
    "description": "キャッシュ未使用・使用時のビルド所要時間(秒)です。",
    "meanCold": "キャッシュ未使用の平均",
    "cached": "使用時の平均",
    "overBudget": "未使用ビルドが目標超過",
    "empty": "ビルド記録はありません。",
    "series": "表示する系列",
    "both": "両方",
    "coldOnly": "キャッシュ未使用",
    "cachedOnly": "キャッシュ使用",
    "table": "ビルドデータを表示",
    "caption": "ビルド時間データ",
    "period": "ビルド",
    "cold": "未使用",
    "cachedHead": "使用",
    "saved": "短縮時間",
    "coldBuild": "キャッシュ未使用",
    "cachedBuild": "キャッシュ使用",
    "budget": "目標"
  },
  "zh": {
    "title": "构建耗时",
    "description": "比较未使用缓存和使用缓存的构建耗时,单位为秒。",
    "meanCold": "未使用缓存平均",
    "cached": "使用缓存平均",
    "overBudget": "次无缓存构建超出目标",
    "empty": "暂无构建记录。",
    "series": "显示系列",
    "both": "全部",
    "coldOnly": "未使用缓存",
    "cachedOnly": "使用缓存",
    "table": "查看构建数据",
    "caption": "构建耗时数据",
    "period": "构建",
    "cold": "未使用缓存",
    "cachedHead": "使用缓存",
    "saved": "节省时间",
    "coldBuild": "未使用缓存",
    "cachedBuild": "使用缓存",
    "budget": "目标"
  }
} as const;

export type BuildDurationPoint = {
  label: string;
  coldSeconds: number;
  cachedSeconds: number;
};

export function BuildDurationChart({
  data,
  budgetSeconds = 220,
  locale = "en",
}: {
  data: readonly BuildDurationPoint[];
  budgetSeconds?: number;
  locale?: keyof typeof copy;
}) {
  const t = copy[locale];
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });
  const [series, setSeries] = React.useState<"both" | "cold" | "cached">("both");
  const coldMean = data.length ? data.reduce((sum, row) => sum + row.coldSeconds, 0) / data.length : 0;
  const cachedMean = data.length ? data.reduce((sum, row) => sum + row.cachedSeconds, 0) / data.length : 0;
  const breaches = data.filter((row) => row.coldSeconds > budgetSeconds).length;

  return (
    <ChartContainer
      data-chart="build-duration"
      title={t.title}
      description={t.description}
      summary={data.length ? `${t.meanCold} ${number.format(coldMean)} s · ${t.cached} ${number.format(cachedMean)} s · ${breaches}/${data.length} ${t.overBudget}` : t.empty}
      actions={
        <label className="grid gap-1.5 text-xs">
          {t.series}
          <Select value={series} onChange={(event) => setSeries(event.target.value as typeof series)}>
            <SelectItem value="both">{t.both}</SelectItem>
            <SelectItem value="cold">{t.coldOnly}</SelectItem>
            <SelectItem value="cached">{t.cachedOnly}</SelectItem>
          </Select>
        </label>
      }
      tableLabel={t.table}
      emptyLabel={t.empty}
      empty={!data.length}
      table={
        <table>
          <caption className="sr-only">{t.caption}</caption>
          <thead><tr><th>{t.period}</th><th>{t.cold} (s)</th><th>{t.cachedHead} (s)</th><th>{t.saved} (s)</th></tr></thead>
          <tbody>{data.map((row) => <tr key={row.label}><th scope="row">{row.label}</th><td>{row.coldSeconds}</td><td>{row.cachedSeconds}</td><td>{row.coldSeconds - row.cachedSeconds}</td></tr>)}</tbody>
        </table>
      }
    >
      <LineChart data={data} accessibilityLayer margin={{ top: 12, right: 12, bottom: 4, left: 0 }}>
        <CartesianGrid vertical={false} stroke="var(--border)" strokeDasharray="3 4" />
        <XAxis dataKey="label" tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
        <YAxis domain={[0, "auto"]} width={52} tickFormatter={(value: number) => `${value}s`} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
        <ChartLegend reference={{ value: `${t.budget} ${number.format(budgetSeconds)} s`, color: "var(--destructive)", dashArray: "4 4" }} />
        <ReferenceLine ifOverflow="extendDomain" y={budgetSeconds} stroke="var(--destructive)" strokeDasharray="4 4" />
        <ChartTooltip formatter={(value) => `${Number(value)} s`} />
        {series !== "cached" && <Line dataKey="coldSeconds" name={t.coldBuild} legendType="plainline" stroke="var(--foreground)" strokeWidth={2.5} dot={{ r: 3 }} isAnimationActive={false} />}
        {series !== "cold" && <Line dataKey="cachedSeconds" name={t.cachedBuild} legendType="plainline" stroke="var(--primary)" strokeWidth={2.5} strokeDasharray="7 4" dot={{ r: 3 }} isAnimationActive={false} />}
      </LineChart>
    </ChartContainer>
  );
}

const exampleBuilds: BuildDurationPoint[] = [
  { label: "B1", coldSeconds: 210, cachedSeconds: 108 },
  { label: "B2", coldSeconds: 195, cachedSeconds: 102 },
  { label: "B3", coldSeconds: 238, cachedSeconds: 114 },
  { label: "B4", coldSeconds: 226, cachedSeconds: 109 },
  { label: "B5", coldSeconds: 204, cachedSeconds: 96 },
  { label: "B6", coldSeconds: 188, cachedSeconds: 93 },
];

export function BuildDurationExample({ locale = "en" }: { locale?: keyof typeof copy }) {
  return <BuildDurationChart data={exampleBuilds} locale={locale} />;
}
운영·개발

서비스 응답 시간

p50·p95 응답 시간을 p95 기준선과 비교합니다.

@neumorphism-ui/chart-service-latency
사용 예제
"use client";
import { ServiceLatencyChart, type ServiceLatencyPoint } from "@/components/blocks/service-latency-chart";

const data: ServiceLatencyPoint[] = [
  { label: "Mon", p50Ms: 80, p95Ms: 210 },
];

export function Example() {
  return <ServiceLatencyChart data={data} locale="ko" />;
}
설치되는 소스
"use client";

import { CartesianGrid, Line, LineChart, ReferenceLine, XAxis, YAxis } from "recharts";

import { ChartContainer, ChartTooltip, ChartLegend } from "@/components/ui/chart";

const copy = {
  "en": {
    "title": "Service latency",
    "description": "Daily p50 and p95 response time against a p95 budget.",
    "worst": "Worst p95",
    "overBudget": "periods over budget",
    "empty": "No latency samples.",
    "table": "View latency data",
    "caption": "Service latency data",
    "period": "Period",
    "budget": "Budget",
    "over": "Over",
    "within": "Within",
    "days": [
      "Mon",
      "Tue",
      "Wed",
      "Thu",
      "Fri",
      "Sat",
      "Sun"
    ]
  },
  "ko": {
    "title": "서비스 지연 시간",
    "description": "일별 p50·p95 응답 시간과 p95 목표값을 비교합니다.",
    "worst": "최대 p95",
    "overBudget": "기간에서 목표 초과",
    "empty": "지연 시간 데이터가 없습니다.",
    "table": "지연 시간 데이터 보기",
    "caption": "서비스 지연 시간 데이터",
    "period": "기간",
    "budget": "목표",
    "over": "초과",
    "within": "이내",
    "days": [
      "월",
      "화",
      "수",
      "목",
      "금",
      "토",
      "일"
    ]
  },
  "ja": {
    "title": "サービスの応答時間",
    "description": "日別のp50・p95応答時間をp95の目標値と比較します。",
    "worst": "最大p95",
    "overBudget": "期間で目標超過",
    "empty": "応答時間データはありません。",
    "table": "応答時間データを表示",
    "caption": "サービス応答時間データ",
    "period": "期間",
    "budget": "目標",
    "over": "超過",
    "within": "以内",
    "days": [
      "月",
      "火",
      "水",
      "木",
      "金",
      "土",
      "日"
    ]
  },
  "zh": {
    "title": "服务延迟",
    "description": "比较每日p50、p95响应时间与p95目标值。",
    "worst": "最高p95",
    "overBudget": "个时段超出目标",
    "empty": "暂无延迟数据。",
    "table": "查看延迟数据",
    "caption": "服务延迟数据",
    "period": "时段",
    "budget": "目标",
    "over": "超出",
    "within": "范围内",
    "days": [
      "周一",
      "周二",
      "周三",
      "周四",
      "周五",
      "周六",
      "周日"
    ]
  }
} as const;

export type ServiceLatencyPoint = {
  label: string;
  p50Ms: number;
  p95Ms: number;
};

export function ServiceLatencyChart({
  data,
  budgetMs = 300,
  locale = "en",
}: {
  data: readonly ServiceLatencyPoint[];
  budgetMs?: number;
  locale?: keyof typeof copy;
}) {
  const t = copy[locale];
  const number = new Intl.NumberFormat(locale);
  const worst = data.length ? Math.max(...data.map((row) => row.p95Ms)) : 0;
  const breaches = data.filter((row) => row.p95Ms > budgetMs);

  return (
    <ChartContainer
      data-chart="service-latency"
      title={t.title}
      description={t.description}
      summary={data.length ? `${t.worst} ${number.format(worst)} ms · ${breaches.length}/${data.length} ${t.overBudget}` : t.empty}
      tableLabel={t.table}
      emptyLabel={t.empty}
      empty={!data.length}
      table={
        <table>
          <caption className="sr-only">{t.caption}</caption>
          <thead><tr><th>{t.period}</th><th>p50 (ms)</th><th>p95 (ms)</th><th>{t.budget}</th></tr></thead>
          <tbody>{data.map((row) => <tr key={row.label}><th scope="row">{row.label}</th><td>{row.p50Ms}</td><td>{row.p95Ms}</td><td>{row.p95Ms > budgetMs ? t.over : t.within}</td></tr>)}</tbody>
        </table>
      }
    >
      <LineChart data={data} accessibilityLayer margin={{ top: 12, right: 12, bottom: 4, left: 0 }}>
        <CartesianGrid vertical={false} stroke="var(--border)" strokeDasharray="3 4" />
        <XAxis dataKey="label" tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
        <YAxis domain={[0, "auto"]} width={52} tickFormatter={(value: number) => `${value}ms`} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
        <ChartLegend reference={{ value: `${t.budget} ${number.format(budgetMs)} ms`, color: "var(--destructive)", dashArray: "4 4" }} />
        <ReferenceLine ifOverflow="extendDomain" y={budgetMs} stroke="var(--destructive)" strokeDasharray="4 4" />
        <ChartTooltip formatter={(value) => `${Number(value)} ms`} />
        <Line dataKey="p50Ms" name="p50" legendType="plainline" stroke="var(--muted-foreground)" strokeWidth={2} strokeDasharray="7 4" dot={false} isAnimationActive={false} />
        <Line dataKey="p95Ms" name="p95" legendType="plainline" stroke="var(--primary)" strokeWidth={2.5} dot={{ r: 3 }} isAnimationActive={false} />
      </LineChart>
    </ChartContainer>
  );
}

const exampleLatency: ServiceLatencyPoint[] = [
  { label: "Mon", p50Ms: 80, p95Ms: 210 },
  { label: "Tue", p50Ms: 76, p95Ms: 195 },
  { label: "Wed", p50Ms: 95, p95Ms: 340 },
  { label: "Thu", p50Ms: 130, p95Ms: 470 },
  { label: "Fri", p50Ms: 100, p95Ms: 330 },
  { label: "Sat", p50Ms: 82, p95Ms: 240 },
  { label: "Sun", p50Ms: 79, p95Ms: 215 },
];

export function ServiceLatencyExample({ locale = "en" }: { locale?: keyof typeof copy }) {
  const data = exampleLatency.map((point, index) => ({ ...point, label: copy[locale].days[index] }));
  return <ServiceLatencyChart data={data} locale={locale} />;
}
운영·개발

릴리스별 설치

신규 설치와 업데이트를 건수 또는 릴리스별 비율로 표시합니다.

@neumorphism-ui/chart-release-activity
사용 예제
"use client";
import { ReleaseActivityChart, type ReleaseActivityPoint } from "@/components/blocks/release-activity-chart";

const data: ReleaseActivityPoint[] = [
  { release: "R1", installs: 148, updates: 62 },
];

export function Example() {
  return <ReleaseActivityChart data={data} locale="ko" />;
}
설치되는 소스
"use client";

import * as React from "react";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";

import { ChartContainer, ChartTooltip, ChartLegend } from "@/components/ui/chart";
import { Select, SelectItem } from "@/components/ui/select";

const copy = {
  "en": {
    "title": "Release activity",
    "description": "New installs and updates by release. Counts are events, not unique users.",
    "total": "Total activity",
    "updateShare": "Update share",
    "empty": "No release activity.",
    "view": "View",
    "count": "Event counts",
    "share": "Share per release",
    "table": "View release data",
    "caption": "Release activity data",
    "release": "Release",
    "installs": "New installs",
    "updates": "Updates"
  },
  "ko": {
    "title": "릴리스 활동",
    "description": "릴리스별 신규 설치와 업데이트 건수입니다. 고유 사용자 수가 아닌 이벤트 수입니다.",
    "total": "전체 활동",
    "updateShare": "업데이트 비율",
    "empty": "릴리스 활동이 없습니다.",
    "view": "보기 방식",
    "count": "이벤트 수",
    "share": "릴리스별 비율",
    "table": "릴리스 데이터 보기",
    "caption": "릴리스 활동 데이터",
    "release": "릴리스",
    "installs": "신규 설치",
    "updates": "업데이트"
  },
  "ja": {
    "title": "リリースの利用状況",
    "description": "リリース別の新規インストールと更新件数です。人数ではなくイベント数です。",
    "total": "合計イベント",
    "updateShare": "更新の割合",
    "empty": "リリースの利用記録はありません。",
    "view": "表示方法",
    "count": "イベント数",
    "share": "リリース別の割合",
    "table": "リリースデータを表示",
    "caption": "リリース利用状況データ",
    "release": "リリース",
    "installs": "新規インストール",
    "updates": "更新"
  },
  "zh": {
    "title": "版本活动",
    "description": "各版本的新安装和更新次数。计数为事件数,而非独立用户数。",
    "total": "总活动数",
    "updateShare": "更新占比",
    "empty": "暂无版本活动。",
    "view": "显示方式",
    "count": "事件数",
    "share": "各版本占比",
    "table": "查看版本数据",
    "caption": "版本活动数据",
    "release": "版本",
    "installs": "新安装",
    "updates": "更新"
  }
} as const;

export type ReleaseActivityPoint = {
  release: string;
  installs: number;
  updates: number;
};

export function ReleaseActivityChart({
  data,
  locale = "en",
}: {
  data: readonly ReleaseActivityPoint[];
  locale?: keyof typeof copy;
}) {
  const t = copy[locale];
  const [view, setView] = React.useState<"count" | "share">("count");
  const installs = data.reduce((sum, row) => sum + row.installs, 0);
  const updates = data.reduce((sum, row) => sum + row.updates, 0);
  const total = installs + updates;

  return (
    <ChartContainer
      data-chart="release-activity"
      title={t.title}
      description={t.description}
      summary={data.length ? `${t.total} ${total.toLocaleString(locale)} · ${t.updateShare} ${total ? ((updates / total) * 100).toFixed(1) : "0.0"}%` : t.empty}
      actions={
        <label className="grid gap-1.5 text-xs">
          {t.view}
          <Select value={view} onChange={(event) => setView(event.target.value as typeof view)}>
            <SelectItem value="count">{t.count}</SelectItem>
            <SelectItem value="share">{t.share}</SelectItem>
          </Select>
        </label>
      }
      tableLabel={t.table}
      emptyLabel={t.empty}
      empty={!data.length}
      table={
        <table>
          <caption className="sr-only">{t.caption}</caption>
          <thead><tr><th>{t.release}</th><th>{t.installs}</th><th>{t.updates}</th><th>{t.updateShare}</th></tr></thead>
          <tbody>{data.map((row) => {
            const rowTotal = row.installs + row.updates;
            return <tr key={row.release}><th scope="row">{row.release}</th><td>{row.installs}</td><td>{row.updates}</td><td>{rowTotal ? ((row.updates / rowTotal) * 100).toFixed(1) + "%" : "—"}</td></tr>;
          })}</tbody>
        </table>
      }
    >
      <AreaChart data={data} stackOffset={view === "share" ? "expand" : "none"} accessibilityLayer margin={{ top: 12, right: 12, bottom: 4, left: 0 }}>
        <CartesianGrid vertical={false} stroke="var(--border)" strokeDasharray="3 4" />
        <XAxis dataKey="release" tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
        <YAxis domain={view === "share" ? [0, 1] : [0, "auto"]} width={52} tickFormatter={(value: number) => view === "share" ? `${Math.round(value * 100)}%` : String(value)} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
        <ChartLegend />
        <ChartTooltip formatter={(value) => Number(value).toLocaleString(locale)} />
        <Area dataKey="installs" name={t.installs} legendType="plainline" stackId="activity" stroke="var(--foreground)" fill="var(--primary)" fillOpacity={0.22} strokeWidth={2} isAnimationActive={false} />
        <Area dataKey="updates" name={t.updates} legendType="plainline" stackId="activity" stroke="var(--muted-foreground)" fill="var(--muted)" fillOpacity={0.8} strokeWidth={2} strokeDasharray="6 4" isAnimationActive={false} />
      </AreaChart>
    </ChartContainer>
  );
}

const exampleActivity: ReleaseActivityPoint[] = [
  { release: "R1", installs: 148, updates: 62 },
  { release: "R2", installs: 232, updates: 104 },
  { release: "R3", installs: 196, updates: 88 },
  { release: "R4", installs: 284, updates: 132 },
  { release: "R5", installs: 341, updates: 176 },
  { release: "R6", installs: 378, updates: 209 },
];

export function ReleaseActivityExample({ locale = "en" }: { locale?: keyof typeof copy }) {
  return <ReleaseActivityChart data={exampleActivity} locale={locale} />;
}
운영·개발

계획 대비 완료

계획한 항목 수와 완료한 항목 수, 두 값의 차이를 확인합니다.

@neumorphism-ui/chart-delivery-capacity
사용 예제
"use client";
import { DeliveryCapacityChart, type DeliveryCapacityPoint } from "@/components/blocks/delivery-capacity-chart";

const data: DeliveryCapacityPoint[] = [
  { period: "S1", planned: 32, delivered: 28 },
];

export function Example() {
  return <DeliveryCapacityChart data={data} locale="ko" />;
}
설치되는 소스
"use client";

import * as React from "react";
import { Bar, BarChart, CartesianGrid, ReferenceLine, XAxis, YAxis } from "recharts";

import { ChartContainer, ChartTooltip, ChartLegend } from "@/components/ui/chart";
import { Select, SelectItem } from "@/components/ui/select";

const copy = {
  "en": {
    "title": "Delivery capacity",
    "description": "Planned and delivered item counts. This is not a productivity score.",
    "planned": "Planned",
    "delivered": "Delivered",
    "netVariance": "Net variance",
    "empty": "No delivery records.",
    "view": "View",
    "compare": "Planned vs delivered",
    "signed": "Signed variance",
    "table": "View delivery data",
    "caption": "Delivery capacity data",
    "period": "Period",
    "variance": "Variance",
    "difference": "Delivered − planned"
  },
  "ko": {
    "title": "계획·완료 수량",
    "description": "계획한 항목과 완료한 항목의 수를 비교합니다. 생산성 점수가 아닙니다.",
    "planned": "계획",
    "delivered": "완료",
    "netVariance": "순차이",
    "empty": "완료 기록이 없습니다.",
    "view": "보기 방식",
    "compare": "계획과 완료 비교",
    "signed": "증감 차이",
    "table": "계획·완료 데이터 보기",
    "caption": "계획·완료 수량 데이터",
    "period": "기간",
    "variance": "차이",
    "difference": "완료 − 계획"
  },
  "ja": {
    "title": "計画・完了件数",
    "description": "計画した項目と完了した項目の件数です。生産性スコアではありません。",
    "planned": "計画",
    "delivered": "完了",
    "netVariance": "合計差分",
    "empty": "完了記録はありません。",
    "view": "表示方法",
    "compare": "計画と完了を比較",
    "signed": "増減の差分",
    "table": "計画・完了データを表示",
    "caption": "計画・完了件数データ",
    "period": "期間",
    "variance": "差分",
    "difference": "完了 − 計画"
  },
  "zh": {
    "title": "计划与完成数量",
    "description": "比较计划和已完成的事项数量。这不是生产力评分。",
    "planned": "计划",
    "delivered": "完成",
    "netVariance": "净差额",
    "empty": "暂无完成记录。",
    "view": "显示方式",
    "compare": "计划与完成对比",
    "signed": "增减差额",
    "table": "查看计划与完成数据",
    "caption": "计划与完成数量数据",
    "period": "时段",
    "variance": "差额",
    "difference": "完成 − 计划"
  }
} as const;

export type DeliveryCapacityPoint = {
  period: string;
  planned: number;
  delivered: number;
};

export function DeliveryCapacityChart({
  data,
  locale = "en",
}: {
  data: readonly DeliveryCapacityPoint[];
  locale?: keyof typeof copy;
}) {
  const t = copy[locale];
  const [view, setView] = React.useState<"compare" | "variance">("compare");
  const planned = data.reduce((sum, row) => sum + row.planned, 0);
  const delivered = data.reduce((sum, row) => sum + row.delivered, 0);
  const chartData = data.map((row) => ({ ...row, variance: row.delivered - row.planned }));

  return (
    <ChartContainer
      data-chart="delivery-capacity"
      title={t.title}
      description={t.description}
      summary={data.length ? `${t.planned} ${planned} · ${t.delivered} ${delivered} · ${t.netVariance} ${delivered - planned >= 0 ? "+" : ""}${delivered - planned}` : t.empty}
      actions={
        <label className="grid gap-1.5 text-xs">
          {t.view}
          <Select value={view} onChange={(event) => setView(event.target.value as typeof view)}>
            <SelectItem value="compare">{t.compare}</SelectItem>
            <SelectItem value="variance">{t.signed}</SelectItem>
          </Select>
        </label>
      }
      tableLabel={t.table}
      emptyLabel={t.empty}
      empty={!data.length}
      table={
        <table>
          <caption className="sr-only">{t.caption}</caption>
          <thead><tr><th>{t.period}</th><th>{t.planned}</th><th>{t.delivered}</th><th>{t.variance}</th></tr></thead>
          <tbody>{chartData.map((row) => <tr key={row.period}><th scope="row">{row.period}</th><td>{row.planned}</td><td>{row.delivered}</td><td>{row.variance > 0 ? "+" + row.variance : row.variance}</td></tr>)}</tbody>
        </table>
      }
    >
      <BarChart data={chartData} accessibilityLayer margin={{ top: 12, right: 12, bottom: 4, left: 0 }}>
        <CartesianGrid vertical={false} stroke="var(--border)" strokeDasharray="3 4" />
        <XAxis dataKey="period" tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
        <YAxis width={44} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
        <ReferenceLine y={0} stroke="var(--foreground)" />
        <ChartLegend />
        <ChartTooltip />
        {view === "variance" ? (
          <Bar dataKey="variance" name={t.difference} fill="var(--primary)" maxBarSize={52} isAnimationActive={false} />
        ) : (
          <>
            <Bar dataKey="planned" name={t.planned} legendType="plainline" fill="var(--muted-foreground)" fillOpacity={0.14} stroke="var(--muted-foreground)" strokeDasharray="4 3" maxBarSize={40} isAnimationActive={false} />
            <Bar dataKey="delivered" name={t.delivered} fill="var(--primary)" maxBarSize={40} isAnimationActive={false} />
          </>
        )}
      </BarChart>
    </ChartContainer>
  );
}

const exampleCapacity: DeliveryCapacityPoint[] = [
  { period: "S1", planned: 32, delivered: 28 },
  { period: "S2", planned: 36, delivered: 39 },
  { period: "S3", planned: 34, delivered: 31 },
  { period: "S4", planned: 38, delivered: 42 },
];

export function DeliveryCapacityExample({ locale = "en" }: { locale?: keyof typeof copy }) {
  return <DeliveryCapacityChart data={exampleCapacity} locale={locale} />;
}
운영·개발

설치 단계별 시간

각 설치 단계의 소요 시간을 밀리초 또는 초로 확인합니다.

@neumorphism-ui/chart-install-diagnostics
사용 예제
"use client";
import { InstallDiagnosticsChart, type InstallDiagnosticPoint } from "@/components/blocks/install-diagnostics-chart";

const data: InstallDiagnosticPoint[] = [
  { stage: "Download", durationMs: 840 },
];

export function Example() {
  return <InstallDiagnosticsChart data={data} locale="ko" />;
}
설치되는 소스
"use client";

import * as React from "react";
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";

import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
import { Select, SelectItem } from "@/components/ui/select";

const copy = {
  "en": {
    "title": "Installation diagnostics",
    "description": "Sequential stage durations for one installation trace.",
    "total": "Total",
    "longest": "Longest stage",
    "empty": "No installation trace.",
    "unit": "Unit",
    "ms": "Milliseconds",
    "seconds": "Seconds",
    "table": "View installation data",
    "caption": "Installation diagnostic data",
    "stage": "Stage",
    "duration": "Duration",
    "share": "Share",
    "stages": [
      "Resolve",
      "Download",
      "Transform",
      "Write"
    ]
  },
  "ko": {
    "title": "설치 단계 진단",
    "description": "한 번의 설치에서 순서대로 실행된 각 단계의 소요 시간입니다.",
    "total": "전체 시간",
    "longest": "가장 오래 걸린 단계",
    "empty": "설치 기록이 없습니다.",
    "unit": "단위",
    "ms": "밀리초",
    "seconds": "초",
    "table": "설치 데이터 보기",
    "caption": "설치 단계 진단 데이터",
    "stage": "단계",
    "duration": "소요 시간",
    "share": "비율",
    "stages": [
      "의존성 확인",
      "다운로드",
      "변환",
      "파일 쓰기"
    ]
  },
  "ja": {
    "title": "インストール診断",
    "description": "1回のインストールで順番に実行された各工程の所要時間です。",
    "total": "合計時間",
    "longest": "最長の工程",
    "empty": "インストール記録はありません。",
    "unit": "単位",
    "ms": "ミリ秒",
    "seconds": "秒",
    "table": "インストールデータを表示",
    "caption": "インストール診断データ",
    "stage": "工程",
    "duration": "所要時間",
    "share": "割合",
    "stages": [
      "依存関係",
      "ダウンロード",
      "変換",
      "書き込み"
    ]
  },
  "zh": {
    "title": "安装诊断",
    "description": "一次安装中按顺序执行的各阶段耗时。",
    "total": "总耗时",
    "longest": "最耗时阶段",
    "empty": "暂无安装记录。",
    "unit": "单位",
    "ms": "毫秒",
    "seconds": "秒",
    "table": "查看安装数据",
    "caption": "安装诊断数据",
    "stage": "阶段",
    "duration": "耗时",
    "share": "占比",
    "stages": [
      "解析依赖",
      "下载",
      "转换",
      "写入"
    ]
  }
} as const;

export type InstallDiagnosticPoint = {
  stage: string;
  durationMs: number;
};

export function InstallDiagnosticsChart({
  data,
  locale = "en",
}: {
  data: readonly InstallDiagnosticPoint[];
  locale?: keyof typeof copy;
}) {
  const t = copy[locale];
  const [unit, setUnit] = React.useState<"ms" | "s">("ms");
  const total = data.reduce((sum, row) => sum + row.durationMs, 0);
  const largest = data.length ? data.reduce((best, row) => row.durationMs > best.durationMs ? row : best) : null;
  const format = (value: number) => unit === "ms" ? `${value} ms` : `${(value / 1000).toFixed(2)} s`;

  return (
    <ChartContainer
      data-chart="install-diagnostics"
      title={t.title}
      description={t.description}
      summary={largest ? `${t.total} ${format(total)} · ${t.longest} ${largest.stage} (${format(largest.durationMs)})` : t.empty}
      actions={
        <label className="grid gap-1.5 text-xs">
          {t.unit}
          <Select value={unit} onChange={(event) => setUnit(event.target.value as typeof unit)}>
            <SelectItem value="ms">{t.ms}</SelectItem>
            <SelectItem value="s">{t.seconds}</SelectItem>
          </Select>
        </label>
      }
      tableLabel={t.table}
      emptyLabel={t.empty}
      empty={!data.length}
      table={
        <table>
          <caption className="sr-only">{t.caption}</caption>
          <thead><tr><th>{t.stage}</th><th>{t.duration}</th><th>{t.share}</th></tr></thead>
          <tbody>{data.map((row) => <tr key={row.stage}><th scope="row">{row.stage}</th><td>{format(row.durationMs)}</td><td>{total ? ((row.durationMs / total) * 100).toFixed(1) + "%" : "—"}</td></tr>)}</tbody>
        </table>
      }
    >
      <BarChart data={data} layout="vertical" accessibilityLayer margin={{ top: 12, right: 12, bottom: 4, left: 0 }}>
        <CartesianGrid horizontal={false} stroke="var(--border)" strokeDasharray="3 4" />
        <XAxis type="number" tickFormatter={(value: number) => format(value)} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
        <YAxis type="category" dataKey="stage" width={locale === "en" ? 72 : 88} tick={{ fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} />
        <ChartTooltip formatter={(value) => format(Number(value))} />
        <Bar dataKey="durationMs" name={t.duration} fill="var(--primary)" radius={[0, 4, 4, 0]} maxBarSize={42} isAnimationActive={false} />
      </BarChart>
    </ChartContainer>
  );
}

const exampleTrace: InstallDiagnosticPoint[] = [
  { stage: "Resolve", durationMs: 120 },
  { stage: "Download", durationMs: 840 },
  { stage: "Transform", durationMs: 210 },
  { stage: "Write", durationMs: 90 },
];

export function InstallDiagnosticsExample({ locale = "en" }: { locale?: keyof typeof copy }) {
  const data = exampleTrace.map((point, index) => ({ ...point, stage: copy[locale].stages[index] }));
  return <InstallDiagnosticsChart data={data} locale={locale} />;
}
Neumorphism UI56 UI · 16 blocks · 80 registry items

shadcn 호환 소스 Registry

GitHubCredits