DataViz.manishdatt.com

One Million Digits of Pi

Palindromes in the first million digits of Pi.

By Manish Datt

TidyTuesday dataset of 2026-03-24

Pi Digits Table

Pi Digits Data

Palindrome graph

Total Palindromes

-

Unique Lengths

-

Max Length

-

Plotting code



<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pi Digits Table</title>
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- Tabulator CSS -->
    <link href="https://unpkg.com/tabulator-tables@6.2.0/dist/css/tabulator.min.css" rel="stylesheet">
    <!-- Tabulator JS -->
    <script type="text/javascript" src="https://unpkg.com/tabulator-tables@6.2.0/dist/js/tabulator.min.js"></script>
    <!-- Highcharts -->
    <script src="https://cdn.jsdelivr.net/npm/highcharts@11/highcharts.js" defer></script>
    <script src="https://cdn.jsdelivr.net/npm/highcharts@11/modules/broken-axis.js" defer></script>
    <script src="https://cdn.jsdelivr.net/npm/highcharts@11/modules/exporting.js" defer></script>
    <script src="https://cdn.jsdelivr.net/npm/highcharts@11/modules/export-data.js" defer></script>
    <style>
        /* Tailwind-styled Tabulator */
        .tabulator {
            width: 100%;
            height: auto;
        }
        .tabulator-row {
            border-bottom: 1px solid #e5e7eb;
        }
        .tabulator-header-cell {
            background-color: #f3f4f6;
            font-weight: 600;
            color: #1f2937;
            border-bottom: 2px solid #d1d5db;
        }
        .tabulator-cell {
            padding: 12px;
            color: #374151;
        }
        .tabulator-row:hover {
            background-color: #f9fafb;
        }
        #pi-digits-table {
            width: 100%;
        }
    </style>
