Above, Below, and Beyond Tech Talk

by Rahel Lüthy

May 24, 2018

React SVG Tooltips

SVG has built-in support for tooltips through its <title> element. However, the rendered text looks very basic and cannot easily by styled:

Tooltips are an important design element when creating information-heavy, visual applications. They allow keeping the user interface clean, providing information only when needed. As Shneiderman (1996) put it in his famous visual information-seeking mantra:

Overview first, zoom and filter, then details-on-demand
Overview first, zoom and filter, then details-on-demand
Overview first, zoom and filter, then details-on-demand

Tooltips cover the details-on-demand aspect. However, in order to provide such details in an appealing way, built-in SVG tooltips are often not sufficient.

SVG Tooltips

There’s actually a pretty good technology out there to design appealing graphical UI elements: SVG! It is way superior to simple text boxes.

In an ideal SVG world, tooltips would:

  1. Be specific to an element
  2. Be visible on demand (mouse hovering)
  3. Support fully customizable SVG contents
  4. Appear always on top

Good News: If you’re using React, I have got you covered! Meet react-svg-tooltip, my npm package that addresses all SVG tooltip needs.

Intro to react-svg-tooltip

The library offers a Tooltip component which can be embedded in any SVG element hierarchy:

import * as React from 'react';
import { Tooltip } from 'react-svg-tooltip';

const App = () => {

    const circleRef = React.createRef<SVGCircleElement>();

    return (
        <div className='App'>
            <svg viewBox='0 0 100 100'>
                <circle ref={circleRef} cx={50} cy={50} r={10} fill='steelblue'/>
                <Tooltip for={circleRef}>
                    <rect x={2} y={2} width={10} height={5} rx={.5} ry={.5} fill='black'/>
                    <text x={5} y={5} fontSize={2} fill='white'>Yay!</text>
                </Tooltip>
            </svg>
        </div>
    );
};

export default App;

Edit pk7p4y9v3q

The component covers all requirements listed above:

  1. Its for property accepts a reference to an element which serves as the mouse trigger
  2. All mouse listener handling is happening behind the scenes
  3. Arbitrary SVG can be used as tooltip contents. In fact, the library itself does not provide an actual tooltip. Instead, it gives you a 0-based coordinate system, so you can place your favorite SVG elements in whichever style suits your needs.
  4. Contents are attached to the root svg element (using a React portal behind the scenes). Thereby your tooltip will always be rendered last, i.e. always on top.

As usual, all code is on GitHub, feedback is very welcome, and PRs are highly appreciated!


May 3, 2018

GitHub Pages Custom Domains Via HTTPS

Two days ago, GitHub announced the support of HTTPS for custom domains. In addition to this blog, I maintain a few websites for friends and family, all configured through A records – time for migration!

Officially, the migration is a simple matter of toggling the “Enforce HTTPS” button, but the button was disabled for all of my sites. The explanation wasn’t particularly encouraging: Unavailable for your site because your domain is not properly configured to support HTTPS.

Well, time for RTFM (not my favorite hobby).

These are the steps that finally worked for me:

HTTPS Migration Steps

Step 0: Mixed Content Prevention

First things first: Prepare your site’s content. Ensure that all your assets (links to CSS, JS, etc.) are loaded via HTTPS. Otherwise browsers might block your “Mixed Content”.

Step 1: DNS A-Record Migration

On your provider’s website, change all A records to point to the following four new IPs:
 

192.30.252.153

192.30.252.154

185.199.108.153

185.199.109.153

185.199.110.153

185.199.111.153
 

This change might take a few hours to become effective (depending on the TTL). Use dig to check whether the new DNS configuration is ready:

dig netzwerg.ch +noall +answer

It should look like this:

; <<>> DiG 9.10.6 <<>> netzwerg.ch +noall +answer
;; global options: +cmd
netzwerg.ch.		10800	IN	A	185.199.110.153
netzwerg.ch.		10800	IN	A	185.199.111.153
netzwerg.ch.		10800	IN	A	185.199.108.153
netzwerg.ch.		10800	IN	A	185.199.109.153

Step 2: Trigger Certificate Generation

In order for GitHub to detect that your site is now ready for migration, you apparently need to trigger a detection script. Any change to your repository’s CNAME file will do, so e.g. make a minor change and revert it, or make a harmless whitespace edit. As a result, GitHub will now generate the certificates necessary for HTTPS.

Step 3: Wait For Certificate Availability

Now the “Enforce HTTPS” button will still be disabled, but the explanation will be much more encouraging: Not yet available for your site because the certificate has not finished being issued. More patience required, we’re getting there…

Step 4: Enforce HTTPS

It might take a few hours, but as soon as this message is gone, you can access your site via HTTPS. Check that everything still works (particularly your new HTTPS asset links). Once you’re happy, toggle the “Enforce HTTPS” switch – Voilà!


April 24, 2018

Position animations across SVG groups

Animated transitions between UI states can greatly enhance the usability of applications because they reduce the likelihood of change blindness.

In a typical web application, UI state is represented by the DOM. Instead of switching from one DOM state to the next instantaneously, animations smoothly transition DOM element properties over time.

On a technical level, a multitude of libraries provide abstractions to create animated transitions with minimal effort.

This is how an animation of an SVG circle would look like with D3.js (notice the “Rerun” button which appears once you hover the right pane):
 

