feat: 热点穿透题材节点按涨幅正蓝负绿、球大小随涨幅绝对值
- 后端题材列表下发 bf3(题材涨幅) - 题材节点: 正涨幅蓝色/负涨幅绿色, 半径按 |bf3| 在 6~20 映射 - 题材标签文字颜色跟随正负 - 信息卡/底部浮层题材分支显示涨幅 - 图例拆分涨/跌两项并说明球大小含义 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -370,7 +370,7 @@ async def _build_theme_graph(sort_field: int, top_n: int) -> dict:
|
||||
|
||||
return {
|
||||
"themes": [
|
||||
{"themeCode": t["themeCode"], "themeName": t["themeName"], "stockCount": t["stockCount"]}
|
||||
{"themeCode": t["themeCode"], "themeName": t["themeName"], "stockCount": t["stockCount"], "bf3": t.get("bf3")}
|
||||
for t in theme_map.values()
|
||||
],
|
||||
"stocks": stocks,
|
||||
|
||||
@@ -165,6 +165,7 @@ export interface GraphTheme {
|
||||
themeCode: string;
|
||||
themeName: string;
|
||||
stockCount: number; // 题材内股票数
|
||||
bf3: number | null; // 题材涨幅
|
||||
}
|
||||
|
||||
export interface GraphStock {
|
||||
|
||||
+47
-14
@@ -35,6 +35,10 @@ const MODES: { key: 1 | 4; label: string }[] = [
|
||||
/* 图视图股票节点上限:按覆盖题材数降序保留最高穿透度的核心股 */
|
||||
const MAX_STOCK_NODES = 800;
|
||||
|
||||
/* 题材节点半径范围:按题材涨幅绝对值映射(涨得越猛球越大) */
|
||||
const THEME_MIN_RADIUS = 6;
|
||||
const THEME_MAX_RADIUS = 20;
|
||||
|
||||
/* 视图切换:关系图 / 核心股列表 */
|
||||
type ViewMode = "graph" | "list";
|
||||
|
||||
@@ -272,6 +276,8 @@ interface SimNode extends SimulationNodeDatum {
|
||||
f100?: string;
|
||||
themeCodes?: string[];
|
||||
code?: string;
|
||||
// theme 专属
|
||||
bf3?: number | null; // 题材涨幅
|
||||
}
|
||||
|
||||
interface SimEdge extends SimulationLinkDatum<SimNode> {
|
||||
@@ -404,7 +410,7 @@ function drawStockNodes(
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
/** 题材节点:蓝色圆点 */
|
||||
/** 题材节点:涨幅正蓝负绿,大小按涨幅绝对值映射 */
|
||||
function drawThemeNodes(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
nodes: SimNode[],
|
||||
@@ -416,12 +422,13 @@ function drawThemeNodes(
|
||||
const isActive = activeId === n.id;
|
||||
const dim = activeSet ? !activeSet.has(n.id) : false;
|
||||
const r = isActive ? n.radius + 3 : n.radius;
|
||||
const isPos = (n.bf3 ?? 0) >= 0;
|
||||
ctx.beginPath();
|
||||
ctx.arc(n.x ?? 0, n.y ?? 0, r, 0, TAU);
|
||||
ctx.fillStyle = isActive ? "#2563eb" : "#3b82f6";
|
||||
ctx.fillStyle = isActive ? (isPos ? "#2563eb" : "#16a34a") : isPos ? "#3b82f6" : "#22c55e";
|
||||
ctx.globalAlpha = dim ? 0.15 : 1;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "#1d4ed8";
|
||||
ctx.strokeStyle = isPos ? "#1d4ed8" : "#15803d";
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
@@ -460,7 +467,8 @@ function drawThemeLabels(
|
||||
if (n.type !== "theme") continue;
|
||||
if (!showAll && activeId !== n.id) continue;
|
||||
const label = isCoarse ? (n.name.length > 5 ? n.name.slice(0, 5) + "…" : n.name) : n.name;
|
||||
haloText(ctx, label, n.x ?? 0, (n.y ?? 0) + n.radius + 11, isCoarse ? 7.5 : 9, "#1e40af");
|
||||
const labelColor = (n.bf3 ?? 0) >= 0 ? "#1e40af" : "#15803d";
|
||||
haloText(ctx, label, n.x ?? 0, (n.y ?? 0) + n.radius + 11, isCoarse ? 7.5 : 9, labelColor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,14 +662,20 @@ function HotMapGraph({ graph }: { graph: ThemeGraph }) {
|
||||
themeCodes: s.themeCodes,
|
||||
radius: Math.min(18, 4 + s.coverCount * 1.3),
|
||||
}));
|
||||
const themeNodes: SimNode[] = graph.themes.map((t) => ({
|
||||
id: `t:${t.themeCode}`,
|
||||
type: "theme" as const,
|
||||
name: t.themeName,
|
||||
code: t.themeCode,
|
||||
coverCount: t.stockCount,
|
||||
radius: 10,
|
||||
}));
|
||||
// 题材涨幅绝对值作为球大小的归一化基准(兜底 ≥1 防除零)
|
||||
const maxAbsBf3 = Math.max(1, ...graph.themes.map((t) => Math.abs(t.bf3 ?? 0)));
|
||||
const themeNodes: SimNode[] = graph.themes.map((t) => {
|
||||
const bf3 = t.bf3 ?? 0;
|
||||
return {
|
||||
id: `t:${t.themeCode}`,
|
||||
type: "theme" as const,
|
||||
name: t.themeName,
|
||||
code: t.themeCode,
|
||||
coverCount: t.stockCount,
|
||||
bf3: t.bf3,
|
||||
radius: THEME_MIN_RADIUS + (Math.abs(bf3) / maxAbsBf3) * (THEME_MAX_RADIUS - THEME_MIN_RADIUS),
|
||||
};
|
||||
});
|
||||
const edgeList: SimEdge[] = buildEdgesFromStocks(kept);
|
||||
|
||||
const stockByIdMap = new Map<string, GraphStock>();
|
||||
@@ -1007,6 +1021,11 @@ function InfoCard({ node, stockById }: { node: SimNode; stockById: Map<string, G
|
||||
) : (
|
||||
<>
|
||||
<p className="font-semibold">{node.name}</p>
|
||||
{node.bf3 != null && (
|
||||
<p className={`text-xs font-bold ${node.bf3 >= 0 ? "text-blue-600" : "text-green-600"}`}>
|
||||
{node.bf3 >= 0 ? "+" : ""}{node.bf3.toFixed(2)}%
|
||||
</p>
|
||||
)}
|
||||
<p className="text-[10px] text-muted-foreground">代码 {node.code} · {node.coverCount} 只股票</p>
|
||||
</>
|
||||
)}
|
||||
@@ -1065,6 +1084,11 @@ function BottomSheet({
|
||||
) : (
|
||||
<>
|
||||
<p className="font-semibold text-sm">{node.name}</p>
|
||||
{node.bf3 != null && (
|
||||
<p className={`text-xs font-bold mt-0.5 ${node.bf3 >= 0 ? "text-blue-600" : "text-green-600"}`}>
|
||||
{node.bf3 >= 0 ? "+" : ""}{node.bf3.toFixed(2)}%
|
||||
</p>
|
||||
)}
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||
代码 {node.code} · 覆盖 {node.coverCount} 只股票
|
||||
</p>
|
||||
@@ -1111,7 +1135,11 @@ function Legend({ maxCover, compact }: { maxCover: number; compact: boolean }) {
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="inline-block rounded-full bg-blue-500" style={{ width: 8, height: 8 }} />
|
||||
题材
|
||||
题材涨
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="inline-block rounded-full bg-green-500" style={{ width: 8, height: 8 }} />
|
||||
题材跌
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -1131,8 +1159,13 @@ function Legend({ maxCover, compact }: { maxCover: number; compact: boolean }) {
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pt-1 border-t border-border/40">
|
||||
<span className="inline-block rounded-full bg-blue-500" style={{ width: 10, height: 10 }} />
|
||||
<span>题材节点</span>
|
||||
<span>题材 · 涨幅为正</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block rounded-full bg-green-500" style={{ width: 10, height: 10 }} />
|
||||
<span>题材 · 涨幅为负</span>
|
||||
</div>
|
||||
<div className="text-[9px] text-muted-foreground pt-0.5">球大小随涨幅绝对值增大</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user