markhneedham/blog huggingface dataset:
React Semantic-UI: Adding a custom icon to open link in a new window
Date: 2020-04-13 00:21:00
Learn how to render a custom icon when using React Semantic-UI.

I've been building a little React app that uses the https://react.semantic-ui.com/[Semantic UI^] library and found myself wanting to render a custom icon.

Semantic UI describes an https://react.semantic-ui.com/elements/icon/[icon^] as "a glyph used to represent something else", and there are a big list of in built icons.
For example, the following code renders a thumbs up icon:

[source,javascript]
----
import {Icon} from "semantic-ui-react";

<Icon name="thumbs up outline icon green large" style={{margin: 0}}/>
----

image::{{<siteurl>}}/uploads/2020/04/thumbs-up.png[title="Thumbs Up Icon"]

I wanted to add an icon to indicate that a link would open in a new window, and came across this https://github.com/Semantic-Org/Semantic-UI-React/issues/931[GitHub issue^] that explains how to add a custom icon.
Instead of using the `name` property, we can use the `className` property to have a icon render based on our own CSS.

Ruard van Elburg explained how to render an open in new window icon in https://stackoverflow.com/a/61104675/1093511[this StackOverflow answer^].

We'll first add the following CSS:

[source,css]
----
i.icon.open-new-window:after {
  content: "\f35d";
}
----

And then can use the `open-new-window` class to render our icon:


[source,javascript]
----
<a href="https://www.neo4j.com" target="_blank">
  <Icon className="open-new-window black"/>
</a>
----

image::{{<siteurl>}}/uploads/2020/04/open-window.png[title="Open Window Icon"]

And we're done!
Google Docs: Find and replace script
Date: 2020-05-12 00:21:00
Learn how to write a script to run find and replace on a Google Doc

I keep track of the podcasts that I've listened to in a Google Doc, having pasted the episode title and podcast name from Player.FM.
The format isn't exactly what I want so I've been running the `Find and Replace` command to update each entry.
This is obviously a very boring task, so I wanted to see if I could automate it.

An example entry in the Google Doc reads like this:

[source, text]
----
* Listened to Worst of the Big 3 by The Tennis Podcast
https://player.fm/1BBKmKQ #nowplaying
----

And I want it to read like this:

[source, text]
----
* Listened to Worst of the Big 3 by The Tennis Podcast - https://player.fm/1BBKmKQ
----

We can do this by doing by replacing:

* `\nht` with `&nbsp;- ht&nbsp;`
* `#nowplaying` with nothing

We can create a Google script by clicking `Tools` > `Script editor`, which will take us to an editor with the following text:

[source,javascript]
----
function myFunction() {

}
----

We can write the following code to update our Google Doc:

