SVG sample output below, shows cropped here but in reality the entire week gets output.

Viikko 14Maanantai 01.4.Tiistai 02.4.Keskiviikko 03.4.Torstai 04.4.Perjantai 05.4.Lauantai 06.4.Sunnuntai 07.4.8:009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:00

PHP source for web UI (I like single page self contained PHP scripts for their portability.)
<?php
$svg_content = "";
if (isset($_GET["weekp"])) {
    $week_input = filter_input(INPUT_GET, 'weekp', FILTER_SANITIZE_STRING);
    // Extract year and week from the weekp value
    $dates = explode(" - ", $week_input);
    if (count($dates) === 2) {
        $startd = date("Y-m-d", strtotime($dates[0]));
        $endd = date("Y-m-d", strtotime($dates[1]));
        if ($startd && $endd) {
            $start_date=$startd;
            $end_date=$endd;
            $current_weekp=htmlspecialchars($week_input);
        }

    $svg_content = generateSvg($start_date);
}
?>
<?php if ($_GET['puresvg'] == 1) {
    header('Content-type: image/svg+xml');
    echo $svg_content;
} else {
?>
    <!DOCTYPE html>
    <html lang="en">

    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.15">
        <meta charset="UTF-8">
        <title>Viikko-kalenteri-kone</title>

        <link rel='stylesheet' href='//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css'>
        <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.7.1/css/bootstrap-datepicker3.css'>

        <style>
            body {
                font-family: Arial, sans-serif;
                padding: 20px;
            }

            h1 {
                margin-top: 0;
                margin-bottom: 0.3em;
            }

            form,
            .buttons {
                margin-bottom: 20px;
            }

            .controls {
                background-color: #f5f5f5;
                position: fixed;
                padding: 3px;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
                z-index: 10;
                max-width: 85%;
                margin-left: 0;
            }

            .controls button {
                max-width: 100%;
            }

            .svg-container {
                overflow-x: auto;
                white-space: nowrap;
            }

            svg {
                display: inline-block;
                max-height: 100%;
                transform: scale(0.75);
                transform-origin: top left;

            }

            input[type="date"],
            button {
                font-size: 16px;
                padding: 10px;
                margin: 5px 0;
                display: block;
                width: 100%;
                box-sizing: border-box;
                /* Ensures padding doesn't affect overall width */
            }

            button {
                background-color: #007bff;
                color: white;
                border: none;
                border-radius: 5px;
                cursor: pointer;
            }

            button:hover {
                background-color: #0056b3;
            }

            @media screen {
                .svg-container {
                    margin-top: 23em !important;
                    /* Adjust based on the height of your controls */
                }
            }

            @media screen and (max-width: 600px) {

                input[type="date"],
                button {
                    font-size: 18px;
                    /* Larger font size for mobile */
                    padding: 15px;
                }

                /* Adjusting for a more modern, mobile-friendly UI */
                body {
                    padding: 10px;
                }

                form,
                .buttons {
                    margin-bottom: 15px;
                }
            }

            @media print {

                .no-print,
                .no-print * {
                    display: none !important;
                }

                .print-only {
                    display: block;
                }
            }

            @media print {

                /* Hide elements with these classes or IDs */
                #weekForm,
                .hide-on-print {
                    display: none !important;
                }
            }

            .svg-container {
                overflow-x: visible;
                margin-top: 0;
                width: auto;
            }

            svg {
                transform: scale(1);
                /* Reset any scaling to fit the print page */
            }
            }

            .datepicker .datepicker-days tr td.active~td,
            .datepicker .datepicker-days tr td.active {
                color: #af1623 !important;
                background: transparent !important;
            }

            .datepicker .datepicker-days tr:hover td {
                color: #000;
                background: #e5e2e3;
                border-radius: 0;
            }
        </style>
        <script src='//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
        <script src='bootstrap.min.3.3.7.js'></script>
        <script src='https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.7.1/js/bootstrap-datepicker.min.js'></script>
        <script>
            window.console = window.console || function(t) {};
        </script>

    </head>

    <body>
        <div class="controls hide-on-print">
            <h1 style="letter-spacing:-0.02em;"><b>Viikko</b><span style="color:green;letter-spacing:-0.04em;">kalenteri</span><b>kone</b></h1>

            <form method="get" id="weekForm">
                <label class="control-label " for="weekpicker">Valitse mik&auml; tahansa p&auml;iv&auml; viikolla jolta <br>haluat kalenterin (klikkaa alla olevaa kentt&auml;&auml;):</label>
                <span class="icon-block ">
                    <input type="text" name="weekp" class="form-control" id="weekpicker">
                    <span class="icon-date"></span>
                </span>
                <div class="week-controls">
                    <!-- <button id="prevWeek" style="display:inline;width:20em;" class="prev-week">Edellinen viikko</button>
          <button id="nextWeek" style="display:inline;width:10em;"  class="next-week">Seuraava</button>-->
                </div>

                <!--<label for="date"></label>
        <input type="date" id="date" name="date" required onchange="updateWeekField()">
       --> <input type="hidden" id="week" name="week">
                <button style="display:block;width:20em;" type="submit">Tee kalenteri</button>
            </form>

            <script>
                // Ensures the week field is updated on form load if there's a query string
                window.onload = function() {
                    if (window.location.search.indexOf('week=') > -1) {
                        updateWeekField(true);
                    }
                }

                function updateWeekField(onLoad = false) {
                    // Get the date input field element
                    const dateInput = document.getElementById('date');
                    // Get the week input field element (hidden input that will hold the week number value)
                    //const weekInput = document.getElementById('week');
                    const weekInput = document.getElementById('weekpicker');
                    if (onLoad) { // Check if the function was called during page load
                        // Parse the current URL query parameters
                        const queryParams = new URLSearchParams(window.location.search);
                        // Extract the 'weekp' value from the query parameters
                        const weekpValue = queryParams.get('weekp');
                        if (weekpValue) { // Check if 'weekp' parameter exists
                            // Set the value of the weekp input field to the 'weekp' value
                            weekInput.value = decodeURIComponent(weekpValue); // Decode URI component to handle special characters
                        }
                    }


                    if (onLoad) { // Check if the function was called during page load
                        // Parse the current URL query parameters
                        const queryParams = new URLSearchParams(window.location.search);
                        // Extract the 'week' value from the query parameters
                        const weekValue = queryParams.get('week');
                        // Extract the year from the week value (format: YYYY-WWW)
                        const year = weekValue.substring(0, 4);
                        // Extract the week number from the week value
                        const weekNumber = weekValue.substring(6);
                        // Create a new Date object for the extracted year
                        const date = new Date(year);
                        // Adjust the date to the start date of the extracted week number (considering the first week starts at day 1)
                        date.setDate(date.getDate() + (weekNumber - 1) * 7);
                        // Set the date input field's value to the calculated date
                        dateInput.valueAsDate = date;
                    } else { // If the function was not called during page load (e.g., user interaction)
                        // Create a Date object from the value of the date input field
                        const date = new Date(dateInput.value);
                        // Calculate the week number for the selected date
                        const weekNumber = getISOWeekFromDate(date);
                        // Extract the year from the selected date
                        const year = date.getFullYear();
                        // Set the week input field's value to the calculated week number in the format YYYY-WWW
                        weekInput.value = `${year}-W${String(weekNumber).padStart(2, '0')}`;
                    }
                }

                $(document).ready(function() {
                    var startDate, endDate;

                    $('#weekpicker').datepicker({
                        autoclose: true,
                        format: 'dd/mm/yyyy',
                        forceParse: false
                    }).on("changeDate", function(e) {
                        var date = e.date;
                        startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay());
                        endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay() + 6);
                        $('#weekpicker').datepicker('update', startDate);
                        $('#weekpicker').val(
                            startDate.getDate() + '.' + (startDate.getMonth() + 1) + '.' + startDate.getFullYear() + ' - ' +
                            endDate.getDate() + '.' + (endDate.getMonth() + 1) + '.' + endDate.getFullYear()
                        );
                    });

                    // Previous week button
                    $('#prevWeek').click(function(e) {
                        var date = $('#weekpicker').datepicker('getDate');
                        startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay() - 7);
                        endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay() - 1);
                        $('#weekpicker').datepicker("setDate", new Date(startDate));
                        $('#weekpicker').val(
                            (startDate.getMonth() + 1) + '/' + startDate.getDate() + '/' + startDate.getFullYear() + ' - ' +
                            (endDate.getMonth() + 1) + '/' + endDate.getDate() + '/' + endDate.getFullYear()
                        );
                        return false;
                    });

                    // Next week button
                    $('#nextWeek').click(function() {
                        var date = $('#weekpicker').datepicker('getDate');
                        startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay() + 7);
                        endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay() + 13);
                        $('#weekpicker').datepicker("setDate", new Date(startDate));
                        $('#weekpicker').val(
                            (startDate.getMonth() + 1) + '/' + startDate.getDate() + '/' + startDate.getFullYear() + ' - ' +
                            (endDate.getMonth() + 1) + '/' + endDate.getDate() + '/' + endDate.getFullYear()
                        );
                        return false;
                    });
                });
                var startDate,
                    endDate;

                $('#weekpicker').datepicker({
                    autoclose: true,
                    format: 'mm/dd/yyyy',
                    forceParse: false,
                    language: 'fi'
                }).on("changeDate", function(e) {
                    //console.log(e.date);
                    var date = e.date;
                    startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay());
                    endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay() + 6);
                    $('#weekpicker').datepicker('update', startDate);
                    $('#weekpicker').val((startDate.getMonth() + 1) + '/' + startDate.getDate() + '/' + startDate.getFullYear() + ' - ' + (endDate.getMonth() + 1) + '/' + endDate.getDate() + '/' + endDate.getFullYear());
                });


                $('#prevWeek').click(function(e) {
                    var date = $('#weekpicker').datepicker('getDate');
                    //dateFormat = "mm/dd/yy"; //$.datepicker._defaults.dateFormat;
                    startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay() - 7);
                    endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay() - 1);
                    $('#weekpicker').datepicker("setDate", new Date(startDate));
                    $('#weekpicker').val((startDate.getMonth() + 1) + '/' + startDate.getDate() + '/' + startDate.getFullYear() + ' - ' + (endDate.getMonth() + 1) + '/' + endDate.getDate() + '/' + endDate.getFullYear());

                    return false;
                });
                $('#nextWeek').click(function() {
                    var date = $('#weekpicker').datepicker('getDate');
                    //dateFormat = "mm/dd/yy"; // $.datepicker._defaults.dateFormat;
                    startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay() + 7);
                    endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay() + 13);
                    $('#weekpicker').datepicker("setDate", new Date(startDate));
                    $('#weekpicker').val((startDate.getMonth() + 1) + '/' + startDate.getDate() + '/' + startDate.getFullYear() + ' - ' + (endDate.getMonth() + 1) + '/' + endDate.getDate() + '/' + endDate.getFullYear());

                    return false;
                });
            </script>


            <?php if (!empty($svg_content)): ?>
                <div>
                    <button onclick="window.print();" style="display:block;width:20em;" class="hide-on-print">Tulosta kalenteri</button>
                </div>
                <?php
                ?>
                <a href="<?php echo $_SERVER['PHP_SELF'] . '?puresvg=1&weekp=' . urlencode($current_weekp); ?>" class='hide-on-print' download>Lataa SVG</a>
                |
                <a href="<?php echo $_SERVER['PHP_SELF'] . '?puresvg=1&weekp=' . urlencode($current_weekp); ?>" class='hide-on-print'>N&auml;yt&auml; vain kalenteri</a>
        </div>
        <div class="svg-container">
            <?php echo $svg_content; ?>
        </div>
    <?php endif; ?>
    </body>

    </html>
