'2025-01-01','cents'=>123456], …]; * $ckLabel = '招商银行'; * $ckKind = 'asset'; // 'asset' 红 / 'debt' 绿(**A 股口径:涨红跌绿**) * $ckWidth = 260; * echo chart_line($ckSeries, $ckLabel, $ckKind, $ckWidth); * * ⚠️ 颜色按**中式财务习惯**:金额变多 = 红,变少 = 绿(与 A 股一致)。 * 这是项目里已经定下的口径(见 Ledger 的 led-asset/led-debt 配色), * 改成欧美口径(涨绿跌红)会让这个页面和账目表对不上。 */ /** * 把一串「日期 → 分值」画成一条折线。 * * @param array $series ['date'=>'Y-m-d','cents'=>int] 列表,按时间正序;空数组返回 '' * @param string $label 图例上的名字 * @param string $kind 'asset' | 'debt'(只影响配色) * @param int $width 画布宽(高度按 3:1 推) * @param bool $showVal 是否在两端标出金额 * @return string SVG(可直接 echo) */ function chart_line(array $series, string $label, string $kind = 'asset', int $width = 260, bool $showVal = true): string { $pts = []; foreach ($series as $p) { $pts[] = ['d' => (string)($p['date'] ?? ''), 'v' => (int)($p['cents'] ?? 0)]; } if (!$pts) { return ''; } $w = max(120, $width); $h = (int)round($w / 3.2); // 内边距:左边留给"最高/最低"标签,下边留给日期,上边留给数值标 $padL = 6; $padR = 6; $padT = 14; $padB = 16; $iw = $w - $padL - $padR; $ih = $h - $padT - $padB; $vals = array_column($pts, 'v'); $min = min($vals); $max = max($vals); // ⚠️ 只有一个点、或所有值相同 → max==min,除零。 // 此时把线画在**中间**(比贴底好:贴底看起来像"跌到 0 了")。 $span = $max - $min; $flat = $span <= 0; if ($flat) { $span = 1; } $n = count($pts); $xy = []; foreach ($pts as $i => $p) { $x = $n === 1 ? ($padL + $iw / 2) : ($padL + $iw * $i / ($n - 1)); $ratio = $flat ? 0.5 : (($p['v'] - $min) / $span); $y = $padT + $ih * (1 - $ratio); $xy[] = [round($x, 2), round($y, 2)]; } $isDebt = $kind === 'debt'; // 中式财务口径:资产涨=红、负债涨=绿(负债变多是"好"的方向相反,同样用绿表示"更多") $stroke = $isDebt ? '#1F7A4D' : '#B0342C'; $fill = $isDebt ? 'rgba(31,122,77,.10)' : 'rgba(176,52,44,.10)'; $poly = []; foreach ($xy as $p) { $poly[] = $p[0] . ',' . $p[1]; } $polyStr = implode(' ', $poly); // 面积:从线走一遍再回到基线 —— 让"变化"比一条细线更醒目 $areaStr = ''; if ($n > 1) { $base = $padT + $ih; $areaStr = $padL . ',' . $base . ' ' . $polyStr . ' ' . ($padL + $iw) . ',' . $base; } $out = ''; // 基线(0 或最低值那一条)—— 让图有个水平参照 $out .= ''; if ($areaStr !== '') { $out .= ''; } if ($n === 1) { $out .= ''; } else { $out .= ''; // 每个盘点点一个小圆点:有几处转折要看得出来 foreach ($xy as $p) { $out .= ''; } } if ($showVal && $n > 0) { // 首尾各标一个数值:光看形状不知道"多少" $first = $pts[0]; $last = $pts[$n - 1]; $fy = max(9, $xy[0][1] - 6); $ly = max(9, $xy[$n - 1][1] - 6); $out .= '' . e(\App\Ledger::money($first['v'])) . ''; if ($n > 1) { $out .= '' . e(\App\Ledger::money($last['v'])) . ''; } } // 日期:首尾各一个(中间的点太密,标了反而糊) $out .= '' . e((string)$pts[0]['d']) . ''; if ($n > 1) { $out .= '' . e((string)$pts[$n - 1]['d']) . ''; } $out .= ''; return $out; }