[source,javascript]
----
function myFunction() {
  // https://docs.google.com/document/d/<document-id>/edit
  let documentId = "<document-id>"
  let document = DocumentApp.openById(documentId);
  let body = document.getBody().getText()
  document.getBody().setText(body.replace(/#nowplaying/g, "").replace(/\nht/g, " - ht"))
}
----

I tried using the `replaceText` function that can be run against the body of the document, but I couldn't figure out how to get that to work with the new line character.

We can then run that manually to update our document, but we can go one better and setup a trigger.
We can setup a trigger via the `Edit` menu:

image::{{<siteurl>}}/uploads/2020/05/triggers.png[title="Triggers"]

Once the next screen loads, we can then click on `Add Trigger` to set everything up:

image::{{<siteurl>}}/uploads/2020/05/add-trigger.png[title="Add Trigger"]

This trigger will run our script once an hour, removing new lines and the `#nowplaying` phrase.

Boredom removed!
Python: Altair - Setting the range of Date values for an axis
Date: 2020-01-14 00:21:00
Learn how to set the range of date values on the x axis of an Altair chart

In my continued experiments with the https://altair-viz.github.io/index.html[Altair visualisation library], I wanted to set a custom range of data values on the x axis of a chart.
In this blog post we'll learn how to do that.

We'll start where we left off in the https://markhneedham.com/blog/2020/01/10/altair-typeerror-object-type-date-not-json-serializable/[last blog post^], with the following code that renders a scatterplot containing the chart position of a song on a certain date:

[source,python]
----
import altair as alt
import pandas as pd
import datetime

df = pd.DataFrame( [
    {"position": 40, "date": datetime.date(2019,9,5)},
    {"position": 31, "date": datetime.date(2019,9,12)},
    {"position": 19, "date": datetime.date(2019,9,19)},
    {"position": 14, "date": datetime.date(2019,9,26)},
    {"position": 7, "date": datetime.date(2019,10,3)},
    {"position": 1, "date": datetime.date(2019,10,10)},
    {"position": 1, "date": datetime.date(2019,10,17)},
    {"position": 1, "date": datetime.date(2019,10,24)},
])
df["date"] = pd.to_datetime(df["date"])

chart = alt.Chart(df).mark_point(color="red").encode(
    x='date',
    y='position')
chart.save("chart.html")
----

If we run this script, we'll have the following chart in `chart.html`:

image::{{<siteurl>}}/uploads/2020/01/altair-dance-monkey.png[title="Chart showing chart positions"]

At the moment the x axis of our chart covers the range of dates included in our DataFrame.
I wanted to have that axis start from 1st January 2019 and end on 31st December 2019, and came across https://github.com/altair-viz/altair/issues/1005[a GitHub issue^] describing how to do this.

One way to do this is to create a Pandas index and then define a custom domain for the `scale` of the x axis.
The code below does this:

[source,python]
----
domain_pd = pd.to_datetime(['2019-01-01', '2019-12-31']).astype(int) / 10 ** 6
chart = alt.Chart(df).mark_point(color="red").encode(
    x=alt.X('date:T', timeUnit='yearmonthdate', scale=alt.Scale(domain=list(domain_pd))),
    y='position')
chart.save("chart.html")
----

If we run this script, we'll have the following chart in `chart.html`:

image::{{<siteurl>}}/uploads/2020/01/altair-dance-monkey-full-year.png[title="Chart showing chart positions"]

Another, simpler way, is to pass in a list containing the start and end dates, as shown below:

[source,python]
----
domain = ["2019-01-01", "2019-12-31"]
chart = alt.Chart(df).mark_point(color="red").encode(
    x=alt.X('date:T', timeUnit='yearmonthdate', scale=alt.Scale(domain=domain)),
    y='position')
chart.save("chart.html")
----

All the code from this blog post is available in a https://gist.github.com/mneedham/e5d347cbffe0b04584a267c0d961e1a1[GitHub Gist^].
git: Ignore local changes on committed (env) file
Date: 2020-12-18 00:44:37
In this post we'll learn how ignore local changes to a committed (environment) file.

Whenever I've writing scripts that rely on credentials defined as environment variables, I like to create a `.env` (or equivalent) file containing those variables.
I then seed that file with placeholder values for each variable and make local changes that aren't checked in.

I recently created the https://github.com/mneedham/materialize-sandbox/tree/main/strava[mneedham/materialize-sandbox/strava^] repository where I'm using this approach with a https://github.com/mneedham/materialize-sandbox/blob/main/strava/.envsettings[`.envsettings`^] file that has the following contents:

.envsettings
[source,text]
----
export CLIENT_ID="client_id"
export CLIENT_SECRET="client_secret"
----

I have that file checked in so that anybody else can clone the repository and update this file with their own credentials.

But I don't want local changes to that file to be tracked, as I might then accidentally commit my credentials.
I'd previously achieved this by adding `.envsettings` to the `.gitignore` file, but that technique doesn't seem to work anymore!

Luckily there is a way around this, using git's https://stackoverflow.com/questions/13630849/git-difference-between-assume-unchanged-and-skip-worktree#[`update-index`^] command, which I learnt about from https://stackoverflow.com/a/13631525/1093511[Borealid's answer on StackOverflow^].

So once I've committed the `.envsettings` file, I can have git ignore any changes by running the following command:

[source,bash]
----
git update-index --skip-worktree strava/.envsettings
----

And now git will ignore any local changes made to this file, which is exactly what I want!
Neo4j Desktop: undefined: Unable to extract host from undefined
Date: 2018-03-20 17:51:10
Learn how to work around a weird message in the Neo4j browser.

During a training session I facilitated today one of the attendees got the following error message while trying to execute a query inside the Neo4j Desktop.

image::{{<siteurl>}}/uploads/2018/03/2018-03-20_17-49-35.png[width="600px"]

This error message happens if we try to run a query when the database hasn't been started, and would usually be accompanied by this screen:

image::{{<siteurl>}}/uploads/2018/03/2018-03-20_17-56-48.png[width="600px"]

On this occasion that wasn't happening, but we can easily fix it by going back to the project screen and starting the database:

image::{{<siteurl>}}/uploads/2018/03/2018-03-20_17-58-54.png[width="600px"]

Once we do that we'll be able to execute queries again.
Running asciidoctor-pdf on TeamCity
Date: 2018-03-13 21:57:14

I've been using https://asciidoctor.org/docs/asciidoctor-pdf/[asciidoctor-pdf] to generate PDF and while I was initially running the tool locally I eventually decided to setup a build on TeamCity.

It was a bit trickier than I expected, mostly because I'm not that familiar with deploying Ruby applications, but I thought I'd capture what I've done for future me.

I have the following +++<cite>+++Gemfile+++</cite>+++ that installs asciidoctor-pdf and its dependencies:

+++<cite>+++Gemfile+++</cite>+++

[source,text]
----

source 'https://rubygems.org'

gem 'prawn'
gem 'addressable'
gem 'prawn-svg'
gem 'prawn-templates'
gem 'asciidoctor-pdf'
----

I don't have permissions to install gems globally on the build agents so I'm bundling those up into the +++<cite>+++vendor+++</cite>+++ directory. It's been a long time since I worked on a Ruby application so perhaps that's par for the course.

[source,bash]
----

bundle install --path vendor/
bundle package
----

On the build agent I'm running the following script:

[source,bash]
----

export PATH="$PATH:/home/teamcity/.gem/ruby/2.3.0/bin"
mkdir $PWD/gems
export GEM_HOME="$PWD/gems"
gem install bundler --user-install --no-rdoc --no-ri && bundle install
./vendor/ruby/2.3.0/bin/asciidoctor-pdf -a allow-uri-read blog.adoc
----

I override where gems should be installed and then execute the +++<cite>+++asciidoctor-pdf+++</cite>+++ executable from the vendor directory.

It all seems to work quite nicely but if there's a better approach that I should be taking so let me know in the comments.
Neo4j: Cypher - Neo.ClientError.Statement.TypeError: Don't know how to add Double and String
Date: 2018-03-14 16:53:33

I recently upgraded a Neo4j backed application from Neo4j 3.2 to Neo4j 3.3 and came across an interesting change in behaviour around type coercion which led to my application throwing a bunch of errors.

In Neo4j 3.2 and earlier if you added a String to a Double it would coerce the Double to a String and concatenate the values. The following would therefore be valid Cypher:

[source,cypher]
----

RETURN toFloat("1.0") + " Mark"

╒══════════╕
│"result"  │
╞══════════╡
│"1.0 Mark"│
└──────────┘
----

This behaviour has changed in the 3.3 series and will instead throw an exception:

[source,cypher]
----

RETURN toFloat("1.0") + " Mark"

Neo.ClientError.Statement.TypeError: Don't know how to add `Double(1.000000e+00)` and `String(" Mark")`
----

We can workaround that by forcing our query to run in 3.2 mode:

[source,cypher]
----

CYPHER 3.2
RETURN toFloat("1.0") + " Mark" AS result
----

or we can convert the Double to a String in our Cypher statement:

[source,cypher]
----

RETURN toString(toFloat("1.0")) + " Mark" AS result
----
Python via virtualenv on Mac OS X: RuntimeError: Python is not installed as a framework.
Date: 2018-05-04 22:03:08
Learn how to work around the RuntimeError: Python is not installed as a framework. error that you might see running matplotlib on Mac OS X.

I've previously written https://markhneedham.com/blog/2016/08/14/python-matplotlibseabornvirtualenv-python-is-not-installed-as-a-framework/[a couple^] https://markhneedham.com/blog/2015/03/26/python-matplotlib-hangs-and-shows-nothing-mac-os-x/[of blog posts^] about my troubles getting matplotlib to play nicely and I run into a slightly different variant today while following Sidath Asiri's https://towardsdatascience.com/hello-world-in-tensorflow-973e6c38e8ed[Hello World in TensorFlow^] tutorial.

When I ran the script using a version of Python installed via virtualenv I got the following exception:

[source,bash]
----
Traceback (most recent call last):
  File "iris.py", line 4, in <module>
    from matplotlib import pyplot as plt
  File "/Users/markneedham/projects/tensorflow-playground/a/lib/python3.6/site-packages/matplotlib/pyplot.py", line 116, in <module>
    _backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup()
  File "/Users/markneedham/projects/tensorflow-playground/a/lib/python3.6/site-packages/matplotlib/backends/__init__.py", line 60, in pylab_setup
    [backend_name], 0)
  File "/Users/markneedham/projects/tensorflow-playground/a/lib/python3.6/site-packages/matplotlib/backends/backend_macosx.py", line 17, in <module>
    from matplotlib.backends import _macosx
