├── .devcontainer └── devcontainer.json ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── annotated_text ├── __init__.py ├── parameters.py └── util.py ├── dev-requirements.txt ├── example.png ├── example.py ├── requirements.txt └── setup.py /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python 3", 3 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 4 | "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", 5 | "customizations": { 6 | "codespaces": { 7 | "openFiles": [ 8 | "README.md", 9 | "example.py" 10 | ] 11 | }, 12 | "vscode": { 13 | "settings": {}, 14 | "extensions": [ 15 | "ms-python.python", 16 | "ms-python.vscode-pylance" 17 | ] 18 | } 19 | }, 20 | "updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y >> annotated_text( 26 | ... "This ", 27 | ... ("is", "verb", "#8ef"), 28 | ... " some ", 29 | ... ("annotated", "adj", "#faa"), 30 | ... ("text", "noun", "#afa"), 31 | ... " for those of ", 32 | ... ("you", "pronoun", "#fea"), 33 | ... " who ", 34 | ... ("like", "verb", "#8ef"), 35 | ... " this sort of ", 36 | ... ("thing", "noun", "#afa"), 37 | ... ) 38 | 39 | >>> annotated_text( 40 | ... "Hello ", 41 | ... annotation("world!", "noun", color="#8ef", border="1px dashed red"), 42 | ... ) 43 | 44 | """ 45 | st.markdown( 46 | util.get_annotated_html(*args), 47 | unsafe_allow_html=True, 48 | ) 49 | -------------------------------------------------------------------------------- /annotated_text/parameters.py: -------------------------------------------------------------------------------- 1 | from htbuilder.units import unit 2 | 3 | # This works in 3.7+: 4 | # from htbuilder.units import rem 5 | # 6 | # ...so we use the 3.7 version of the code above here: 7 | rem = unit.rem 8 | 9 | 10 | # Colors from the Streamlit palette. 11 | # These are red-70, orange-70, ..., violet-70, gray-70. 12 | PALETTE = [ 13 | "#ff4b4b", 14 | "#ffa421", 15 | "#ffe312", 16 | "#21c354", 17 | "#00d4b1", 18 | "#00c0f2", 19 | "#1c83e1", 20 | "#803df5", 21 | "#808495", 22 | ] 23 | 24 | OPACITIES = [ 25 | "33", "66", 26 | ] 27 | 28 | PADDING=(rem(0.25), rem(0.5)) 29 | BORDER_RADIUS=rem(0.5) 30 | LABEL_FONT_SIZE=rem(0.75) 31 | LABEL_OPACITY=0.5 32 | LABEL_SPACING=rem(0.5) 33 | SHOW_LABEL_SEPARATOR=True 34 | -------------------------------------------------------------------------------- /annotated_text/util.py: -------------------------------------------------------------------------------- 1 | import html 2 | 3 | from htbuilder import H, HtmlElement, styles 4 | from htbuilder.units import unit 5 | 6 | # This works in 3.7+: 7 | # from htbuilder import span 8 | # 9 | # ...so we use the 3.7 version of the code above here: 10 | try: 11 | span = H.span 12 | except AttributeError: 13 | from htbuilder import span 14 | 15 | import annotated_text.parameters as p 16 | 17 | def annotation(body, label="", background=None, color=None, **style): 18 | """Build an HtmlElement span object with the given body and annotation label. 19 | 20 | The end result will look something like this: 21 | 22 | [body | label] 23 | 24 | Parameters 25 | ---------- 26 | body : string 27 | The string to put in the "body" part of the annotation. 28 | label : string 29 | The string to put in the "label" part of the annotation. 30 | background : string or None 31 | The color to use for the background "chip" containing this annotation. 32 | If None, will use a random color based on the label. 33 | color : string or None 34 | The color to use for the body and label text. 35 | If None, will use the document's default text color. 36 | style : dict 37 | Any CSS you want to apply to the containing "chip". This is useful for things like 38 | 39 | 40 | Examples 41 | -------- 42 | 43 | Produce a simple annotation with default colors: 44 | 45 | >>> annotation("apple", "fruit") 46 | 47 | Produce an annotation with custom colors: 48 | 49 | >>> annotation("apple", "fruit", background="#FF0", color="black") 50 | 51 | Produce an annotation with crazy CSS: 52 | 53 | >>> annotation("apple", "fruit", background="#FF0", border="1px dashed red") 54 | 55 | """ 56 | 57 | color_style = {} 58 | 59 | if color: 60 | color_style['color'] = color 61 | 62 | if background: 63 | background_color = background 64 | else: 65 | label_sum = sum(ord(c) for c in label) 66 | background_color = p.PALETTE[label_sum % len(p.PALETTE)] 67 | background_opacity = p.OPACITIES[label_sum % len(p.OPACITIES)] 68 | background = background_color + background_opacity 69 | 70 | label_element = "" 71 | 72 | if label: 73 | separator = "" 74 | 75 | if p.SHOW_LABEL_SEPARATOR: 76 | separator = span( 77 | style=styles( 78 | border_left=f"1px solid", 79 | opacity=0.1, 80 | margin_left=p.LABEL_SPACING, 81 | align_self="stretch", 82 | ) 83 | ), 84 | 85 | label_element = ( 86 | separator, 87 | span( 88 | style=styles( 89 | margin_left=p.LABEL_SPACING, 90 | font_size=p.LABEL_FONT_SIZE, 91 | opacity=p.LABEL_OPACITY, 92 | ) 93 | )( 94 | html.escape(label), 95 | ) 96 | ) 97 | 98 | return ( 99 | span( 100 | style=styles( 101 | display="inline-flex", 102 | flex_direction="row", 103 | align_items="center", 104 | background=background, 105 | border_radius=p.BORDER_RADIUS, 106 | padding=p.PADDING, 107 | overflow="hidden", 108 | line_height=1, 109 | **color_style, 110 | **style,) 111 | )( 112 | html.escape(body), 113 | label_element, 114 | ) 115 | ) 116 | 117 | 118 | def get_annotated_html(*args): 119 | """Writes text with annotations into an HTML string. 120 | 121 | Parameters 122 | ---------- 123 | *args : see annotated_text() 124 | 125 | Returns 126 | ------- 127 | str 128 | An HTML string. 129 | """ 130 | 131 | return str(get_annotated_element(*args)) 132 | 133 | 134 | def get_annotated_element(*args): 135 | """Writes text with annotations into an HTBuilder HtmlElement object. 136 | 137 | Parameters 138 | ---------- 139 | *args : see annotated_text() 140 | 141 | Returns 142 | ------- 143 | HtmlElement 144 | An HTBuilder HtmlElement object. 145 | """ 146 | 147 | out = span() 148 | 149 | for arg in args: 150 | if isinstance(arg, str): 151 | out(html.escape(arg)) 152 | 153 | elif isinstance(arg, HtmlElement): 154 | out(arg) 155 | 156 | elif isinstance(arg, tuple): 157 | out(annotation(*arg)) 158 | 159 | elif isinstance(arg, list): 160 | out(get_annotated_element(*arg)) 161 | 162 | else: 163 | raise Exception("Oh noes!") 164 | 165 | return out 166 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | twine 2 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tvst/st-annotated-text/bbb2209cbd609dd3eade11f76065dc5e797de777/example.png -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from annotated_text import annotated_text 3 | 4 | st.set_page_config(page_title="Annotated Text", page_icon="📝") 5 | 6 | """ 7 | # 📝 Annotated Text 8 | 9 | This app shows off the [Annotated Text component](https://github.com/tvst/st-annotated-text) for 10 | Streamlit. 11 | 12 | If you want to try this at home, you'll first need to install it: 13 | 14 | ```python 15 | pip install st-annotated-text 16 | ``` 17 | 18 | 19 | ## Basic example 20 | 21 | Annotations are just tuples: 22 | """ 23 | 24 | with st.echo(): 25 | from annotated_text import annotated_text 26 | 27 | annotated_text( 28 | "This ", 29 | ("is", "Verb"), 30 | " some ", 31 | ("annotated", "Adj"), 32 | ("text", "Noun"), 33 | " for those of ", 34 | ("you", "Pronoun"), 35 | " who ", 36 | ("like", "Verb"), 37 | " this sort of ", 38 | ("thing", "Noun"), 39 | ". ", 40 | "And here's a ", 41 | ("word", ""), 42 | " with a fancy background but no label.", 43 | ) 44 | 45 | "" 46 | 47 | """ 48 | ## Nested arguments 49 | 50 | You can also pass lists (and lists within lists!) as an argument: 51 | """ 52 | 53 | 54 | with st.echo(): 55 | my_list = [ 56 | "Hello ", 57 | [ 58 | "my ", 59 | ("dear", "Adj"), 60 | " ", 61 | ], 62 | ("world", "Noun"), 63 | ".", 64 | ] 65 | 66 | annotated_text(my_list) 67 | 68 | 69 | "" 70 | "" 71 | 72 | """ 73 | ## Customization 74 | """ 75 | 76 | """ 77 | ### Custom colors 78 | 79 | If the annotation tuple has more than 2 items, the 3rd will be used as the background color and the 4th as the foreground color: 80 | """ 81 | 82 | with st.echo(): 83 | annotated_text( 84 | "This ", 85 | ("is", "Verb", "#8ef"), 86 | " some ", 87 | ("annotated", "Adj", "#faa"), 88 | ("text", "Noun", "#afa"), 89 | " for those of ", 90 | ("you", "Pronoun", "#fea"), 91 | " who ", 92 | ("like", "Verb", "#8ef"), 93 | " this sort of ", 94 | ("thing", "Noun", "#afa"), 95 | ". " 96 | "And here's a ", 97 | ("word", "", "#faf"), 98 | " with a fancy background but no label.", 99 | ) 100 | 101 | "" 102 | "" 103 | 104 | """ 105 | ### Custom styles 106 | 107 | You can customize a bunch of different styles by overriding the variables 108 | set in the `annotated_text.parameters` module. For example: 109 | 110 | ```python 111 | from annotated_text import annotated_text, parameters 112 | 113 | parameters.SHOW_LABEL_SEPARATOR = False 114 | parameters.BORDER_RADIUS = 0 115 | parameters.PADDING = "0 0.25rem" 116 | ``` 117 | 118 | For more configurable parameters, see the 119 | [parameters.py source file](https://github.com/tvst/st-annotated-text/blob/master/annotated_text/parameters.py). 120 | """ 121 | 122 | "" 123 | "" 124 | 125 | """ 126 | ### Even more customization 127 | 128 | If you want to go beyond the customizations above, you can bring your own CSS! 129 | """ 130 | 131 | with st.echo(): 132 | from annotated_text import annotated_text, annotation 133 | 134 | annotated_text( 135 | "Hello ", 136 | annotation("world!", "noun", font_family="Comic Sans MS", border="2px dashed red"), 137 | ) 138 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit 2 | htbuilder 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="st-annotated-text", 8 | version="4.0.2", 9 | author="Thiago Teixeira", 10 | author_email="me@thiagot.com", 11 | description="A simple component to display annotated text in Streamlit apps.", 12 | license="Apache 2", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/tvst/st-annotated-text", 16 | packages=setuptools.find_packages(exclude=["tests", "tests.*"]), 17 | install_requires=["htbuilder"], 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: Apache Software License", 21 | "Operating System :: OS Independent", 22 | ], 23 | python_requires=">=3.5", 24 | ) 25 | --------------------------------------------------------------------------------