</head>
<body class="bg-gray-50">
    <div class="min-h-screen p-8">
        <div class="max-w-6xl mx-auto w-full overflow-hidden">
            <h1 class="text-3xl font-bold text-gray-900 mb-2">Pi Digits Data</h1>
            
            <div class="bg-white rounded-lg shadow-md overflow-hidden">
                <div id="pi-digits-table"></div>
            </div>

            <div class="mt-8">
                <div class="flex items-center justify-between mb-4">
                    <h2 class="text-2xl font-bold text-gray-900">Palindrome graph</h2>
                    <button id="download-chart-btn" class="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded-lg transition duration-200">
                        Download SVG
                    </button>
                </div>
                <div class="bg-white rounded-lg shadow-md p-6" style="max-width: 800px;">
                    <div id="palindrome-nontrivial-chart" style="height: 400px;"></div>
                </div>
                
                <!-- Summary Cards -->
                <div class="grid grid-cols-2 md:grid-cols-3 gap-4 mt-6 w-full">
                    <div class="bg-white rounded-lg shadow-md p-4 border-l-4 border-blue-500">
                        <p class="text-gray-600 text-sm font-medium">Total Palindromes</p>
                        <p class="text-3xl font-bold text-gray-900" id="total-palindromes">-</p>
                    </div>
                    <div class="bg-white rounded-lg shadow-md p-4 border-l-4 border-green-500">
                        <p class="text-gray-600 text-sm font-medium">Unique Lengths</p>
                        <p class="text-3xl font-bold text-gray-900" id="unique-lengths">-</p>
                    </div>
                    <div class="bg-white rounded-lg shadow-md p-4 border-l-4 border-purple-500">
                        <p class="text-gray-600 text-sm font-medium">Max Length</p>
                        <p class="text-3xl font-bold text-gray-900" id="max-length">-</p>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
        // ===== GLOBAL CHART REFERENCE =====
        let globalChart = null;

        // ===== PALINDROME DETECTION FUNCTIONS =====
        function isTrivialPalindrome(s) {
            // A trivial palindrome has all characters the same
            const uniqueChars = new Set(s);
            return uniqueChars.size === 1;
        }

        function expandAroundCenter(str, left, right) {
            // Expand around center and return palindrome
            while (left >= 0 && right < str.length && str[left] === str[right]) {
                left--;
                right++;
            }
            return str.substring(left + 1, right);
        }

        function findAllNontrivialPalindromes(str, minLength = 2) {
            // Find all non-trivial palindromes (excluding repeated digits)
            const palindromes = [];
            let trivialCount = 0;

            for (let i = 0; i < str.length; i++) {
                // Odd length palindromes (single character center)
                const p1 = expandAroundCenter(str, i, i);
                if (p1.length >= minLength) {
                    if (isTrivialPalindrome(p1)) {
                        trivialCount++;
                    } else {
                        palindromes.push({
                            palindrome: p1,
                            length: p1.length,
                            position: str.indexOf(p1)
                        });
                    }
                }

                // Even length palindromes (two character center)
                const p2 = expandAroundCenter(str, i, i + 1);
                if (p2.length >= minLength) {
                    if (isTrivialPalindrome(p2)) {
                        trivialCount++;
                    } else {
                        palindromes.push({
                            palindrome: p2,
                            length: p2.length,
                            position: str.indexOf(p2)
                        });
                    }
                }

                if ((i + 1) % 100000 === 0) {
                    console.log(`Searched up to position ${i + 1}... Found ${palindromes.length} non-trivial palindromes`);
                }
            }

            console.log(`Total trivial palindromes excluded: ${trivialCount}`);
            return palindromes;
        }

        function calculatePalindromeStats(palindromes) {
            // Calculate frequency by length and find longest
            const frequencyByLength = {};
            let longestPalindrome = '';
            let maxLength = 0;
            const longestByLength = {};  // Track longest palindrome for each length
            const positionByLength = {};  // Track position for each length

            for (const pal of palindromes) {
                const len = pal.length;
                frequencyByLength[len] = (frequencyByLength[len] || 0) + 1;
                
                // Track longest palindrome for this length with position
                if (!longestByLength[len]) {
                    longestByLength[len] = pal.palindrome;
                    positionByLength[len] = {
                        start: pal.position,
                        end: pal.position + len - 1
                    };
                }
                
                if (len > maxLength) {
                    maxLength = len;
                    longestPalindrome = pal.palindrome;
                }
            }

            return {
                frequencyByLength,
                longestPalindrome,
                maxLength,
                longestByLength,
                positionByLength,
                total: palindromes.length
            };
        }

        // ===== MAIN EXECUTION =====
        fetch("pi_digits.csv")
            .then(response => response.text())
            .then(csvText => {
                // Parse CSV manually
                const lines = csvText.trim().split('\n');
                const headers = lines[0].split(',');
                
                const data = lines.slice(1).map(line => {
                    const values = line.split(',');
                    return {
                        digit_position: parseInt(values[0]),
                        digit: parseInt(values[1])
                    };
                });

                // Initialize Tabulator table
                var table = new Tabulator("#pi-digits-table", {
                    data: data,
                    pagination: "local",
                    paginationSize: 10,
                    layout: "fitData",
                    columns: [
                        { title: "Position", field: "digit_position", sorter: "number", width: 120 },
                        { title: "Digit", field: "digit", sorter: "number", width: 100 }
                    ],
                    headerSort: true
                });

                // ===== PALINDROME ANALYSIS - Compute using JS =====
                console.log('Computing non-trivial palindromes from CSV data...');
                
                // Create decimal string from CSV data
                const piDecimalStr = data.map(d => d.digit).join('');
                console.log(`Total pi digits: ${piDecimalStr.length}`);

                // Find all non-trivial palindromes
                const startTime = performance.now();
                const allNontrivialPalindromes = findAllNontrivialPalindromes(piDecimalStr, 2);
                const endTime = performance.now();
                
                console.log(`Found ${allNontrivialPalindromes.length} non-trivial palindromes in ${(endTime - startTime).toFixed(2)}ms`);

                // Calculate statistics
                const stats = calculatePalindromeStats(allNontrivialPalindromes);
                console.log(`Longest non-trivial palindrome: ${stats.longestPalindrome} (${stats.maxLength} digits)`);

                // Prepare frequency data
                const frequencyNontrivial = stats.frequencyByLength;
                const lengthsNT = Object.keys(frequencyNontrivial).map(Number).sort((a, b) => a - b);

                // ===== NON-TRIVIAL PALINDROMES CHART =====
                const chartLengthsNT = lengthsNT.map(String);
                const chartFrequenciesNT = lengthsNT.map(len => frequencyNontrivial[len]);

                // Prepare data for Highcharts
                const chartData = chartLengthsNT.map((length, index) => ({
                    name: length,
                    y: chartFrequenciesNT[index],
                    percentage: ((chartFrequenciesNT[index] / stats.total) * 100).toFixed(2)
                }));

                // Ensure Highcharts is loaded before using it
                const createChart = () => {
                    if (typeof Highcharts !== 'undefined') {
                        globalChart = Highcharts.chart('palindrome-nontrivial-chart', {
                            chart: {
                                type: 'column',
                                backgroundColor: '#F4FFF0',
                                marginTop: 20
                            },
                            title: {
                                text: 'There are about 100K palindromes (excluding repeated digits) in the first million digits of pi.',
                                margin: 1,
                            },
                            xAxis: {
                                categories: chartLengthsNT,
                                title: {
                                    text: 'Palindrome Length'
                                },
                                crosshair: true
                            },
                            yAxis: {
                                title: {
                                    text: 'Counts'
                                },
                                tickInterval: 10000,
                                endOnTick: false,
                                breaks: [{
                                    from: 10000,
                                    to: 70000,
                                    breakSize: 2000
                                }],
                                labels: {
                                    formatter: function() {
                                        const formatted = (this.value / 1000) + 'K';
                                        if (this.value === 10000 || this.value === 70000) {
                                            return '<b>' + formatted + '</b>';
                                        }
                                        return formatted;
                                    }
                                }
                            },
                            tooltip: {
                                headerFormat: '<b>Length: {point.x}</b><br/>',
                                pointFormat: 'Count: {point.y:,.0f}<br/>Percentage: {point.percentage}%',
                                shared: false
                            },
                            legend: {
                                enabled: false
                            },
                            credits: {
                                enabled: false
                            },
                            exporting: {
                                enabled: false
                            },
                            plotOptions: {
                                column: {
                                    dataLabels: {
                                        enabled: true,
                                        formatter: function() {
                                            return this.y.toLocaleString();
                                        },
                                        style: {
                                            fontSize: '14px',
                                            fontWeight: 'bold'
                                        }
                                    },
                                    colorByPoint: false
                                }
                            },
                            series: [{
                                name: 'Frequency',
                                data: chartData.map(d => d.y),
                                color: {
                                    linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
                                    stops: [
                                        [0, '#f59e0b'],
                                        [0.5, '#ec4899'],
                                        [1, '#8b5cf6']
                                    ]
                                }
                            }]
                        });

                        // Add annotation text using renderer
                        // 13-digit palindrome (top)
                        const pos13 = stats.positionByLength[stats.maxLength];
                        globalChart.renderer.text(
                            stats.longestPalindrome,
                            globalChart.plotLeft + globalChart.plotWidth - 50,  // absolute x (right side)
                            globalChart.plotTop + 150,  // absolute y (top)
                            false  // html: false - use plain SVG text
                        ).attr({
                            'text-anchor': 'end',
                            'font-size': '14px',
                            'font-weight': 'bold',
                            'fill': '#333333'
                        }).add();
                        
                        // Add position label below the palindrome
                        globalChart.renderer.text(
                            `[${pos13.start+1}:${pos13.end+1}]`,
                            globalChart.plotLeft + globalChart.plotWidth - 50,  // absolute x (right side)
                            globalChart.plotTop + 165,  // absolute y (below text)
                            false  // html: false - use plain SVG text
                        ).attr({
                            'text-anchor': 'end',
                            'font-size': '12px',
                            'fill': '#333333'
                        }).add();

                        // 12-digit palindrome (below 13-digit)
                        if (stats.longestByLength[12]) {
                            const pos12 = stats.positionByLength[12];
                            globalChart.renderer.text(
                                stats.longestByLength[12],
                                globalChart.plotLeft + globalChart.plotWidth - 100,  // absolute x (right side)
                                globalChart.plotTop + 215,  // absolute y (below top)
                                false  // html: false - use plain SVG text
                            ).attr({
                                'text-anchor': 'end',
                                'font-size': '14px',
                                'font-weight': 'bold',
                                'fill': '#333333'
                            }).add();
                            
                            // Add position label below the palindrome
                            globalChart.renderer.text(
                                `[${pos12.start+1}:${pos12.end+1}]`,
                                globalChart.plotLeft + globalChart.plotWidth - 100,  // absolute x (right side)
                                globalChart.plotTop + 230,  // absolute y (below text)
                                false  // html: false - use plain SVG text
                            ).attr({
                                'text-anchor': 'end',
                                'font-size': '12px',
                                'fill': '#333333'
                            }).add();
                        }

                        // Delay arrow drawing to ensure chart is fully rendered
                        setTimeout(() => {
                            // Draw arrow from 13-digit bar to annotation
                            const barIndex13 = lengthsNT.indexOf(stats.maxLength);  // Find actual index of 13-digit
                            console.log(`Drawing 13 arrow - barIndex13: ${barIndex13}, points length: ${globalChart.series[0].points.length}`);
                            
                            if (barIndex13 >= 0 && globalChart.series[0].points[barIndex13]) {
                                const barPoint13 = globalChart.series[0].points[barIndex13];
                                console.log(`barPoint13 plotX: ${barPoint13.plotX}, plotY: ${barPoint13.plotY}`);

                                const arrowStartX13 = globalChart.plotLeft + barPoint13.plotX;
                                const arrowStartY13 = globalChart.plotTop + barPoint13.plotY - 25;
                                const arrowEndX13 = globalChart.plotLeft + globalChart.plotWidth - 50;
                                const arrowEndY13 = globalChart.plotTop + 170;

                                // Draw line for 13-digit
                                globalChart.renderer.path([
                                    'M', arrowStartX13, arrowStartY13,
                                    'L', arrowEndX13, arrowEndY13
                                ]).attr({
                                    stroke: '#666666',
                                    'stroke-width': 2,
                                    'stroke-dasharray': '5,5'
                                }).add();

                                // Draw arrowhead for 13-digit
                                let dx13 = arrowEndX13 - arrowStartX13;
                                let dy13 = arrowEndY13 - arrowStartY13;
                                let angle13 = Math.atan2(dy13, dx13);
                                const arrowSize = 8;
                                let tipX13 = arrowEndX13;
                                let tipY13 = arrowEndY13;
                                let leftX13 = tipX13 - arrowSize * Math.cos(angle13 - Math.PI / 6);
                                let leftY13 = tipY13 - arrowSize * Math.sin(angle13 - Math.PI / 6);
                                let rightX13 = tipX13 - arrowSize * Math.cos(angle13 + Math.PI / 6);
                                let rightY13 = tipY13 - arrowSize * Math.sin(angle13 + Math.PI / 6);

                                globalChart.renderer.path([
                                    'M', leftX13, leftY13,
                                    'L', tipX13, tipY13,
                                    'L', rightX13, rightY13,
                                    'Z'
                                ]).attr({
                                    fill: '#666666',
                                    stroke: '#666666'
                                }).add();
                            } else {
                                console.log('13-digit bar point not found');
                            }

                            // 12-digit arrow
                            if (stats.longestByLength[12]) {
                                const barIndex12 = lengthsNT.indexOf(12);  // Find actual index of 12-digit
                                console.log(`Drawing 12 arrow - barIndex12: ${barIndex12}, points length: ${globalChart.series[0].points.length}`);

                                if (barIndex12 >= 0 && globalChart.series[0].points[barIndex12]) {
                                    const barPoint12 = globalChart.series[0].points[barIndex12];
                                    console.log(`barPoint12 plotX: ${barPoint12.plotX}, plotY: ${barPoint12.plotY}`);

                                    const arrowStartX12 = globalChart.plotLeft + barPoint12.plotX;
                                    const arrowStartY12 = globalChart.plotTop + barPoint12.plotY - 25;
                                    const arrowEndX12 = globalChart.plotLeft + globalChart.plotWidth - 100;
                                    const arrowEndY12 = globalChart.plotTop + 240;

                                    // Draw line for 12-digit
                                    globalChart.renderer.path([
                                        'M', arrowStartX12, arrowStartY12,
                                        'L', arrowEndX12, arrowEndY12
                                    ]).attr({
                                        stroke: '#666666',
                                        'stroke-width': 2,
                                        'stroke-dasharray': '5,5'
                                    }).add();

                                    // Draw arrowhead for 12-digit
                                    let dx12 = arrowEndX12 - arrowStartX12;
                                    let dy12 = arrowEndY12 - arrowStartY12;
                                    let angle12 = Math.atan2(dy12, dx12);
                                    const arrowSize = 8;
                                    let tipX12 = arrowEndX12;
                                    let tipY12 = arrowEndY12;
                                    let leftX12 = tipX12 - arrowSize * Math.cos(angle12 - Math.PI / 6);
                                    let leftY12 = tipY12 - arrowSize * Math.sin(angle12 - Math.PI / 6);
                                    let rightX12 = tipX12 - arrowSize * Math.cos(angle12 + Math.PI / 6);
                                    let rightY12 = tipY12 - arrowSize * Math.sin(angle12 + Math.PI / 6);

                                    globalChart.renderer.path([
                                        'M', leftX12, leftY12,
                                        'L', tipX12, tipY12,
                                        'L', rightX12, rightY12,
                                        'Z'
                                    ]).attr({
                                        fill: '#666666',
                                        stroke: '#666666'
                                    }).add();
                                } else {
                                    console.log('12-digit bar point not found');
                                }
                            }
                        }, 100);  // Wait 100ms for chart to fully render
                        
                        // Populate summary cards
                        document.getElementById('total-palindromes').textContent = stats.total.toLocaleString();
                        document.getElementById('unique-lengths').textContent = Object.keys(stats.frequencyByLength).length;
                        document.getElementById('max-length').textContent = stats.maxLength;

                        // Add download button event listener
                        document.getElementById('download-chart-btn').addEventListener('click', () => {
                            if (globalChart) {
                                /**
                                 * Highcharts getSVG() may not include all dynamically added renderer elements
                                 * (arrows and text annotations). This is a known limitation.
                                 * The rendered chart is visible on screen but export doesn't capture
                                 * elements added via chart.renderer after chart initialization.
                                 */
                                globalChart.downloadSVG = globalChart.downloadSVG || function() {
                                    // Get the chart container and clone the current SVG with all rendered elements
                                    const chartSVG = this.getSVG();
                                    
                                    // Parse the SVG to create a more complete export
                                    let svgContent = chartSVG;
                                    
                                    // Get all SVG elements from the chart container
                                    const chartContainer = document.getElementById('palindrome-nontrivial-chart');
                                    if (chartContainer) {
                                        const existingSVG = chartContainer.querySelector('svg');
                                        if (existingSVG) {
                                            // The visible SVG includes all renderer elements
                                            svgContent = existingSVG.outerHTML;
                                        }
                                    }
                                    
                                    const blob = new Blob([svgContent], { type: 'image/svg+xml' });
                                    const url = URL.createObjectURL(blob);
                                    const link = document.createElement('a');
                                    link.href = url;
                                    link.download = 'palindrome-chart.svg';
                                    document.body.appendChild(link);
                                    link.click();
                                    document.body.removeChild(link);
                                    URL.revokeObjectURL(url);
                                };
                                globalChart.downloadSVG();
                            }
                        });
                    } else {
                        // Retry if Highcharts not loaded yet
                        setTimeout(createChart, 100);
                    }
                };

                createChart();
            })
            .catch(error => console.error("Error loading CSV:", error));
    </script>
</body>