RuntimeError: Python is not installed as a framework. The Mac OS X backend will not be able to function correctly if Python is not installed as a framework. See the Python documentation for more information on installing Python as a framework on Mac OS X. Please either reinstall Python as a framework, or try one of the other backends. If you are using (Ana)Conda please install python.app and replace the use of 'python' with 'pythonw'. See 'Working with Matplotlib on OSX' in the Matplotlib FAQ for more information.
----

The solution this time was actually the same as it was in my post where I https://markhneedham.com/blog/2015/03/26/python-matplotlib-hangs-and-shows-nothing-mac-os-x/[described matplotlib hanging^].
Instead of:

[source,python]
----
from matplotlib import pyplot as plt
----

We need to define a different backend, like this:

[source,python]
----
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
----

I also learnt an even simpler way of solving the problem via https://stackoverflow.com/questions/34977388/matplotlib-runtimeerror-python-is-not-installed-as-a-framework[this StackOverflow post^].
We can achieve the same outcome by creating a file at `~/.matplotlib/matplotlibrc` and populating it with the following text:

[source,bash]
----
$ cat ~/.matplotlib/matplotlibrc
backend: TkAgg
----

Now we can go back to importing matplotlib without the need to define the backend each time.
Success!
Asciidoctor: Creating a macro
Date: 2018-02-19 20:51:31

