Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/charts.yml
Original file line number Diff line number Diff line change
@@ -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
83 changes: 83 additions & 0 deletions internal/metrics/score.go
Original file line number Diff line number Diff line change
@@ -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
}
198 changes: 198 additions & 0 deletions internal/viz/radar_svg.go
Original file line number Diff line number Diff line change
@@ -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(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
`, g.Width, g.Height))
buf.WriteString(`<style>
`)
buf.WriteString(`.grid-line { stroke: #ddd; stroke-width: 1; fill: none; }
`)
buf.WriteString(`.axis-line { stroke: #999; stroke-width: 2; }
`)
buf.WriteString(`.strengths-area { fill: rgba(76, 175, 80, 0.3); stroke: #4CAF50; stroke-width: 2; }
`)
buf.WriteString(`.weaknesses-area { fill: rgba(244, 67, 54, 0.3); stroke: #F44336; stroke-width: 2; }
`)
buf.WriteString(`.label-text { font-family: Arial, sans-serif; font-size: 12px; text-anchor: middle; }
`)
buf.WriteString(`.title-text { font-family: Arial, sans-serif; font-size: 16px; font-weight: bold; text-anchor: middle; }
`)
buf.WriteString(`.score-text { font-family: Arial, sans-serif; font-size: 14px; text-anchor: middle; }
`)
buf.WriteString(`</style>
`)

// Background circle grids
g.drawGrids(&buf)

// Combine all metrics for comprehensive radar
allMetrics := append(scoreData.Strengths, scoreData.Weaknesses...)
if len(allMetrics) == 0 {
// Default metrics if none provided
allMetrics = []metrics.MetricScore{
{Name: "Quality", Score: 75, Max: 100},
{Name: "Coverage", Score: 60, Max: 100},
{Name: "Performance", Score: 85, Max: 100},
{Name: "Security", Score: 70, Max: 100},
{Name: "Maintainability", Score: 80, Max: 100},
}
}

// Draw axes and labels
g.drawAxesAndLabels(&buf, allMetrics)

// Draw radar areas
if len(scoreData.Strengths) > 0 {
g.drawRadarArea(&buf, scoreData.Strengths, "strengths-area")
}
if len(scoreData.Weaknesses) > 0 {
g.drawRadarArea(&buf, scoreData.Weaknesses, "weaknesses-area")
}

// Draw combined area if we have both or default
if len(scoreData.Strengths) == 0 && len(scoreData.Weaknesses) == 0 {
g.drawRadarArea(&buf, allMetrics, "strengths-area")
}

// Title
buf.WriteString(fmt.Sprintf(`<text x="%d" y="30" class="title-text">CodeCompass Radar Chart</text>
`, g.CenterX))

// Overall score
buf.WriteString(fmt.Sprintf(`<text x="%d" y="%d" class="score-text">Overall Score: %.1f</text>
`, g.CenterX, g.Height-20, scoreData.OverallScore))

// Legend
g.drawLegend(&buf)

buf.WriteString(`</svg>
`)
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(`<circle cx="%d" cy="%d" r="%d" class="grid-line" />
`, 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(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="axis-line" />
`, 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(`<text x="%d" y="%d" class="label-text">%s</text>
`, 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(`<polygon points="%s" class="%s" />
`, 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(`<circle cx="%d" cy="%d" r="4" fill="#333" />
`, 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(`<rect x="%d" y="%d" width="15" height="15" fill="rgba(76, 175, 80, 0.3)" stroke="#4CAF50" />
`, legendX, legendY))
buf.WriteString(fmt.Sprintf(`<text x="%d" y="%d" class="label-text" text-anchor="start">Strengths (≥75)</text>
`, legendX+20, legendY+12))

// Weaknesses legend
buf.WriteString(fmt.Sprintf(`<rect x="%d" y="%d" width="15" height="15" fill="rgba(244, 67, 54, 0.3)" stroke="#F44336" />
`, legendX, legendY+20))
buf.WriteString(fmt.Sprintf(`<text x="%d" y="%d" class="label-text" text-anchor="start">Weaknesses (<75)</text>
`, legendX+20, legendY+32))
}
24 changes: 17 additions & 7 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) |
Expand Down Expand Up @@ -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:
Expand All @@ -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.
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.