Skip to content

Commit 76a6d8b

Browse files
authored
Merge pull request #11 from stefmolin/exercise-timer
Add exercise timer, clock, and time warning to hotkey tools. skip-checks: true
2 parents 0e9d5bc + a1c13f7 commit 76a6d8b

16 files changed

+972
-35
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ You should have basic knowledge of Python and be comfortable working in Jupyter
3434
0. Install Python >= version 3.8 and <= version 3.11 OR install [Anaconda](https://docs.anaconda.com/anaconda/install/)/[Miniconda](https://docs.conda.io/en/latest/miniconda.html). Note that Anaconda/Miniconda is recommended if you are working on a Windows machine and are not very comfortable with the command line. Alternatively, you can use [this](https://mybinder.org/v2/gh/stefmolin/pandas-workshop/main?urlpath=lab) Binder environment if you don't want to install anything on your machine.
3535
1. Fork this repository:
3636

37-
![location of fork button in GitHub](./images/fork_button.png)
37+
![location of fork button in GitHub](./media/fork_button.png)
3838

3939
2. Clone your forked repository:
4040

41-
![location of clone button in GitHub](./images/clone_button.png)
41+
![location of clone button in GitHub](./media/clone_button.png)
4242

4343
3. Create and activate a Python virtual environment:
4444
- If you installed Anaconda/Miniconda, use `conda` (on Windows, these commands should be run in **Anaconda Prompt**):
@@ -67,11 +67,11 @@ You should have basic knowledge of Python and be comfortable working in Jupyter
6767

6868
5. Navigate to the `0-check_your_env.ipynb` notebook in the `notebooks/` folder:
6969

70-
![open 0-check_your_env.ipynb](./images/open_env_check_notebook.png)
70+
![open 0-check_your_env.ipynb](./media/open_env_check_notebook.png)
7171

7272
6. Run the notebook to confirm everything is set up properly:
7373

74-
![check env](./images/env_check.png)
74+
![check env](./media/env_check.png)
7575

7676
---
7777

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: pandas_workshop
22
channels:
33
- conda-forge
4-
dependencies:
4+
dependencies:
55
- python>=3.8.0,<=3.11.10
66
- jupyterlab=3.5.2
77
- matplotlib=3.6.3
File renamed without changes.
File renamed without changes.
File renamed without changes.

media/time_is_up.m4a

22.1 KB
Binary file not shown.

slides/generate_slides.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ else
3737
--to slides \
3838
--template=$TEMPLATE_TYPE \
3939
--TemplateExporter.extra_template_basedirs="$SLIDES_DIR"/templates \
40-
--output-dir "$SLIDES_DIR"/html \
40+
--output-dir="$SLIDES_DIR"/html \
4141
"$SLIDES_DIR"/*.ipynb;
4242

4343
# delete the combined notebook

slides/html/0-introduction.slides.html

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14686,6 +14686,10 @@
1468614686
<img alt="View repo on GitHub" src="https://img.shields.io/badge/view-repo-orange?logo=github" style="max-width: 100%; margin: 20px 0 0 0;">
1468714687
</a>
1468814688
</div>
14689+
<!-- here's an alternate clock location under title
14690+
<div class="clock" style="margin-right: 5px; position: absolute; top: 40px;"></div>
14691+
-->
14692+
<div class="clock" style="margin-right: 5px; float: right;"></div>
1468914693
</div>
1469014694
<div class="slides"><section ><section >
1469114695
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
@@ -14778,7 +14782,8 @@ <h2 id="Let's-get-started">Let's get started<a class="anchor-link" href="#Let's-
1477814782
},
1477914783
[
1478014784
"https://unpkg.com/reveal.js@4.0.2/dist/reveal.js",
14781-
"https://unpkg.com/reveal.js@4.0.2/plugin/notes/notes.js"
14785+
"https://unpkg.com/reveal.js@4.0.2/plugin/notes/notes.js",
14786+
"https://cdn.jsdelivr.net/npm/luxon@3.2.1/build/global/luxon.min.js"
1478214787
],
1478314788

1478414789
function(Reveal, RevealNotes){
@@ -14801,9 +14806,7 @@ <h2 id="Let's-get-started">Let's get started<a class="anchor-link" href="#Let's-
1480114806
}
1480214807
});
1480314808

14804-
function isVisible(display) {
14805-
return display === 'none' ? 'block' : 'none';
14806-
}
14809+
const isVisible = (display) => display === 'none' ? 'block' : 'none';
1480714810

1480814811
// add custom controls
1480914812
Reveal.addKeyBinding(
@@ -14836,6 +14839,120 @@ <h2 id="Let's-get-started">Let's get started<a class="anchor-link" href="#Let's-
1483614839
}
1483714840
);
1483814841

14842+
// for working with dates/times
14843+
const { DateTime, Duration } = luxon;
14844+
14845+
// add exercise timer
14846+
let updateExerciseTimerInterval;
14847+
Reveal.addKeyBinding(
14848+
{ keyCode: 84, key: "T", description: "Start the exercise timer" },
14849+
() => {
14850+
let exerciseTime = prompt("Timer duration in minutes (leave empty to cancel)");
14851+
const cleanup = () => {
14852+
clearInterval(updateExerciseTimerInterval);
14853+
$('.exercise-timer').remove();
14854+
}
14855+
if (exerciseTime !== undefined && exerciseTime !== "" && exerciseTime !== null){
14856+
if (!isNaN(exerciseTime) && exerciseTime >= 0.1) {
14857+
exerciseTime = Duration.fromMillis(exerciseTime * 60 * 1000);
14858+
const countDownTime = DateTime.now().plus(exerciseTime);
14859+
14860+
const getTimeText = (elapsedTime) => `⏱ ${elapsedTime.toFormat('mm:ss')}`;
14861+
14862+
if ($('.exercise-timer').length === 0) { // add to DOM if not already there
14863+
$('.reveal').append(
14864+
`<aside class="exercise-timer" style="display: block; position: absolute; `
14865+
+ `top: auto; left: 15px; right: auto; bottom: 36px;`
14866+
+ `font-size: 50px;"></aside>`
14867+
);
14868+
}
14869+
14870+
// in case the key is pressed while a timer is already running, clear the old one
14871+
if (!isNaN(updateExerciseTimerInterval)) clearInterval(updateExerciseTimerInterval);
14872+
14873+
// write the starting time minus the 1 second delay before the update
14874+
const updateFrequency = 1000;
14875+
$('.exercise-timer').text(getTimeText(exerciseTime.minus({ milliseconds: updateFrequency })));
14876+
14877+
// update the remaining time each second
14878+
const updateTimer = () => {
14879+
const now = DateTime.now();
14880+
const elapsedTime = countDownTime.diff(now);
14881+
const secondsRemaining = elapsedTime.as('seconds')
14882+
14883+
if (secondsRemaining >= 1) $('.exercise-timer').text(getTimeText(elapsedTime));
14884+
else if (secondsRemaining >= 0) {
14885+
$('.exercise-timer').text(getTimeText(Duration.fromMillis(0)));
14886+
const audio = new Audio('../../media/time_is_up.m4a'); // TODO: switch to URL of hosted version
14887+
audio.play();
14888+
}
14889+
else if (secondsRemaining >= -5) $('.exercise-timer').text("TIME'S UP").css('color', 'red');
14890+
else cleanup();
14891+
}
14892+
updateExerciseTimerInterval = setInterval(updateTimer, updateFrequency);
14893+
}
14894+
}
14895+
else cleanup();
14896+
}
14897+
);
14898+
14899+
// add clock
14900+
let updateClockInterval;
14901+
Reveal.addKeyBinding(
14902+
{ keyCode: 67, key: "C", description: "Toggle the clock" },
14903+
() => {
14904+
if (!isNaN(updateClockInterval)) {
14905+
clearInterval(updateClockInterval);
14906+
$('.clock').text('');
14907+
updateClockInterval = 'cleared';
14908+
}
14909+
else {
14910+
const updateTime = () => $('.clock').text(DateTime.now().toFormat('hh:mm a'));
14911+
updateTime();
14912+
updateClockInterval = setInterval(updateTime, 1000);
14913+
}
14914+
}
14915+
);
14916+
14917+
// show clock in red when 5 minutes from desired time
14918+
let updateFMWInterval;
14919+
Reveal.addKeyBinding(
14920+
{ keyCode: 90, key: "Z", description: "Set 5-minute warning" },
14921+
() => {
14922+
const input = prompt("What is the ending time? (leave empty to clear)");
14923+
const cleanup = () => {
14924+
clearInterval(updateFMWInterval);
14925+
$('.clock').text('').css('color', 'black');
14926+
updateFMWInterval = 'cleared';
14927+
}
14928+
if (input !== undefined && input !== "" && input !== null && input.includes(':')){
14929+
14930+
if (!isNaN(updateFMWInterval)) cleanup();
14931+
14932+
const [time, ampm] = input.split(' ');
14933+
let [endHour, endMinute] = time.split(':').map((x) => parseInt(x));
14934+
const militaryTime = ampm === undefined;
14935+
let shiftHours = militaryTime ? 0 : (ampm.toLowerCase() === 'pm' ? 12 : 0);
14936+
14937+
const endAt = DateTime.now().startOf('minute').set({
14938+
hour: endHour + shiftHours,
14939+
minute: endMinute
14940+
});
14941+
const warnAt = endAt.minus({ minutes: 5 });
14942+
14943+
const checkTime = () => {
14944+
const now = DateTime.now();
14945+
if (now >= endAt) cleanup();
14946+
else if (now >= warnAt) {
14947+
$('.clock').text(now.toFormat('hh:mm a')).css('color', 'red');
14948+
}
14949+
}
14950+
updateFMWInterval = setInterval(checkTime, 1000);
14951+
}
14952+
else cleanup();
14953+
}
14954+
);
14955+
1483914956
var update = function(event){
1484014957
if(MathJax.Hub.getAllJax(Reveal.getCurrentSlide())){
1484114958
MathJax.Hub.Rerender(Reveal.getCurrentSlide());

slides/html/1-getting_started_with_pandas.slides.html

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14686,6 +14686,10 @@
1468614686
<img alt="View repo on GitHub" src="https://img.shields.io/badge/view-repo-orange?logo=github" style="max-width: 100%; margin: 20px 0 0 0;">
1468714687
</a>
1468814688
</div>
14689+
<!-- here's an alternate clock location under title
14690+
<div class="clock" style="margin-right: 5px; position: absolute; top: 40px;"></div>
14691+
-->
14692+
<div class="clock" style="margin-right: 5px; float: right;"></div>
1468914693
</div>
1469014694
<div class="slides"><section ><section >
1469114695
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
@@ -18107,7 +18111,8 @@ <h2 id="Section-1-Complete-%F0%9F%8E%89">Section 1 Complete &#127881;<a class="a
1810718111
},
1810818112
[
1810918113
"https://unpkg.com/reveal.js@4.0.2/dist/reveal.js",
18110-
"https://unpkg.com/reveal.js@4.0.2/plugin/notes/notes.js"
18114+
"https://unpkg.com/reveal.js@4.0.2/plugin/notes/notes.js",
18115+
"https://cdn.jsdelivr.net/npm/luxon@3.2.1/build/global/luxon.min.js"
1811118116
],
1811218117

1811318118
function(Reveal, RevealNotes){
@@ -18130,9 +18135,7 @@ <h2 id="Section-1-Complete-%F0%9F%8E%89">Section 1 Complete &#127881;<a class="a
1813018135
}
1813118136
});
1813218137

18133-
function isVisible(display) {
18134-
return display === 'none' ? 'block' : 'none';
18135-
}
18138+
const isVisible = (display) => display === 'none' ? 'block' : 'none';
1813618139

1813718140
// add custom controls
1813818141
Reveal.addKeyBinding(
@@ -18165,6 +18168,120 @@ <h2 id="Section-1-Complete-%F0%9F%8E%89">Section 1 Complete &#127881;<a class="a
1816518168
}
1816618169
);
1816718170

18171+
// for working with dates/times
18172+
const { DateTime, Duration } = luxon;
18173+
18174+
// add exercise timer
18175+
let updateExerciseTimerInterval;
18176+
Reveal.addKeyBinding(
18177+
{ keyCode: 84, key: "T", description: "Start the exercise timer" },
18178+
() => {
18179+
let exerciseTime = prompt("Timer duration in minutes (leave empty to cancel)");
18180+
const cleanup = () => {
18181+
clearInterval(updateExerciseTimerInterval);
18182+
$('.exercise-timer').remove();
18183+
}
18184+
if (exerciseTime !== undefined && exerciseTime !== "" && exerciseTime !== null){
18185+
if (!isNaN(exerciseTime) && exerciseTime >= 0.1) {
18186+
exerciseTime = Duration.fromMillis(exerciseTime * 60 * 1000);
18187+
const countDownTime = DateTime.now().plus(exerciseTime);
18188+
18189+
const getTimeText = (elapsedTime) => `⏱ ${elapsedTime.toFormat('mm:ss')}`;
18190+
18191+
if ($('.exercise-timer').length === 0) { // add to DOM if not already there
18192+
$('.reveal').append(
18193+
`<aside class="exercise-timer" style="display: block; position: absolute; `
18194+
+ `top: auto; left: 15px; right: auto; bottom: 36px;`
18195+
+ `font-size: 50px;"></aside>`
18196+
);
18197+
}
18198+
18199+
// in case the key is pressed while a timer is already running, clear the old one
18200+
if (!isNaN(updateExerciseTimerInterval)) clearInterval(updateExerciseTimerInterval);
18201+
18202+
// write the starting time minus the 1 second delay before the update
18203+
const updateFrequency = 1000;
18204+
$('.exercise-timer').text(getTimeText(exerciseTime.minus({ milliseconds: updateFrequency })));
18205+
18206+
// update the remaining time each second
18207+
const updateTimer = () => {
18208+
const now = DateTime.now();
18209+
const elapsedTime = countDownTime.diff(now);
18210+
const secondsRemaining = elapsedTime.as('seconds')
18211+
18212+
if (secondsRemaining >= 1) $('.exercise-timer').text(getTimeText(elapsedTime));
18213+
else if (secondsRemaining >= 0) {
18214+
$('.exercise-timer').text(getTimeText(Duration.fromMillis(0)));
18215+
const audio = new Audio('../../media/time_is_up.m4a'); // TODO: switch to URL of hosted version
18216+
audio.play();
18217+
}
18218+
else if (secondsRemaining >= -5) $('.exercise-timer').text("TIME'S UP").css('color', 'red');
18219+
else cleanup();
18220+
}
18221+
updateExerciseTimerInterval = setInterval(updateTimer, updateFrequency);
18222+
}
18223+
}
18224+
else cleanup();
18225+
}
18226+
);
18227+
18228+
// add clock
18229+
let updateClockInterval;
18230+
Reveal.addKeyBinding(
18231+
{ keyCode: 67, key: "C", description: "Toggle the clock" },
18232+
() => {
18233+
if (!isNaN(updateClockInterval)) {
18234+
clearInterval(updateClockInterval);
18235+
$('.clock').text('');
18236+
updateClockInterval = 'cleared';
18237+
}
18238+
else {
18239+
const updateTime = () => $('.clock').text(DateTime.now().toFormat('hh:mm a'));
18240+
updateTime();
18241+
updateClockInterval = setInterval(updateTime, 1000);
18242+
}
18243+
}
18244+
);
18245+
18246+
// show clock in red when 5 minutes from desired time
18247+
let updateFMWInterval;
18248+
Reveal.addKeyBinding(
18249+
{ keyCode: 90, key: "Z", description: "Set 5-minute warning" },
18250+
() => {
18251+
const input = prompt("What is the ending time? (leave empty to clear)");
18252+
const cleanup = () => {
18253+
clearInterval(updateFMWInterval);
18254+
$('.clock').text('').css('color', 'black');
18255+
updateFMWInterval = 'cleared';
18256+
}
18257+
if (input !== undefined && input !== "" && input !== null && input.includes(':')){
18258+
18259+
if (!isNaN(updateFMWInterval)) cleanup();
18260+
18261+
const [time, ampm] = input.split(' ');
18262+
let [endHour, endMinute] = time.split(':').map((x) => parseInt(x));
18263+
const militaryTime = ampm === undefined;
18264+
let shiftHours = militaryTime ? 0 : (ampm.toLowerCase() === 'pm' ? 12 : 0);
18265+
18266+
const endAt = DateTime.now().startOf('minute').set({
18267+
hour: endHour + shiftHours,
18268+
minute: endMinute
18269+
});
18270+
const warnAt = endAt.minus({ minutes: 5 });
18271+
18272+
const checkTime = () => {
18273+
const now = DateTime.now();
18274+
if (now >= endAt) cleanup();
18275+
else if (now >= warnAt) {
18276+
$('.clock').text(now.toFormat('hh:mm a')).css('color', 'red');
18277+
}
18278+
}
18279+
updateFMWInterval = setInterval(checkTime, 1000);
18280+
}
18281+
else cleanup();
18282+
}
18283+
);
18284+
1816818285
var update = function(event){
1816918286
if(MathJax.Hub.getAllJax(Reveal.getCurrentSlide())){
1817018287
MathJax.Hub.Rerender(Reveal.getCurrentSlide());

0 commit comments

Comments
 (0)