Interactive Plot Customizations
This notebook will walk you through some of the customization options that are available for interactive plots in DataMapPlot. There are too many options available to fully cover all that can be done, so this notebook will instead highlight some of the major options and give some ideas for how some of the more flexible options might be used to create extra interactive features for your plot. To get started we’ll need to import DataMapPlot.
[1]:
import datamapplot
To demonstrate what DataMapPlot can do we’ll need some data. The examples directory of the DataMapPlot repository contains some pre-prepared datasets for experimenting with. We’ll grab one of those. In practice we need a data map – as set of 2d coordinates, one per data sample we are mapping – and multiple layers of labels idenityfing the “topic” of a data sample, usually based on clusters in the data map, at varying levels of granularity. In this case we’ll use data from the titles and abstracts of papers from the machine learning section of the ArXiv preprint server.
[2]:
import numpy as np
import requests
import io
base_url = "https://github.com/TutteInstitute/datamapplot"
data_map_file = requests.get(
f"{base_url}/raw/main/examples/arxiv_ml_data_map.npy"
)
arxivml_data_map = np.load(io.BytesIO(data_map_file.content))
arxivml_label_layers = []
for layer_num in range(5):
label_file = requests.get(
f"{base_url}/raw/interactive/examples/arxiv_ml_layer{layer_num}_cluster_labels.npy"
)
arxivml_label_layers.append(np.load(io.BytesIO(label_file.content), allow_pickle=True))
hover_data_file = requests.get(
f"{base_url}/raw/interactive/examples/arxiv_ml_hover_data.npy"
)
arxiv_hover_data = np.load(io.BytesIO(hover_data_file.content), allow_pickle=True)
Let’s start by making a relatively basic interactive plot with DataMapPlot so we have an idea of what the starting point looks like, and can better understand what the various customizations we will be applying can do for us.
[3]:
plot = datamapplot.create_interactive_plot(
arxivml_data_map,
arxivml_label_layers[0],
arxivml_label_layers[2],
arxivml_label_layers[4],
font_family="Cinzel",
)
plot
[3]:
The most basic thing you can do is to provide your plot with a title and, preferably, a sub-title. This can be done with the title and sub_title keyword arguments. DataMapPlot has default placement and formatting for the title and sub-title that should be adequate for most needs. You can override this formatting using some of the custom_css
options you’ll see later, but this shoudl suffice for now.
In general the title should be short, while the sub-title can carry more information. If your title or sub-title get too long it will mess with the formatting. For our current data map we can use "ArXiv ML Landscape"
as the title, and a slightly longer sub-title of "Papers from the machine learning section of ArXiv"
.
You might also want a logo in your interactive plot. You can add this with the logo
keyword. Unlike the static plot logos you do not need a numpy based encoding of the image; instead you only need a URL where the image to use as the logo can be found. By default the logo is added to the bottom right corner, and you can control sizing with the logo_width
keyword argument.
[4]:
plot = datamapplot.create_interactive_plot(
arxivml_data_map,
arxivml_label_layers[0],
arxivml_label_layers[2],
arxivml_label_layers[4],
font_family="Cinzel",
title="ArXiv ML Landscape",
sub_title="Papers from the machine learning section of ArXiv",
logo="https://upload.wikimedia.org/wikipedia/commons/thumb/b/bc/ArXiv_logo_2022.svg/320px-ArXiv_logo_2022.svg.png"
)
plot
[4]:
Another feature that may be useful is the ability to search the data map for specific items. Basic functionality for this is provided with the enable_search
keyword, which will add a text search box that provides live interactive search of the hover_text
array. In our case the hover text was the title of the paper, so we have live search on the paper titles. You can try out the search below – search for “clustering” or “language”, or something of your choice, and see how the plot only
displays points with the search string in the title.
[5]:
plot = datamapplot.create_interactive_plot(
arxivml_data_map,
arxivml_label_layers[0],
arxivml_label_layers[2],
arxivml_label_layers[4],
font_family="Cinzel",
hover_text=arxiv_hover_data,
enable_search=True,
)
plot
[5]:
What if you want to search something other than the hover text? It is possible to supply extra data associated to each point in the form of a dataframe, and link the search to a field from that data. To provide an example, suppose we would like to search through the topic labels rather than the titles of the papers (the hover text). First we need to construct such a DataFrame. For that we’ll need pandas.
[6]:
import pandas as pd
For each point we’ll make a comma separated list of the topic names (at any layer) associated to the point, but skipping over “Unlabelled” as a topic. We can then package that up in a DataFrame which we can pass into create_interactive_plot
.
[7]:
topics_per_point = [
", ".join([label for label in labels if label != "Unlabelled"])
for labels in zip(arxivml_label_layers[0], arxivml_label_layers[2], arxivml_label_layers[4])
]
topics_dataframe = pd.DataFrame({"topics": topics_per_point})
Any extra data associated to points can be passed via the extra_point_data
keyword argument. In our case we have passed a simple DataFrame with just one column, but we could provide more information (and we’ll see examples of that later). To enable the search function to search through alternative data you need to provide the column name (from the extra_point_data
DataFrame) that you want the search to operate on. Note that this should be a column of text data! In our case this means we
specify that we want to search the "topics"
column by setting search_field="topics"
.
[8]:
plot = datamapplot.create_interactive_plot(
arxivml_data_map,
arxivml_label_layers[0],
arxivml_label_layers[2],
arxivml_label_layers[4],
font_family="Cinzel",
hover_text=arxiv_hover_data,
extra_point_data=topics_dataframe,
enable_search=True,
search_field="topics",
)
plot
[8]:
Now our search operates on the topic names rather than the titles. Try it yourself live on the plot above.
A useful feature of the interactive plot output is that it is a single HTML file, with all the data compressed and provided inline within the file itself. This makes it extremely portable – you can send the file to anyone, copy it somewhere, upload it to a webserver, etc. and have it “just work”. Unfortunately if you have a large dataset (usually due to voluminous text in the hover/tooltip data) this can result in very large files being created, and a single giant HTML file can become unwieldy.
You can get around this by setting inline_data=False
, which will instead write the data out to separate (compressed) files, and adjustin the HTML output accordingly. For example, we could produce the plot as before, but set inline_data=False
:
[9]:
plot = datamapplot.create_interactive_plot(
arxivml_data_map,
arxivml_label_layers[0],
arxivml_label_layers[2],
arxivml_label_layers[4],
font_family="Cinzel",
title="ArXiv ML Landscape",
sub_title="Papers from the machine learning section of ArXiv",
inline_data=False,
)
plot
[9]:
The result seems to be the same. We can actually take a look at the HTML that was produced. If we’re going to do that, let’s make sure we can pretty-print it with syntax highlighting:
[10]:
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import Terminal256Formatter
The plot objects produced by create_interactive_plot
display inline in notebooks, and can be saved using the save
method, but simply calling str
on them will output the raw HTML that was produced. So let’s call str
on our plot to extract the HTML string, and then print it below (don’t do this if you have the data inline; it will be very large!):
[11]:
html_str = str(plot)
lexer = get_lexer_by_name("html")
formatter = Terminal256Formatter(style='solarized-light')
result = highlight(html_str, lexer, formatter)
print(result)
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>ArXiv ML Landscape</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" />
<script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #ffffff;
}
#deck-container {
width: 100vw;
height: 100vh;
}
#deck-container canvas {
z-index: 1;
background: #ffffff;
}
.deck-tooltip {
font-size: 0.8em;
font-family: Cinzel;
font-weight: 300;
color: #000000 !important;
background-color: #ffffffaa !important;
border-radius: 12px;
box-shadow: 2px 3px 10px #aaaaaa44;
max-width: 25%;
}
#loading {
width: 100%;
height: 100%;
top: 0px;
left: 0px;
position: absolute;
display: block;
z-index: 99
}
#loading-image {
position: absolute;
top: 45%;
left: 47.5%;
z-index: 100
}
#title-container {
position: absolute;
top: 0;
left: 0;
margin: 16px;
padding: 12px;
border-radius: 16px;
line-height: 0.95;
z-index: 2;
font-family: Cinzel;
color: #000000;
background: #ffffffaa;
box-shadow: 2px 3px 10px #aaaaaa44;
}
</style>
</head>
<body>
<div id="loading">
<img id="loading-image" src="https://i.gifer.com/ZKZg.gif" alt="Loading..." width="5%"/>
</div>
<div id="title-container">
<span style="font-family:Cinzel;font-size:36pt;color:#000000">
ArXiv ML Landscape
</span><br/>
<span style="font-family:Cinzel;font-size:18pt;color:#777777">
Papers from the machine learning section of ArXiv
</span>
</div>
<div id="deck-container">
</div>
</body>
<script type="module">
import { ArrowLoader } from 'https://cdn.jsdelivr.net/npm/@loaders.gl/arrow@4.1.0-alpha.10/+esm'
import { JSONLoader } from 'https://cdn.jsdelivr.net/npm/@loaders.gl/json@4.0.5/+esm'
import { ZipLoader } from 'https://cdn.jsdelivr.net/npm/@loaders.gl/zip@4.1.0-alpha.10/+esm'
import { CSVLoader } from 'https://cdn.jsdelivr.net/npm/@loaders.gl/csv@4.1.0-alpha.10/+esm'
const pointData = await loaders.load("datamapplot_point_df.arrow", ArrowLoader);
const unzippedHoverData = await loaders.load("datamapplot_point_hover_data.zip", ZipLoader);
const hoverData = await loaders.parse(unzippedHoverData["point_hover_data.arrow"], ArrowLoader);
const unzippedLabelData = await loaders.load("datamapplot_label_data.zip", ZipLoader);
const labelData = await loaders.parse(unzippedLabelData["label_data.json"], JSONLoader);
const DATA = {src: pointData.data, length: pointData.data.x.length}
const container = document.getElementById('deck-container');
const pointLayer = new deck.ScatterplotLayer({
id: 'dataPointLayer',
data: DATA,
getPosition: (object, {index, data}) => {
return [data.src.x[index], data.src.y[index]];
},
getRadius: 0.028345125091052373,
getFillColor: (object, {index, data}) => {
return [
data.src.r[index],
data.src.g[index],
data.src.b[index],
180
]
},
getLineColor: (object, {index, data}) => {
return [
data.src.r[index],
data.src.g[index],
data.src.b[index],
32
]
},
getLineColor: [250, 250, 250, 128],
getLineWidth: 0.001,
highlightColor: [170, 0, 0, 187],
lineWidthMaxPixels: 8,
lineWidthMinPixels: 0.1,
radiusMaxPixels: 24,
radiusMinPixels: 0.01,
radiusUnits: "common",
lineWidthUnits: "common",
autoHighlight: true,
pickable: true,
stroked: true
});
const labelLayer = new deck.TextLayer({
id: "textLabelLayer",
data: labelData,
pickable: false,
getPosition: d => [d.x, d.y],
getText: d => d.label,
getColor: d => [d.r, d.g, d.b],
getSize: d => d.size,
sizeScale: 1,
sizeMinPixels: 18,
sizeMaxPixels: 36,
outlineWidth: 8,
outlineColor: [238, 238, 238, 221],
getBackgroundColor: [255, 255, 255, 64],
getBackgroundPadding: [15, 15, 15, 15],
background: true,
characterSet: "auto",
fontFamily: "Cinzel",
fontWeight: 900,
lineHeight: 0.95,
fontSettings: {"sdf": true},
getTextAnchor: "middle",
getAlignmentBaseline: "center",
lineHeight: 0.95,
elevation: 100,
// CollideExtension options
collisionEnabled: true,
getCollisionPriority: d => d.size,
collisionTestProps: {
sizeScale: 3,
sizeMaxPixels: 36 * 2,
sizeMinPixels: 18 * 2
},
extensions: [new deck.CollisionFilterExtension()],
});
const deckgl = new deck.DeckGL({
container: container,
initialViewState: {
latitude: 3.497722,
longitude: 8.693474,
zoom: 4.307548408960062
},
controller: true,
layers: [pointLayer, labelLayer],
getTooltip: null
});
document.getElementById("loading").style.display = "none";
</script>
</html>
What we have is a very simple short HTML file with a little CSS for styling, and some javascript to load the data and create the plot. All the data is in seprate files: datamapplot_point_df.arrow
, datamapplot_point_hover_data.zip
and datamapplot_label_data.zip
. If you want more control over the filenames the keyword argument offline_data_prefix
allows you to specify a specific prefix for the files (the default is "datamapplot"
) so you can have data for multiple different
plots all in the same folder. Just be aware that you will have to provide these data files along with the HTML for the plot to work – but if your goal is to serve it from a webserver this is easy to arrange.
Now let’s look at some of the more complex customisations that can be done. For that it will help to have a slightly richer dataset with extra data associated to it. For this we’ll use the CORD19 dataset for which we have extra information about the citation count and the primary field of study.
[12]:
data_map_file = requests.get(
f"{base_url}/raw/main/examples/CORD19-subset-data-map.npy"
)
cord19_data_map = np.load(io.BytesIO(data_map_file.content))
label_file = requests.get(
f"{base_url}/raw/interactive/examples/CORD19-subset-cluster_labels.npy"
)
cord19_labels = np.load(io.BytesIO(label_file.content), allow_pickle=True)
title_file = requests.get(
f"{base_url}/raw/interactive/examples/CORD19-subset-titles.npy"
)
cord19_titles = np.load(io.BytesIO(title_file.content), allow_pickle=True)
count_file = requests.get(
f"{base_url}/raw/interactive/examples/CORD19-subset-citation_counts.npy"
)
cord19_citation_counts = np.load(io.BytesIO(count_file.content))
field_file = requests.get(
f"{base_url}/raw/interactive/examples/CORD19-subset-field_of_study.npy"
)
cord19_field_of_study = np.load(io.BytesIO(field_file.content), allow_pickle=True)
To make this custom plot we are going to colour points by the field of study and have all this extra information, the field of study and citation count, appear in the hover tooltip. For this we’ll need a custom colour palette, and seaborn and matplotlib will make that easier to build.
[13]:
import seaborn as sns
from matplotlib.colors import rgb2hex
Now we can build out colour palette.
[14]:
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)
We create a Dataframe of extra data, much as we did above, but this time we have two columns: the citation_count
and the primary_field
. We can make use of that to also construct a colour for each point using the colour mapping. We are also going to size points by the log of the citation count, and colour them by primary field, but see the Sizing Options and Colout Options documentation for more details on how to do that.
[15]:
cord19_extra_data = pd.DataFrame(
{"citation_count":cord19_citation_counts, "primary_field":cord19_field_of_study}
)
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)
Instead we are going to focus on making a custom hover tooltip that makes use of all of the extra data we have created. We can do this using the hover_text_template
keyword, which takes a string of custom HTML to use to create the tooltip. We’ll start with a very simple example, just to show how the process works, and then construct a more complex one.
The hover_text_template
should be HTML, formatted however you wish, with field names from the extra_point_data
enclosed in curly braces wherever you want value for the point hovered over from the extra_point_data
filled in. Similarly using hover_text
in curly braces will substitute in the specific value of the hover_text
for that point. So, if we want a tooltip that gives the title (provided as the hover_text
), primary field of study, and citation count we could write
something like:
[16]:
hover_text_template = """
<div>
<p>TITLE: {hover_text}</p>
<p>PRIMARY FIELD OF STUDY: {primary_field}</p>
<p>CITATION COUNT: {citation_count}</p>
</div>
"""
We then pass that template and the extra point data in to create_interactive_plot
:
[17]:
plot = datamapplot.create_interactive_plot(
cord19_data_map,
cord19_labels,
hover_text=cord19_titles,
title="CORD-19 Data Map",
sub_title="A data map of papers relating to COVID-19",
font_family="Cinzel",
initial_zoom_fraction=0.4,
logo="https://allenai.org/newsletters/archive/2023-03-newsletter_files/927c3ca8-6c75-862c-ee5d-81703ef10a8d.png",
logo_width=128,
inline_data=False,
offline_data_prefix="cord-1",
color_label_text=False,
marker_size_array=marker_size_array,
marker_color_array=marker_color_array,
noise_color="#aaaaaa44",
extra_point_data=cord19_extra_data,
hover_text_html_template=hover_text_template,
)
plot
[17]:
Now the hover based tooltips provide all the information we want to each point. It is not especially pretty though. Since we have the full power of HTML and CSS at our disposal to format the tooltip contents let’s do something fancier. We can create little “badges” for the field of study and citation count, and we can even colour the field of study badge using the same colour mapping. This is a little more text, so let’s use some string formatting to get all of that done:
[18]:
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"""
<div>
<div style="font-size:12pt;padding:2px;">{{hover_text}}</div>
<div style="background-color:{{color}};color:#fff;{badge_css}">{{primary_field}}</div>
<div style="background-color:#eeeeeeff;{badge_css}">citation count: {{citation_count}}</div>
</div>
"""
You can see the end result is much as before – HTML with curly based field names from the extra point data wherever we want to substitute in point specific values.
[19]:
print(hover_text_template)
<div>
<div style="font-size:12pt;padding:2px;">{hover_text}</div>
<div style="background-color:{color};color:#fff;
border-radius:6px;
width:fit-content;
max-width:75%;
margin:2px;
padding: 2px 10px 2px 10px;
font-size: 10pt;
">{primary_field}</div>
<div style="background-color:#eeeeeeff;
border-radius:6px;
width:fit-content;
max-width:75%;
margin:2px;
padding: 2px 10px 2px 10px;
font-size: 10pt;
">citation count: {citation_count}</div>
</div>
We can now do exactly as before, but this time with the much fancier formatted tooltip:
[20]:
plot = datamapplot.create_interactive_plot(
cord19_data_map,
cord19_labels,
hover_text=cord19_titles,
title="CORD-19 Data Map",
sub_title="A data map of papers relating to COVID-19",
font_family="Cinzel",
initial_zoom_fraction=0.4,
logo="https://allenai.org/newsletters/archive/2023-03-newsletter_files/927c3ca8-6c75-862c-ee5d-81703ef10a8d.png",
logo_width=128,
color_label_text=False,
inline_data=False,
offline_data_prefix="cord-2",
marker_size_array=marker_size_array,
marker_color_array=marker_color_array,
noise_color="#aaaaaa44",
extra_point_data=cord19_extra_data,
hover_text_html_template=hover_text_template,
)
plot
[20]:
Now the hover looks much better, and we are starting to take advantage of what HTML and CSS has to offer. Obviously there is more that you can do. If you have images associated to each point you could include URLs to the images in the extra_point_data
, have an hover_text_template
that has something like <img src={img_url}>
and quickly and easily have image hover. The possibilities, if you are willing to put in a little work, are nearly endless.
Of course that only gets us as far as having rich hover tooltips. What if we want to add whole other elements to the plot. For example since we have points coloured by field of study wouldn’t it be beneficial to have a legend showing which colour corresponds to which field of study? We can actually add anything we wish to the plot via the custom_html
keyword argument, and style it via the custom_css
keyword argument, which will add to the CSS header and HTML body of the output. So, let’s
construct a nicely formatted legend in a combination of HTML and CSS:
[21]:
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%;
}
"""
custom_html = """
<div id="legend">
"""
for field, color in color_mapping.items():
custom_html += f' <div class="row"><div id="{field}" class="box" style="background-color:{color};padding:0px 0 1px 0;text-align:center"></div>{field}</div>\n'
custom_html += """
</div>
"""
We are constructing the (somewhat repetitive) HTML programatically from the colour mapping we created, so let’s look at what we have actually created in the way of HTML:
[22]:
print(highlight(custom_html, lexer, formatter))
<div id="legend">
<div class="row"><div id="Medicine" class="box" style="background-color:#bbbbbb;padding:0px 0 1px 0;text-align:center"></div>Medicine</div>
<div class="row"><div id="Biology" class="box" style="background-color:#e31a1c;padding:0px 0 1px 0;text-align:center"></div>Biology</div>
<div class="row"><div id="Chemistry" class="box" style="background-color:#fd8e3c;padding:0px 0 1px 0;text-align:center"></div>Chemistry</div>
<div class="row"><div id="Physics" class="box" style="background-color:#fed977;padding:0px 0 1px 0;text-align:center"></div>Physics</div>
<div class="row"><div id="Business" class="box" style="background-color:#88419d;padding:0px 0 1px 0;text-align:center"></div>Business</div>
<div class="row"><div id="Economics" class="box" style="background-color:#8c97c6;padding:0px 0 1px 0;text-align:center"></div>Economics</div>
<div class="row"><div id="Political Science" class="box" style="background-color:#c0d4e6;padding:0px 0 1px 0;text-align:center"></div>Political Science</div>
<div class="row"><div id="Psychology" class="box" style="background-color:#234da0;padding:0px 0 1px 0;text-align:center"></div>Psychology</div>
<div class="row"><div id="Sociology" class="box" style="background-color:#2498c1;padding:0px 0 1px 0;text-align:center"></div>Sociology</div>
<div class="row"><div id="Geography" class="box" style="background-color:#73c8bd;padding:0px 0 1px 0;text-align:center"></div>Geography</div>
<div class="row"><div id="History" class="box" style="background-color:#d6efb3;padding:0px 0 1px 0;text-align:center"></div>History</div>
<div class="row"><div id="Computer Science" class="box" style="background-color:#008080;padding:0px 0 1px 0;text-align:center"></div>Computer Science</div>
<div class="row"><div id="Engineering" class="box" style="background-color:#4da6a6;padding:0px 0 1px 0;text-align:center"></div>Engineering</div>
<div class="row"><div id="Mathematics" class="box" style="background-color:#9bcdcd;padding:0px 0 1px 0;text-align:center"></div>Mathematics</div>
<div class="row"><div id="Environmental Science" class="box" style="background-color:#a16868;padding:0px 0 1px 0;text-align:center"></div>Environmental Science</div>
<div class="row"><div id="Geology" class="box" style="background-color:#d0ac94;padding:0px 0 1px 0;text-align:center"></div>Geology</div>
<div class="row"><div id="Materials Science" class="box" style="background-color:#e9e9b6;padding:0px 0 1px 0;text-align:center"></div>Materials Science</div>
<div class="row"><div id="Art" class="box" style="background-color:#38384e;padding:0px 0 1px 0;text-align:center"></div>Art</div>
<div class="row"><div id="Philosophy" class="box" style="background-color:#707b90;padding:0px 0 1px 0;text-align:center"></div>Philosophy</div>
<div class="row"><div id="Unknown" class="box" style="background-color:#a9c8c8;padding:0px 0 1px 0;text-align:center"></div>Unknown</div>
</div>
Not exactly pretty HTML, but it will do the job. The CSS code will put the "legend"
div at the top right. Also note that the z-index
is set to 2. If you want your additional HTML elements to show up above the plot you will need to make sure they have a z-index
of at least 2. Now we simply pass the custom_html
and custom_css
in to create_interactive_plot
and have it add that to our output:
[23]:
plot = datamapplot.create_interactive_plot(
cord19_data_map,
cord19_labels,
hover_text=cord19_titles,
title="CORD-19 Data Map",
sub_title="A data map of papers relating to COVID-19",
font_family="Cinzel",
initial_zoom_fraction=0.4,
logo="https://allenai.org/newsletters/archive/2023-03-newsletter_files/927c3ca8-6c75-862c-ee5d-81703ef10a8d.png",
logo_width=128,
color_label_text=False,
inline_data=False,
offline_data_prefix="cord-3",
marker_size_array=marker_size_array,
marker_color_array=marker_color_array,
noise_color="#aaaaaa44",
extra_point_data=cord19_extra_data,
hover_text_html_template=hover_text_template,
custom_css=custom_css,
custom_html=custom_html,
)
plot
[23]:
The result is a plot just as before, but now with a nicely formatted custom legend in the top right. Obviously you can add whatever additional HTML content you wish to your plot in much the same way.
Such an extra element is just going to be static however. To make it interactive you would need to add javascript actions. Fortunately the custom_js
keyword argument let’s you do exactly this. It will add custom javascript code to the end of the javascript code black required for constructing the plot. That means we can add custom javascript associated to any extra HTML elements we add to the plot.
In our case it might be nice if we could click on the colour swatches in the legend and have that select the points in the plot from that specific field of study. This amounts to standard javascript with an addEventListener
attaching an action to the legend when it is clicked. To select out points we can make use of the selectPoints
function added to the javascript when search is enabled – it takes a value to be checked, and a function mapping a point index to a boolean of whether the
point should be selected ot not. In our case that is the id of the div that was clicked (if we didn’t click a swatch, don’t select anything), and a check of whether the extra data primary_field at that index matches the id of the div that was clicked.
To make the javascript a little more interesting we’ll add a check mark for the selected field, and ensure that selecting via search will clear the check mark.
[24]:
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 = "";
}
}
});
"""
We can now do exactly as we did before (being sure to enable search so we have the selectPoints
javascript function available), adding in our custom javascript code:
[28]:
plot = datamapplot.create_interactive_plot(
cord19_data_map,
cord19_labels,
hover_text=cord19_titles,
title="CORD-19 Data Map",
sub_title="A data map of papers relating to COVID-19",
font_family="Cinzel",
initial_zoom_fraction=0.4,
logo="https://allenai.org/newsletters/archive/2023-03-newsletter_files/927c3ca8-6c75-862c-ee5d-81703ef10a8d.png",
logo_width=128,
color_label_text=False,
inline_data=False,
offline_data_prefix="cord-4",
marker_size_array=marker_size_array,
marker_color_array=marker_color_array,
noise_color="#aaaaaa44",
extra_point_data=cord19_extra_data,
hover_text_html_template=hover_text_template,
enable_search=True,
custom_css=custom_css,
custom_html=custom_html,
custom_js=custom_js,
)
plot
[28]:
Now we can click on colour swatches in the legend to select points by primary field (go ahead and try it in the live plot above). The entire plot itself in built out of Deck.gl so if you are willing to dig into javascript you can do pretty much whatever you wish (including adding entirely new layers to the plot, changing click and drag actions, etc.). So ultimately you can customise the output essentially as you wish. The main goal of DataMapPlot is to make it easy to get good looking results quickly and easily, so hopefully you won’t have too much need to go too far.