See the Pen D3.js Animation by Rahel Lüthy (@netzwerg) on CodePen.


 

D3.js is interpolating the circle’s cx attribute from 10 to 190 pixels over the course of a second.

Well, this is a simple example because the SVG scene graph is simple – it just contains one circle. Unfortunately, SVG scene graphs tend to get very complex in real applications. One way to tame this complexity, is by breaking applications into components, each responsible for rendering a sub-graph of the final SVG.

Components FTW

Component-based libraries like React come in handy here: Each component is responsible for rendering a small, human-digestible SVG chunk.

Here’s a React component which renders a box with a circle at its center:

const Box = ({width, height}: BoxProps) => {
    return (
        <g>
            <rect width={width} height={height}/>
            <circle cx={width / 2} cy={height / 2}/>
        </g>
    );
};

Using simple composition, we can then stack two boxes on top of each other:

<g>
    <Box width={width} height={height / 2} />
</g>
<g transform={`translate(0,${height / 2})`}>
    <Box width={width} height={height / 2} />
</g>

Note how the second box gets moved to the bottom by translating its container group by height/2.

To make things a bit more interesting, let’s make the circle alternate between boxes at a fixed interval:

type State = {
    readonly box: 'UPPER' | 'LOWER';
};

class App extends React.Component<object, State> {

    private readonly circleId = 'circleId';
    private timer: Timer;

    constructor(props: object) {
        super(props);
        this.state = {box: 'UPPER'};
    }

    componentDidMount() {
        this.timer = setInterval(() => this.switchBox(), 1000);
    }

    switchBox() {
        this.setState(prevState => ({box: prevState.box === 'UPPER' ? 'LOWER' : 'UPPER'}));
    }

    render() {

        const viewBox = {width: 100, height: 100};
        const margin = {top: 10, right: 10, bottom: 10, left: 10};
        const width = viewBox.width - margin.left - margin.right;
        const height = viewBox.height - margin.top - margin.bottom;

        return (
            <div className="App">
                <svg viewBox={`0 0 ${viewBox.width} ${viewBox.height}`}>
                    <g transform={`translate(${margin.left}, ${margin.top})`}>
                        <g>
                            <Box
                                circleId={this.circleId}
                                showCircle={this.state.box === 'UPPER'}
                                width={width}
                                height={height / 2}
                            />
                        </g>
                        <g transform={`translate(0,${height / 2})`}>
                            <Box
                                circleId={this.circleId}
                                showCircle={this.state.box === 'LOWER'}
                                width={width}
                                height={height / 2}
                            />
                        </g>
                    </g>
                </svg>
            </div>
        );
    }

    componentWillUnmount() {
        clearInterval(this.timer);
    }

}

Complete code on GitHub

Smoothly Moving Circles

Blinking UIs are almost never a good idea, so back to animations! Smoothly moving the circle between boxes looks way better:

We already know how to animate a circle’s position with D3.js, so this should be simple, right?

Well… the circle’s y-coordinate in the upper box is height/2, and its coordinate in the lower box is height/2, too! So how are we supposed to animate between these identical states?!

I vividly remember my own confusion when I first encountered this problem in one of our research projects.

The complexity is caused by the fact that our components all use their own, 0-based coordinate system. But that’s exactly what made the components simple in the first place, so we don’t want to give this up!

Animations Across Component Boundaries

Situation recap:

What we have (and want to keep): Simple components with 0-based coordinate systems

What we want: Animations across component boundaries

There’s no easy fix, but we know that animations are simple as long as they are happening on a common coordinate system. Thus the idea is straightforward: We keep using 0-based components, but switch to a common coordinate system while performing an animation. One very suitable common coordinate system is the view port coordinate system of our SVG’s root node.

Here’s the rough recipe:

And in code (remember, the complete project is on GitHub):

const circle = this.svgRoot.getElementById(this.circleId) as SVGCircleElement;

if (circle) {

    // (0) Calculate current coordinates relative to global view port
    const currentCoordinates = this.getCoordinates(this.svgRoot, circle);

    const previousCoordinates = this.coordinateCache || currentCoordinates;

    this.coordinateCache = currentCoordinates;

    const easingFunction = currentCoordinates.cy > previousCoordinates.cy ? easeBounceOut : easeCubicInOut;

    // (1) This clone will be used for the animation
    const animatedCircle = circle.cloneNode(true) as SVGCircleElement;

    // (2) Attach to root element (animated x/y coordinates are in the system of the global view port)
    this.svgRoot.appendChild(animatedCircle);

    // (3) The DOM already contains the circle at the new position -> hide it until the animation is over
    select(circle)
        .attr('visibility', 'hidden');

    // (4) The actual animation
    select(animatedCircle)
        .attr('visibility', 'visible')
        .attr('cx', previousCoordinates.cx)
        .attr('cy', previousCoordinates.cy)
        .transition()
        .duration(1000)
        .ease(easingFunction)
        .attr('cx', currentCoordinates.cx)
        .attr('cy', currentCoordinates.cy)
        .remove() // (5) Detach the animated circle once we're done
        .on('end', () => { // (6) Un-hide new state (already properly placed in the DOM)
            select(circle)
                .attr('visibility', 'visible');
        });

}

Phew, some things are harder than they should be – let me know if there’s a simpler way!


Older Posts » Archive