<?php } ?>
<?php
// Function to generate SVG content
function generateSvg($start_date)
{
    $finnish_days = ['Maanantai', 'Tiistai', 'Keskiviikko', 'Torstai', 'Perjantai', 'Lauantai', 'Sunnuntai'];
    $week_number = date('W', strtotime($start_date));
    $year = date('o', strtotime($start_date)); // 'o' gives the ISO-8601 week-numbering year, which is what we need here.

    $margin = 20; // Margin between days
    $num_hours = 14; // Number of hour slots (8-21, inclusive)
    $hour_height = 40;
    $hour_number_width = 70; // Additional space for hour numbers

    // Adjusted SVG dimensions to accommodate margins and hour numbers
    $width = 1000 + (count($finnish_days) - 1) * $margin + $hour_number_width;
    $height = ($num_hours + 2) * $hour_height; // Add extra space for the last hour

    // Adjusted calculations for layout
    $day_width = ($width - $margin * (count($finnish_days) - 1) - $hour_number_width) / count($finnish_days);
    $svg_start = "<svg width=\"$width\" height=\"$height\" xmlns=\"http://www.w3.org/2000/svg\" style=\"font-family:sans-serif;\">";

    // Week Number and Year
    $week_number_svg = "<text x=\"" . ($width / 2) . "\" y=\"20\" font-size=\"24\" text-anchor=\"middle\">Viikko $week_number, $year</text>";

    // Drawing Days, Dates, and Vertical Lines
    $days_svg = $lines_svg = $hours_svg = '';
    for ($i = 0; $i < count($finnish_days); $i++) {
        $day = $finnish_days[$i];
        // Format the date to remove leading zeros from the month
        $date = date("j.n.", strtotime($start_date . " +$i days"));
        $x_position = $i * ($day_width + $margin) + $hour_number_width;
        $days_svg .= "<text x=\"" . ($x_position + $day_width / 2) . "\" y=\"50\" font-size=\"20\" text-anchor=\"middle\">$day $date</text>";
    }
    // Drawing Hours and Shading, Adjust for Hour Number Space
    for ($i = 0; $i < $num_hours; $i++) {
        $hour = 8 + $i;
        $y_position = $hour_height * ($i + 1) + 30;
        $shade_of_grey = $i % 2 == 0 ? "#f0f0f0" : "#d0d0d0";
        $hours_svg .= "<rect x=\"$hour_number_width\" y=\"$y_position\" width=\"" . ($width - $hour_number_width) . "\" height=\"$hour_height\" fill=\"$shade_of_grey\" />";
        $hours_svg .= "<text x=\"10\" y=\"" . ($y_position + $hour_height / 2 + 5) . "\" font-size=\"20\" text-anchor=\"start\">$hour:00</text>";
    }

    for ($i = 1; $i < count($finnish_days); $i++) {
        $x_position = $i * ($day_width + $margin) + $hour_number_width - $margin / 2;
        $lines_svg .= "<line x1=\"$x_position\" y1=\"60\" x2=\"$x_position\" y2=\"$height\" stroke=\"black\" stroke-width=\"2\"/>";
    }

    $svg_end = '</svg>';

    // Complete SVG
    return $svg_start . $week_number_svg . $days_svg . $hours_svg . $lines_svg . $svg_end;
}
?>

