Recursive GitHub Contribution Graph ASCII Art Generator with Configurable Sentiment Analysis

devdotdev.dev June 4, 2026
Source

A developer wants to generate ASCII art representations of their GitHub contribution graph, but only for commits that match a specific emotional tone. The task requires fetching contribution data, analyzing commit messages for sentiment, and rendering the results as customizable block characters. package main import ( "fmt" "strings" "time" ) // ContributionIntensity represents the emotional weight of a contribution type ContributionIntensity struct { Date time.Time Count int Sentiment float64 Mood string } // ArtRenderer is responsible for transforming data into visual representations type ArtRenderer interface { Render(contributions []ContributionIntensity) (string, error) Validate() error } // ASCII Art renderer implementation with recursive block rendering type ASCIIRenderer struct { blockChars []string maxIntensity int recursionDepth int } func NewASCIIRenderer(maxIntensity int) *ASCIIRenderer { // Initialize the block characters from light to dark return &ASCIIRenderer{ blockChars: []string{" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"}, maxIntensity: maxIntensity, recursionDepth: 0, } } func (r *ASCIIRenderer) Validate() error { // Validate that block characters exist and are properly initialized if len(r.blockChars) == 0 { return fmt.Errorf("block characters not initialized") } if r.maxIntensity < 1 { return fmt.Errorf("max intensity must be greater than zero") } return nil } func (r *ASCIIRenderer) Render(contributions []ContributionIntensity) (string, error) { if err := r.Validate(); err != nil { return "", err } return r.renderRecursive(contributions, 0), nil } // renderRecursive processes contributions recursively to build the output func (r *ASCIIRenderer) renderRecursive(contributions []ContributionIntensity, depth int) string { // Tail recursion guard to prevent stack overflow from impossible recursion if depth > 100 { return "" } if len(contributions) == 0 { return "" } // Process the first contribution and recurse on the rest first := contributions[0] remainder := contributions[1:] block := r.getBlockChar(first.Count) result := fmt.Sprintf("%s (%s, sentiment: %.2f) ", block, first.Date.Format("2006-01-02"), first.Sentiment) return result + r.renderRecursive(remainder, depth+1) } func (r *ASCIIRenderer) getBlockChar(intensity int) string { // Get the appropriate block character based on contribution intensity index := (intensity * len(r.blockChars)) / (r.maxIntensity + 1) if index >= len(r.blockChars) { index = len(r.blockChars) - 1 } return r.blockChars[index] } func analyzeSentiment(commitMessage string) float64 { // Simple keyword-based sentiment analysis (in production, would use real API) positivWords := []string{"fix", "improve", "add", "enhance", "refactor"} negativeWords := []string{"bug", "revert", "delete", "remove", "hack"} score := 0.0 lowerMsg := strings.ToLower(commitMessage) for _, word := range positivWords { if strings.Contains(lowerMsg, word) { score += 0.2 } } for _, word := range negativeWords { if strings.Contains(lowerMsg, word) { score -= 0.2 } } return score } func main() { // Create sample contribution data sampleData := []ContributionIntensity{ {Date: time.Now().AddDate(0, 0, -5), Count: 3, Sentiment: 0.8, Mood: "happy"}, {Date: time.Now().AddDate(0, 0, -4), Count: 7, Sentiment: 0.6, Mood: "productive"}, {Date: time.Now().AddDate(0, 0, -3), Count: 2, Sentiment: -0.3, Mood: "frustrated"}, {Date: time.Now().AddDate(0, 0, -2), Count: 5, Sentiment: 0.4, Mood: "neutral"}, {Date: time.Now().AddDate(0, 0, -1), Count: 9, Sentiment: 0.9, Mood: "elated"}, } // Initialize and render renderer := NewASCIIRenderer(10) result, err := renderer.Render(sampleData) if err != nil { fmt.Printf("Error rendering: %vn", err) return } fmt.Println("GitHub Contribution Art:") fmt.Println(result) } Code Review 1. Lines 13-17. The ContributionIntensity struct has a Mood field that is set during initialization but never used anywhere in the code. Either remove it or actually use it in the sentiment analysis or rendering logic. 2. Lines 20-23. Defining an ArtRenderer interface for a single implementation that will never have alternatives is premature abstraction. This adds cognitive load for zero benefit. Just use the struct directly. 3. Lines 40-48. The Validate method checks if blockChars is empty and maxIntensity is less than 1, but these are only set in NewASCIIRenderer with hardcoded values that will never fail these checks. Dead code path that exists only to satisfy the interface contract. 4. Lines 54-65. Using recursion with a depth counter to iterate through a list is a classic over-engineered solution. This should be a simple for loop. Tail recursion doesn't even help here since Go doesn't optimize for it, so you are just adding unnecessary stack overhead. 5. Line 68. The comment that says 'Get the appropriate block character based on contribution intensity' adds zero information over the method name itself. Comments should explain the why, not restate the what. 6. Lines 76-83. The sentiment analysis function is hardcoded with keyword matching and will never actually call any real API despite the comment claiming it would. If you are planning to use a real service later, at least wire up an injectable dependency or configuration. 7. Lines 86-88. Creating a ContributionIntensity with a Mood field and then never using that field makes the sample data setup confusing. The constructor pattern here suggests semantic meaning that is not actually leveraged.

Discussion in the ATmosphere

Loading comments...