@xpert-ai/plugin-live-dashboard 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.xpertai-plugin/plugin.json +10 -2
- package/README.md +70 -0
- package/assets/branding/cover-v1.prompt.txt +27 -0
- package/assets/branding/cover-v1.webp +0 -0
- package/assets/previews/customer-brief.png +0 -0
- package/assets/previews/profit-atlas.png +0 -0
- package/assets/previews/trend-workspace.png +0 -0
- package/dist/index.js +14 -1
- package/dist/lib/agent.schema.d.ts +43 -37
- package/dist/lib/agent.schema.js +2 -1
- package/dist/lib/app-config.d.ts +62 -0
- package/dist/lib/app-config.js +38 -0
- package/dist/lib/artifact-type.provider.d.ts +15 -0
- package/dist/lib/artifact-type.provider.js +78 -0
- package/dist/lib/assistant-template.d.ts +2 -0
- package/dist/lib/assistant-template.js +7 -0
- package/dist/lib/content.provider.js +13 -5
- package/dist/lib/dashboard-navigation.d.ts +58 -14
- package/dist/lib/dashboard-navigation.js +11 -10
- package/dist/lib/dashboard-view.schema.d.ts +197 -46
- package/dist/lib/dashboard-view.schema.js +8 -1
- package/dist/lib/dashboard.service.d.ts +39 -2
- package/dist/lib/dashboard.service.js +52 -28
- package/dist/lib/document-libraries.d.ts +1 -1
- package/dist/lib/document-libraries.js +3 -2
- package/dist/lib/document-network.d.ts +1 -1
- package/dist/lib/document-network.js +2 -6
- package/dist/lib/editing-feedback.d.ts +5 -0
- package/dist/lib/editing-feedback.js +19 -0
- package/dist/lib/editing-patch.d.ts +22 -0
- package/dist/lib/editing-patch.js +46 -0
- package/dist/lib/editing-session.d.ts +167 -0
- package/dist/lib/editing-session.js +53 -0
- package/dist/lib/editing.d.ts +92 -60
- package/dist/lib/editing.js +36 -24
- package/dist/lib/html-guidance.d.ts +1 -1
- package/dist/lib/html-guidance.js +1 -1
- package/dist/lib/middleware.js +48 -20
- package/dist/lib/monitor.d.ts +70 -0
- package/dist/lib/monitor.js +101 -0
- package/dist/lib/plugin.js +5 -1
- package/dist/lib/project.d.ts +106 -0
- package/dist/lib/project.js +17 -0
- package/dist/lib/projects.schema.d.ts +49 -0
- package/dist/lib/projects.schema.js +7 -0
- package/dist/lib/projects.service.d.ts +23 -0
- package/dist/lib/projects.service.js +65 -0
- package/dist/lib/prompt-workflows.d.ts +3 -2
- package/dist/lib/prompt-workflows.js +17 -3
- package/dist/lib/provider-options.controller.d.ts +10 -0
- package/dist/lib/provider-options.controller.js +49 -0
- package/dist/lib/resource-card.js +1 -1
- package/dist/lib/runtime-context.d.ts +3 -0
- package/dist/lib/runtime-context.js +13 -0
- package/dist/lib/save-evidence.d.ts +1 -1
- package/dist/lib/templates.d.ts +2 -0
- package/dist/lib/templates.js +55 -0
- package/dist/lib/view.provider.d.ts +97 -7
- package/dist/lib/view.provider.js +39 -10
- package/dist/libraries/TREND-WORKSPACE-ICONS-LICENSE.txt +43 -0
- package/dist/libraries/catalog.json +1 -1
- package/dist/remote/app.css +1 -1
- package/dist/remote/app.js +33 -33
- package/dist/remote/build-manifest.json +118 -32
- package/dist/sandbox-actions/action.json +1 -1
- package/dist/sandbox-actions/bundle/host.js +2 -2
- package/dist/sandbox-actions/bundle/resource-cache.mjs +2 -1
- package/dist/sandbox-actions/bundle/validate.mjs +1 -1
- package/docs/docs.json +7 -0
- package/docs/frontend-libraries.mdx +12 -4
- package/docs/html-mode.md +3 -3
- package/docs/quick-start-acceptance.mdx +50 -0
- package/docs/quick-start.mdx +64 -0
- package/docs/superstore.mdx +38 -0
- package/docs/team-sharing.mdx +6 -2
- package/docs/trend-workspace-acceptance.mdx +43 -0
- package/docs/trend-workspace.mdx +30 -0
- package/docs/verification.mdx +4 -2
- package/docs/visual-styles-acceptance.mdx +40 -0
- package/docs/visual-styles.mdx +46 -0
- package/package.json +25 -18
- package/scripts/import-prompts.mjs +108 -0
- package/skills/live-dashboard/SKILL.md +27 -3
- package/skills/live-dashboard/examples/README.md +25 -0
- package/skills/live-dashboard/examples/catalog.json +79 -0
- package/skills/live-dashboard/examples/production-quality/README.md +25 -0
- package/skills/live-dashboard/examples/production-quality/dashboard.html +129 -0
- package/skills/live-dashboard/examples/production-quality/definition.json +67 -0
- package/skills/live-dashboard/examples/production-quality/demo.csv +7 -0
- package/skills/live-dashboard/examples/production-quality/demo.json +44 -0
- package/skills/live-dashboard/examples/production-quality/expected.json +26 -0
- package/skills/live-dashboard/examples/production-quality/preview.png +0 -0
- package/skills/live-dashboard/examples/production-quality/query.json +53 -0
- package/skills/live-dashboard/examples/regional-sales/README.md +25 -0
- package/skills/live-dashboard/examples/regional-sales/dashboard-create.json +35 -0
- package/skills/live-dashboard/examples/regional-sales/definition.json +35 -0
- package/skills/live-dashboard/examples/regional-sales/demo.csv +5 -0
- package/skills/live-dashboard/examples/regional-sales/demo.json +18 -0
- package/skills/live-dashboard/examples/regional-sales/expected.json +19 -0
- package/skills/live-dashboard/examples/regional-sales/preview.png +0 -0
- package/skills/live-dashboard/examples/regional-sales/query.json +45 -0
- package/skills/live-dashboard/examples/sales-overview/README.md +25 -0
- package/skills/live-dashboard/examples/sales-overview/dashboard.html +129 -0
- package/skills/live-dashboard/examples/sales-overview/definition.json +57 -0
- package/skills/live-dashboard/examples/sales-overview/demo.csv +7 -0
- package/skills/live-dashboard/examples/sales-overview/demo.json +38 -0
- package/skills/live-dashboard/examples/sales-overview/expected.json +22 -0
- package/skills/live-dashboard/examples/sales-overview/preview.png +0 -0
- package/skills/live-dashboard/examples/sales-overview/query.json +52 -0
- package/skills/live-dashboard/examples/superstore/README.md +64 -0
- package/skills/live-dashboard/examples/superstore/THIRD_PARTY_LICENSE.txt +207 -0
- package/skills/live-dashboard/examples/superstore/build_data.py +155 -0
- package/skills/live-dashboard/examples/superstore/build_pages.py +129 -0
- package/skills/live-dashboard/examples/superstore/checks.json +104 -0
- package/skills/live-dashboard/examples/superstore/customers-mobile-dark.png +0 -0
- package/skills/live-dashboard/examples/superstore/customers-select.png +0 -0
- package/skills/live-dashboard/examples/superstore/customers.html +316 -0
- package/skills/live-dashboard/examples/superstore/customers.png +0 -0
- package/skills/live-dashboard/examples/superstore/customers.queries.json +512 -0
- package/skills/live-dashboard/examples/superstore/expected.json +4069 -0
- package/skills/live-dashboard/examples/superstore/import.json +200 -0
- package/skills/live-dashboard/examples/superstore/orders.csv +10195 -0
- package/skills/live-dashboard/examples/superstore/overview-mobile-dark.png +0 -0
- package/skills/live-dashboard/examples/superstore/overview-select.png +0 -0
- package/skills/live-dashboard/examples/superstore/overview.html +310 -0
- package/skills/live-dashboard/examples/superstore/overview.png +0 -0
- package/skills/live-dashboard/examples/superstore/overview.queries.json +636 -0
- package/skills/live-dashboard/examples/superstore/page.js +109 -0
- package/skills/live-dashboard/examples/superstore/profitability-mobile-dark.png +0 -0
- package/skills/live-dashboard/examples/superstore/profitability-select.png +0 -0
- package/skills/live-dashboard/examples/superstore/profitability.html +307 -0
- package/skills/live-dashboard/examples/superstore/profitability.png +0 -0
- package/skills/live-dashboard/examples/superstore/profitability.queries.json +334 -0
- package/skills/live-dashboard/examples/superstore/provenance.json +24 -0
- package/skills/live-dashboard/examples/superstore/semantic-model.json +220 -0
- package/skills/live-dashboard/examples/superstore/test_data.py +62 -0
- package/skills/live-dashboard/references/html-runtime.md +1 -1
- package/skills/live-dashboard/themes/README.md +50 -0
- package/skills/live-dashboard/themes/customer-brief/README.md +47 -0
- package/skills/live-dashboard/themes/customer-brief/charts.js +17 -0
- package/skills/live-dashboard/themes/customer-brief/layout.html +22 -0
- package/skills/live-dashboard/themes/customer-brief/theme.css +108 -0
- package/skills/live-dashboard/themes/profit-atlas/README.md +43 -0
- package/skills/live-dashboard/themes/profit-atlas/charts.js +30 -0
- package/skills/live-dashboard/themes/profit-atlas/layout.html +19 -0
- package/skills/live-dashboard/themes/profit-atlas/theme.css +75 -0
- package/skills/live-dashboard/themes/trend-workspace/ICON_LICENSE.txt +43 -0
- package/skills/live-dashboard/themes/trend-workspace/README.md +144 -0
- package/skills/live-dashboard/themes/trend-workspace/charts.js +60 -0
- package/skills/live-dashboard/themes/trend-workspace/icons.js +11 -0
- package/skills/live-dashboard/themes/trend-workspace/layout.html +45 -0
- package/skills/live-dashboard/themes/trend-workspace/select.js +127 -0
- package/skills/live-dashboard/themes/trend-workspace/theme.css +137 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// No rows or backend endpoints belong here. All panels use governed query results.
|
|
2
|
+
(async () => {
|
|
3
|
+
const live = window.xpertLiveArtifact;
|
|
4
|
+
const {enhanceSelect:enhanceDashboardSelect,charts:TrendWorkspaceCharts}=window.TrendWorkspace;
|
|
5
|
+
const status = document.getElementById('status');
|
|
6
|
+
const selects = [...document.querySelectorAll('select[data-dashboard-control]')];
|
|
7
|
+
const datasets = new Map();
|
|
8
|
+
const chartIds = ['chart-primary', 'chart-secondary', ...(config.key === 'overview' ? ['chart-profit'] : [])];
|
|
9
|
+
const charts = chartIds.map(id => window.echarts.init(document.getElementById(id), null, {renderer:'svg'}));
|
|
10
|
+
const controls = {year:'',region:'',category:''};
|
|
11
|
+
const integer = new Intl.NumberFormat('en-US',{maximumFractionDigits:0});
|
|
12
|
+
const decimal = new Intl.NumberFormat('en-US',{minimumFractionDigits:2,maximumFractionDigits:2});
|
|
13
|
+
const fields = config.measures;
|
|
14
|
+
let failure = false;
|
|
15
|
+
function number(value) {
|
|
16
|
+
if ((typeof value !== 'number' && typeof value !== 'string') || String(value).trim() === '') throw Error('查询结果缺少必要指标。');
|
|
17
|
+
const n=Number(value);if(!Number.isFinite(n)||Math.abs(n)>Number.MAX_SAFE_INTEGER)throw Error('数值超出安全展示范围。');return n;
|
|
18
|
+
}
|
|
19
|
+
function precise(value) {
|
|
20
|
+
// Keep decimal strings exact until two-place display rounding; Number is only for ratios/plots.
|
|
21
|
+
const raw=String(value);if(!/^-?\d+(\.\d+)?$/.test(raw))throw Error('金额格式不符合数据契约。');
|
|
22
|
+
const negative=raw.startsWith('-');const [whole,tail='']=raw.replace('-','').split('.');
|
|
23
|
+
let cents=BigInt(whole)*100n+BigInt((tail+'00').slice(0,2));if(Number(tail[2]??0)>=5)cents+=1n;
|
|
24
|
+
return (negative&¢s?'-':'')+(cents/100n).toLocaleString('en-US')+'.'+String(cents%100n).padStart(2,'0');
|
|
25
|
+
}
|
|
26
|
+
function ratio(a,b){return b?decimal.format(a/b*100)+'%':'—';}
|
|
27
|
+
function clear(message,isError=false){
|
|
28
|
+
document.querySelectorAll('output').forEach(el=>el.textContent='—');
|
|
29
|
+
document.getElementById('details-rows').replaceChildren();charts.forEach(chart=>{ chart.clear(); chart.getDom().removeAttribute('aria-label'); });
|
|
30
|
+
status.textContent=message;status.classList.toggle('error',isError);
|
|
31
|
+
document.getElementById('freshness').textContent='尚无当前查询结果';
|
|
32
|
+
}
|
|
33
|
+
function fail(message){failure=true;datasets.clear();clear(message,true);}
|
|
34
|
+
function table(rows,dimension){
|
|
35
|
+
const max=Math.max(1,...rows.map(row=>number(row.Sales)));
|
|
36
|
+
document.getElementById('details-rows').replaceChildren(...rows.map(row=>{
|
|
37
|
+
const tr=document.createElement('tr');
|
|
38
|
+
const values=config.key==='customers'
|
|
39
|
+
?[row[dimension],precise(row.Sales),integer.format(number(row.Orders)),integer.format(number(row.Customers)),integer.format(number(row.ReturnedOrders)),ratio(number(row.ReturnedOrders),number(row.Orders)),precise(row.ReturnedSales)]
|
|
40
|
+
:[row[dimension],precise(row.Sales),precise(row.Profit),ratio(number(row.Profit),number(row.Sales)),integer.format(number(row.Orders)),integer.format(number(row.Customers))];
|
|
41
|
+
if(config.key==='overview')values.splice(4);
|
|
42
|
+
for(const [index,value]of values.entries()){
|
|
43
|
+
const td=document.createElement('td');td.textContent=String(value);
|
|
44
|
+
if(config.key!=='customers'&&(index===2||index===3)&&number(row.Profit)<0)td.className='negative';
|
|
45
|
+
if(index===1){const bar=document.createElement('span');bar.className='table-bar';bar.style.width=(Math.max(0,number(row.Sales))/max*100)+'%';bar.setAttribute('aria-hidden','true');td.append(bar);}
|
|
46
|
+
tr.append(td);
|
|
47
|
+
}return tr;
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
function render(){
|
|
51
|
+
if(failure||config.bindings.some(id=>!datasets.has(id)))return;
|
|
52
|
+
const summary=datasets.get('summary').rows;
|
|
53
|
+
if(!summary.length||number(summary[0].Orders)===0){clear('当前筛选下没有订单,请调整年份、区域或品类。');return;}
|
|
54
|
+
if(summary.length!==1)throw Error('总计必须由一个后端聚合行提供。');
|
|
55
|
+
const s=summary[0];const sales=number(s.Sales),profit=config.key==='customers'?0:number(s.Profit),orders=number(s.Orders),customers=number(s.Customers),returned=config.key==='customers'?number(s.ReturnedOrders):0;
|
|
56
|
+
const values=config.key==='customers'
|
|
57
|
+
?[integer.format(customers),integer.format(orders),orders?decimal.format(sales/orders):'—',integer.format(returned),ratio(returned,orders)]
|
|
58
|
+
:[precise(s.Sales),precise(s.Profit),ratio(profit,sales),integer.format(orders),integer.format(customers)];
|
|
59
|
+
values.forEach((value,i)=>{document.getElementById('metric-'+i).textContent=value;});
|
|
60
|
+
const chartsUI = TrendWorkspaceCharts;
|
|
61
|
+
const ranked = (rows, field) => [...rows].sort((a,b) => number(b[field]) - number(a[field]));
|
|
62
|
+
if(config.key==='overview'){
|
|
63
|
+
const trend=[...datasets.get('trend').rows].sort((a,b)=>a.OrderMonth.localeCompare(b.OrderMonth));
|
|
64
|
+
chartsUI.line(charts[0],trend,{dimension:'OrderMonth',measure:'Sales',label:'销售额(USD)'});
|
|
65
|
+
chartsUI.line(charts[2],trend,{dimension:'OrderMonth',measure:'Profit',label:'利润(USD)',color:'teal'});
|
|
66
|
+
chartsUI.horizontal(charts[1],ranked(datasets.get('regions').rows,'Sales'),{dimension:'Region',measure:'Sales',label:'销售额(USD)'});
|
|
67
|
+
table(ranked(datasets.get('categories').rows,'Sales'),'Category');
|
|
68
|
+
}else if(config.key==='profitability'){
|
|
69
|
+
const rows=ranked(datasets.get('subcategories').rows,'Profit');
|
|
70
|
+
chartsUI.horizontal(charts[0],rows,{dimension:'SubCategory',measure:'Profit',label:'利润(USD)',color:'teal'});
|
|
71
|
+
chartsUI.scatter(charts[1],rows,{dimension:'SubCategory',x:'Sales',y:'Profit',xLabel:'销售额(USD)',yLabel:'利润(USD)'});
|
|
72
|
+
table([...rows].reverse(),'SubCategory');
|
|
73
|
+
}else{
|
|
74
|
+
chartsUI.horizontal(charts[0],ranked(datasets.get('segments').rows,'Sales'),{dimension:'Segment',measure:'Sales',label:'销售额(USD)'});
|
|
75
|
+
chartsUI.horizontal(charts[1],ranked(datasets.get('regions').rows,'ReturnedOrders'),{dimension:'Region',measure:'ReturnedOrders',label:'退货订单数',decimals:0});
|
|
76
|
+
table(datasets.get('segments').rows,'Segment');
|
|
77
|
+
}
|
|
78
|
+
status.textContent='所有指标与图表已按当前筛选更新。';status.classList.remove('error');
|
|
79
|
+
const times=config.bindings.map(id=>datasets.get(id).freshness?.sourceFetchedAt).filter(Boolean).sort();
|
|
80
|
+
document.getElementById('freshness').textContent=times.length?'查询时间:'+new Date(times[0]).toLocaleString('zh-CN'):'查询已完成(来源未提供时间)';
|
|
81
|
+
}
|
|
82
|
+
async function queryAll(){
|
|
83
|
+
failure=false;datasets.clear();clear('正在查询当前筛选…');selects.forEach(el=>el.disabled=true);
|
|
84
|
+
try{const results=await Promise.allSettled(config.bindings.map(id=>live.query(id,{...controls})));if(results.some(r=>r.status==='rejected'))throw Error('查询失败');}
|
|
85
|
+
catch{fail('查询未完成,请检查数据权限或刷新重试。');}
|
|
86
|
+
finally{selects.forEach(el=>el.disabled=false);}
|
|
87
|
+
}
|
|
88
|
+
try{
|
|
89
|
+
const context=await live.ready();document.body.classList.toggle('dark',context.theme==='dark');
|
|
90
|
+
if(config.bindings.length!==context.bindingIds.length||config.bindings.some(id=>!context.bindingIds.includes(id)))throw Error('看板缺少必要的数据绑定,请重新按完整配方制作。');
|
|
91
|
+
Object.assign(controls,context.defaults);
|
|
92
|
+
for(const id of config.bindings)live.subscribe(id,dataset=>{
|
|
93
|
+
try{
|
|
94
|
+
if(!dataset.completeness||dataset.completeness.truncated)throw Error('查询结果不完整,请缩小范围后重试。');
|
|
95
|
+
for(const row of dataset.rows){for(const field of fields){if(row[field]===null||row[field]===undefined){if(field==='Sales'||field==='Profit'||field==='Quantity'||field==='ReturnedSales'){if(Number(row.Orders)===0)continue;}throw Error('必要指标为空,无法展示完整结果。');}number(row[field]);}}
|
|
96
|
+
failure=false;datasets.set(id,dataset);render();
|
|
97
|
+
}catch(error){fail(error.message||'查询结果不可用。');}
|
|
98
|
+
});
|
|
99
|
+
for(const select of selects){
|
|
100
|
+
const key=select.dataset.dashboardControl;
|
|
101
|
+
for(const [value,label]of config.options[key])select.add(new Option(label,JSON.stringify(value)));
|
|
102
|
+
select.value=JSON.stringify(controls[key]);
|
|
103
|
+
enhanceDashboardSelect(select);
|
|
104
|
+
select.addEventListener('change',()=>{controls[key]=JSON.parse(select.value);void queryAll();});
|
|
105
|
+
}
|
|
106
|
+
const observer=new ResizeObserver(()=>charts.forEach(chart=>chart.resize()));observer.observe(document.querySelector('.analysis'));
|
|
107
|
+
await queryAll();
|
|
108
|
+
}catch(error){fail(error.message||'看板初始化失败,请刷新重试。');}
|
|
109
|
+
})();
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-Hans">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>Superstore · 商品盈利分析</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body data-page="profitability" data-dashboard-theme="profit-atlas@1">
|
|
9
|
+
<main class="workspace">
|
|
10
|
+
<header class="atlas-header">
|
|
11
|
+
<div>
|
|
12
|
+
<h1>商品盈利分析</h1>
|
|
13
|
+
<p class="description">看清销售与利润之间的差距</p>
|
|
14
|
+
<span class="demo">Superstore 演示数据 · 2021–2024</span>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="filters" aria-label="分析筛选">
|
|
17
|
+
<div class="filter-field">
|
|
18
|
+
<label for="filter-year">年份</label>
|
|
19
|
+
<select id="filter-year" data-dashboard-control="year" disabled>
|
|
20
|
+
</select>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="filter-field">
|
|
23
|
+
<label for="filter-region">区域</label>
|
|
24
|
+
<select id="filter-region" data-dashboard-control="region" disabled>
|
|
25
|
+
</select>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="filter-field">
|
|
28
|
+
<label for="filter-category">品类</label>
|
|
29
|
+
<select id="filter-category" data-dashboard-control="category" disabled>
|
|
30
|
+
</select>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</header>
|
|
34
|
+
<section class="atlas-metrics" aria-label="当前筛选主要指标">
|
|
35
|
+
<div class="metric">
|
|
36
|
+
<p>销售额(USD)</p>
|
|
37
|
+
<output id="metric-0">—</output>
|
|
38
|
+
<small>订单行销售额合计</small>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="metric">
|
|
41
|
+
<p>利润(USD)</p>
|
|
42
|
+
<output id="metric-1">—</output>
|
|
43
|
+
<small>订单行利润合计</small>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="metric">
|
|
46
|
+
<p>利润率</p>
|
|
47
|
+
<output id="metric-2">—</output>
|
|
48
|
+
<small>利润 ÷ 销售额</small>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="metric">
|
|
51
|
+
<p>订单数</p>
|
|
52
|
+
<output id="metric-3">—</output>
|
|
53
|
+
<small>Order ID 去重</small>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="metric">
|
|
56
|
+
<p>客户数</p>
|
|
57
|
+
<output id="metric-4">—</output>
|
|
58
|
+
<small>Customer ID 去重</small>
|
|
59
|
+
</div>
|
|
60
|
+
</section>
|
|
61
|
+
<div class="analysis">
|
|
62
|
+
<div class="atlas-plots">
|
|
63
|
+
<section class="panel">
|
|
64
|
+
<h2>销售额与利润关系</h2>
|
|
65
|
+
<p class="note">每个点代表一个子品类,悬停查看金额</p>
|
|
66
|
+
<div id="chart-secondary" class="chart" role="img" aria-label="销售额与利润关系">
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
<section class="panel">
|
|
70
|
+
<h2>子品类利润排名</h2>
|
|
71
|
+
<p class="note">USD · 红色表示负利润</p>
|
|
72
|
+
<div id="chart-primary" class="chart" role="img" aria-label="子品类利润排序">
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="full-details">
|
|
77
|
+
<section class="details">
|
|
78
|
+
<h2>子品类盈利明细</h2>
|
|
79
|
+
<p class="note">按利润升序 · 完整明细可滚动查看</p>
|
|
80
|
+
<div class="table-scroll" tabindex="0" role="region" aria-label="子品类盈利明细">
|
|
81
|
+
<table>
|
|
82
|
+
<thead>
|
|
83
|
+
<tr>
|
|
84
|
+
<th scope="col">子品类</th>
|
|
85
|
+
<th scope="col">销售额(USD)</th>
|
|
86
|
+
<th scope="col">利润(USD)</th>
|
|
87
|
+
<th scope="col">利润率</th>
|
|
88
|
+
<th scope="col">订单数</th>
|
|
89
|
+
<th scope="col">客户数</th>
|
|
90
|
+
</tr>
|
|
91
|
+
</thead>
|
|
92
|
+
<tbody id="details-rows">
|
|
93
|
+
</tbody>
|
|
94
|
+
</table>
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
</div>
|
|
98
|
+
<p id="status" class="status" role="status">正在加载…</p>
|
|
99
|
+
<details class="methodology">
|
|
100
|
+
<summary>数据与指标口径</summary>
|
|
101
|
+
<p>来源:Tableau Superstore 虚构教学工作簿,2021–2024 年,金额单位 USD。退货表先按订单去重,再关联订单行。退货关联销售额不等于退款或收入损失。利润率为合计利润除以合计销售额;订单数与客户数由后端在当前范围内去重,不能累加分组计数。各看板筛选状态独立。</p>
|
|
102
|
+
</details>
|
|
103
|
+
</div>
|
|
104
|
+
<footer>
|
|
105
|
+
<span>2021–2024 · 演示数据 · Data Xpert</span>
|
|
106
|
+
<span id="freshness">尚无当前查询结果</span>
|
|
107
|
+
</footer>
|
|
108
|
+
</main>
|
|
109
|
+
<script>const config={
|
|
110
|
+
"key": "profitability",
|
|
111
|
+
"title": "商品盈利分析",
|
|
112
|
+
"description": "找到贡献利润的商品,以及高销售背后的亏损",
|
|
113
|
+
"bindings": [
|
|
114
|
+
"summary",
|
|
115
|
+
"subcategories"
|
|
116
|
+
],
|
|
117
|
+
"panels": [
|
|
118
|
+
"子品类利润排序",
|
|
119
|
+
"销售额与利润关系"
|
|
120
|
+
],
|
|
121
|
+
"notes": [
|
|
122
|
+
"USD · 红色表示负利润",
|
|
123
|
+
"每个点代表一个子品类,悬停查看金额"
|
|
124
|
+
],
|
|
125
|
+
"dimension": "子品类",
|
|
126
|
+
"table": "子品类盈利明细",
|
|
127
|
+
"options": {
|
|
128
|
+
"year": [
|
|
129
|
+
[
|
|
130
|
+
"",
|
|
131
|
+
"全部年份"
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
"2021",
|
|
135
|
+
"2021"
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
"2022",
|
|
139
|
+
"2022"
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"2023",
|
|
143
|
+
"2023"
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
"2024",
|
|
147
|
+
"2024"
|
|
148
|
+
]
|
|
149
|
+
],
|
|
150
|
+
"region": [
|
|
151
|
+
[
|
|
152
|
+
"",
|
|
153
|
+
"全部区域"
|
|
154
|
+
],
|
|
155
|
+
[
|
|
156
|
+
"Central",
|
|
157
|
+
"Central"
|
|
158
|
+
],
|
|
159
|
+
[
|
|
160
|
+
"East",
|
|
161
|
+
"East"
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
"South",
|
|
165
|
+
"South"
|
|
166
|
+
],
|
|
167
|
+
[
|
|
168
|
+
"West",
|
|
169
|
+
"West"
|
|
170
|
+
]
|
|
171
|
+
],
|
|
172
|
+
"category": [
|
|
173
|
+
[
|
|
174
|
+
"",
|
|
175
|
+
"全部品类"
|
|
176
|
+
],
|
|
177
|
+
[
|
|
178
|
+
"Furniture",
|
|
179
|
+
"Furniture"
|
|
180
|
+
],
|
|
181
|
+
[
|
|
182
|
+
"Office Supplies",
|
|
183
|
+
"Office Supplies"
|
|
184
|
+
],
|
|
185
|
+
[
|
|
186
|
+
"Technology",
|
|
187
|
+
"Technology"
|
|
188
|
+
]
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
"measures": [
|
|
192
|
+
"Sales",
|
|
193
|
+
"Profit",
|
|
194
|
+
"Orders",
|
|
195
|
+
"Customers"
|
|
196
|
+
]
|
|
197
|
+
};
|
|
198
|
+
// No rows or backend endpoints belong here. All panels use governed query results.
|
|
199
|
+
(async () => {
|
|
200
|
+
const live = window.xpertLiveArtifact;
|
|
201
|
+
const {enhanceSelect:enhanceDashboardSelect,charts:DashboardCharts}=window.ProfitAtlas;
|
|
202
|
+
const status = document.getElementById('status');
|
|
203
|
+
const selects = [...document.querySelectorAll('select[data-dashboard-control]')];
|
|
204
|
+
const datasets = new Map();
|
|
205
|
+
const chartIds = ['chart-primary', 'chart-secondary', ...(config.key === 'overview' ? ['chart-profit'] : [])];
|
|
206
|
+
const charts = chartIds.map(id => window.echarts.init(document.getElementById(id), null, {renderer:'svg'}));
|
|
207
|
+
const controls = {year:'',region:'',category:''};
|
|
208
|
+
const integer = new Intl.NumberFormat('en-US',{maximumFractionDigits:0});
|
|
209
|
+
const decimal = new Intl.NumberFormat('en-US',{minimumFractionDigits:2,maximumFractionDigits:2});
|
|
210
|
+
const fields = config.measures;
|
|
211
|
+
let failure = false;
|
|
212
|
+
function number(value) {
|
|
213
|
+
if ((typeof value !== 'number' && typeof value !== 'string') || String(value).trim() === '') throw Error('查询结果缺少必要指标。');
|
|
214
|
+
const n=Number(value);if(!Number.isFinite(n)||Math.abs(n)>Number.MAX_SAFE_INTEGER)throw Error('数值超出安全展示范围。');return n;
|
|
215
|
+
}
|
|
216
|
+
function precise(value) {
|
|
217
|
+
// Keep decimal strings exact until two-place display rounding; Number is only for ratios/plots.
|
|
218
|
+
const raw=String(value);if(!/^-?\d+(\.\d+)?$/.test(raw))throw Error('金额格式不符合数据契约。');
|
|
219
|
+
const negative=raw.startsWith('-');const [whole,tail='']=raw.replace('-','').split('.');
|
|
220
|
+
let cents=BigInt(whole)*100n+BigInt((tail+'00').slice(0,2));if(Number(tail[2]??0)>=5)cents+=1n;
|
|
221
|
+
return (negative&¢s?'-':'')+(cents/100n).toLocaleString('en-US')+'.'+String(cents%100n).padStart(2,'0');
|
|
222
|
+
}
|
|
223
|
+
function ratio(a,b){return b?decimal.format(a/b*100)+'%':'—';}
|
|
224
|
+
function clear(message,isError=false){
|
|
225
|
+
document.querySelectorAll('output').forEach(el=>el.textContent='—');
|
|
226
|
+
document.getElementById('details-rows').replaceChildren();charts.forEach(chart=>{ chart.clear(); chart.getDom().removeAttribute('aria-label'); });
|
|
227
|
+
status.textContent=message;status.classList.toggle('error',isError);
|
|
228
|
+
document.getElementById('freshness').textContent='尚无当前查询结果';
|
|
229
|
+
}
|
|
230
|
+
function fail(message){failure=true;datasets.clear();clear(message,true);}
|
|
231
|
+
function table(rows,dimension){
|
|
232
|
+
const max=Math.max(1,...rows.map(row=>number(row.Sales)));
|
|
233
|
+
document.getElementById('details-rows').replaceChildren(...rows.map(row=>{
|
|
234
|
+
const tr=document.createElement('tr');
|
|
235
|
+
const values=config.key==='customers'
|
|
236
|
+
?[row[dimension],precise(row.Sales),integer.format(number(row.Orders)),integer.format(number(row.Customers)),integer.format(number(row.ReturnedOrders)),ratio(number(row.ReturnedOrders),number(row.Orders)),precise(row.ReturnedSales)]
|
|
237
|
+
:[row[dimension],precise(row.Sales),precise(row.Profit),ratio(number(row.Profit),number(row.Sales)),integer.format(number(row.Orders)),integer.format(number(row.Customers))];
|
|
238
|
+
if(config.key==='overview')values.splice(4);
|
|
239
|
+
for(const [index,value]of values.entries()){
|
|
240
|
+
const td=document.createElement('td');td.textContent=String(value);
|
|
241
|
+
if(config.key!=='customers'&&(index===2||index===3)&&number(row.Profit)<0)td.className='negative';
|
|
242
|
+
if(index===1){const bar=document.createElement('span');bar.className='table-bar';bar.style.width=(Math.max(0,number(row.Sales))/max*100)+'%';bar.setAttribute('aria-hidden','true');td.append(bar);}
|
|
243
|
+
tr.append(td);
|
|
244
|
+
}return tr;
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
function render(){
|
|
248
|
+
if(failure||config.bindings.some(id=>!datasets.has(id)))return;
|
|
249
|
+
const summary=datasets.get('summary').rows;
|
|
250
|
+
if(!summary.length||number(summary[0].Orders)===0){clear('当前筛选下没有订单,请调整年份、区域或品类。');return;}
|
|
251
|
+
if(summary.length!==1)throw Error('总计必须由一个后端聚合行提供。');
|
|
252
|
+
const s=summary[0];const sales=number(s.Sales),profit=config.key==='customers'?0:number(s.Profit),orders=number(s.Orders),customers=number(s.Customers),returned=config.key==='customers'?number(s.ReturnedOrders):0;
|
|
253
|
+
const values=config.key==='customers'
|
|
254
|
+
?[integer.format(customers),integer.format(orders),orders?decimal.format(sales/orders):'—',integer.format(returned),ratio(returned,orders)]
|
|
255
|
+
:[precise(s.Sales),precise(s.Profit),ratio(profit,sales),integer.format(orders),integer.format(customers)];
|
|
256
|
+
values.forEach((value,i)=>{document.getElementById('metric-'+i).textContent=value;});
|
|
257
|
+
const chartsUI = DashboardCharts;
|
|
258
|
+
const ranked = (rows, field) => [...rows].sort((a,b) => number(b[field]) - number(a[field]));
|
|
259
|
+
if(config.key==='overview'){
|
|
260
|
+
const trend=[...datasets.get('trend').rows].sort((a,b)=>a.OrderMonth.localeCompare(b.OrderMonth));
|
|
261
|
+
chartsUI.line(charts[0],trend,{dimension:'OrderMonth',measure:'Sales',label:'销售额(USD)'});
|
|
262
|
+
chartsUI.line(charts[2],trend,{dimension:'OrderMonth',measure:'Profit',label:'利润(USD)',color:'teal'});
|
|
263
|
+
chartsUI.horizontal(charts[1],ranked(datasets.get('regions').rows,'Sales'),{dimension:'Region',measure:'Sales',label:'销售额(USD)'});
|
|
264
|
+
table(ranked(datasets.get('categories').rows,'Sales'),'Category');
|
|
265
|
+
}else if(config.key==='profitability'){
|
|
266
|
+
const rows=ranked(datasets.get('subcategories').rows,'Profit');
|
|
267
|
+
chartsUI.horizontal(charts[0],rows,{dimension:'SubCategory',measure:'Profit',label:'利润(USD)',color:'teal'});
|
|
268
|
+
chartsUI.scatter(charts[1],rows,{dimension:'SubCategory',x:'Sales',y:'Profit',xLabel:'销售额(USD)',yLabel:'利润(USD)'});
|
|
269
|
+
table([...rows].reverse(),'SubCategory');
|
|
270
|
+
}else{
|
|
271
|
+
chartsUI.horizontal(charts[0],ranked(datasets.get('segments').rows,'Sales'),{dimension:'Segment',measure:'Sales',label:'销售额(USD)'});
|
|
272
|
+
chartsUI.horizontal(charts[1],ranked(datasets.get('regions').rows,'ReturnedOrders'),{dimension:'Region',measure:'ReturnedOrders',label:'退货订单数',decimals:0});
|
|
273
|
+
table(datasets.get('segments').rows,'Segment');
|
|
274
|
+
}
|
|
275
|
+
status.textContent='所有指标与图表已按当前筛选更新。';status.classList.remove('error');
|
|
276
|
+
const times=config.bindings.map(id=>datasets.get(id).freshness?.sourceFetchedAt).filter(Boolean).sort();
|
|
277
|
+
document.getElementById('freshness').textContent=times.length?'查询时间:'+new Date(times[0]).toLocaleString('zh-CN'):'查询已完成(来源未提供时间)';
|
|
278
|
+
}
|
|
279
|
+
async function queryAll(){
|
|
280
|
+
failure=false;datasets.clear();clear('正在查询当前筛选…');selects.forEach(el=>el.disabled=true);
|
|
281
|
+
try{const results=await Promise.allSettled(config.bindings.map(id=>live.query(id,{...controls})));if(results.some(r=>r.status==='rejected'))throw Error('查询失败');}
|
|
282
|
+
catch{fail('查询未完成,请检查数据权限或刷新重试。');}
|
|
283
|
+
finally{selects.forEach(el=>el.disabled=false);}
|
|
284
|
+
}
|
|
285
|
+
try{
|
|
286
|
+
const context=await live.ready();document.body.classList.toggle('dark',context.theme==='dark');
|
|
287
|
+
if(config.bindings.length!==context.bindingIds.length||config.bindings.some(id=>!context.bindingIds.includes(id)))throw Error('看板缺少必要的数据绑定,请重新按完整配方制作。');
|
|
288
|
+
Object.assign(controls,context.defaults);
|
|
289
|
+
for(const id of config.bindings)live.subscribe(id,dataset=>{
|
|
290
|
+
try{
|
|
291
|
+
if(!dataset.completeness||dataset.completeness.truncated)throw Error('查询结果不完整,请缩小范围后重试。');
|
|
292
|
+
for(const row of dataset.rows){for(const field of fields){if(row[field]===null||row[field]===undefined){if(field==='Sales'||field==='Profit'||field==='Quantity'||field==='ReturnedSales'){if(Number(row.Orders)===0)continue;}throw Error('必要指标为空,无法展示完整结果。');}number(row[field]);}}
|
|
293
|
+
failure=false;datasets.set(id,dataset);render();
|
|
294
|
+
}catch(error){fail(error.message||'查询结果不可用。');}
|
|
295
|
+
});
|
|
296
|
+
for(const select of selects){
|
|
297
|
+
const key=select.dataset.dashboardControl;
|
|
298
|
+
for(const [value,label]of config.options[key])select.add(new Option(label,JSON.stringify(value)));
|
|
299
|
+
select.value=JSON.stringify(controls[key]);
|
|
300
|
+
enhanceDashboardSelect(select);
|
|
301
|
+
select.addEventListener('change',()=>{controls[key]=JSON.parse(select.value);void queryAll();});
|
|
302
|
+
}
|
|
303
|
+
const observer=new ResizeObserver(()=>charts.forEach(chart=>chart.resize()));observer.observe(document.querySelector('.analysis'));
|
|
304
|
+
await queryAll();
|
|
305
|
+
}catch(error){fail(error.message||'看板初始化失败,请刷新重试。');}
|
|
306
|
+
})();
|
|
307
|
+
</script></body></html>
|
|
Binary file
|