// Copyright 2022 E99p1ant. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package template import ( "fmt" "strconv" "strings" ) // 图表辅助函数。 // // 全站数据页刻意不引入任何前端图表库:本站历史上为消除一个 1.6KB 的外部 defer // 脚本就把页脚计时器内联了,再挂一个 60KB+ 的 CDN 依赖并不划算。 // 需要坐标的只有折线(SVG),柱状 / 环形 / 热力图都能用 CSS 直接画。 // 因此这里只把「折线要的坐标」算出来,其余交给模板里的百分比与 grid。 // NumInt 把整数格式化成带千分位分隔的字符串,如 56321 → "56,321"。 func NumInt(v int64) string { sign := "" if v < 0 { sign = "-" v = -v } s := strconv.FormatInt(v, 10) if len(s) <= 3 { return sign + s } var b strings.Builder lead := len(s) % 3 if lead > 0 { b.WriteString(s[:lead]) } for i := lead; i < len(s); i += 3 { if b.Len() > 0 { b.WriteByte(',') } b.WriteString(s[i : i+3]) } return sign + b.String() } // PctText 返回 part 占 total 的百分比,保留一位小数(不含 % 号,便于模板拼接)。 func PctText(part, total int64) string { if total <= 0 { return "0.0" } return strconv.FormatFloat(float64(part)/float64(total)*100, 'f', 1, 64) } // ratio 把数值序列归一化到 [0,1],空序列或全零时返回全 0(不会出现除零)。 func ratioOf(values []int64) ([]float64, float64) { maxV := int64(0) for _, v := range values { if v > maxV { maxV = v } } out := make([]float64, len(values)) if maxV == 0 { return out, 0 } for i, v := range values { out[i] = float64(v) / float64(maxV) } return out, float64(maxV) } // Polyline 生成折线图的 points 属性值。 // // w/h 是 viewBox 尺寸,padTop/padBottom 给顶部留呼吸空间、底部留给轴标签。 // 返回 "x1,y1 x2,y2 ...";空数据返回空串(模板里 SVG 折线自然消失)。 func Polyline(values []int64, w, h, padTop, padBottom float64) string { n := len(values) if n == 0 { return "" } ratios, _ := ratioOf(values) usable := h - padTop - padBottom if usable < 1 { usable = 1 } var b strings.Builder for i, r := range ratios { var x float64 if n == 1 { x = w / 2 } else { x = float64(i) * w / float64(n-1) } y := padTop + usable - r*usable if i > 0 { b.WriteByte(' ') } fmt.Fprintf(&b, "%.1f,%.1f", x, y) } return b.String() } // AreaPath 把同一组数值渲染成闭合的面积路径,用于折线下方的浅色填充。 func AreaPath(values []int64, w, h, padTop, padBottom float64) string { points := Polyline(values, w, h, padTop, padBottom) if points == "" { return "" } return "M0," + strconv.FormatFloat(h, 'f', 1, 64) + " L" + strings.ReplaceAll(points, " ", " L") + " L" + strconv.FormatFloat(w, 'f', 1, 64) + "," + strconv.FormatFloat(h, 'f', 1, 64) + " Z" } // HeatLevel 把热力图的一个格子映射到 0-4 的强度档位(0 表示没有提问)。 // 用档位而不是连续透明度:连续值在小方块上肉眼分不出来,反而让人怀疑渲染出错。 func HeatLevel(n, max int64) int { if n <= 0 || max <= 0 { return 0 } switch { case n*4 <= max: return 1 case n*2 <= max: return 2 case n*4 <= max*3: return 3 default: return 4 } }