I've been writing the https://neo4j.com/tag/twin4j/[TWIN4j blog] for almost a year now and during that time I've written a few different http://asciidoc.org/chunked/ch21.html[asciidoc macros] to avoid repetition.

The most recent one I wrote does the formatting around the Featured Community Member of the Week. I call it like this from the asciidoc, passing in the name of the person and a link to an image:

[source,text]
----

featured::https://s3.amazonaws.com/dev.assets.neo4j.com/wp-content/uploads/20180202004247/this-week-in-neo4j-3-february-2018.jpg[name="Suellen Stringer-Hye"]
----

The code for the macro has two parts. The first is some wiring code that registers the macro with Asciidoctor:

+++<cite>+++lib/featured-macro.rb+++</cite>+++

[source,ruby]
----

RUBY_ENGINE == 'opal' ? (require 'featured-macro/extension') : (require_relative 'featured-macro/extension')

Asciidoctor::Extensions.register do
  if (@document.basebackend? 'html') && (@document.safe < SafeMode::SECURE)
    block_macro FeaturedBlockMacro
  end
end
----

And this is the code for the macro itself:

+++<cite>+++lib/featured-macro/extension.rb+++</cite>+++

[source,ruby]
----

require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'

include ::Asciidoctor

class FeaturedBlockMacro < Extensions::BlockMacroProcessor
  use_dsl

  named :featured

  def process parent, target, attrs
    name = attrs["name"]

    html = %(<div class="imageblock image-heading">
                <div class="content">
                    <img src="#{target}" alt="#{name} - This Week’s Featured Community Member" width="800" height="400">
                </div>
            </div>
            <p style="font-size: .8em; line-height: 1.5em;" align="center">
              <strong>#{name} - This Week's Featured Community Member</strong>
            </p>
)

    create_pass_block parent, html, attrs, subs: nil
  end
end
----

When we convert the asciidoc into HTML we need to tell asciidoctor about the macro, which we can do like this:

[source,bash]
----

asciidoctor template.adoc \
  -r ./lib/featured-macro.rb \
  -o -
----

And that's it!
matplotlib - MatplotlibDeprecationWarning: Adding an axes using the same arguments as a previous axes currently reuses the earlier instance.  In a future version, a new instance will always be created and returned.
Date: 2018-09-18 07:56:00
Learn how to avoid a MatplotlibDeprecationWarning message when editing axes.

In my last post I showed how to link:/blog/2018/09/18/matplotlib-remove-axis-legend/[remove axes legends from a matplotlib chart^], and while writing the post I actually had the change the code I used as my initial approach is now deprecated.

As in the previous post, we'll first import pandas and matplotlib:

[source, python]
----
import pandas as pd

import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt

plt.style.use('fivethirtyeight')
----

And we'll still use this DataFrame:

[source, python]
----
df = pd.DataFrame({"label": ["A", "B", "C", "D"], "count": [12, 19, 5, 10]})
----

My initial approach to remove all legends was this:

[source, python]
----
df.plot(kind='bar', x='label', y='count', legend=None)
plt.axes().xaxis.set_label_text("")
plt.tight_layout()
plt.savefig("/tmp/matplotlib_no_x_no_y.svg")
plt.close()
----

If we run this code the chart will still be generated, but we'll also receive the following error message:

[source, text]
----
/Users/markneedham/projects/matplotlib-examples/a/lib/python3.6/site-packages/matplotlib/cbook/deprecation.py:107: MatplotlibDeprecationWarning: Adding an axes using the same arguments as a previous axes currently reuses the earlier instance.  In a future version, a new instance will always be created and returned.  Meanwhile, this warning can be suppressed, and the future behavior ensured, by passing a unique label to each axes instance.
  warnings.warn(message, mplDeprecation, stacklevel=1)
