diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c87779..b6f7f91 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,21 +8,23 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v1 - name: Install node uses: actions/setup-node@v1 with: - node-version: '10.x' + node-version: '13.x' - name: Install Python uses: actions/setup-python@v1 with: - python-version: '3.7' + python-version: '3.8' architecture: 'x64' - name: Install dependencies - run: python -m pip install jupyterlab==1.2.6 + run: | + python -m pip install jupyterlab==2.1.1 + python -m pip install ipywidgets - name: Build the extension run: | jlpm && jlpm run build diff --git a/README.md b/README.md index 10256e0..cb258d8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # widget-periodictable -![Build](https://github.com/osscar-org/widget-periodictable/workflows/Build/badge.svg) +![Build](https://github.com/osscar-org/widget-periodictable/workflows/Build/badge.svg?branch=develop) A jupyter widget to select chemical elements from the periodic table. This is a update version of the widget from: @@ -14,7 +14,7 @@ https://github.com/aiidalab/aiidalab-widget-periodictable ## Usage and try it on Binder -[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/osscar-org/widget-periodictable/master?urlpath=%2Flab%2Ftree%2Fexamples%2Fintroduction.ipynb) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/osscar-org/widget-periodictable/develop?urlpath=%2Flab%2Ftree%2Fexamples%2Fintroduction.ipynb) ## Installation @@ -37,6 +37,16 @@ the nbextension: ```bash jupyter nbextension enable --py [--sys-prefix|--user|--system] widget_periodictable ``` + +## Selection and disable + +By clicking on the elements, one can select and disable the elements. The selected elements can be divided into different states with custom colors. +One can obtain the element list by state. + +```bash +PTable.get_elements_by_state(1) +``` + # Acknowlegements We acknowledge support from: diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb index f42ec16..541c571 100644 --- a/examples/introduction.ipynb +++ b/examples/introduction.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -19,12 +19,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "312dc9bc4c7645ba8711de89900c888a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "PTableWidget(noselect_color='blue', selected_colors=['red', 'green', 'yellow'], states=3)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Show the widget\n", - "widget = PTableWidget()\n", + "widget = PTableWidget(states = 3, selected_colors = ['red', 'green', 'yellow'], noselect_color='blue')\n", "widget" ] }, @@ -44,9 +59,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9f716d75f43f46e0913e2b4e19f04f04", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(Button(button_style='success', description='Get the currently selected values', layout=Layout(w…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "output = widgets.Output()\n", "\n", @@ -76,9 +106,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ba9d09402e0c43fe95a68fcb72561881", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Checkbox(value=False, description='Disable oxygen')" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "toggle_disabled = widgets.Checkbox(\n", " value=\"O\" in widget.disabled_elements,\n", @@ -114,9 +159,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "24001f1755fe492c97818b9fb70cf517", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(button_style='success', description='Select only Li and H (from python)', layout=Layout(width='300px'),…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "def on_set_from_ptyhon(event):\n", " # NOTE! If you put an element which does not exist, it will stay forever in the list, but it's ignored\n", @@ -141,9 +201,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c99467dc0c284c1b9e4bb718730c8366", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(button_style='success', description='Make noble gases bold', layout=Layout(width='300px'), style=Button…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "def get_noble_gases_state():\n", " label_deactivate = \"Make noble gases bold\"\n", @@ -188,6 +263,15 @@ "button_noble.on_click(on_toggle_noble_gases)\n", "button_noble" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This work has been done with the support of the EPFL Open Science Fund [OSSCAR](http://www.osscar.org).\n", + "\n", + "" + ] } ], "metadata": { @@ -206,7 +290,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.1" + "version": "3.8.2" } }, "nbformat": 4, diff --git a/package.json b/package.json index 9e510d2..7b44c16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "widget-periodictable", - "version": "0.3.0", + "version": "1.1.0", "description": "A jupyter widget to select chemical elements from the periodic table.", "keywords": [ "jupyter", @@ -49,7 +49,7 @@ "watch:nbextension": "webpack --watch" }, "dependencies": { - "@jupyter-widgets/base": "^1.1.10 || ^2" + "@jupyter-widgets/base": "^1.1.10 || ^2 || ^3" }, "devDependencies": { "@phosphor/application": "^1.6.0", @@ -75,7 +75,7 @@ "source-map-loader": "^0.2.4", "style-loader": "^1.0.0", "ts-loader": "^5.2.1", - "typescript": "~3.1.2", + "typescript": "~3.8", "webpack": "^4.20.2", "webpack-cli": "^3.1.2" }, diff --git a/periodictable.png b/periodictable.png index 111c574..c5364d8 100644 Binary files a/periodictable.png and b/periodictable.png differ diff --git a/src/plugin.ts b/src/plugin.ts index ed718d1..8612152 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -29,7 +29,7 @@ const examplePlugin: IPlugin, void> = { requires: [IJupyterWidgetRegistry], activate: activateWidgetExtension, autoStart: true -}; +} as unknown as IPlugin, void>; export default examplePlugin; diff --git a/src/widget.ts b/src/widget.ts index 42e0ea4..80c2785 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -22,29 +22,29 @@ import '../css/ptable.css'; // These assumptions are used both in the generation of the elementList // and in the template. var elementTable: string[][] = [ - ["H", "", "", "", "", "", "", "", "","", "", "", "", "", "", "", "", "He"], - ["Li", "Be", "", "", "", "", "", "", "","", "", "", "B", "C", "N", "O", "F", "Ne"], - ["Na", "Mg", "", "", "", "", "", "", "","", "", "", "Al", "Si", "P", "S", "Cl", "Ar"], - ["K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co","Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr"], - ["Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh","Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe"], - ["Cs", "Ba", "*", "Hf", "Ta", "W", "Re", "Os", "Ir","Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn"], - ["Fr", "Ra", "*", "Rf", "Db", "Sg", "Bh", "Hs", "Mt","Ds", "Rg", "Cn", "Nh", "Fi", "Mc", "Lv", "Ts", "Og"], - ["", "", "", "", "", "", "", "", "","", "", "", "", "", "", "", "", ""], - ["", "", "*", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu","Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu"], - ["", "", "*", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am","Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr"] + ["H", "", "", "", "", "", "", "", "","", "", "", "", "", "", "", "", "He"], + ["Li", "Be", "", "", "", "", "", "", "","", "", "", "B", "C", "N", "O", "F", "Ne"], + ["Na", "Mg", "", "", "", "", "", "", "","", "", "", "Al", "Si", "P", "S", "Cl", "Ar"], + ["K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co","Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr"], + ["Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh","Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe"], + ["Cs", "Ba", "*", "Hf", "Ta", "W", "Re", "Os", "Ir","Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn"], + ["Fr", "Ra", "*", "Rf", "Db", "Sg", "Bh", "Hs", "Mt","Ds", "Rg", "Cn", "Nh", "Fi", "Mc", "Lv", "Ts", "Og"], + ["", "", "", "", "", "", "", "", "","", "", "", "", "", "", "", "", ""], + ["", "", "*", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu","Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu"], + ["", "", "*", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am","Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr"] ]; // Flat list of elements, used for validation and cleaning up of the // selectedElements list. let elementList: string[] = []; for (let elementRow of elementTable) { - for (let elementName of elementRow) { - if ( (elementName === "") || (elementName == "*" ) ) { - } - else { - elementList.push(elementName); - } + for (let elementName of elementRow) { + if ( (elementName === "") || (elementName == "*" ) ) { } + else { + elementList.push(elementName); + } + } } export @@ -62,9 +62,9 @@ class MCPTableModel extends DOMWidgetModel { } static serializers: ISerializers = { - ...DOMWidgetModel.serializers, - // Add any extra serializers here - } + ...DOMWidgetModel.serializers, + // Add any extra serializers here + } static model_name = 'MCPTableModel'; static model_module = MODULE_NAME; @@ -77,109 +77,138 @@ class MCPTableModel extends DOMWidgetModel { export class MCPTableView extends DOMWidgetView { - // TODO: move template to external file to make it more readable, see - // http://codebeerstartups.com/2012/12/how-to-improve-templates-in-backbone-js-learning-backbone-js/ - tableTemplate = _.template( '<% for (let elementRow of elementTable) { print("
"); for (let elementName of elementRow) { if ( (elementName === "") || (elementName == "*" ) ) { %>' + ' <%= elementName %>' + '<% } else { %>' + ' noselect element-<%= elementName %><% if (selectedElements.includes(elementName) && (! disabledElements.includes(elementName)) ) { print(" elementOn"); } %>"><% print(displayNamesReplacements[elementName] || elementName); %>' + '<% } }; print("
"); } %>'); - - render() { - // I render the widget - this.rerenderScratch(); - // I bind on_change events - this.model.on('change:selected_elements', this.rerenderScratch, this); - this.model.on('change:disabled_elements', this.rerenderScratch, this); - this.model.on('change:display_names_replacements', this.rerenderScratch, this); - } + // TODO: move template to external file to make it more readable, see + // http://codebeerstartups.com/2012/12/how-to-improve-templates-in-backbone-js-learning-backbone-js/ + tableTemplate = _.template( '<% for (let elementRow of elementTable)'+ + ' { print("
"); for (let elementName of elementRow)'+ + ' { if ( (elementName === "") || (elementName == "*" ) ) { %>' + + ' <%= elementName %>' + '<% } else { %>' + + ' '+ + ' noselect element-<%= elementName %><% if (selectedElements.includes(elementName) && ' + + '(! disabledElements.includes(elementName)) ) { print(" elementOn"); } %>" '+ + 'style="background-color: <% if (disabledElements.includes(elementName)) {print(disabledColor)}' + + 'else if (selectedElements.includes(elementName)) { i = selectedElements.indexOf(elementName); print(selectedColors[selectedStates[i]]);} else{print(noselectColor)} %>" '+ + 'title="state: <% if (selectedElements.includes(elementName)) { i = selectedElements.indexOf(elementName); print(selectedStates[i]);} '+ + 'else if (disabledElements.includes(elementName)){print("disabled");} else {print("noselcted");} %>" ><% '+ + 'print(displayNamesReplacements[elementName] || elementName); %>' + + '<% } }; print("
"); } %>'); + + render() { + // I render the widget + this.rerenderScratch(); + // I bind on_change events + this.model.on('change:selected_elements', this.rerenderScratch, this); + this.model.on('change:selected_states', this.rerenderScratch, this); + this.model.on('change:disabled_elements', this.rerenderScratch, this); + this.model.on('change:display_names_replacements', this.rerenderScratch, this); + } - events(): {[e: string]: string} { - return {"click .periodic-table-entry": "toggleElement", "click .periodic-table-disabled": "toggleElement"}; -} + events(): {[e: string]: string} { + return {"click .periodic-table-entry": "toggleElement", "click .periodic-table-disabled": "toggleElement"}; + } - toggleElement(event: any) { - let classNames: string[] = _.map(event.target.classList, function(a: string){return a}); - let elementName: string = _.chain(classNames) - .filter(function(a: any){return a.startsWith('element-')}) - .map(function(a: any){return a.slice("element-".length);}) - .first() - .value(); - - var isOn = _.includes(classNames, 'elementOn'); - var isDisabled = _.includes(classNames, "periodic-table-disabled"); - // If this button is disabled, do not do anything - // (Actually, this function should not be triggered if the button - // is disabled, this is just a safety measure) - // if (isDisabled) return; - - // Check if we understood which element we are - if (typeof elementName !== 'undefined') { - var currentList = this.model.get('selected_elements'); - var currentDisabledList = this.model.get('disabled_elements'); - // NOTE! it is essential to duplicate the list, - // otherwise the value will not be updated. - var newList = currentList.slice(); - var newDisabledList = currentDisabledList.slice(); - - if (isOn) { - // remove the element from the selected_elements - newList = _.without(newList, elementName); - newDisabledList.push(elementName); - // Swap CSS state - event.target.classList.remove('elementOn'); - } - else if (isDisabled) { - newDisabledList = _.without(newDisabledList, elementName); - event.target.classList.remove('periodic-table-disabled'); - } - else { - // add the element from the selected_elements - newList.push(elementName); - // Swap CSS state - event.target.classList.add('elementOn'); - } - - // Update the model (send back data to python) - this.model.set('selected_elements', newList); - this.model.set('disabled_elements', newDisabledList); - this.touch(); + toggleElement(event: any) { + let classNames: string[] = _.map(event.target.classList, function(a: string){return a}); + let elementName: string = _.chain(classNames) + .filter(function(a: any){return a.startsWith('element-')}) + .map(function(a: any){return a.slice("element-".length);}) + .first() + .value(); + + var isOn = _.includes(classNames, 'elementOn'); + // If this button is disabled, do not do anything + // (Actually, this function should not be triggered if the button + // is disabled, this is just a safety measure) + // if (isDisabled) return; + + let states = this.model.get("states"); + + // Check if we understood which element we are + if (typeof elementName !== 'undefined') { + var currentList = this.model.get('selected_elements'); + var currentStatesList = this.model.get('selected_states'); + // NOTE! it is essential to duplicate the list, + // otherwise the value will not be updated. + var newList = currentList.slice(); + var newStatesList = currentStatesList.slice(); + var num = newList.indexOf(elementName); + + if (isOn) { + // remove the element from the selected_elements + + if (newStatesList[num] < states -1){ + newStatesList[num]++; } - } - - rerenderScratch() { -// Re-render full widget when the list of selected elements -// changed from python - var selectedElements = this.model.get('selected_elements'); - var disabledElements = this.model.get('disabled_elements'); - var newSelectedElements = selectedElements.slice(); - -// Here I want to clean up the two elements lists, to avoid -// to have unknown elements in the selectedElements, and -// to remove disabledElements from the selectedElements list. -// I use s variable to check if anything changed, so I send -// back the data to python only if needed - - var selectedElementsLength = newSelectedElements.length; -// Remove disabled elements from the selectedElements list -// newSelectedElements = _.difference(newSelectedElements, disabledElements); -// Remove unknown elements from the selectedElements list -// newSelectedElements = _.intersection(newSelectedElements, elementList); - var changed = newSelectedElements.length != selectedElementsLength; - -// call the update (to python) only if I actually removed/changed -// something - if (changed) { -// Make a copy before setting - this.model.set('selected_elements', newSelectedElements); - this.touch(); + else{ + newList = _.without(newList, elementName); + newStatesList.splice(num, 1); + // Swap CSS state + event.target.classList.remove('elementOn'); } + } + else { + // add the element from the selected_elements + newList.push(elementName); + newStatesList.push(0); + // Swap CSS state + event.target.classList.add('elementOn'); + } + + // Update the model (send back data to python) + this.model.set('selected_elements', newList); + this.model.set('selected_states', newStatesList); + this.touch(); + } + } -// Render the full widget using the template - this.el.innerHTML = '
' + - this.tableTemplate({ - elementTable: elementTable, - displayNamesReplacements: this.model.get('display_names_replacements'), - selectedElements: newSelectedElements, - disabledElements: disabledElements - }) + - '
'; + rerenderScratch() { + // Re-render full widget when the list of selected elements + // changed from python + var selectedElements = this.model.get('selected_elements'); + var disabledElements = this.model.get('disabled_elements'); + var disabledColor = this.model.get('disabled_color'); + var noselectColor = this.model.get('noselect_color'); + var selectedColors = this.model.get('selected_colors'); + var selectedStates = this.model.get('selected_states'); + var newSelectedElements = selectedElements.slice(); + var newSelectedColors = selectedColors.slice(); + var newSelectedStates = selectedStates.slice(); + + // Here I want to clean up the two elements lists, to avoid + // to have unknown elements in the selectedElements, and + // to remove disabledElements from the selectedElements list. + // I use s variable to check if anything changed, so I send + // back the data to python only if needed + + var selectedElementsLength = newSelectedElements.length; + // Remove disabled elements from the selectedElements list + // newSelectedElements = _.difference(newSelectedElements, disabledElements); + // Remove unknown elements from the selectedElements list + // newSelectedElements = _.intersection(newSelectedElements, elementList); + var changed = newSelectedElements.length != selectedElementsLength; + + // call the update (to python) only if I actually removed/changed + // something + if (changed) { + // Make a copy before setting + this.model.set('selected_elements', newSelectedElements); + this.touch(); } + // Render the full widget using the template + this.el.innerHTML = '
' + + this.tableTemplate({ + elementTable: elementTable, + displayNamesReplacements: this.model.get('display_names_replacements'), + selectedElements: newSelectedElements, + disabledElements: disabledElements, + disabledColor: disabledColor, + noselectColor: noselectColor, + selectedColors: newSelectedColors, + selectedStates: newSelectedStates + }) + + '
'; + } + } diff --git a/version_check.py b/version_check.py index 447e6bb..9c22aac 100644 --- a/version_check.py +++ b/version_check.py @@ -1,9 +1,11 @@ from setupbase import get_version from os.path import join as pjoin -import json +import json +from widget_periodictable._frontend import module_version name = 'widget_periodictable' +module_version = module_version[1:] version_py = get_version(pjoin(name, '_version.py')) with open('package.json') as json_file: @@ -11,9 +13,8 @@ version_npm = data['version'] -if version_py != version_npm : +if version_py != version_npm or module_version != version_npm: raise ValueError('The version number are NOT equal') else: print(version_py) - print('Check fine for the version number') diff --git a/widget_periodictable/_frontend.py b/widget_periodictable/_frontend.py index 1a3cf2c..b4b66d5 100644 --- a/widget_periodictable/_frontend.py +++ b/widget_periodictable/_frontend.py @@ -9,4 +9,4 @@ """ module_name = "widget-periodictable" -module_version = "^0.1.0" +module_version = "^1.1.0" diff --git a/widget_periodictable/_version.py b/widget_periodictable/_version.py index f44ab0e..347eabc 100644 --- a/widget_periodictable/_version.py +++ b/widget_periodictable/_version.py @@ -4,5 +4,5 @@ # Copyright (c) Giovanni Pizzi and Dou Du. # Distributed under the terms of the Modified BSD License. -version_info = (0, 3, 0) +version_info = (1, 1, 0) __version__ = ".".join(map(str, version_info)) diff --git a/widget_periodictable/periodic_table.py b/widget_periodictable/periodic_table.py index 116ca8d..1248d91 100644 --- a/widget_periodictable/periodic_table.py +++ b/widget_periodictable/periodic_table.py @@ -9,10 +9,9 @@ """ from ipywidgets import DOMWidget -from traitlets import Unicode, Int, List, Dict +from traitlets import Unicode, Int, List, Dict from ._frontend import module_name, module_version - class PTableWidget(DOMWidget): """TODO: Add docstring here """ @@ -25,4 +24,26 @@ class PTableWidget(DOMWidget): selected_elements = List([]).tag(sync=True) disabled_elements = List([]).tag(sync=True) display_names_replacements = Dict({}).tag(sync=True) + disabled_color = Unicode('gray').tag(sync=True) + noselect_color = Unicode('pink').tag(sync=True) + states = Int(1).tag(sync=True) + selected_states = List([]).tag(sync=True) + selected_colors = List([]).tag(sync=True) + + + def __init__(self, states = 1, disabled_color = 'gray', noselect_color = 'pink', selected_colors = ["#a6cee3", "#b2df8a", "#fdbf6f", "#6a3d9a", "#b15928", "#e31a1c", "#1f78b4", "#33a02c", "#ff7f00", "#cab2d6", "#ffff99"]): + super(PTableWidget, self).__init__() + self.states = states + self.disabled_color = disabled_color + self.noselect_color = noselect_color + self.selected_colors = selected_colors + + if len(selected_colors) < states: + self.selected_colors = selected_colors + ["#a6cee3", "#b2df8a", "#fdbf6f", "#6a3d9a", "#b15928", "#e31a1c", "#1f78b4", "#33a02c", "#ff7f00", "#cab2d6", "#ffff99"] + def get_elements_by_state(self, state): + x = []; + for i, j in enumerate(self.selected_states): + if j == state: + x.append(self.selected_elements[i]) + return x