Python source for just calendar creation, less advanced:

Earlier Python version
from datetime import datetime, timedelta

# Date settings
start_date = datetime(2024, 4, 1)
finnish_days = ['Maanantai', 'Tiistai', 'Keskiviikko', 'Torstai', 'Perjantai', 'Lauantai', 'Sunnuntai']

# Calculate week number
week_number = start_date.isocalendar()[1]

# Generate dates for the week, including the month number
week_dates = [(start_date + timedelta(days=i)).strftime("%d.%-m.") for i in range(7)]

# SVG adjustments
margin = 20  # Margin between days
num_hours = 14  # Number of hour slots (8-21, inclusive)
hour_height = 40
hour_number_width = 70  # Additional space for hour numbers

# Adjusted SVG dimensions to accommodate margins and hour numbers
width = 1000 + (len(finnish_days) - 1) * margin + hour_number_width
height = (num_hours + 1) * hour_height  # Add extra space for the last hour

# Adjusted calculations for layout
day_width = (width - margin * (len(finnish_days) - 1) - hour_number_width) / len(finnish_days)

# Starting SVG
svg_start = f'<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg" style="font-family:sans-serif;">'

# Week Number
week_number_svg = f'<text x="{width/2}" y="20" font-size="24" text-anchor="middle">Viikko {week_number}</text>'

