跳到正文
Neumorphism UI
文档样式图表模板
—
ZH
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

渠道比较

所选期间的渠道总计。柱形从零开始。

访问: 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%

缺失日期显示为空白而非零。访问量为零时转化率未定义(—)。

运维与开发图表

与产品分析分开,展示构建、延迟、发布、交付和安装阶段等运行指标。

构建耗时

比较未使用缓存和使用缓存的构建耗时,单位为秒。

未使用缓存平均 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="zh" />;
}
安装源码
"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="zh" />;
}
安装源码
"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>;
}
产品分析

转化率

每日转化率;缺失值不会显示为零。

@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="zh" />;
}
安装源码
"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="zh" />;
}
安装源码
"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="zh" />;
}
安装源码
"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="zh" />;
}
安装源码
"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="zh" />;
}
安装源码
"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="zh" />;
}
安装源码
"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