----

The problem is on the 2nd line.
Instead of mutating the axes via the `plt` object, we need to capture the `AxesSubplot` object returned by the `plot()` function and update that instead:

[source, python]
----
ax = df.plot(kind='bar', x='label', y='count', legend=None)
ax.xaxis.set_label_text("")
plt.tight_layout()
plt.savefig("/tmp/matplotlib_no_x_no_y.svg")
plt.close()
----

If we run this version of the code our chart will be generated and we won't have that deprecation message anymore.
matplotlib - Remove axis legend
Date: 2018-09-18 07:55:00
Learn how to remove all axis legends from a matplotlib chart.

I've been working with matplotlib a bit recently, and I wanted to remove all axis legends from my chart.
It took me a bit longer than I expected to figure it out so I thought I'd write it up.

Before we do anything let's import matplotlib as well as pandas, since we're going to plot data from a pandas DataFrame.

[source, python]
----
import pandas as pd

import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt

plt.style.use('fivethirtyeight')
----

Next we'll create a DataFrame with one column called `label` and another called `count`:

[source, python]
----
df = pd.DataFrame({"label": ["A", "B", "C", "D"], "count": [12, 19, 5, 10]})
----

We want to plot a bar chart with the label on the x-axis and the count on the y-axis.
The following code will plot a chart and store it in an SVG file:

[source, python]
----
df.plot(kind='bar', x='label', y='count')
plt.tight_layout()
plt.savefig("/tmp/matplotlib_legends.svg")
plt.close()
----

This is what the chart looks like:

image::{{<siteurl>}}/uploads/2018/09/matplotlib_legends.svg[]

We've got legends for both axes, but we can pass in `legend=None` to the `plot()` function which should sort that out:

[source, python]
----
df.plot(kind='bar', x='label', y='count', legend=None)
plt.tight_layout()
plt.savefig("/tmp/matplotlib_no_y.svg")
plt.close()
----

This is what the chart looks like now:

image::{{<siteurl>}}/uploads/2018/09/matplotlib_no_y.svg[]

The y-axis legend has been removed, but the x-axis one is still there.
After a bit of searching I found the https://matplotlib.org/api/_as_gen/matplotlib.axis.Axis.set_label_text.html[set_label_text^] function, which we can use like this:

[source, python]
----
ax = df.plot(kind='bar', x='label', y='count', legend=None)
ax.xaxis.set_label_text("")
plt.tight_layout()
plt.savefig("/tmp/matplotlib_no_x_no_y.svg")
plt.close()
----

And now we finally have a chart with all legends removed:

image::{{<siteurl>}}/uploads/2018/09/matplotlib_no_x_no_y.svg[]

Success!
matplotlib - Create a histogram/bar chart for ratings/full numbers
Date: 2018-09-24 07:55:00
Learn how to create a matplotlib histogram for star ratings.

In my continued work with matplotlib I wanted to plot a histogram (or bar chart) for a bunch of star ratings to see how they were distributed.

Before we do anything let's import matplotlib as well as pandas:

[source, python]
----
import random
import pandas as pd

import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt

plt.style.use('fivethirtyeight')
----

Next we'll create an array of randomly chosen star ratings between 1 and 5:

[source, python]
----
stars = pd.Series([random.randint(1, 5) for _ in range(0, 100)])
----

We want to plot a histogram showing the proportion for each rating.
The following code will plot a chart and store it in an SVG file:

[source, python]
----
_, ax1 = plt.subplots()
ax1.hist(stars, 5)
plt.tight_layout()
plt.savefig("/tmp/hist.svg")
plt.close()
----

This is what the chart looks like:

image::{{<siteurl>}}/uploads/2018/09/hist.svg[]

This is ok, but the labels on the x axis are a bit weird - the value for each rating doesn't align with the corresponding bar.
I came across https://stackoverflow.com/questions/18393887/how-to-create-a-bar-chart-histogram-with-bar-per-discrete-value[this StackOverflow post^], which shows how to solve this problem by using a bar chart instead.
I ended up with this code:

[source, python]
----
_, ax2 = plt.subplots()

stars_histogram = stars.value_counts().sort_index()
stars_histogram /= float(stars_histogram.sum())
stars_histogram *= 100

stars_histogram.plot(kind="bar", width=1.0)
plt.tight_layout()
plt.savefig("/tmp/bar.svg")
plt.close()
----

This is what the chart looks like now:

image::{{<siteurl>}}/uploads/2018/09/bar.svg[]

Much better!