Last active
September 18, 2024 13:11
-
-
Save no-today/60bae910f3cee67e403a4af79ad72a9e to your computer and use it in GitHub Desktop.
Sankey diagram & Swift UI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- https://observablehq.com/d/4c6e7c61c31e12f2 --> | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<script src="https://d3js.org/d3.v7.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/d3-sankey.min.js"></script> | |
<style> | |
html body { | |
margin: 0; | |
padding: 0; | |
height: 100%; | |
} | |
</style> | |
</head> | |
<body> | |
<!--<div id="log"></div>--> | |
<div id="sankey"></div> | |
<script> | |
function drawChart(config, nodes, links) { | |
drawSankeyDiagram(config, preprocessingData(nodes, links)) | |
} | |
function preprocessingData(nodes, links) { | |
const mapping = nodes.reduce((acc, item) => { | |
acc.set(item.name, item) | |
return acc; | |
}, new Map()); | |
// 总资产 | |
const totalAssets = links.filter(e => mapping.get(e.source).tag === 1) | |
.reduce((acc, cur) => acc + cur.value, 0) | |
// 总负债 | |
const totalDebt = links.filter(e => mapping.get(e.source).tag === 3) | |
.reduce((acc, cur) => acc + cur.value, 0); | |
// 总资产节点名称 | |
const assetsNodeName = nodes.filter(e => e.tag === 1)[0].name | |
// 净资产节点名称 | |
const netAssetsNodeName = nodes.filter(e => e.tag === 2)[0].name | |
links.push({source: netAssetsNodeName, target: assetsNodeName, value: totalAssets - totalDebt}) | |
// 每个节点所占百分比,都是以总资产为基准 | |
const targetTotals = {}; | |
links.forEach(link => { | |
if (!targetTotals[link.target]) { | |
targetTotals[link.target] = 0; | |
} | |
targetTotals[link.target] += link.value; | |
}); | |
links.forEach(link => { | |
const key = link.target === assetsNodeName ? link.source : link.target; | |
const entry = mapping.get(key); | |
if (entry) { | |
entry.percentage = (link.value / targetTotals[assetsNodeName] * 100).toFixed(2); | |
} else { | |
console.error(`No mapping found for key: ${key}`); | |
} | |
}); | |
return {nodes, links, totalAssets, totalDebt}; | |
} | |
/** | |
* 格式化金额 | |
* @param {number} value - 数值 | |
* @param {boolean} isPositive - 是否为正数 | |
* @param {string} mode - 展示模式 | |
* @returns {string} - 格式化后的金额字符串 | |
*/ | |
function formatAmount(value, isPositive, percentage, mode) { | |
if (mode === 0 || mode === 3) { | |
return ""; | |
} | |
if (mode === 2) { | |
return `${percentage}%`; | |
} | |
// 判断符号,根据 isPositive 参数决定是正号还是负号 | |
let sign = isPositive ? '' : '-'; | |
// 判断数值是否大于等于 10000 | |
if (value >= 10000) { | |
// 将数值除以 10000 转换为以 '万' 为单位的数值 | |
let formattedValue = (value / 10000).toFixed(2); // 保留两位小数 | |
// 使用正则表达式去掉末尾多余的 '.00' 或 '0' | |
formattedValue = formattedValue.replace(/\\.00$|0$/, ''); | |
// 返回带符号和 '万' 单位的格式化字符串 | |
return `${sign}${formattedValue}万`; | |
} else { | |
// 数值小于 10000,直接展示 | |
return `${sign}${value}`; | |
} | |
} | |
// https://github.com/observablehq/stdlib/blob/main/src/dom/uid.js | |
// DOM.uid() can only be used in an observable environment | |
var count = 0; | |
function uid(name) { | |
return new Id("O-" + (name == null ? "" : name + "-") + ++count); | |
} | |
function Id(id) { | |
this.id = id; | |
this.href = new URL(`#${id}`, location) + ""; | |
} | |
Id.prototype.toString = function () { | |
return "url(" + this.href + ")"; | |
}; | |
function drawSankeyDiagram(config, data) { | |
// Create a SVG container. | |
const svg = d3.select('#sankey').append("svg") | |
.attr("viewBox", [0, 0, config.width, config.height]) | |
.attr("style", `max-width: 100%; height: auto; font: ${config.fontSize}px sans-serif;`); | |
// Constructs and configures a Sankey generator. | |
// .extent([[x0, y0], [x1, y1]]) | |
const sankey = d3.sankey() | |
.nodeId(d => d.name) | |
.nodeAlign(d3[config.nodeAlign]) // d3.sankeyLeft, etc. | |
.nodeWidth(config.nodeWidth) | |
.nodePadding(config.nodePadding) | |
.extent([[config.extents[0], config.extents[1]], [config.width - config.extents[2], config.height - config.extents[3]]]); | |
// Applies it to the data. We make a copy of the nodes and links objects | |
// so as to avoid mutating the original. | |
const {nodes, links} = sankey({ | |
nodes: data.nodes.map(d => Object.assign({}, d)), | |
links: data.links.map(d => Object.assign({}, d)) | |
}); | |
// 总资产 (目标节点 | |
const totalAssetsNode = nodes.filter(e => e.tag === 1)[0] | |
// 净资产 (起始节点 | |
const netAssetsNode = nodes.filter(e => e.tag === 2)[0] | |
// Creates the rects that represent the nodes. | |
const rect = svg.append("g") | |
.attr("stroke", 'transparent') | |
.selectAll() | |
.data(nodes) | |
.join("rect") | |
.attr("x", d => d.tag === 1 ? d.x0 - config.boldWidth - config.nodeWidth - config.dividerWidth : d.x0) | |
.attr("y", d => d.y0) | |
.attr("height", d => d.y1 - d.y0 - (d.tag === 1 ? netAssetsNode.y1 - netAssetsNode.y0 : 0)) // 净资产不是负债的一部分 | |
.attr("width", d => d.x1 - d.x0 + (d.tag === 1 ? config.boldWidth : 0)) | |
.attr("fill", d => d.color); | |
// 补偿总资产矩形 | |
svg.append("rect") | |
.attr("x", totalAssetsNode.x0 + config.nodeWidth) // 矩形的 x 坐标 | |
.attr("y", totalAssetsNode.y0) // 矩形的 y 坐标 | |
.attr("width", config.nodeWidth + config.boldWidth) // 矩形的宽度 | |
.attr("height", totalAssetsNode.y1 - totalAssetsNode.y0) // 矩形的高度 | |
.attr("fill", config.totalAssetsRectColor ? config.totalAssetsRectColor : config.totalAssetsTextColor) // 矩形的填充颜色 | |
// Creates the paths that represent the links. | |
const link = svg.append("g") | |
.attr("fill", "none") | |
.attr("stroke-opacity", 0.5) | |
.selectAll() | |
.data(links) | |
.join("g") | |
.style("mix-blend-mode", "multiply"); | |
// Creates a gradient, if necessary, for the source-target color option. | |
if (config.linkColor === "source-target") { | |
const gradient = link.append("linearGradient") | |
.attr("id", d => (d.uid = uid("link")).id) | |
.attr("gradientUnits", "userSpaceOnUse") | |
.attr("x1", d => d.source.x1) | |
.attr("x2", d => d.target.x0); | |
gradient.append("stop") | |
.attr("offset", "0%") | |
.attr("stop-color", d => d.source.color); | |
gradient.append("stop") | |
.attr("offset", "100%") | |
.attr("stop-color", d => d.target.color); | |
} | |
link.append("path") | |
.attr("d", d => { | |
// 总资产微调 | |
if (d.target.tag === 1) { | |
// 获取默认的路径字符串 | |
const defaultPath = d3.sankeyLinkHorizontal()(d); | |
// 解析路径字符串 | |
const pathCommands = defaultPath.split(/(?=[A-Z])/); // 按大写字母分割路径命令 | |
// 对路径进行微调,例如将起始点的 x 坐标平移 | |
const adjustedPathCommands = pathCommands.map(command => { | |
if (command.startsWith("M")) { // 起始点命令 | |
// const values = command.slice(1).split(",").map(Number); | |
// values[0] += config.dividerWidth; // 调整起始点的 x 坐标 | |
// return `M${values.join(",")}`; | |
return command; | |
} else if (command.startsWith("C")) { // 贝塞尔曲线控制点命令 | |
const values = command.slice(1).split(",").map(Number); | |
// values[0] += config.dividerWidth; // 调整第一个控制点的 x 坐标 | |
// values[2] += config.dividerWidth; // 调整第二个控制点的 x 坐标 | |
// 保持第一个控制点和起始点不变,调整第二个控制点和终点的 x 坐标 | |
values[2] -= config.dividerWidth; // 调整第二个控制点的 x 坐标,向左移动 | |
values[4] -= config.dividerWidth; // 调整终点的 x 坐标,向左移动 | |
return `C${values.join(",")}`; | |
} | |
return command; | |
}); | |
// 生成调整后的路径字符串 | |
return adjustedPathCommands.join(""); | |
} else { | |
return d3.sankeyLinkHorizontal()(d); | |
} | |
}) | |
.attr("stroke", config.linkColor === "source-target" ? (d) => d.uid | |
: config.linkColor === "source" ? (d) => d.source.color | |
: config.linkColor === "target" ? (d) => d.target.color | |
: config.linkColor) | |
.attr("stroke-width", d => Math.max(0.5, d.width)); | |
// Adds labels on the nodes. | |
svg.append("g") | |
.selectAll() | |
.data(nodes) | |
.join("text") | |
.attr("x", d => config.titleMode === 2 && !d.tag ? d.x1 + config.titlePadding : d.x0 - config.titlePadding) | |
.attr("y", d => (d.y1 + d.y0) / 2) | |
.attr("dy", "0.35em") | |
.attr("text-anchor", d => config.titleMode === 2 && !d.tag ? "start" : "end") | |
.text(d => d.tag === 1 ? '' : `${d.name} ${formatAmount(d.value, d.tag !== 3, d.percentage, config.displayMode)}`) | |
// ------------------------------------------------------------------------ | |
const rectHeightCompensate = () => { | |
return config.displayMode === 1 ? config.assetsRectHeight : config.assetsRectHeight * 0.5 | |
} | |
const rectWidthCompensate = () => { | |
return config.displayMode === 1 ? config.assetsRectWidth : config.assetsRectWidth * 0.7 | |
} | |
const rectTextXCompensate = () => { | |
return config.displayMode > 1 ? 0 : config.assetsRectTextX | |
} | |
// 负债 | |
const totalDebtRect = svg.append("rect") | |
.attr("x", totalAssetsNode.x0 - config.dividerWidth - rectWidthCompensate()) // 矩形的 x 坐标 | |
.attr("y", totalAssetsNode.y0 - config.assetsRectPadding - rectHeightCompensate()) // 矩形的 y 坐标 | |
.attr("width", rectWidthCompensate()) // 矩形的宽度 | |
.attr("height", rectHeightCompensate()) // 矩形的高度 | |
.attr("fill", "rgba(128, 128, 128, 0.2)") // 矩形的填充颜色 | |
.attr("rx", config.assetsRectCornerRadius) // 矩形的圆角水平半径 | |
.attr("ry", config.assetsRectCornerRadius) // 矩形的圆角垂直半径 | |
.attr('opacity', config.displayMode === 1 ? 1 : 0) | |
// 矩形内的标题 | |
svg.append("text") | |
.attr("x", Number(totalDebtRect.attr("x")) + Number(totalDebtRect.attr('width')) - rectTextXCompensate()) | |
.attr("y", Number(totalDebtRect.attr("y"))) | |
.attr("dy", "1.25em") // 调整垂直位置 | |
.attr("text-anchor", "end") | |
.text(config.totalDebtText) | |
.attr("fill", config.totalDebtTextColor) | |
.style("font-weight", "bold") | |
.style("font-size", config.assetsRectFontSize) | |
.attr('opacity', config.displayMode !== 0 ? 1 : 0) | |
// 矩形内的数值 | |
svg.append("text") | |
.attr("x", Number(totalDebtRect.attr("x")) + Number(totalDebtRect.attr('width')) - rectTextXCompensate()) // 控制文本位置 | |
.attr("y", Number(totalDebtRect.attr("y"))) | |
.attr("dy", "2.75em") | |
.attr("text-anchor", "end") | |
.text(`${formatAmount(data.totalDebt, true, null, 1)}`) | |
.style("font-size", config.fontSize) | |
.attr('opacity', config.displayMode === 1 ? 1 : 0) | |
// 总资产 | |
const totalAssetsRect = svg.append("rect") | |
.attr("x", Number(totalDebtRect.attr("x")) + Number(totalDebtRect.attr('width')) + config.nodeWidth + config.dividerWidth) | |
.attr("y", totalDebtRect.attr("y")) | |
.attr("width", rectWidthCompensate()) | |
.attr("height", rectHeightCompensate()) | |
.attr("fill", "rgba(128, 128, 128, 0.2)") | |
.attr("rx", config.assetsRectCornerRadius) | |
.attr("ry", config.assetsRectCornerRadius) | |
.attr('opacity', config.displayMode === 1 ? 1 : 0) | |
svg.append("text") | |
.attr("x", Number(totalAssetsRect.attr("x")) + rectTextXCompensate()) | |
.attr("y", Number(totalAssetsRect.attr("y"))) | |
.attr("dy", "1.25em") | |
.attr("text-anchor", "start") | |
.text(config.totalAssetsText) | |
.attr("fill", config.totalAssetsTextColor) | |
.style("font-weight", "bold") | |
.style("font-size", config.assetsRectFontSize) | |
.attr('opacity', config.displayMode !== 0 ? 1 : 0) | |
svg.append("text") | |
.attr("x", Number(totalAssetsRect.attr("x")) + rectTextXCompensate()) | |
.attr("y", Number(totalAssetsRect.attr("y"))) | |
.attr("dy", "2.75em") | |
.attr("text-anchor", "start") | |
.text(`${formatAmount(data.totalAssets, true, null, 1)}`) | |
.style("font-size", config.fontSize) | |
.attr('opacity', config.displayMode === 1 ? 1 : 0) | |
} | |
// ------------------------------------------------------------------------------------------ | |
const cfg = { | |
linkColor: 'source-target', | |
nodeAlign: 'sankeyJustify', | |
displayMode: 3, | |
titleMode: 1, | |
totalAssetsText: "总资产", | |
totalDebtText: "负债", | |
totalAssetsRectColor: "", // 总资产右矩形颜色, 如果为空则复用 totalAssetsTextColor | |
totalAssetsTextColor: "#8F8DB5", | |
totalDebtTextColor: "#990000", | |
width: 700, | |
height: 200, | |
fontSize: 10, | |
nodeWidth: 5, | |
nodePadding: 13, | |
titlePadding: 5, | |
extents: [90, 22, 5, 5], | |
boldWidth: 7, | |
dividerWidth: 2, | |
assetsRectHeight: 33, | |
assetsRectWidth: 55, | |
assetsRectPadding: 8, | |
assetsRectCornerRadius: 5, | |
assetsRectTextX: 5, | |
assetsRectTextDy: 1.25, | |
assetsRectFontSize: 11 | |
} | |
const nodes = [ | |
// 总资产的颜色反直觉, 实际上总资产分为左右两个颜色了, 这个颜色是左侧的, 右侧的是 config.totalAssetsRectColor | |
{name: "总资产", color: "#A4A3AB", tag: 1}, | |
{name: "信用卡", color: "#A8A7B0", tag: 3}, | |
{name: "房屋贷款", color: "#A8A7B0", tag: 3}, | |
{name: "车辆贷款", color: "#A8A7B0", tag: 3}, | |
{name: "个人贷款", color: "#A8A7B0", tag: 3}, | |
{name: "净资产", color: "#81CDB4", tag: 2}, | |
{name: "投资理财", color: "#908DA2"}, | |
{name: "后备隐藏能源", color: "#716C89"}, | |
{name: "固定资产", color: "#839FAD"}, | |
{name: "房产(自住)", color: "#5C8FA1"}, | |
{name: "汽车", color: "#5C8FA1"}, | |
{name: "应收款", color: "#908DA2"}, | |
{name: "借给他人的钱", color: "#716C89"} | |
]; | |
const links = [ | |
{source: "信用卡", target: "总资产", value: 3000}, | |
{source: "房屋贷款", target: "总资产", value: 500000}, | |
{source: "车辆贷款", target: "总资产", value: 40000}, | |
{source: "个人贷款", target: "总资产", value: 30000}, | |
{source: "总资产", target: "固定资产", value: 850000}, | |
{source: "固定资产", target: "房产(自住)", value: 800000}, | |
{source: "固定资产", target: "汽车", value: 50000}, | |
{source: "总资产", target: "投资理财", value: 50000}, | |
{source: "投资理财", target: "后备隐藏能源", value: 50000}, | |
{source: "总资产", target: "应收款", value: 20000}, | |
{source: "应收款", target: "借给他人的钱", value: 20000} | |
]; | |
// ------------------------------------------------------------------------------------------ | |
// const log = (msg) => { | |
// const p = document.createElement('p') | |
// p.textContent = msg | |
// document.querySelector('#log').append(p) | |
// } | |
// 接收 iOS 发过来的参数 | |
try { | |
webkit.messageHandlers.bridge.onMessage = (c, ns, ls) => { | |
// log('Params config: ' + JSON.stringify(c)) | |
// log('Params nodes: ' + JSON.stringify(ns)) | |
// log('Params links: ' + JSON.stringify(ls)) | |
drawChart(c, ns, ls) | |
} | |
} catch (e) { | |
} | |
// 当页面加载完成时发送请求给 iOS | |
window.onload = () => { | |
try { | |
webkit.messageHandlers.bridge.postMessage('Ready') | |
} catch (e) { | |
drawChart(cfg, nodes, links) | |
} | |
} | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SwiftUIView.swift | |
// | |
// | |
// Created by no-today on 2024/6/14. | |
// | |
import SwiftUI | |
import WebKit | |
struct SankeyDiagram: UIViewRepresentable { | |
var config: SankeyConfig | |
var nodes: [SankeyNode] | |
var links: [NodeLink] | |
var isScrollEnabled: Bool | |
init(_ config: SankeyConfig, _ nodes: [SankeyNode], _ links: [NodeLink], _ isScrollEnabled: Bool = true | |
) { | |
self.config = config | |
self.nodes = nodes | |
self.links = links | |
self.isScrollEnabled = isScrollEnabled | |
} | |
// https://gist.github.com/JSerZANP/ea300d419bfafa79e4f8c0af42d8fec6 | |
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler { | |
var webView: WKWebView? | |
var parent: SankeyDiagram | |
init(_ parent: SankeyDiagram) { | |
self.parent = parent | |
} | |
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { | |
self.webView = webView | |
} | |
// Receive message from webview | |
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { | |
print("Received: \(message.body)") | |
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { | |
self.drawSankeyDiagram(self.parent.config, self.parent.nodes, self.parent.links) | |
} | |
} | |
func drawSankeyDiagram(_ config: SankeyConfig, _ nodes: [SankeyNode], _ links: [NodeLink]) { | |
let cfg = toJSONString(config) ?? "" | |
let ns = toJSONString(nodes) ?? "" | |
let ls = toJSONString(links) ?? "" | |
print("SankeyDiagram width: \(config.width), height: \(config.height)") | |
// print("Nodes: \(ns)") | |
// print("Links: \(ls)") | |
self.webView?.evaluateJavaScript("webkit.messageHandlers.bridge.onMessage(\(cfg), \(ns), \(ls))") | |
} | |
func toJSONString<T: Codable>(_ object: T) -> String? { | |
let encoder = JSONEncoder() | |
encoder.outputFormatting = .prettyPrinted | |
do { | |
let jsonData = try encoder.encode(object) | |
return String(data: jsonData, encoding: .utf8) | |
} catch { | |
print("Error encoding object to JSON: \(error)") | |
return nil | |
} | |
} | |
} | |
func makeCoordinator() -> Coordinator { | |
return Coordinator(self) | |
} | |
public func makeUIView(context: Context) -> WKWebView { | |
let coordinator = makeCoordinator() | |
let userContentController = WKUserContentController() | |
userContentController.add(coordinator, name: "bridge") | |
let configuration = WKWebViewConfiguration() | |
configuration.userContentController = userContentController | |
let _wkwebview = WKWebView(frame: .zero, configuration: configuration) | |
_wkwebview.navigationDelegate = coordinator | |
_wkwebview.isOpaque = false | |
_wkwebview.scrollView.showsVerticalScrollIndicator = false | |
_wkwebview.scrollView.showsHorizontalScrollIndicator = false | |
_wkwebview.scrollView.isScrollEnabled = isScrollEnabled | |
_wkwebview.scrollView.contentInset = .zero | |
_wkwebview.scrollView.contentInsetAdjustmentBehavior = .never | |
_wkwebview.scrollView.scrollIndicatorInsets = .zero | |
return _wkwebview | |
} | |
func updateUIView(_ webView: WKWebView, context: Context) { | |
guard let path: String = Bundle.main.path(forResource: "sankey-diagram", ofType: "html") else { return } | |
let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false) | |
webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl) | |
} | |
} | |
#Preview { | |
SankeyDiagram(SankeyConfig(width: 700, extents: [90, 90, 5, 5]), nodes, links) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SankeyNode.swift | |
// | |
// | |
// Created by no-today on 2024/6/17. | |
// | |
struct SankeyNode: Codable { | |
/// Node name | |
var name: String | |
/// Node color, used to control the color of rectangle node | |
var color: String | |
/// Special marks | |
/// 1: Total assets | |
/// 2: Net assets | |
/// 3: Debt | |
var tag: Int? | |
} | |
struct NodeLink: Codable { | |
/// Starting point of data flow, is the node name | |
var source: String | |
/// The end point of the data flow | |
var target: String | |
/// Numerical value | |
var value: Float | |
} | |
struct SankeyConfig: Codable { | |
/// The color used to control the filling between the source node and the target node | |
/// - static: No color, default gray | |
/// - source-target: Creates a gradient for the source-target color option. | |
/// - source: Use source color | |
/// - target: Use target color | |
var linkColor: String = "source" | |
/// Alignment | |
/// - sankeyLeft | |
/// - sankeyRight | |
/// - sankeyCenter | |
/// - sankeyJustify | |
var nodeAlign: String = "sankeyJustify" | |
/// Amount display mode | |
/// 0: Hide amount | |
/// 1: Show amount | |
/// 2: Percentage of display amount | |
var displayMode: DisplayMode = .show | |
/// Asset title location | |
/// 1: Always Left, | |
/// 2: Left and right | |
var titleMode: Int = 1 | |
var totalAssetsText: String = "总资产" | |
var totalDebtText: String = "负债" | |
var totalAssetsRectColor: String? | |
var totalAssetsTextColor: String = "#8F8DB5" | |
var totalDebtTextColor: String = "#990000" | |
/// Width of the chart view | |
var width: Int | |
/// Height of the chart view | |
var height: Int = 300 | |
/// Global font size | |
var fontSize: Int = 10 | |
/// Width of the rectangle node | |
var nodeWidth: Int = 5 | |
/// Node vertical spacing | |
var nodePadding: Int = 15 | |
/// The distance between the title and the rectangle | |
var titlePadding: Int = 5 | |
/// White space around | |
/// [x0, y0, x1, y1] | |
var extents: [Int] | |
/// Total assets rectangle extra bold width, based on the node width | |
var boldWidth: Int = 5 | |
/// Total assets additional interval width, based on the node width | |
var dividerWidth: Int = 5 | |
/// Summary box height | |
var assetsRectHeight: Int = 33 | |
/// Summary box width | |
var assetsRectWidth: Int = 55 | |
var assetsRectPadding: Int = 8 | |
var assetsRectCornerRadius: Int = 5 | |
var assetsRectTextX: Int = 5 | |
var assetsRectTextDy: Float = 1.25 | |
var assetsRectFontSize: Int = 11 | |
} | |
enum DisplayMode: String, CaseIterable, Codable { | |
case show = "金额" | |
case percentage = "比例" | |
case hide = "隐藏金额" | |
case headline = "标题" | |
var id: Int { | |
switch self { | |
case .hide: return 0 | |
case .show: return 1 | |
case .percentage: return 2 | |
case .headline: return 3 | |
} | |
} | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
try container.encode(self.id) | |
} | |
} | |
let nodes = [ | |
SankeyNode(name: "总资产", color: "#928DBD", tag: 1), | |
SankeyNode(name: "信用卡", color: "#A8A7B0", tag: 3), | |
SankeyNode(name: "房屋贷款", color: "#A8A7B0", tag: 3), | |
SankeyNode(name: "车辆贷款", color: "#A8A7B0", tag: 3), | |
SankeyNode(name: "个人贷款", color: "#A8A7B0", tag: 3), | |
SankeyNode(name: "净资产", color: "#81CDB4", tag: 2), | |
SankeyNode(name: "投资理财", color: "#908DA2"), | |
SankeyNode(name: "后备隐藏能源", color: "#716C89"), | |
SankeyNode(name: "固定资产", color: "#839FAD"), | |
SankeyNode(name: "房产(自住)", color: "#5C8FA1"), | |
SankeyNode(name: "汽车", color: "#5C8FA1"), | |
SankeyNode(name: "应收款", color: "#908DA2"), | |
SankeyNode(name: "借给他人的钱", color: "#716C89") | |
] | |
let links = [ | |
NodeLink(source: "信用卡", target: "总资产", value: 3000), | |
NodeLink(source: "房屋贷款", target: "总资产", value: 500000), | |
NodeLink(source: "车辆贷款", target: "总资产", value: 40000), | |
NodeLink(source: "个人贷款", target: "总资产", value: 30000), | |
NodeLink(source: "总资产", target: "固定资产", value: 850000), | |
NodeLink(source: "固定资产", target: "房产(自住)", value: 800000), | |
NodeLink(source: "固定资产", target: "汽车", value: 50000), | |
NodeLink(source: "总资产", target: "投资理财", value: 50000), | |
NodeLink(source: "投资理财", target: "后备隐藏能源", value: 50000), | |
NodeLink(source: "总资产", target: "应收款", value: 20000), | |
NodeLink(source: "应收款", target: "借给他人的钱", value: 20000) | |
] | |
let nodes1 = [ | |
SankeyNode(name: "总资产", color: "#A4A3AB", tag: 1), | |
SankeyNode(name: "4项负债", color: "#A8A7B0", tag: 3), | |
SankeyNode(name: "净资产", color: "#81CDB4", tag: 2), | |
SankeyNode(name: "投资理财", color: "#908DA2"), | |
SankeyNode(name: "后备隐藏能源", color: "#716C89"), | |
SankeyNode(name: "固定资产", color: "#839FAD"), | |
SankeyNode(name: "房产(自住)", color: "#F4F3F700"), | |
SankeyNode(name: "汽车", color: "#F4F3F700"), | |
SankeyNode(name: "应收款", color: "#F4F3F700"), | |
SankeyNode(name: "借给他人的钱", color: "#F4F3F700") | |
] | |
let links1 = [ | |
NodeLink(source: "4项负债", target: "总资产", value: 573000), | |
NodeLink(source: "总资产", target: "固定资产", value: 850000), | |
NodeLink(source: "固定资产", target: "房产(自住)", value: 800000), | |
NodeLink(source: "固定资产", target: "汽车", value: 50000), | |
NodeLink(source: "总资产", target: "投资理财", value: 50000), | |
NodeLink(source: "总资产", target: "应收款", value: 20000), | |
NodeLink(source: "投资理财", target: "后备隐藏能源", value: 50000), | |
NodeLink(source: "应收款", target: "借给他人的钱", value: 20000) | |
] |
Author
no-today
commented
Sep 18, 2024


Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment