From 500cc19537e8d0c028ef66d9d74dd66b4538cc4e Mon Sep 17 00:00:00 2001 From: Dou Du Date: Sun, 10 Mar 2024 15:43:43 +0100 Subject: [PATCH] add all anywidget file --- .gitignore | 9 + README.md | 5 + examples/introduction.ipynb | 339 +++++++++++++++++++++++++++ js/widget.css | 68 ++++++ js/widget.js | 257 ++++++++++++++++++++ package.json | 15 ++ pyproject.toml | 31 +++ src/widget_periodictable/__init__.py | 139 +++++++++++ src/widget_periodictable/utils.py | 166 +++++++++++++ 9 files changed, 1029 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 examples/introduction.ipynb create mode 100644 js/widget.css create mode 100644 js/widget.js create mode 100644 package.json create mode 100644 pyproject.toml create mode 100644 src/widget_periodictable/__init__.py create mode 100644 src/widget_periodictable/utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0873457 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules +.venv +dist + +# Python +__pycache__ +.ipynb_checkpoints + +src/widget_periodictable/static diff --git a/README.md b/README.md new file mode 100644 index 0000000..da857da --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# widget_periodictable + +```sh +pip install widget_periodictable +``` diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb new file mode 100644 index 0000000..b216a5b --- /dev/null +++ b/examples/introduction.ipynb @@ -0,0 +1,339 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The widget periodic table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "from widget_periodictable import PTableWidget\n", + "from ipywidgets import Layout" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualize the element grid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Show the widget\n", + "widget = PTableWidget(states = 3, selected_elements = {\"C\": 0, \"Si\": 1, \"Ge\": 2}, \n", + " selected_colors = ['red', 'green', 'cyan'], \n", + " disabled_elements = ['B', 'Al', 'Ga'],\n", + " unselected_color='blue', border_color = 'white', width = '40px')\n", + "display(widget)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set the states of the elements\n", + "\n", + "The periodic table allows users to customize the states of the selected elements. \n", + "If one do not give the selected element's state, it will set the state as zero." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Init the selected elements by using a dictionary:\n", + "\n", + "```python\n", + "widget.selected_elements = {\"La\": 0, \"Ce\": 1, \"Pr\": 2}\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widget.selected_elements = {\"La\": 0, \"Ce\": 1, \"Pr\": 2}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Change or set element state by:\n", + "\n", + "```python\n", + "widget.set_element_state(\"Nd\",0)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widget.set_element_state(\"Nd\",0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, you cannot use widget.selected_elements[\"Nd\"] = 1 to set the states of the elements." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get the elements have the same state:\n", + "\n", + "```python\n", + "widget.get_elements_by_state(0)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widget.get_elements_by_state(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get the selected values in python" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Check which elements are currently selected" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "output = widgets.Output()\n", + "\n", + "def on_get_in_python(event):\n", + " output.clear_output()\n", + " with output:\n", + " print(\n", + " \"Currently selected values:\", \n", + " widget.selected_elements)\n", + "\n", + "button2 = widgets.Button(\n", + " description=\"Get the currently selected values\", \n", + " button_style='success',\n", + " layout={'width': '300px'}\n", + ")\n", + "button2.on_click(on_get_in_python)\n", + "vbox = widgets.VBox([button2, output])\n", + "vbox" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Play with enabling/disabling some elements" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "toggle_disabled = widgets.Checkbox(\n", + " value=\"O\" in widget.disabled_elements,\n", + " description='Disable oxygen',\n", + " disabled=False\n", + ")\n", + "\n", + "def on_change_disabled(event):\n", + " if toggle_disabled.value:\n", + " # It's set, meaning we want to disable oxygen\n", + " widget.disabled_elements = [\"O\"]\n", + " else:\n", + " widget.disabled_elements = []\n", + "toggle_disabled.observe(on_change_disabled, names='value')\n", + "\n", + "def on_change(event):\n", + " \"\"\"\n", + " Update the toggle value if manually changing the disabled_elements list.\n", + " \"\"\"\n", + " toggle_disabled.value = \"O\" in widget.disabled_elements\n", + "widget.observe(on_change, names='disabled_elements', type='change') \n", + " \n", + "toggle_disabled" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Freeze (disable clicking events) the whole periodic table " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "toggle_freeze = widgets.Checkbox(\n", + " value= False,\n", + " description='Freeze periodic table:',\n", + " disabled=False\n", + ")\n", + "\n", + "def on_freeze_change(event):\n", + " widget.disabled = toggle_freeze.value\n", + " \n", + "toggle_freeze.observe(on_freeze_change, names='value')\n", + "\n", + "toggle_freeze" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set the selected values from python\n", + "Choose the selected values from python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def on_set_from_python(event):\n", + " # NOTE! If you put an element which does not exist, it will stay forever in the list, but it's ignored\n", + " widget.selected_elements = {\"Li\":0, \"H\":0}\n", + "\n", + "button = widgets.Button(\n", + " description=\"Select only Li and H (from python)\", \n", + " button_style='success',\n", + " layout={'width': '300px'}\n", + ")\n", + "button.on_click(on_set_from_python)\n", + "button" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Change the displayed string for some elements\n", + "Note that you should pass valid HTML strings, as they will not be escaped. On the other hand this allows to use HTML to change the class, color, ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_noble_gases_state():\n", + " label_deactivate = \"Make noble gases bold\"\n", + " label_activate = \"Make noble gases not bold\"\n", + " def deactivate_noble_gases(event):\n", + " widget.display_names_replacements = {}\n", + " def activate_noble_gases(event):\n", + " widget.display_names_replacements = {\n", + " elem_name: \"{}\".format(elem_name)\n", + " for elem_name in ['He', 'Ne', 'Ar', 'Kr', 'Xe', 'Rn', 'Og']\n", + " }\n", + "\n", + " if 'He' in widget.display_names_replacements:\n", + " return {\n", + " 'is_active': True,\n", + " 'toggler_function': deactivate_noble_gases,\n", + " 'toggled_label': label_deactivate,\n", + " 'current_label': label_activate\n", + " }\n", + " else:\n", + " return {\n", + " 'is_active': True,\n", + " 'toggler_function': activate_noble_gases,\n", + " 'toggled_label': label_activate,\n", + " 'current_label': label_deactivate\n", + " }\n", + "\n", + "button_noble = widgets.Button(\n", + " description=get_noble_gases_state()['current_label'], \n", + " button_style='success',\n", + " layout={'width': '300px'}\n", + ")\n", + " \n", + "def on_toggle_noble_gases(event):\n", + " \"\"\"Toggle the state of the button and of the .\"\"\"\n", + " state = get_noble_gases_state() \n", + " # Change the table\n", + " state['toggler_function'](event)\n", + " # Change the button description\n", + " button_noble.description = state['toggled_label']\n", + " \n", + "button_noble.on_click(on_toggle_noble_gases)\n", + "button_noble" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" + }, + "voila": { + "authors": "Dou Du, Casper Welzel Andersen and Giovanni Pizzi" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/js/widget.css b/js/widget.css new file mode 100644 index 0000000..9ea7a37 --- /dev/null +++ b/js/widget.css @@ -0,0 +1,68 @@ +.periodic-table-entry { + border: 1px solid; + border-color: #cc7777; + border-radius: 3px; + width: 38px; + height: 38px; + display: table-cell; + text-align: center; + vertical-align: middle; + background-color: #ffaaaa; +} + +.periodic-table-disabled { + border-radius: 3px; + width: 38px; + height: 38px; + display: table-cell; + text-align: center; + vertical-align: middle; + background-color: #999999; +} + +.periodic-table-empty { + border: 0px; + width: 38px; + height: 38px; + display: table-cell; + text-align: center; + vertical-align: middle; +} + +.periodic-table-row { + display: table-row; +} + +.periodic-table-body { + display: table; + border-spacing: 4px; + margin: 10px; +} + +.periodic-table-entry:hover { + background-color: #cc7777; + transform: scale(1.4); +} + +.periodic-table-entry.elementOn { + background-color: #aaaaff; + border: 1px solid #7777cc; + border-radius: 4px; +} + + +.periodic-table-entry.elementOn:hover { + background-color: #7777cc; + border: 1px solid #7777cc; + border-radius: 4px; +} + +.noselect { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome and Opera */ + } diff --git a/js/widget.js b/js/widget.js new file mode 100644 index 0000000..d2fc0e5 --- /dev/null +++ b/js/widget.js @@ -0,0 +1,257 @@ +import * as _ from 'underscore'; +import $ from 'jquery'; +import './widget.css'; + +const elementTable = [ + ['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']]; + +const tableTemplate = _.template( + '<% for (let elementRow of elementTable) { ' + + 'print("
"); ' + + 'for (let elementName of elementRow) { ' + + 'if ( (elementName === "") || (elementName == "*" ) || (elementName == "#" ) ) { %>' + + ' <%= elementName %>' + + '<% } else { %>' + + ' ' + + ' noselect element-<%= elementName %><% if (selectedElements.includes(elementName) && (! disabledElements.includes(elementName)) ) { print(" elementOn"); } %>" ' + + 'style="width: <%= elementWidth %>; height: <%= elementWidth %>; ' + + 'border-color: <% if (disabled) { colors = borderColor.replace(/[^\\d,]/g, "").split(","); ' + + 'red = Math.round(255 - 0.38 * ( 255 - parseInt(colors[0], 10) )); ' + + 'green = Math.round(255 - 0.38 * ( 255 - parseInt(colors[1], 10) )); ' + + 'blue = Math.round(255 - 0.38 * ( 255 - parseInt(colors[2], 10) )); ' + + 'print("rgb(" + red.toString(10) + "," + green.toString(10) + "," + blue.toString(10) + ")"); ' + + '} else { print(borderColor); } %>; ' + + 'background-color: <% if (disabledElements.includes(elementName)) { color = disabledColor; } ' + + 'else if (selectedElements.includes(elementName)) { ' + + 'i = selectedElements.indexOf(elementName); color = selectedColors[selectedStates[i]]; ' + + '} else { color = unselectedColor; } ' + + 'if (disabled) { colors = color.replace(/[^\\d,]/g, "").split(","); ' + + 'red = Math.round(255 - 0.38 * ( 255 - parseInt(colors[0], 10) )); ' + + 'green = Math.round(255 - 0.38 * ( 255 - parseInt(colors[1], 10) )); ' + + 'blue = Math.round(255 - 0.38 * ( 255 - parseInt(colors[2], 10) )); ' + + 'print("rgb(" + red.toString(10) + "," + green.toString(10) + "," + blue.toString(10) + ")"); ' + + '} else { print(color); } %>"' + + // 'title="state: <% if (selectedElements.includes(elementName)) { i = selectedElements.indexOf(elementName); print(selectedStates[i]);} '+ + // 'else if (disabledElements.includes(elementName)){print("disabled");} else {print("unselected");} %>" ><% '+ + '><% print(displayNamesReplacements[elementName] || elementName); %>' + + '<% } }; print("
"); } %>', +); + +const elementList = []; +for (const elementRow of elementTable) { + for (const elementName of elementRow) { + if (elementName === '' || elementName === '*') { + continue; + } else { + elementList.push(elementName); + } + } +} + +function render({ model, el }) { + rerenderScratch({ el, model }); + + model.on('change:selected_elements', () => { + rerenderScratch({ el, model }); + }); + + model.on('change:disabled_elements', () => { + rerenderScratch({ el, model }); + }); + + model.on('change:display_names_replacements', () => { + rerenderScratch({ el, model }); + }); + + model.on('change:border_color', () => { + renderBorder(model.get('border_color')); + }); + + model.on('change:width', () => { + rerenderScratch({ el, model }); + }); + + model.on('change:disabled', () => { + rerenderScratch({ el, model }); + }); +} + +function rerenderScratch({ el, model }) { + // Re-render full widget when the list of selected elements + // changed from python + const selectedElements = model.get('selected_elements'); + const disabledElements = model.get('disabled_elements'); + const disabledColor = model.get('disabled_color'); + const unselectedColor = model.get('unselected_color'); + const selectedColors = model.get('selected_colors'); + const newSelectedColors = selectedColors.slice(); + const elementWidth = model.get('width'); + const borderColor = model.get('border_color'); + + let newSelectedElements = []; + const newSelectedStates = []; + + if ('Du' in selectedElements) { + return; + } + + for (const key in selectedElements) { + newSelectedElements.push(key); + newSelectedStates.push(selectedElements[key]); + } + + if (newSelectedElements.length !== newSelectedStates.length) { + return; + } + + // Here I want to clean up the two elements lists, to avoid + // to have unknown elements in the selectedElements, and + // to remove disabled Elements from the selectedElements list. + // I use s variable to check if anything changed, so I send + // back the data to python only if needed + + const 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); + + const changed = newSelectedElements.length !== selectedElementsLength; + + // call the update (to python) only if I actually removed/changed + // something + if (changed) { + // Make a copy before setting + // while (newSelectedElements.length > newSelectedStates.length){ + // newSelectedStates.push(0); + // }; + + for (const key in selectedElements) { + if (!newSelectedElements.includes(key)) { + delete selectedElements[key]; + } + } + + model.set('selected_elements', selectedElements); + model.save_changes(); + } + + // Render the full widget using the template + el.innerHTML = + '
' + + tableTemplate({ + elementTable: elementTable, + displayNamesReplacements: model.get('display_names_replacements'), + selectedElements: newSelectedElements, + disabledElements: disabledElements, + disabledColor: disabledColor, + unselectedColor: unselectedColor, + selectedColors: newSelectedColors, + selectedStates: newSelectedStates, + elementWidth: elementWidth, + borderColor: borderColor, + disabled: model.get('disabled'), + }) + + '
'; + + $(() => { + $('.periodic-table-entry').on('click', (event) => { + toggleElement({ el, model, event }); + }); + }); +}; + +function toggleElement({ el, model, event }) { + const classNames = _.map(event.target.classList, (a) => { + return a; + }); + const elementName = _.chain(classNames) + .filter((a) => { + return a.startsWith('element-'); + }) + .map((a) => { + return a.slice('element-'.length); + }) + .first() + .value(); + + const isOn = _.includes(classNames, 'elementOn'); + const 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) + + const states = model.get('states'); + const disabled = model.get('disabled'); + + if (disabled) { + return; + }; + + // Check if we understood which element we are + if (typeof elementName !== 'undefined') { + const currentList = model.get('selected_elements'); + // NOTE! it is essential to duplicate the list, + // otherwise the value will not be updated. + + let newList = []; + const newStatesList = []; + + for (const key in currentList) { + newList.push(key); + newStatesList.push(currentList[key]); + }; + + const num = newList.indexOf(elementName); + + if (isOn) { + // remove the element from the selected_elements + + if (newStatesList[num] < states - 1) { + newStatesList[num]++; + currentList[elementName] = newStatesList[num]; + } else { + newList = _.without(newList, elementName); + newStatesList.splice(num, 1); + delete currentList[elementName]; + // Swap CSS state + event.target.classList.remove('elementOn'); + } + } else if (!isDisabled) { + // add the element from the selected_elements + newList.push(elementName); + newStatesList.push(0); + currentList[elementName] = 0; + // Swap CSS state + event.target.classList.add('elementOn'); + } else { + return; + }; + + // Update the model (send back data to python) + // I have to make some changes, since there is some issue + // for Dict in Traitlets, which cannot trigger the update + model.set('selected_elements', { Du: 0 }); + model.set('selected_elements', currentList); + model.save_changes(); + }; +}; + +function renderBorder(color) { + const a = document.getElementsByClassName('periodic-table-entry'); + + for (let i = 0; i < a.length; i++) { + a[i].style.border = '1px solid ' + color; + } +}; + +export default { render }; diff --git a/package.json b/package.json new file mode 100644 index 0000000..7f70cbb --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "scripts": { + "dev": "npm run build -- --sourcemap=inline --watch", + "build": "esbuild js/widget.js --minify --format=esm --bundle --outdir=src/widget_periodictable/static" + }, + "dependencies": { + "underscore": "^1.13.6", + "jquery": "^3.7.1" + }, + "devDependencies": { + "esbuild": "^0.20.0", + "eslint": "^8.57.0", + "eslint-config-google": "^0.14.0" + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b11432a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "widget_periodictable" +version = "4.0.5" +dependencies = ["anywidget>=0.9.3"] +readme = "README.md" + +[project.optional-dependencies] +dev = ["watchfiles", "jupyterlab"] + +# automatically add the dev feature to the default env (e.g., hatch shell) +[tool.hatch.envs.default] +features = ["dev"] + + +[tool.hatch.build] +only-packages = true +artifacts = ["src/widget_periodictable/static/*"] + +[tool.hatch.build.hooks.jupyter-builder] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = ["src/widget_periodictable/static/widget.js"] +skip-if-exists = ["src/widget_periodictable/static/widget.js"] +dependencies = ["hatch-jupyter-builder>=0.5.0"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] +npm = "npm" +build_cmd = "build" diff --git a/src/widget_periodictable/__init__.py b/src/widget_periodictable/__init__.py new file mode 100644 index 0000000..7b9a622 --- /dev/null +++ b/src/widget_periodictable/__init__.py @@ -0,0 +1,139 @@ +import importlib.metadata +import pathlib + +import anywidget +import traitlets +from traitlets import validate, observe, TraitError + +from .utils import color_as_rgb, CHEMICAL_ELEMENTS +from copy import deepcopy + +try: + __version__ = importlib.metadata.version("widget_periodictable") +except importlib.metadata.PackageNotFoundError: + __version__ = "unknown" + + +class PTableWidget(anywidget.AnyWidget): + _esm = pathlib.Path(__file__).parent / "static" / "widget.js" + _css = pathlib.Path(__file__).parent / "static" / "widget.css" + + selected_elements = traitlets.Dict({}).tag(sync=True) + disabled_elements = traitlets.List([]).tag(sync=True) + display_names_replacements = traitlets.Dict({}).tag(sync=True) + disabled_color = traitlets.Unicode('gray').tag(sync=True) + unselected_color = traitlets.Unicode('pink').tag(sync=True) + states = traitlets.Int(1).tag(sync=True) + selected_colors = traitlets.List([]).tag(sync=True) + border_color = traitlets.Unicode('#cc7777').tag(sync=True) + disabled = traitlets.Bool(False, help="Enable or disable user changes.").tag(sync=True) + width = traitlets.Unicode('38px').tag(sync=True) + allElements = traitlets.List(CHEMICAL_ELEMENTS).tag(sync=True) + + _STANDARD_COLORS = [ + "#a6cee3", + "#b2df8a", + "#fdbf6f", + "#6a3d9a", + "#b15928", + "#e31a1c", + "#1f78b4", + "#33a02c", + "#ff7f00", + "#cab2d6", + "#ffff99", + ] + + def __init__( + self, + states=1, + selected_elements=None, + disabled_elements=None, + disabled_color=None, + unselected_color=None, + selected_colors=[], + border_color=None, + width=None, + layout=None, + ): + super(PTableWidget, self).__init__() + self.states = states if states else 1 + self.selected_elements = selected_elements if selected_elements else {} + self.disabled_elements = disabled_elements if disabled_elements else [] + self.disabled_color = disabled_color if disabled_color else 'gray' + self.unselected_color = unselected_color if unselected_color else 'pink' + self.selected_colors = ( + selected_colors if selected_colors else self._STANDARD_COLORS + ) + self.border_color = border_color if border_color else '#cc7777' + self.width = width if width else '38px' + + if layout is not None: + self.layout = layout + + if len(selected_colors) < states: + self.selected_colors = selected_colors + self._STANDARD_COLORS * ( + 1 + (states - len(selected_colors)) // len(self._STANDARD_COLORS) + ) + self.selected_colors = self.selected_colors[:states] + + def set_element_state(self, elementName, state): + if elementName not in self.allElements: + raise TraitError('Element not found') + if state not in range(self.states): + raise TraitError('State value is wrong') + x = deepcopy(self.selected_elements) + x[elementName] = state + self.selected_elements = x + + @validate('disabled_color', 'unselected_color', 'border_color') + def _color_change(self, proposal): + """Convert to rgb(X, Y, Z) type color""" + return color_as_rgb(proposal['value']) + + @validate('selected_colors') + def _selectedColors_change(self, proposal): + """Convert to rgb(X, Y, Z) type color""" + res = [] + for color in proposal['value']: + res.append(color_as_rgb(color)) + return res + + @validate('selected_elements') + def _selectedElements_change(self, proposal): + for x, y in proposal['value'].items(): + if x not in self.allElements and x != 'Du': + raise TraitError('Element not found') + if not isinstance(y, int) or y not in range(self.states): + raise TraitError('State value is wrong') + return proposal['value'] + + @observe('disabled_elements') + def _disabledList_change(self, change): + for i in change['new']: + if i in self.selected_elements: + del self.selected_elements[i] + + @observe('states') + def _states_change(self, change): + if change['new'] < 1: + raise TraitError('State value cannot smaller than 1') + else: + if len(self.selected_colors) < change["new"]: + self.selected_colors = self.selected_colors + self._STANDARD_COLORS * ( + 1 + + (change["new"] - len(self.selected_colors)) + // len(self._STANDARD_COLORS) + ) + self.selected_colors = self.selected_colors[: change["new"]] + elif len(self.selected_colors) > change["new"]: + self.selected_colors = self.selected_colors[: change["new"]] + + def get_elements_by_state(self, state): + if state not in range(self.states): + raise TraitError("State value is wrong") + else: + return [ + i for i in self.selected_elements if self.selected_elements[i] == state + ] + diff --git a/src/widget_periodictable/utils.py b/src/widget_periodictable/utils.py new file mode 100644 index 0000000..ad48ca4 --- /dev/null +++ b/src/widget_periodictable/utils.py @@ -0,0 +1,166 @@ +import re + + +HTML_COLOR_MAP = { + "white": (255,) * 3, + "silver": tuple(round(0.75 * i) for i in (255,) * 3), + "gray": tuple(round(0.5 * i) for i in (255,) * 3), + "grey": tuple(round(0.5 * i) for i in (255,) * 3), + "black": (0,) * 3, + "red": (255, 0, 0), + "maroon": (round(0.5 * 255), 0, 0), + "yellow": (255, 255, 0), + "olive": tuple(round(0.5 * i) for i in (255, 255, 0)), + "lime": (0, 255, 0), + "green": (0, round(0.5 * 255), 0), + "aqua": (0, 255, 255), + "teal": tuple(round(0.5 * i) for i in (0, 255, 255)), + "blue": (0, 0, 255), + "navy": (0, 0, round(0.5 * 255)), + "fuchsia": (255, 0, 255), + "purple": tuple(round(0.5 * i) for i in (255, 0, 255)), + "pink": (255, 192, 203), +} + + +def color_as_rgb(color: str) -> str: + """Convert hex and named color to rgb formatting""" + if not color: + return "" + + if re.match(r"#[a-fA-F0-9]{6}", color): + # Hex color + color = color.lstrip("#") + color = tuple(int(color[i : i + 2], 16) for i in (0, 2, 4)) + elif re.match(r"rgb\([0-9]+,[0-9]+,[0-9]+\)", color): + # RGB color + return color + else: + # Color name + color = HTML_COLOR_MAP.get(color) + + if color is None: + return "" + return "".join(f"rgb{color!r}".split(" ")) + + +CHEMICAL_ELEMENTS = [ + "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", +]