├── .gitignore ├── colorthemes.pdf ├── example ├── 2023.pdf ├── 2023.png ├── 2023.typ ├── .create_outputs.nu ├── 2023.toml └── 2023.json ├── lib ├── default-blocks.pdf ├── languages.toml ├── default-blocks.typ ├── helper.typ └── colorthemes.toml ├── typst.toml ├── colorthemes.typ ├── LICENSE ├── CHANGELOG.md ├── README.md └── timetable.typ /.gitignore: -------------------------------------------------------------------------------- 1 | timetable.pdf 2 | lib/helper.pdf -------------------------------------------------------------------------------- /colorthemes.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ludwig-austermann/typst-timetable/HEAD/colorthemes.pdf -------------------------------------------------------------------------------- /example/2023.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ludwig-austermann/typst-timetable/HEAD/example/2023.pdf -------------------------------------------------------------------------------- /example/2023.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ludwig-austermann/typst-timetable/HEAD/example/2023.png -------------------------------------------------------------------------------- /lib/default-blocks.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ludwig-austermann/typst-timetable/HEAD/lib/default-blocks.pdf -------------------------------------------------------------------------------- /example/2023.typ: -------------------------------------------------------------------------------- 1 | #import "../timetable.typ": timetable 2 | 3 | #set page(margin: 0.5cm, height: auto) 4 | 5 | #timetable( 6 | toml("2023.toml"), 7 | //language: "it", 8 | //date: [this year], 9 | //show-header: false, 10 | //show-alternatives: false, 11 | //show-description: false, 12 | //color-theme: "Set1_9", 13 | ) -------------------------------------------------------------------------------- /typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "timetable" 3 | version = "0.2.0" 4 | entrypoint = "timetable.typ" 5 | authors = ["Ludwig Austermann"] 6 | license = "MIT" 7 | description = "Create beautiful timetables from json/toml." 8 | repository = "https://github.com/ludwig-austermann/typst-timetable" 9 | exclude = ["examples"] 10 | 11 | # for usage with https://github.com/ludwig-austermann/typ-pack 12 | [packaging] 13 | include = ["lib/*.typ", "lib/*.toml", "example/*.typ", "example/*.pdf"] 14 | prescript = "example/.create_outputs.nu" -------------------------------------------------------------------------------- /lib/languages.toml: -------------------------------------------------------------------------------- 1 | [de] 2 | weekdays = ["MO", "DI", "MI", "DO", "FR"] 3 | title = "Stundenplan" 4 | from = "von" 5 | to = "bis" 6 | of = "von" 7 | alternatives = "Alternativen" 8 | description = "Beschreibung" 9 | 10 | [en] 11 | weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri"] 12 | title = "Timetable" 13 | from = "from" 14 | to = "to" 15 | of = "of" 16 | alternatives = "Alternatives" 17 | description = "Description" 18 | 19 | [it] 20 | weekdays = ["Lun", "Mar", "Mer", "Gio", "Ven"] 21 | title = "Orario" 22 | from = "da" 23 | to = "a" 24 | of = "di" 25 | alternatives = "Alternative" 26 | description = "Descrizione" 27 | 28 | [fr] 29 | weekdays = ["Lun", "Mar", "Mer", "Jeu", "Ven"] 30 | title = "Horaire" 31 | from = "de" 32 | to = "à" 33 | of = "de" 34 | alternatives = "Alternative" 35 | description = "Description" 36 | -------------------------------------------------------------------------------- /colorthemes.typ: -------------------------------------------------------------------------------- 1 | #let colorthemes = toml("lib/colorthemes.toml").pairs().map( 2 | theme => ( 3 | theme.at(0), 4 | theme.at(1).map(c => box(height: 20pt, width: 20pt, fill: color.rgb(c.at(0), c.at(1), c.at(2)))) 5 | ) 6 | ) 7 | #let max-size = colorthemes.fold( 8 | 0, 9 | (acc, x) => calc.max( 10 | acc, 11 | x.at(1).len() 12 | ) 13 | ) 14 | 15 | = Color themes / palette in this package 16 | Taken from `ColorSchemes.jl` package with can be found here: #link("https://github.com/JuliaGraphics/ColorSchemes.jl") 17 | 18 | #table( 19 | columns: max-size + 1, 20 | inset: 1pt, 21 | row-gutter: 9pt, 22 | stroke: none, 23 | align: horizon, 24 | none, ..range(1, max-size + 1).map(x => align(center, str(x))), 25 | ..colorthemes.map( 26 | x => { 27 | let l = x.at(1).len() 28 | if l < max-size { 29 | (raw(x.at(0)), ..(x.at(1) + (none,) * (max-size - l))) 30 | } else { 31 | (raw(x.at(0)), ..x.at(1)) 32 | } 33 | } 34 | ).flatten() 35 | ) -------------------------------------------------------------------------------- /lib/default-blocks.typ: -------------------------------------------------------------------------------- 1 | #let display-time(time) = { 2 | let hour = int(time) 3 | let minute = 60 * (time - hour) 4 | if hour < 10 { [0] } 5 | str(hour) 6 | [:] 7 | if minute < 10 { [0] } 8 | str(minute) 9 | } 10 | 11 | #let event-cell(event, show-time: false, show-day: false, unique: true) = { 12 | box(stroke: (left: event.color + 3pt), inset: (left: 5pt, y: 2pt), { 13 | strong(event.abbrv) 14 | h(1fr) 15 | event.kind 16 | linebreak() 17 | set text(9pt) 18 | event.room 19 | if not unique { 20 | h(1fr) 21 | emoji.warning 22 | } 23 | if show-time { 24 | linebreak() 25 | if show-day { event.day + ": "} 26 | display-time(event.start) 27 | [ -- ] 28 | display-time(event.end) 29 | } 30 | }) 31 | } 32 | 33 | #let time-cell(time, lang-dict) = align(horizon + right, { 34 | if time.keys().contains("display") { 35 | time.display 36 | } else { 37 | lang-dict.from + " " 38 | display-time(time.start) 39 | linebreak() 40 | lang-dict.to + " " 41 | display-time(time.end) 42 | } 43 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ludwig Austermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.2.0-beta.1 2 | ## code facing 3 | - added color themes. you can specify one of the color themes as given in `colorthemes.pdf` and overwrite special colors in the data file 4 | - added description table, showing further information about your courses 5 | ## data / dictionary 6 | - added `duration` the new `defaults`, so that only `start` or `end` has to be specified for events 7 | - added `hide` for courses 8 | - added `description` table to specify description table 9 | - added `course -> hide-discription` option, to hide course from description 10 | 11 | # v0.2.0-beta 12 | - entrypoint is now `timetable.typ` 13 | - added a `typst.toml` 14 | - removed page sizing from function 15 | - added arguments `show-header`, `show-alternatives` 16 | - *changed `show_time` argument to `show-time` for the data dictionary* 17 | - added `priority` argument to data dictionary 18 | - using now tablex 19 | - because of tablex, rowspans are used if events are longer than duration 20 | - added `tablex-args` to timetable arguments to modify style 21 | - added `event-cell` and `time-cell` to timetable arguments, to modify the blocks. The default functions live in `lib\default-blocks.typ` -------------------------------------------------------------------------------- /example/.create_outputs.nu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nu 2 | # nushell script to create all outputs of the .typ documents in pdf and png 3 | 4 | let source_files = (ls *.typ | get name) 5 | print $'(ansi bo)found following typst files:(ansi reset) ($source_files | str join ", ")' 6 | 7 | def compile_typst [file, --png, --svg] { 8 | let run_result = (if $png { 9 | let file_without_ext = ($file | str replace -r '(.*)\.typ' '$1') 10 | do { typst c $file $'($file_without_ext).png' --root .. } 11 | } else if $svg { 12 | let file_without_ext = ($file | str replace -r '(.*)\.typ' '$1') 13 | do { typst c $file $'($file_without_ext).svg' --root .. } 14 | } else { 15 | do { typst c $file --root .. } 16 | } | complete) 17 | if $run_result.exit_code == 0 { 18 | if $png { 19 | print $"- Compiled `($file)` to png" 20 | } else if $svg { 21 | print $"- Compiled `($file)` to svg" 22 | } else { 23 | print $"- Compiled `($file)` to pdf" 24 | } 25 | } else { 26 | print $"- (ansi red) Got error while compiling `($file):`(ansi reset)" 27 | print $run_result.stderr 28 | } 29 | } 30 | 31 | open 2023.toml | to json | save 2023.json -f 32 | print "- Overwritten `2023.json`" 33 | 34 | for file in $source_files { 35 | compile_typst $file 36 | compile_typst $file --png 37 | compile_typst $file --svg 38 | } 39 | 40 | print $'(ansi bo)Done!' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typst-timetable 2 | [GitHub Repository including Examples and Changelog](https://github.com/ludwig-austermann/typst-timetable) 3 | 4 | A typst template for timetables 5 | 6 | ## Features 7 | A resulting timetable looks like this: 8 | ![example](example/2023.png) 9 | 10 | - Collision detection 11 | - Automatic extension over multiple fields / cells / time slots 12 | - ... 13 | 14 | ## Usage 15 | The main difficulty lies in defining the dictionary with the necessary data. Take a look into the example to see how a `json` or `toml` file can be used to specify the data, which can then be included into `.typ` files. 16 | 17 | ### Functions 18 | The exposed `timetable` function takes the following arguments: 19 | - `all-data`: is the necessary data input 20 | - `language: "en"`: the language to use for weekdays and other terms 21 | - `date: datetime.today().display("[day].[month].[year]")`: the date to be displayed in the header 22 | - `show-header: true`: if to show the header 23 | - `show-alternatives: true`: if to show collisions and their corresponding alternatives 24 | - `show-description: true`: if to show the description table 25 | - `tablex-args: (:)`: arguments to be passed to the underlying tablex table, to overwrite the style 26 | - `event-cell: default-blocks.event-cell`: how to display the events 27 | - `time-cell: default-blocks.time-cell`: how to display the time cells 28 | - `color-theme: tab`: for automatical coloring of courses 29 | 30 | ### Data Dictionary 31 | ``` 32 | general 33 | period 34 | person 35 | times: array 36 | start [end - defaults.duration] 37 | end [start + defaults.duration] 38 | display: string [start "--" end] 39 | show-time [false] 40 | 41 | defaults 42 | duration [2] 43 | 44 | description?: array 45 | id 46 | title 47 | type: text|link|content [text] 48 | 49 | courses: {abbrv}?: string -> 50 | {description.id}? 51 | color? 52 | priority [0] 53 | hide [false] 54 | hide-description [false] 55 | events: {eventtype}?: string -> 56 | day 57 | start [end - default.duration] 58 | end [start + default.duration] 59 | room 60 | priority [thiscourse.priority] 61 | hide [false] 62 | ``` 63 | 64 | Here `?` denotes optional values, `[]` the corresponding default value, `{}` parametrices keys and `->` denotes another dictionary. 65 | 66 | Necessary are only a few options, for a quick start look at this simplified version: 67 | ``` 68 | general 69 | period 70 | person 71 | times: array 72 | start or end 73 | 74 | courses: {abbrv}?: string -> 75 | events: {eventtype}?: string -> 76 | day 77 | start or end 78 | room 79 | ``` 80 | 81 | Take a look at the example files, currently only the `toml` ones show all options. 82 | 83 | ## Typst Usage Tipp 84 | I plan to eventually release this as a package to typst packages. Until then, or additionally, you can place this in your local packages repo. If you use the web app, -------------------------------------------------------------------------------- /example/2023.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | period = "Summer 2023" 3 | person = "Max Mustermann" 4 | 5 | [[general.times]] 6 | start = 8 7 | end = 10 8 | 9 | [[general.times]] 10 | start = 10 11 | end = 12 12 | 13 | [[general.times]] 14 | start = 12 15 | end = 14 16 | 17 | [[general.times]] 18 | start = 14 19 | end = 16 20 | 21 | [[general.times]] 22 | start = 16 23 | end = 18 24 | 25 | [[general.times]] 26 | start = 18 27 | end = 24 28 | display = "evening" 29 | show-time = true 30 | 31 | [defaults] 32 | duration = 2 33 | 34 | [[description]] 35 | id = "name" 36 | title = "Course name" 37 | [[description]] 38 | id = "info" 39 | title = "Add. info" 40 | type = "content" 41 | [[description]] 42 | id = "url" 43 | title = "Website" 44 | type = "link" 45 | 46 | # main section defining the courses 47 | 48 | [courses."Ana I"] 49 | name = "Analysis I" 50 | url = "https://en.wikipedia.org/wiki/Mathematical_analysis" 51 | events.VL = [ 52 | { day = "Mon", end = 12, room = "big room 1", priority = 5 }, 53 | { day = "Tue", end = 12, room = "big room 2" } 54 | ] 55 | events.UE = [ 56 | { day = "Fri", end = 16, room = "small room 5" } 57 | ] 58 | 59 | [courses."LinA I"] 60 | name = "Linear Algebra I" 61 | info = "#box(fill: red, inset: 2pt)[required]" 62 | url = "https://en.wikipedia.org/wiki/Linear_algebra" 63 | events.VL = [ 64 | { day = "Mon", start = 16, room = "online" }, 65 | { day = "Tue", start = 16, room = "online" } 66 | ] 67 | events.UE = [ 68 | { day = "Mon", start = 10, room = "small room 1" } 69 | ] 70 | 71 | [courses."Alg I"] 72 | name = "Algebra I" 73 | url = "https://en.wikipedia.org/wiki/Algebra" 74 | events.VL = [ 75 | { day = "Tue", start = 12, room = "?" }, 76 | { day = "Wed", start = 12, room = "big room 1" } 77 | ] 78 | events.UE = [ 79 | { day = "Wed", start = 14, room = "small room 2" } 80 | ] 81 | 82 | [courses.Stoch] 83 | name = "Stochastic" 84 | url = "https://en.wikipedia.org/wiki/Stochastic" 85 | color = "yellow" 86 | events.SEM = [ 87 | { day = "Thu", start = 14, room = "small room 2" } 88 | ] 89 | 90 | [courses."Geo I"] 91 | name = "Geometry I" 92 | url = "https://en.wikipedia.org/wiki/Geometry" 93 | priority = 3 94 | 95 | [[courses."Geo I".events.VL]] 96 | day = "Tue" 97 | start = 10 98 | room = "small room 5" 99 | 100 | [courses.Hacking] 101 | hide = true 102 | 103 | [courses.Sport] 104 | hide-description = true 105 | color = "lime" 106 | 107 | [[courses.Sport.events."💃"]] 108 | day = "Mon" 109 | start = 18 110 | end = 19.5 111 | room = "pretty place" 112 | 113 | [[courses.Sport.events."💪"]] 114 | day = "Tue" 115 | start = 18.5 116 | end = 19.5 117 | room = "fancy place" 118 | 119 | [[courses.Sport.events.""]] 120 | day = "Thu" 121 | start = 18.5 122 | end = 19.5 123 | room = "fancy place" 124 | hide = true 125 | 126 | [[courses.Sport.events."🏊"]] 127 | day = "Fri" 128 | start = 18.5 129 | end = 19.5 130 | room = "" 131 | 132 | [courses.Work] 133 | color = "gray" 134 | hide-description = true 135 | 136 | [[courses.Work.events.""]] 137 | day = "Mon" 138 | start = 12 139 | end = 16 140 | room = "Office" 141 | 142 | [[courses.Work.events.""]] 143 | day = "Thu" 144 | start = 8 145 | end = 14 146 | room = "Home Office" 147 | 148 | [[courses.Work.events.""]] 149 | day = "Fri" 150 | start = 8 151 | end = 14 152 | room = "Home Office" -------------------------------------------------------------------------------- /timetable.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tablex:0.0.5": tablex, rowspanx 2 | #import "lib/helper.typ" as lib 3 | #import "lib/default-blocks.typ" 4 | 5 | #let timetable( 6 | all-data, 7 | language: "en", 8 | date: datetime.today().display("[day].[month].[year]"), 9 | show-header: true, 10 | show-alternatives: true, 11 | show-description: true, 12 | tablex-args: (:), 13 | event-cell: default-blocks.event-cell, 14 | time-cell: default-blocks.time-cell, 15 | color-theme: "tab", 16 | ) = { 17 | let lang-dict = if type(language) == str { 18 | lib.load-language(language) 19 | } else { language } 20 | 21 | let colors = if type(color-theme) == str { 22 | lib.load-color-theme(color-theme) 23 | } else { color-theme } 24 | 25 | let (times, courses, description, slots, alts) = lib.process-timetable-data(all-data, colors) 26 | 27 | let final-data = times.enumerate().map( 28 | time => ( 29 | time-cell(time.at(1), lang-dict), 30 | lib.weekdays.enumerate().map(d => slots.at(d.at(0)).at(time.at(0))).map(ev => 31 | if ev == none { 32 | [] 33 | } else if ev.at("occupied", default: false) { 34 | () 35 | } else { 36 | let cell = event-cell( 37 | ev, 38 | show-time: time.at(1).at("show-time", default: false), 39 | unique: ev.at("unique", default: true) 40 | ) 41 | if ev.duration > 0 { rowspanx(ev.duration + 1, cell) } else { cell } 42 | } 43 | ).flatten() 44 | ).flatten() 45 | ).flatten() 46 | 47 | // Title 48 | if show-header { 49 | text(16pt, strong(lang-dict.title + " " + all-data.general.period)) 50 | " " + lang-dict.of + " " + all-data.general.person 51 | if date != none { 52 | h(1fr) 53 | date 54 | } 55 | } 56 | 57 | // Main Timetable 58 | tablex( 59 | columns: (auto, 1fr, 1fr, 1fr, 1fr, 1fr), 60 | stroke: ( 61 | paint: gray, 62 | thickness: 0.5pt, 63 | dash: "dashed" 64 | ), 65 | ..tablex-args, 66 | [], ..lang-dict.weekdays.map(day => align(center, day)), 67 | ..final-data, 68 | ) 69 | 70 | show link: underline 71 | 72 | // Alternatives 73 | if show-alternatives and alts.len() > 0 { 74 | text(14pt, lang-dict.alternatives + ":") 75 | v(-12pt) 76 | table( 77 | columns: (1fr, 1fr, 1fr, 1fr, 1fr), 78 | column-gutter: 5pt, 79 | //stroke: gray + 0.5pt, 80 | stroke: none, 81 | ..alts.map( 82 | ev => event-cell(ev, show-time: true, show-day: true) 83 | ) 84 | ) 85 | } 86 | 87 | // Abbreviations 88 | if show-description { 89 | text(14pt, lang-dict.description + ":") 90 | v(-6pt) 91 | style(sty => { 92 | let h = measure([Hello], sty).height 93 | table( 94 | columns: description.len() + 2, 95 | stroke: ( 96 | paint: gray, 97 | thickness: 0.5pt, 98 | dash: "dashed" 99 | ), 100 | ..tablex-args, // at the moment tablex does not span the whole width 101 | none, none, ..description.map(d => strong(d.title)), 102 | ..courses.filter( 103 | c => not c.at("hide-description", default: false) 104 | ).map( 105 | c => ( 106 | rect(fill: c.color, width: h, height: h), 107 | strong(c.abbrv), 108 | ..description.map( 109 | d => (d.contentfn)(c.at(d.id, default: "")) // wraps content in link or other stuff 110 | ) 111 | ) 112 | ).flatten() 113 | ) 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/helper.typ: -------------------------------------------------------------------------------- 1 | #let weekdays = ("Mon", "Tue", "Wed", "Thu", "Fri") 2 | 3 | #let load-language(lang, language-file: "languages.toml") = { 4 | let language-dict = toml(language-file) 5 | if lang in language-dict { 6 | language-dict.at(lang) 7 | } else { 8 | panic("Unfortunately, `" + lang + "` does not exist in `" + language-file + "`.") 9 | } 10 | } 11 | 12 | #let load-color-theme(theme-name, themes-file: "colorthemes.toml") = { 13 | let theme-dict = toml(themes-file) 14 | if theme-name in theme-dict { 15 | theme-dict.at(theme-name).map(c => color.rgb(c.at(0), c.at(1), c.at(2))) 16 | } else { 17 | panic("Color Theme `" + theme-name + "` does not exist. Alternatives are: {" + theme-dict.keys().join(", ") + "}") 18 | } 19 | } 20 | 21 | #let description-parser(data) = if "description" in data { 22 | data.description.map(d => { 23 | if "title" not in d { d.insert("title", upper(d.id)) } 24 | let dtype = d.at("type", default: "text") 25 | if dtype == "link" { 26 | d.contentfn = x => if x == "" { x } else { link(x, "link") } 27 | } else if dtype == "content" { 28 | d.contentfn = x => eval(x, mode: "markup") 29 | } else { 30 | d.contentfn = x => x 31 | } 32 | d 33 | }) 34 | } else { () } 35 | 36 | #let courses-parser(data, colors) = { 37 | let colors = colors.rev() 38 | let courses = () 39 | 40 | for (cabbrv, cvalues) in data.courses.pairs() { 41 | if cvalues.at("hide", default: false) { 42 | continue 43 | } 44 | if "color" in cvalues { 45 | cvalues.color = eval(cvalues.color) 46 | } else { 47 | cvalues.color = colors.pop() 48 | } 49 | cvalues.abbrv = cabbrv // handle abbreviation and name differently 50 | cvalues.priority = cvalues.at("priority", default: 0) 51 | courses.push(cvalues) 52 | } 53 | 54 | courses 55 | } 56 | 57 | #let process-timetable-data(data, colors) = { 58 | let time-overlap(ev, time) = time.start <= ev.start and ev.start < time.end or time.start < ev.end and ev.end <= time.end or ev.start < time.start and time.end < ev.end 59 | 60 | let defaults = data.at("defaults", default: (:)) 61 | let default-duration = defaults.at("duration", default: 2) 62 | 63 | let slots = weekdays.map(_ => data.general.times.map(_ => none)) 64 | let alts = () 65 | let times = data.general.times.map( 66 | time => ( 67 | ..time, 68 | start: if "start" in time { time.start } else { time.end - default-duration }, 69 | end: if "end" in time { time.end } else { time.start + default-duration } 70 | ) 71 | ) 72 | 73 | let courses = courses-parser(data, colors) 74 | 75 | for (i, day) in weekdays.enumerate() { 76 | let day-evs = courses.map( 77 | course => course.at("events", default: (:)).pairs().map( 78 | evtype => evtype.at(1).filter( 79 | ev => not ev.at("hide", default: false) and ev.day == day 80 | ).map(k => ( 81 | ..course, // get all properties from the course 82 | ..k, // get all properties from the event, included later hence can overwrite course properties (e.g. for priority) 83 | kind: evtype.at(0), 84 | // change if absent with special values 85 | start: if "start" in k { k.start } else { k.end - default-duration }, 86 | end: if "end" in k { k.end } else { k.start + default-duration } 87 | )).flatten() 88 | ).flatten() 89 | ).flatten().sorted(key: ev => ev.priority).rev() 90 | 91 | for ev in day-evs { 92 | for (j, time) in times.enumerate() { 93 | if time-overlap(ev, time) { 94 | if slots.at(i).at(j) == none { 95 | // also check the duration 96 | let duration = times.slice(j + 1).enumerate() 97 | .find(x => not time-overlap(ev, x.at(1))) 98 | let duration = if duration == none { 0 } else { duration.at(0) } 99 | ev.insert("duration", duration) 100 | 101 | slots.at(i).at(j) = ev 102 | if duration > 0 { 103 | for k in range(duration) { 104 | slots.at(i).at(j + k + 1) = ("occupied": true) // notify that this spot is already occupied 105 | } 106 | } 107 | break 108 | } else { 109 | alts.push(ev) 110 | slots.at(i).at(j).insert("unique", false) 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | let description = description-parser(data) 118 | 119 | (times, courses, description, slots, alts) 120 | } -------------------------------------------------------------------------------- /lib/colorthemes.toml: -------------------------------------------------------------------------------- 1 | tab = [ [31,180,119], [255,14,127], [44,44,160], [214,40,39], [148,189,103], [140,75,86], [227,194,119], [127,127,127], [188,34,189], [23,207,190] ] 2 | Accent_8 = [ [127,127,201], [190,212,174], [253,134,192], [255,153,255], [56,176,108], [240,127,2], [191,23,91], [102,102,102] ] 3 | Dark2_8 = [ [27,119,158], [217,2,95], [117,179,112], [231,138,41], [102,30,166], [230,2,171], [166,29,118], [102,102,102] ] 4 | Paired_12 = [ [166,227,206], [31,180,120], [178,138,223], [51,44,160], [251,153,154], [227,28,26], [253,111,191], [255,0,127], [202,214,178], [106,154,61], [255,153,255], [177,40,89] ] 5 | Set1_9 = [ [228,28,26], [55,184,126], [77,74,175], [152,163,78], [255,0,127], [255,51,255], [166,40,86], [247,191,129], [153,153,153] ] 6 | Set3_12 = [ [141,199,211], [255,179,255], [190,218,186], [251,114,128], [128,211,177], [253,98,180], [179,105,222], [252,229,205], [217,217,217], [188,189,128], [204,197,235], [255,111,237] ] 7 | seaborn_bright = [ [2,255,62], [255,0,124], [26,56,201], [232,11,0], [139,226,43], [159,0,72], [241,193,76], [163,163,163], [255,0,196], [0,255,215] ] 8 | seaborn_bright6 = [ [2,255,62], [26,56,201], [232,11,0], [139,226,43], [255,0,196], [0,255,215] ] 9 | seaborn_colorblind = [ [1,178,115], [222,5,143], [2,115,158], [213,0,94], [204,188,120], [202,97,145], [251,228,175], [148,148,148], [236,51,225], [86,233,180] ] 10 | seaborn_colorblind6 = [ [1,178,115], [2,115,158], [213,0,94], [204,188,120], [236,51,225], [86,233,180] ] 11 | seaborn_dark = [ [0,127,28], [177,13,64], [18,28,113], [140,0,8], [89,113,30], [89,13,47], [162,130,53], [60,60,60], [184,10,133], [0,116,99] ] 12 | seaborn_dark6 = [ [0,127,28], [18,28,113], [140,0,8], [89,113,30], [184,10,133], [0,116,99] ] 13 | seaborn_deep = [ [76,176,114], [221,82,132], [85,104,168], [196,82,78], [129,179,114], [147,96,120], [218,195,139], [140,140,140], [204,116,185], [100,205,181] ] 14 | seaborn_deep6 = [ [76,176,114], [85,104,168], [196,82,78], [129,179,114], [204,116,185], [100,205,181] ] 15 | seaborn_muted = [ [72,208,120], [238,74,133], [106,100,204], [214,95,95], [149,180,108], [140,60,97], [220,192,126], [121,121,121], [213,103,187], [130,226,198] ] 16 | seaborn_muted6 = [ [72,208,120], [106,100,204], [214,95,95], [149,180,108], [213,103,187], [130,226,198] ] 17 | seaborn_pastel = [ [161,244,201], [255,130,180], [141,161,229], [255,155,159], [208,255,187], [222,155,187], [250,228,176], [207,207,207], [255,163,254], [185,240,242] ] 18 | seaborn_pastel6 = [ [161,244,201], [141,161,229], [255,155,159], [208,255,187], [255,163,254], [185,240,242] ] 19 | tableau_10 = [ [78,167,121], [242,43,142], [225,89,87], [118,178,183], [89,79,161], [237,72,201], [176,161,122], [255,167,157], [156,95,117], [186,172,176] ] 20 | tableau_green_orange_teal = [ [78,80,159], [135,128,209], [239,12,138], [252,109,198], [60,188,168], [152,228,217], [148,35,163], [195,61,206], [160,0,132], [247,42,212], [38,126,137], [141,168,191] ] 21 | tableau_jewel_bright = [ [235,44,30], [253,48,111], [249,41,167], [249,60,210], [95,104,187], [100,204,205], [145,234,220], [164,213,164], [187,229,201] ] 22 | tableau_red_blue_brown = [ [70,157,111], [145,215,179], [237,74,68], [254,162,181], [157,96,118], [215,166,181], [56,196,150], [160,238,212], [186,69,126], [57,127,184], [200,59,19], [234,131,135] ] 23 | tableau_summer = [ [191,2,178], [185,93,202], [207,83,62], [241,141,120], [0,179,162], [151,208,207], [243,70,165], [247,128,196] ] 24 | tableau_sunset_sunrise = [ [51,140,96], [151,165,104], [231,138,113], [246,87,186], [237,70,120], [213,69,76], [184,64,24] ] 25 | tableau_superfishel_stone = [ [99,180,136], [255,52,174], [239,106,111], [140,202,194], [85,137,173], [195,63,188], [187,147,118], [186,148,160], [169,174,181], [118,118,118] ] 26 | Archambault = [ [136,220,160], [56,97,26], [124,115,75], [237,140,150], [171,41,51], [231,41,132], [249,74,209] ] 27 | Java = [ [102,113,49], [207,54,58], [234,40,116], [226,138,153], [12,86,113] ] 28 | Johnson = [ [160,0,14], [208,0,78], [246,0,194], [0,168,134], [19,105,43] ] 29 | mk_12 = [ [159,98,1], [0,129,159], [255,175,90], [0,207,252], [132,205,0], [0,249,141], [0,249,194], [255,253,178], [164,34,1], [226,52,1], [255,58,110], [255,59,195] ] 30 | mk_8 = [ [0,0,0], [34,178,113], [61,233,183], [247,165,72], [53,115,155], [213,0,94], [230,0,159], [240,66,228] ] 31 | okabe_ito = [ [230,0,159], [86,233,180], [0,115,158], [240,66,228], [0,178,114], [213,0,94], [204,167,121], [0,0,0] ] 32 | tol_bright = [ [68,170,119], [238,119,102], [34,51,136], [204,68,187], [102,238,204], [170,119,51], [187,187,187] ] 33 | tol_light = [ [119,221,170], [238,102,136], [238,136,221], [255,187,170], [153,255,221], [68,153,187], [187,51,204], [170,0,170], [221,221,221] ] 34 | tol_medcontrast = [ [102,204,153], [0,136,68], [238,102,204], [153,85,68], [153,0,119], [238,170,153] ] 35 | tol_muted = [ [51,136,34], [136,238,204], [68,153,170], [17,51,119], [153,51,153], [221,119,204], [204,119,102], [136,85,34], [170,153,68] ] 36 | tol_vibrant = [ [238,51,119], [0,187,119], [51,238,187], [238,119,51], [204,17,51], [0,136,153], [187,187,187] ] -------------------------------------------------------------------------------- /example/2023.json: -------------------------------------------------------------------------------- 1 | { 2 | "courses": 3 | { 4 | "Alg I": 5 | { 6 | "events": 7 | { 8 | "UE": 9 | [ 10 | { 11 | "day": "Wed", 12 | "room": "small room 2", 13 | "start": 14 14 | } 15 | ], 16 | "VL": 17 | [ 18 | { 19 | "day": "Tue", 20 | "room": "?", 21 | "start": 12 22 | }, 23 | { 24 | "day": "Wed", 25 | "room": "big room 1", 26 | "start": 12 27 | } 28 | ] 29 | }, 30 | "name": "Algebra I", 31 | "url": "https://en.wikipedia.org/wiki/Algebra" 32 | }, 33 | "Ana I": 34 | { 35 | "events": 36 | { 37 | "UE": 38 | [ 39 | { 40 | "day": "Fri", 41 | "end": 16, 42 | "room": "small room 5" 43 | } 44 | ], 45 | "VL": 46 | [ 47 | { 48 | "day": "Mon", 49 | "end": 12, 50 | "priority": 5, 51 | "room": "big room 1" 52 | }, 53 | { 54 | "day": "Tue", 55 | "end": 12, 56 | "room": "big room 2" 57 | } 58 | ] 59 | }, 60 | "name": "Analysis I", 61 | "url": "https://en.wikipedia.org/wiki/Mathematical_analysis" 62 | }, 63 | "Geo I": 64 | { 65 | "events": 66 | { 67 | "VL": 68 | [ 69 | { 70 | "day": "Tue", 71 | "room": "small room 5", 72 | "start": 10 73 | } 74 | ] 75 | }, 76 | "name": "Geometry I", 77 | "priority": 3, 78 | "url": "https://en.wikipedia.org/wiki/Geometry" 79 | }, 80 | "Hacking": 81 | { 82 | "hide": true 83 | }, 84 | "LinA I": 85 | { 86 | "events": 87 | { 88 | "UE": 89 | [ 90 | { 91 | "day": "Mon", 92 | "room": "small room 1", 93 | "start": 10 94 | } 95 | ], 96 | "VL": 97 | [ 98 | { 99 | "day": "Mon", 100 | "room": "online", 101 | "start": 16 102 | }, 103 | { 104 | "day": "Tue", 105 | "room": "online", 106 | "start": 16 107 | } 108 | ] 109 | }, 110 | "info": "#box(fill: red, inset: 2pt)[required]", 111 | "name": "Linear Algebra I", 112 | "url": "https://en.wikipedia.org/wiki/Linear_algebra" 113 | }, 114 | "Sport": 115 | { 116 | "color": "lime", 117 | "events": 118 | { 119 | "": 120 | [ 121 | { 122 | "day": "Thu", 123 | "end": 19.5, 124 | "hide": true, 125 | "room": "fancy place", 126 | "start": 18.5 127 | } 128 | ], 129 | "🏊": 130 | [ 131 | { 132 | "day": "Fri", 133 | "end": 19.5, 134 | "room": "", 135 | "start": 18.5 136 | } 137 | ], 138 | "💃": 139 | [ 140 | { 141 | "day": "Mon", 142 | "end": 19.5, 143 | "room": "pretty place", 144 | "start": 18 145 | } 146 | ], 147 | "💪": 148 | [ 149 | { 150 | "day": "Tue", 151 | "end": 19.5, 152 | "room": "fancy place", 153 | "start": 18.5 154 | } 155 | ] 156 | }, 157 | "hide-description": true 158 | }, 159 | "Stoch": 160 | { 161 | "color": "yellow", 162 | "events": 163 | { 164 | "SEM": 165 | [ 166 | { 167 | "day": "Thu", 168 | "room": "small room 2", 169 | "start": 14 170 | } 171 | ] 172 | }, 173 | "name": "Stochastic", 174 | "url": "https://en.wikipedia.org/wiki/Stochastic" 175 | }, 176 | "Work": 177 | { 178 | "color": "gray", 179 | "events": 180 | { 181 | "": 182 | [ 183 | { 184 | "day": "Mon", 185 | "end": 16, 186 | "room": "Office", 187 | "start": 12 188 | }, 189 | { 190 | "day": "Thu", 191 | "end": 14, 192 | "room": "Home Office", 193 | "start": 8 194 | }, 195 | { 196 | "day": "Fri", 197 | "end": 14, 198 | "room": "Home Office", 199 | "start": 8 200 | } 201 | ] 202 | }, 203 | "hide-description": true 204 | } 205 | }, 206 | "defaults": 207 | { 208 | "duration": 2 209 | }, 210 | "description": 211 | [ 212 | { 213 | "id": "name", 214 | "title": "Course name" 215 | }, 216 | { 217 | "id": "info", 218 | "title": "Add. info", 219 | "type": "content" 220 | }, 221 | { 222 | "id": "url", 223 | "title": "Website", 224 | "type": "link" 225 | } 226 | ], 227 | "general": 228 | { 229 | "period": "Summer 2023", 230 | "person": "Max Mustermann", 231 | "times": 232 | [ 233 | { 234 | "end": 10, 235 | "start": 8 236 | }, 237 | { 238 | "end": 12, 239 | "start": 10 240 | }, 241 | { 242 | "end": 14, 243 | "start": 12 244 | }, 245 | { 246 | "end": 16, 247 | "start": 14 248 | }, 249 | { 250 | "end": 18, 251 | "start": 16 252 | }, 253 | { 254 | "display": "evening", 255 | "end": 24, 256 | "show-time": true, 257 | "start": 18 258 | } 259 | ] 260 | } 261 | } --------------------------------------------------------------------------------