diff --git a/examples/widgets.ipynb b/examples/widgets.ipynb index 07f7e48..fc01ac8 100644 --- a/examples/widgets.ipynb +++ b/examples/widgets.ipynb @@ -1,13 +1,38 @@ { "cells": [ { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "id": "8ea1654d-6607-46dc-ad2f-60d58b451741", "metadata": {}, - "outputs": [], "source": [ - "library(\"ywidgets\")" + "# Simple Widgets" + ] + }, + { + "cell_type": "markdown", + "id": "4e52fba0-5678-47bf-bd14-ff64e0b09077", + "metadata": {}, + "source": [ + "A *widget* is an object whose attributes are tracked and can be modified externally.\n", + "Typically, these attributes can be modified by the user through the Jupyter UI.\n", + "With this library, they can also be modified by another user.\n", + "The [Yrs](https://github.com/y-crdt/y-crdt) library and its R bindings\n", + "[Yr](https://github.com/y-crdt/yr) are used to manage the widget state and manage\n", + "concurrent modifications via a `yr::Doc`.\n", + "\n", + "To define a widget, we first need to create a class that will hold the attributes.\n", + "In the case of a Jupyter widget, we use `make_comm_widget`.\n", + "Comm widget are widgets that synchronize their state over a Jupyter Comm channel,\n", + "but all we need to know is that they are what should be used in Jupyter." + ] + }, + { + "cell_type": "markdown", + "id": "6390bdd0-1536-4968-893a-a7b42b092576", + "metadata": {}, + "source": [ + "## A minimal slider\n", + "Let's create a slider class:" ] }, { @@ -17,7 +42,7 @@ "metadata": {}, "outputs": [], "source": [ - "IntSlider <- make_comm_widget(\n", + "IntSlider <- ywidgets::make_comm_widget(\n", " \"IntSlider\",\n", " value = 50L,\n", " min = 0L,\n", @@ -26,6 +51,14 @@ ")" ] }, + { + "cell_type": "markdown", + "id": "96a9461a-7b08-4f20-92ee-93949fd80d29", + "metadata": {}, + "source": [ + "And creating an instance of that class let us display the actual slider" + ] + }, { "cell_type": "code", "execution_count": null, @@ -37,6 +70,27 @@ "s" ] }, + { + "cell_type": "markdown", + "id": "8f84fb24-fe50-48da-801d-ff226cd1fb44", + "metadata": {}, + "source": [ + "This class has some special functions called `mime_types` and `mime_bundle` that are use to tell Jupyter how to display it.\n", + "The slider wideget did not automatically acquire a UI.\n", + "These functions returned information that Jupyter could match with the [Yjs-widgets](https://github.com/QuantStack/yjs-widgets/) frontend extension to display something.\n", + "In particular, the class name `IntSlider` matches one of the few classes in Yjs-widget meant for testing that is available.\n", + "It handles the user interactions and communications with our R part of the widget" + ] + }, + { + "cell_type": "markdown", + "id": "332f30f7-a77f-4d8f-af88-8ccdc1dec711", + "metadata": {}, + "source": [ + "You can try to change the value of the slider in the UI above with your mouse, and revaluate the the cell below every time.\n", + "You will see a different value every time matching the slider UI." + ] + }, { "cell_type": "code", "execution_count": null, @@ -47,6 +101,14 @@ "s$value" ] }, + { + "cell_type": "markdown", + "id": "03f46546-2153-470d-864e-206c5d49120f", + "metadata": {}, + "source": [ + "The other way around, if you modify the local R widget, you will see the slider UI change." + ] + }, { "cell_type": "code", "execution_count": null, @@ -57,6 +119,16 @@ "s$value = 0" ] }, + { + "cell_type": "markdown", + "id": "230d21e6-fb58-4b9f-8942-2441b6923915", + "metadata": {}, + "source": [ + "A given slider in the UI is tied to a specific object (not class) in R.\n", + "For instance creating a second object produce a similar looking slider in the UI, but its state value and visiual representation differ.\n", + "We can also take this opportunity to show that `new` can override the default values." + ] + }, { "cell_type": "code", "execution_count": null, @@ -64,10 +136,20 @@ "metadata": {}, "outputs": [], "source": [ - "s2 <- IntSlider$new()\n", + "s2 <- IntSlider$new(value = 2, max = 5)\n", "s2" ] }, + { + "cell_type": "markdown", + "id": "c4ad9c7c-f3de-4fc3-a32a-01afef72baff", + "metadata": {}, + "source": [ + "On the contrary, displaying the first widget object again shows the same slider value as the first one in the UI.\n", + "They *are* the same widget, and changing one will change all their representation.\n", + "For instance, mouving the slider below with your mouse will also modify the first one." + ] + }, { "cell_type": "code", "execution_count": null, @@ -78,6 +160,15 @@ "s" ] }, + { + "cell_type": "markdown", + "id": "78995b86-c05c-4902-807e-607220a2a7eb", + "metadata": {}, + "source": [ + "## Another Text widget\n", + "This time we make another widget that displays a `textarea` to let a user freely enter text." + ] + }, { "cell_type": "code", "execution_count": null, @@ -85,7 +176,7 @@ "metadata": {}, "outputs": [], "source": [ - "Textarea <- make_comm_widget(\n", + "Textarea <- ywidgets::make_comm_widget(\n", " \"Textarea\",\n", " value = \"\",\n", " rows = 0L,\n", @@ -124,6 +215,323 @@ "source": [ "t$value <- \"Hello world!\"" ] + }, + { + "cell_type": "markdown", + "id": "6fab0037-1c88-442d-abe5-d99420285a9b", + "metadata": {}, + "source": [ + "## Reacting to user events\n", + "We can register a function to be called every time the user interacts with our slider.\n", + "This is useful to redraw a plot, or run some computation based on user-defined values.\n", + "To do this we use the `connect` method to compute the Fibonacci number of the slider's value." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc072682-59c6-4909-8a75-855854badbb5", + "metadata": {}, + "outputs": [], + "source": [ + "fibonacci <- function(n) {\n", + " if (n <= 0) return(NA)\n", + " if (n == 1) return(0)\n", + " if (n == 2) return(1)\n", + " a <- 0\n", + " b <- 1\n", + " for (i in 3:n) {\n", + " temp <- a + b\n", + " a <- b\n", + " b <- temp\n", + " }\n", + " return(b)\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "b044b603-989f-4395-8f77-f5f7a9e03a56", + "metadata": {}, + "source": [ + "Below, we use `value` as a parameter to `connect` to state that we want to listen to the changes on the `value` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34330600-d5fd-4d16-8cb4-795a5a2a5e4b", + "metadata": {}, + "outputs": [], + "source": [ + "s3 <- IntSlider$new()\n", + "s3$connect(\n", + " value = function(n) cat(paste0(\"fibonacci(\", n, \") = \", fibonacci(n), \"\\n\"))\n", + ")\n", + "s3" + ] + }, + { + "cell_type": "markdown", + "id": "7b7258d7-40d2-49a9-b1ae-86113812a099", + "metadata": {}, + "source": [ + "`value` is only a convention for `IntSlider` used in Yjs-widgets but does not hold special meaning for r-ywidgets.\n", + "We could also have listened on `min`, `max`, and `step`." + ] + }, + { + "cell_type": "markdown", + "id": "8b911077-782a-40d1-9e0b-61ad727d0570", + "metadata": {}, + "source": [ + "## Complex data type" + ] + }, + { + "cell_type": "markdown", + "id": "737b4c1d-c2f2-499a-b2e0-86295295235a", + "metadata": {}, + "source": [ + "More complex data types such as `Array` and `Map` can be tracked as widget attributes.\n", + "Because our widget library is backed by [CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type),\n", + "we need to use the CRDT types from [Yr](https://github.com/y-crdt/yr).\n", + "\n", + "Under the hood, the integers used in the previous slider were transformed as an `Any` object.\n", + "The `IntSlider` definition is a convenience for:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9fa9f926-2698-4805-a402-a5aa5c3b1474", + "metadata": {}, + "outputs": [], + "source": [ + "IntSlider <- ywidgets::make_comm_widget(\n", + " \"IntSlider\",\n", + " value = yr::Prelim$any(50L),\n", + " min = yr::Prelim$any(0L),\n", + " max = yr::Prelim$any(100L),\n", + " step = yr::Prelim$any(1L)\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "e2c3c35f-76b1-4ba0-9565-98be97d866a2", + "metadata": {}, + "source": [ + "`Prelim` stands for preliminary. It is used to create an object that will be inserted into a CRDT (such as the widget's root `yr::Doc`) and be explicit about how it should be handled.\n", + "\n", + "Types (and their `Prelim`) that will be of particular interest are `Array`, `Map` and `Text`.\n", + "Let's use them to create a new widget.\n", + "Because we do not have a frontend representation for this widget, we will only interact with it via R." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77c3bc6e-5376-46b9-8755-8a389ef608a4", + "metadata": {}, + "outputs": [], + "source": [ + "Dummy <- ywidgets::make_comm_widget(\n", + " \"Dummy\",\n", + " sequence = yr::Prelim$array(list(7, 8, 9)),\n", + " number = yr::Prelim$any(33)\n", + ")\n", + "d <- Dummy$new()" + ] + }, + { + "cell_type": "markdown", + "id": "fc9c0741-92cc-42fc-8b49-9b919ca3f8c3", + "metadata": {}, + "source": [ + "The `Prelim` is converted to a CRDT and the `sequence` attribute is of type `ArrayRef`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1c3c417b-f1af-4b40-ab96-22dd669d001e", + "metadata": {}, + "outputs": [], + "source": [ + "d$sequence" + ] + }, + { + "cell_type": "markdown", + "id": "d2fb155e-3ff1-4e7b-9958-df309b25edb4", + "metadata": {}, + "source": [ + "To use the array methods such as `get`, `insert` *etc*, we need to use a slightly more complex syntax.\n", + "The reason for this is that to interact with the `yr::Doc` we need most of the time to pass a `Transaction`.\n", + "In short, this is a record that is used to tracks changes to the CRDT, but we don't really need to know more, simply how it interact with the widget API.\n", + "\n", + "There are two way to get a transaction, `with_read` for readonly access and `with_write` for modifications.\n", + "These function will create a transaction and manage mandatory cleanup after it is no longer needed.\n", + "For instance to `get` the element at position `1`, we can use the following." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "902c2ce6-407d-468a-8ca7-3383412ee8ef", + "metadata": {}, + "outputs": [], + "source": [ + "sequence_elem_1 <- d$with_read(\n", + " function(transaction) { d$sequence$get(transaction, 1) }\n", + ")\n", + "sequence_elem_1" + ] + }, + { + "cell_type": "markdown", + "id": "6348088b-7430-4b0c-8c69-0a840ea2cfa8", + "metadata": {}, + "source": [ + "Modifying the sequence is similar. Because arrays are also containers, we need to specify what types of value to insert." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ea06803-ae61-41d7-a8ce-96b0980783a5", + "metadata": {}, + "outputs": [], + "source": [ + "# Insert an element and retrieve the length\n", + "d$with_write(function(t) {\n", + " d$sequence$insert(t, 1, yr::Prelim$any(33))\n", + " d$sequence$len(t)\n", + "})" + ] + }, + { + "cell_type": "markdown", + "id": "a60329cb-f980-401a-b323-0db4b4069295", + "metadata": {}, + "source": [ + "Previously, we were able to modify the slider value using a much simpler and familiar syntax.\n", + "\n", + "```r\n", + "d$number <- 42\n", + "```\n", + "\n", + "In reality, a transaction is also involved and managed automatically.\n", + "This syntaxic sugar is possible for whole value replacement, but not methods.\n", + "It is somehow equivalent to the following (fictive) code.\n", + "\n", + "```r\n", + "d$with_write(function(t) {\n", + " d$number$set(t, 42) # set does not really exist\n", + "})\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "9abe2753-9fab-4115-9d77-605627cc4695", + "metadata": {}, + "source": [ + "## Any vs CRDT" + ] + }, + { + "cell_type": "markdown", + "id": "43d70442-f3da-40b1-b554-4f09e09af4fa", + "metadata": {}, + "source": [ + "In the previous section, we have used `yr::Prelim$array` to create an `ArrayRef` but we did not mention that we can also create an array inside an `Any` (with `yr::Prelim$any(list(7, 8, 9)`).\n", + "Similarly, for a string, we can choose between a `Text` or a string stored inside an `Any`.\n", + "\n", + "`Text`, `Array` and `Map` are the three main types that can be represented as an `Any` or a CRDT.\n", + "Let us now explain how to choose between one or the other.\n", + "\n", + "With a rich CRDT `Text` object, everything happens as we are used to when collaborating a shared document online.\n", + "Modifications from both users are taken into account during the merge.\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
User 1User 2
Synchronization
Hello PaulHello Paul
\n", + " Hello Paul, how are you?
\n", + " insert \", how are you?\" at end\n", + "
Hello Paul
Hello Paul, how are you?\n", + " Hello Paul
\n", + " delete \"Hello \" at start\n", + "
Synchronization
Paul, how are you?Paul, how are you?
\n", + "\n", + "On the contrary, using a string inside `Any`, the whole string is considered as a single entity that is replaced as a whole.\n", + "If multiple changes happen between synchronization, the last one will win.\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
User 1User 2
Synchronization
Hello PaulHello Paul
\n", + " Hello Paul, how are you?
\n", + " insert \", how are you?\" at end\n", + "
Hello Paul
Hello Paul, how are you?\n", + " Hello Paul
\n", + " delete \"Hello \" at start\n", + "
Synchronization
PaulPaul
\n", + "\n", + "So choosing between a rich `Text` or an `Any` string is a matter of whether we are interested in the changes that happen inside the string.\n", + "For something like a paragraph, `Text` is appropriate to merge all changes.\n", + "On the contrary, if we use a string to represent a single word like a tag, then inner changes do not matter. Whole string substitution is not only more appropriate but also has smaller footprint." + ] } ], "metadata": {