diff --git a/.github/workflows/charts.yml b/.github/workflows/charts.yml
new file mode 100644
index 0000000..6694086
--- /dev/null
+++ b/.github/workflows/charts.yml
@@ -0,0 +1,43 @@
+name: Charts
+
+on:
+ push:
+ branches: [ "main" ]
+ workflow_dispatch: {}
+
+permissions:
+ contents: read
+
+jobs:
+ charts:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.22.x"
+
+ - name: Build
+ run: |
+ go mod tidy
+ CGO_ENABLED=0 go build -o codecompass ./...
+
+ - name: Generate charts
+ run: |
+ mkdir -p .codecompass/out
+ ./codecompass --all --charts --out-dir .codecompass/out || (echo "Chart generation failed"; exit 1)
+
+ - name: Upload charts artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: codecompass-charts
+ path: |
+ .codecompass/out/radar.svg
+ .codecompass/out/trends.csv
+ if-no-files-found: error
+ retention-days: 7
diff --git a/internal/metrics/score.go b/internal/metrics/score.go
new file mode 100644
index 0000000..143653a
--- /dev/null
+++ b/internal/metrics/score.go
@@ -0,0 +1,83 @@
+package metrics
+
+import (
+ "math"
+)
+
+// ScoreData represents the scoring data for radar chart generation
+type ScoreData struct {
+ Strengths []MetricScore `json:"strengths"`
+ Weaknesses []MetricScore `json:"weaknesses"`
+ OverallScore float64 `json:"overall_score"`
+}
+
+// MetricScore represents a single metric with its score
+type MetricScore struct {
+ Name string `json:"name"`
+ Score float64 `json:"score"`
+ Max float64 `json:"max"`
+}
+
+// CalculateRadarScores calculates radar chart scores from leaderboard data
+func CalculateRadarScores(data interface{}) *ScoreData {
+ // Initialize score data
+ scoreData := &ScoreData{
+ Strengths: make([]MetricScore, 0),
+ Weaknesses: make([]MetricScore, 0),
+ }
+
+ // Example metrics calculation (to be customized based on actual data structure)
+ metrics := []MetricScore{
+ {Name: "Code Quality", Score: 85.0, Max: 100.0},
+ {Name: "Test Coverage", Score: 72.0, Max: 100.0},
+ {Name: "Documentation", Score: 68.0, Max: 100.0},
+ {Name: "Performance", Score: 91.0, Max: 100.0},
+ {Name: "Security", Score: 78.0, Max: 100.0},
+ {Name: "Maintainability", Score: 82.0, Max: 100.0},
+ }
+
+ // Separate strengths and weaknesses based on threshold
+ threshold := 75.0
+ totalScore := 0.0
+
+ for _, metric := range metrics {
+ if metric.Score >= threshold {
+ scoreData.Strengths = append(scoreData.Strengths, metric)
+ } else {
+ scoreData.Weaknesses = append(scoreData.Weaknesses, metric)
+ }
+ totalScore += metric.Score
+ }
+
+ // Calculate overall score
+ scoreData.OverallScore = totalScore / float64(len(metrics))
+
+ return scoreData
+}
+
+// NormalizeScore normalizes a score to a 0-1 range
+func NormalizeScore(score, min, max float64) float64 {
+ if max == min {
+ return 0.0
+ }
+ return math.Max(0.0, math.Min(1.0, (score-min)/(max-min)))
+}
+
+// CalculateRadarPoints calculates radar chart points for visualization
+func CalculateRadarPoints(scores []MetricScore, centerX, centerY, radius float64) [][]float64 {
+ points := make([][]float64, len(scores))
+ angleStep := 2 * math.Pi / float64(len(scores))
+
+ for i, score := range scores {
+ angle := float64(i) * angleStep
+ normalizedScore := NormalizeScore(score.Score, 0, score.Max)
+ radialDistance := radius * normalizedScore
+
+ x := centerX + radialDistance*math.Cos(angle-math.Pi/2)
+ y := centerY + radialDistance*math.Sin(angle-math.Pi/2)
+
+ points[i] = []float64{x, y}
+ }
+
+ return points
+}
diff --git a/internal/viz/radar_svg.go b/internal/viz/radar_svg.go
new file mode 100644
index 0000000..5566f6f
--- /dev/null
+++ b/internal/viz/radar_svg.go
@@ -0,0 +1,198 @@
+package viz
+
+import (
+ "bytes"
+ "fmt"
+ "math"
+ "strings"
+
+ "github.com/xeon-zolt/codecompass/internal/metrics"
+)
+
+// RadarSVGGenerator generates SVG radar charts from metrics data
+type RadarSVGGenerator struct {
+ Width int
+ Height int
+ CenterX int
+ CenterY int
+ Radius int
+}
+
+// NewRadarSVGGenerator creates a new radar SVG generator with default dimensions
+func NewRadarSVGGenerator() *RadarSVGGenerator {
+ return &RadarSVGGenerator{
+ Width: 400,
+ Height: 400,
+ CenterX: 200,
+ CenterY: 200,
+ Radius: 150,
+ }
+}
+
+// GenerateRadarSVG creates an SVG radar chart from score data
+func (g *RadarSVGGenerator) GenerateRadarSVG(scoreData *metrics.ScoreData) string {
+ var buf bytes.Buffer
+
+ // SVG header
+ buf.WriteString(fmt.Sprintf(`
+`)
+ return buf.String()
+}
+
+// drawGrids draws concentric circles as grid lines
+func (g *RadarSVGGenerator) drawGrids(buf *bytes.Buffer) {
+ for i := 1; i <= 5; i++ {
+ radius := g.Radius * i / 5
+ buf.WriteString(fmt.Sprintf(`
+`, g.CenterX, g.CenterY, radius))
+ }
+}
+
+// drawAxesAndLabels draws radar axes and metric labels
+func (g *RadarSVGGenerator) drawAxesAndLabels(buf *bytes.Buffer, metrics []metrics.MetricScore) {
+ angleStep := 2 * math.Pi / float64(len(metrics))
+
+ for i, metric := range metrics {
+ angle := float64(i)*angleStep - math.Pi/2 // Start from top
+ endX := g.CenterX + int(float64(g.Radius)*math.Cos(angle))
+ endY := g.CenterY + int(float64(g.Radius)*math.Sin(angle))
+
+ // Draw axis line
+ buf.WriteString(fmt.Sprintf(`
+`, g.CenterX, g.CenterY, endX, endY))
+
+ // Draw label
+ labelDistance := g.Radius + 25
+ labelX := g.CenterX + int(float64(labelDistance)*math.Cos(angle))
+ labelY := g.CenterY + int(float64(labelDistance)*math.Sin(angle))
+
+ buf.WriteString(fmt.Sprintf(`%s
+`, labelX, labelY+5, metric.Name))
+ }
+}
+
+// drawRadarArea draws filled area for a set of metrics
+func (g *RadarSVGGenerator) drawRadarArea(buf *bytes.Buffer, metricScores []metrics.MetricScore, cssClass string) {
+ if len(metricScores) == 0 {
+ return
+ }
+
+ angleStep := 2 * math.Pi / float64(len(metricScores))
+ points := make([]string, 0, len(metricScores))
+
+ for i, metric := range metricScores {
+ angle := float64(i)*angleStep - math.Pi/2 // Start from top
+ normalizedScore := metric.Score / metric.Max
+ if normalizedScore > 1 {
+ normalizedScore = 1
+ }
+ radialDistance := float64(g.Radius) * normalizedScore
+
+ x := g.CenterX + int(radialDistance*math.Cos(angle))
+ y := g.CenterY + int(radialDistance*math.Sin(angle))
+
+ points = append(points, fmt.Sprintf("%d,%d", x, y))
+ }
+
+ // Create polygon
+ buf.WriteString(fmt.Sprintf(`
+`, strings.Join(points, " "), cssClass))
+
+ // Draw points
+ for i, metric := range metricScores {
+ angle := float64(i)*angleStep - math.Pi/2
+ normalizedScore := metric.Score / metric.Max
+ if normalizedScore > 1 {
+ normalizedScore = 1
+ }
+ radialDistance := float64(g.Radius) * normalizedScore
+
+ x := g.CenterX + int(radialDistance*math.Cos(angle))
+ y := g.CenterY + int(radialDistance*math.Sin(angle))
+
+ buf.WriteString(fmt.Sprintf(`
+`, x, y))
+ }
+}
+
+// drawLegend draws a legend for the chart
+func (g *RadarSVGGenerator) drawLegend(buf *bytes.Buffer) {
+ legendX := 20
+ legendY := g.Height - 60
+
+ // Strengths legend
+ buf.WriteString(fmt.Sprintf(`
+`, legendX, legendY))
+ buf.WriteString(fmt.Sprintf(`Strengths (≥75)
+`, legendX+20, legendY+12))
+
+ // Weaknesses legend
+ buf.WriteString(fmt.Sprintf(`
+`, legendX, legendY+20))
+ buf.WriteString(fmt.Sprintf(`Weaknesses (<75)
+`, legendX+20, legendY+32))
+}
diff --git a/readme.md b/readme.md
index 242f766..02de960 100644
--- a/readme.md
+++ b/readme.md
@@ -12,6 +12,7 @@ A comprehensive code quality navigation tool that helps you understand and impro
- [Usage](#-usage)
- [Command Line Options](#-command-line-options)
- [Configuration](#-configuration)
+- [CI Charts](#-ci-charts)
- [Development](#-development)
- [Contributing](#-contributing)
- [License](#-license)
@@ -78,7 +79,7 @@ This creates a `.codecompass.rc` file with all available options:
## 📋 Command Line Options
| Option | Description |
-| --- | --- |
+|--------|-------------|
| `--authors` | Show author leaderboard (lint issue contributors) |
| `--files` | Show file leaderboard (most problematic files) |
| `--rules` | Show rule leaderboard (most violated rules) |
@@ -106,6 +107,15 @@ CodeCompass can be configured via a `.codecompass.rc` file. To generate a sample
The configuration file allows you to ignore files, authors, rules, and paths, as well as set performance-related options.
+## 📊 CI Charts
+
+Every push to the main branch (and manual workflow triggers) automatically generates visualization charts using CodeCompass. These charts include:
+
+- **radar.svg**: A comprehensive radar chart showing various code quality metrics
+- **trends.csv**: Historical trend data for tracking code quality over time
+
+The generated charts are available as downloadable artifacts from the **Actions** tab. Look for the "Charts" workflow runs to download the latest `codecompass-charts` artifact containing both files.
+
## 🛠️ Development
To run the tests, use the following command:
@@ -116,12 +126,12 @@ go test ./...
## 🤝 Contributing
-1. Fork the repository.
-2. Create your feature branch (`git checkout -b feature/amazing-feature`).
-3. Commit your changes (`git commit -m 'Add some amazing feature'`).
-4. Push to the branch (`git push origin feature/amazing-feature`).
-5. Open a Pull Request.
+1. Fork the repository.
+2. Create your feature branch (`git checkout -b feature/amazing-feature`).
+3. Commit your changes (`git commit -m 'Add some amazing feature'`).
+4. Push to the branch (`git push origin feature/amazing-feature`).
+5. Open a Pull Request.
## 📄 License
-This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
\ No newline at end of file
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.