diff --git a/datamapplot/interactive_rendering.py b/datamapplot/interactive_rendering.py index 3facec7..f281a30 100644 --- a/datamapplot/interactive_rendering.py +++ b/datamapplot/interactive_rendering.py @@ -193,10 +193,10 @@ const unzippedLabelData = fflate.gunzipSync(labelDataBuffer); const labelData = await loaders.parse(unzippedLabelData, JSONLoader); {% else %} - const pointData = await loaders.load("point_df.arrow", ArrowLoader); - const unzippedHoverData = await loaders.load("point_hover_data.zip", ZipLoader); + const pointData = await loaders.load("{{file_prefix}}_point_df.arrow", ArrowLoader); + const unzippedHoverData = await loaders.load("{{file_prefix}}_point_hover_data.zip", ZipLoader); const hoverData = await loaders.parse(unzippedHoverData["point_hover_data.arrow"], ArrowLoader); - const unzippedLabelData = await loaders.load("label_data.zip", ZipLoader); + const unzippedLabelData = await loaders.load("{{file_prefix}}_label_data.zip", ZipLoader); const labelData = await loaders.parse(unzippedLabelData["label_data.json"], JSONLoader); {% endif %} @@ -518,6 +518,7 @@ def render_html( initial_zoom_fraction=1.0, background_color=None, darkmode=False, + offline_data_prefix=None, tooltip_css=None, hover_text_html_template=None, extra_point_data=None, @@ -658,9 +659,14 @@ def render_html( darkmode: bool (optional, default=False) Whether to use darkmode. + offline_data_prefix: str or None (optional, default=None) + If ``inline_data=False`` a number of data files will be created storing data for + the plot and referenced by the HTML file produced. If not none then this will provide + a prefix on the filename of all the files created. + tooltip_css: str or None (optional, default=None) Custom CSS used to fine the properties of the tooltip. If ``None`` a default - CSS style will be used. This should simply the the required CSS directives + CSS style will be used. This should simply be the required CSS directives specific to the tooltip. hover_text_html_template: str or None (optional, default=None) @@ -833,14 +839,16 @@ def render_html( label_data_json = label_dataframe.to_json(orient="records") gzipped_label_data = gzip.compress(bytes(label_data_json, "utf-8")) base64_label_data = base64.b64encode(gzipped_label_data).decode() + file_prefix = None else: base64_point_data = "" base64_hover_data = "" base64_label_data = "" - point_data.to_feather("point_df.arrow", compression="uncompressed") + file_prefix = offline_data_prefix if offline_data_prefix is not None else "datamapplot" + point_data.to_feather(f"{file_prefix}_point_df.arrow", compression="uncompressed") hover_data.to_feather("point_hover_data.arrow", compression="uncompressed") with zipfile.ZipFile( - "point_hover_data.zip", + f"{file_prefix}_point_hover_data.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9, @@ -849,7 +857,7 @@ def render_html( os.remove("point_hover_data.arrow") label_dataframe.to_json("label_data.json", orient="records") with zipfile.ZipFile( - "label_data.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9 + f"{file_prefix}_label_data.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9 ) as f: f.write("label_data.json") os.remove("label_data.json") @@ -906,6 +914,7 @@ def render_html( base64_point_data=base64_point_data, base64_hover_data=base64_hover_data, base64_label_data=base64_label_data, + file_prefix=file_prefix, point_size=point_size, point_outline_color=point_outline_color, point_line_width=point_line_width, diff --git a/examples/cord19_extra_data.arrow b/examples/cord19_extra_data.arrow new file mode 100644 index 0000000..446b084 Binary files /dev/null and b/examples/cord19_extra_data.arrow differ diff --git a/examples/cord19_marker_size_array.npy b/examples/cord19_marker_size_array.npy new file mode 100644 index 0000000..947ad74 Binary files /dev/null and b/examples/cord19_marker_size_array.npy differ diff --git a/examples/plot_interactive_cord19.py b/examples/plot_interactive_cord19.py new file mode 100644 index 0000000..608e762 --- /dev/null +++ b/examples/plot_interactive_cord19.py @@ -0,0 +1,51 @@ +""" +Interactive CORD-19 +------------------- + +Demonstrating interactive plotting with colormaps and search with the CORD-19 large data map. + +For a full size version see +https://lmcinnes.github.io/datamapplot_examples/CORD19_data_map_example.html +""" +import numpy as np +import bz2 +import datamapplot +import colorcet + +cord19_data_map = np.load("cord19_umap_vectors.npy") +cord19_label_layers = [] +for i in range(6): + cord19_label_layers.append( + np.load(f"cord19_layer{i}_cluster_labels.npy", allow_pickle=True) + ) +cord19_hover_text = [ + x.decode("utf-8").strip() + for x in bz2.open( + "cord19_large_hover_text.txt.bz2", + mode="r" + ) +] +cord19_marker_size_array = np.log(1+np.load("cord19_marker_size_array.npy")) + +plot = datamapplot.create_interactive_plot( + cord19_data_map, + cord19_label_layers[0], + cord19_label_layers[1], + cord19_label_layers[2], + cord19_label_layers[3], + cord19_label_layers[4], + cord19_label_layers[5], + hover_text=cord19_hover_text, + initial_zoom_fraction=0.4, + title="CORD-19 Data Map", + sub_title="A data map of papers relating to COVID-19 and SARS-CoV-2", + font_family="Cinzel", + logo="https://allenai.org/newsletters/archive/2023-03-newsletter_files/927c3ca8-6c75-862c-ee5d-81703ef10a8d.png", + logo_width=128, + marker_size_array=cord19_marker_size_array, + cmap=colorcet.cm.CET_C2s, + noise_color="#aaaaaa66", + cluster_boundary_polygons=True, + enable_search=True, +) +plot \ No newline at end of file diff --git a/examples/plot_interactive_custom_cord19.py b/examples/plot_interactive_custom_cord19.py new file mode 100644 index 0000000..1955e60 --- /dev/null +++ b/examples/plot_interactive_custom_cord19.py @@ -0,0 +1,169 @@ +""" +Interactive CORD-19 +------------------- + +Demonstrating interactive plotting and what can be achieved with the extra options available +via ``custom_html``, ``custom_css`` and ``custom_js`` to construct a clickable legend for +selecting subsets of data based on the field of research (click on the colour swatches +in the legend to select a specific field). + +For a full size version see +https://lmcinnes.github.io/datamapplot_examples/CORD19_customised_example.html +""" +import numpy as np +import pandas as pd +import bz2 +import seaborn as sns +from matplotlib.colors import rgb2hex + +import datamapplot + +cord19_data_map = np.load("cord19_umap_vectors.npy") +cord19_label_layers = [] +for i in range(6): + cord19_label_layers.append( + np.load(f"cord19_layer{i}_cluster_labels.npy", allow_pickle=True) + ) +cord19_hover_text = [ + x.decode("utf-8").strip() + for x in bz2.open( + "cord19_large_hover_text.txt.bz2", + mode="r" + ) +] + +color_mapping = {} +color_mapping["Medicine"] = "#bbbbbb" +for key, color in zip(("Biology", "Chemistry", "Physics"), sns.color_palette("YlOrRd_r", 3)): + color_mapping[key] = rgb2hex(color) +for key, color in zip(("Business", "Economics", "Political Science"), sns.color_palette("BuPu_r", 3)): + color_mapping[key] = rgb2hex(color) +for key, color in zip(("Psychology", "Sociology", "Geography", "History"), sns.color_palette("YlGnBu_r", 4)): + color_mapping[key] = rgb2hex(color) +for key, color in zip(("Computer Science", "Engineering", "Mathematics"), sns.color_palette("light:teal_r", 4)[:-1]): + color_mapping[key] = rgb2hex(color) +for key, color in zip(("Environmental Science", "Geology", "Materials Science"), sns.color_palette("pink", 3), ): + color_mapping[key] = rgb2hex(color) +for key, color in zip(("Art", "Philosophy", "Unknown"), sns.color_palette("bone", 3)): + color_mapping[key] = rgb2hex(color) + +cord19_extra_data = pd.read_feather("cord19_extra_data.arrow") +cord19_extra_data["color"] = cord19_extra_data.primary_field.map(color_mapping) +marker_color_array = cord19_extra_data.primary_field.map(color_mapping) +marker_size_array = np.log(1 + cord19_extra_data.citation_count.values) + +# Add custom CSS to style the legend element we will add to the plot +custom_css = """ +.row { + display : flex; + align-items : center; +} +.box { + height:10px; + width:10px; + border-radius:2px; + margin-right:5px; +} +#legend { + position: absolute; + top: 0; + right: 0; + margin: 16px; + padding: 12px; + border-radius: 16px; + z-index: 2; + background: #ffffffcc; + font-family: Cinzel; + font-size: 8pt; + box-shadow: 2px 3px 10px #aaaaaa44; +} +#title-container { + max-width: 75%; +} +""" +# Construct HTML for the legend +custom_html = """ +
+""" +for field, color in color_mapping.items(): + custom_html += f'
{field}
\n' +custom_html += """ +
+""" + +# Create a custom tooltip, highlighting the field of research and citation count +badge_css = """ + border-radius:6px; + width:fit-content; + max-width:75%; + margin:2px; + padding: 2px 10px 2px 10px; + font-size: 10pt; +""" +hover_text_template = f""" +
+
{{hover_text}}
+
{{primary_field}}
+
citation count: {{citation_count}}
+
+""" + +# Add custom javascript to make the legend interactive/clickable, +# and interact with search selection +custom_js = """ + const legend = document.getElementById('legend'); + legend.addEventListener('click', function (event) { + const primary_field = event.srcElement.id; + selectPoints(primary_field, (i) => (hoverData.data.primary_field[i] == primary_field)); + for (const row of legend.children) { + for (const item of row.children) { + if (item.id == primary_field) { + item.innerHTML = "✓"; + } else { + item.innerHTML = ""; + } + } + } + search.value = ""; + }); + + search.addEventListener("input", (event) => { + for (const row of legend.children) { + for (const item of row.children) { + item.innerHTML = ""; + } + } + }); +""" + +plot = datamapplot.create_interactive_plot( + cord19_data_map, + cord19_label_layers[0], + cord19_label_layers[1], + cord19_label_layers[2], + cord19_label_layers[3], + cord19_label_layers[4], + cord19_label_layers[5], + hover_text=cord19_hover_text, + initial_zoom_fraction=0.4, + title="CORD-19 Data Map", + sub_title="A data map of papers relating to COVID-19", + font_family="Cinzel", + logo="https://allenai.org/newsletters/archive/2023-03-newsletter_files/927c3ca8-6c75-862c-ee5d-81703ef10a8d.png", + logo_width=128, + color_label_text=False, + marker_size_array=marker_size_array, + marker_color_array=marker_color_array, + point_radius_max_pixels=16, + noise_color="#aaaaaa44", + cluster_boundary_polygons=True, + color_cluster_boundaries=False, + extra_point_data=cord19_extra_data, + hover_text_html_template=hover_text_template, + on_click="window.open(`http://google.com/search?q=\"{hover_text}\"`)", + enable_search=True, + custom_css=custom_css, + custom_html=custom_html, + custom_js=custom_js, +) +plot \ No newline at end of file