# Drawing Days, Dates
days_svg = ''
for i, (day, date) in enumerate(zip(finnish_days, week_dates), start=1):
    x_position = (i-1) * (day_width + margin) + hour_number_width
    days_svg += f'<text x="{x_position + day_width/2}" y="50" font-size="20" text-anchor="middle">{day} {date}</text>'

# Drawing Hours and Shading, Adjust for Hour Number Space
hours_svg = ''
for i, hour in enumerate(range(8, 22)):
    y_position = hour_height * (i + 1) + 30
    shade_of_grey = "#f0f0f0" if i % 2 == 0 else "#d0d0d0"
    hours_svg += f'<rect x="{hour_number_width}" y="{y_position}" width="{width - hour_number_width}" height="{hour_height}" fill="{shade_of_grey}" />'
    hours_svg += f'<text x="10" y="{y_position + hour_height/2 + 5}" font-size="20" text-anchor="start">{hour}:00</text>'

# Drawing Vertical Lines on Top
lines_svg = ''
for i in range(1, len(finnish_days)):
    x_position = i * (day_width + margin) + hour_number_width - margin / 2
    lines_svg += f'<line x1="{x_position}" y1="60" x2="{x_position}" y2="{height}" stroke="black" stroke-width="2"/>'

# Ending SVG
svg_end = '</svg>'

# Complete SVG
svg_content = svg_start + week_number_svg + days_svg + hours_svg + lines_svg + svg_end

# Displaying or saving the SVG content
print(svg_content)

Leave a comment

Your email address will not be published. Required fields are marked *

Comment moderation is enabled. Your comment may take some time to appear.