SVG sample output below, shows cropped here but in reality the entire week gets output.
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ä tahansa päivä viikolla jolta <br>haluat kalenterin (klikkaa alla olevaa kenttää):</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äytä 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)