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(` +`, g.Width, g.Height)) + buf.WriteString(` +`) + + // 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(`CodeCompass Radar Chart +`, g.CenterX)) + + // Overall score + buf.WriteString(fmt.Sprintf(`Overall Score: %.1f +`, g.CenterX, g.Height-20, scoreData.OverallScore)) + + // Legend + g.drawLegend(&buf) + + buf.WriteString(` +`) + 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.