Morningstar Design System
Back to Blog

Building an Interactive Donut Chart

Exploring how to add a hover flag and focused data to a visualization built with Morningstar Chart Elements

July 14, 2022—Morningstar Chart Elements (MCE) offer a modular approach to creating data visualizations, providing teams with the flexibility to tailor charts to solve unique challenges in Morningstar products.

Final composition of Donut Chart with Hover Flag.

Here we'll explore building a simple donut chart component with Vue that combines multiple Morningstar Chart Elements and Morningstar Design System (MDS) components. Then we'll enhance our component with the Hover Flag element.

For guidance on how to get started with MDS Chart Elements, see About Chart Elements.

For guidance on how to get started with MDS Components, see Getting Started for Engineers.

For a more thorough example, check out the Donut Chart Example CodePen.

Overview

Our goal is to build a component to help us visualize the asset allocation breakdown of a fund via a donut plot. This component should be in color and feature a legend alongside the visualization. In addition, the component should include a table of the input data to provide an accessible alternative to screen-readers.

NOTE: The Donut Plot is one of a few chart elements that does not implement a mouse prop to accept events from an <mce-pointer> element. Please keep that in mind if you're following along with a different plot element.



You can see the final result and code for this exercise here: MCE Hover Flag Demo Final. In addition, you can follow along with each step of our progression via the CodePen link at the end of each section.

Part 1

Chart Data & Schema

We'll need some asset allocation data for our plot in the form of an Array. In the real world we'd likely get this data from an external API but we'll hard code things for now. Here is an example of our initial schema:

const data = [
    {
        id: "AssetAllocCash",
        label: "Cash",
        value: 4.74688
    },
    {
        id: "AssetAllocNotClassified",
        label: "Not Classified",
        value: 1.53654
    },
    {
        id: "AssetAllocNonUSEquity",
        label: "Non-U.S. Equity",
        value: 27.24799
    },
    {
        id: "AssetAllocOther",
        label: "Other",
        value: 0.00000
    },
    {
        id: "AssetAllocUSEquity",
        label: "U.S. Equity",
        value: 37.11866
    },
    {
        id: "AssetAllocBond",
        label: "Fixed Income",
        value: 29.34991
    }
];

Each object in the array has a unique id, a label for display, and the underlying value as a Number.

Chart Colors and Asset Allocation Classes

Asset allocation types map to the Asset Allocation Named Palette in the MCE Color Utility. Let's import that palette for later use:

import { assetAllocation } from '@mce/color';

NOTE: If you're using CodePen to follow along, the syntax will be slightly different:

const { assetAllocation } = window.utilities.color;

We need a way to map our data to its respective color in the palette so we'll modify our data schema and add a type property to each data item that corresponds to its color name:

const data = [
    {
        id: "AssetAllocCash",
        label: "Cash",
        value: 4.74688,
        type: "cash"
    },
    {
        id: "AssetAllocNotClassified",
        label: "Not Classified",
        value: 1.53654,
        type: "not-classified"
    },
    {
        id: "AssetAllocNonUSEquity",
        label: "Non-U.S. Equity",
        value: 27.24799,
        type: "alternative"
    },
    {
        id: "AssetAllocOther",
        label: "Other",
        value: 0.00000,
        type: "other"
    },
    {
        id: "AssetAllocUSEquity",
        label: "U.S. Equity",
        value: 37.11866,
        type: "equity"
    },
    {
        id: "AssetAllocBond",
        label: "Fixed Income",
        value: 29.34991,
        type: "fixed-income"
    }
];

Component Data Model

We're now ready to stub out the beginning of our component's data model.

The Donut Plot adds a 3px gap between slices to maintain an accessible color contrast ratio. Knowing this, we don't want data items with a value of zero taking up space in our plot. However, we do want to show them in our legend and table.

We'll use some computed properties to massage our data to account for this as well as map our data types to their respective colors:

import { assetAllocation } from '@mce/color';
import { format } from 'd3-format';

const data = [
    // see above
    // {}, {}, {}...
];

const app = new Vue({
    el: "#app",
    data() {
        return {
            // Descriptive Labels
            chartAriaLabel:
                "Vanguard Global Wellington Fund Admiral Shares (VGWAX)",
            chartAriaDescription:
                "The allocation breakdown across asset classes for the Vanguard Global Wellington Fund Admiral Shares (VGWAX) fund. The fund is well diversified across asset classes with 37% allocated to U.S. Equity, 29% Fixed Income, 27% Non-U.S. Equity, and around 5% Cash."
            // Dimensions
            plotHeight: 350,
            plotWidth: 350,
            legendRightWidth: 170,
        };
    },
    computed: {
        fullData() {
            // Include zero values in the legend
            return data.sort((a, b) => b.value - a.value);
        },
        chartData() {
            // Ignore zero values to avoid unwanted pie padAngle slices
            return this.fullData.filter((d) => d.value > 0);
        },
        chartColors() {
            return this.fullData.map((d) => assetAllocation[d.type]);
        }
    },
    filters: {
        formatPercent(value) {
            return format(",.2%")(value / 100);
        }
    },
});

Component Template

We'll wrap our content in an MDS Section component to give it a nice styled title and some padding.

We'll place an MDS Layout element inside, and then provide an MCE Donut Plot and MCE Legend to its default and legend-right slots respectively.

<div id="app">
    <div>
        <mds-section
            bold
            :title="chartAriaLabel"
            :aria-label="chartAriaLabel"
            aria-describedby="aria-description"
        >
            <span id="aria-description"> {{ chartAriaDescription }}</span>
            <div class="wrapper">
                <mce-layout
                    :plot-width="plotWidth"
                    :plot-height="plotHeight"
                    :legend-right-width="legendRightWidth"
                >
                    <template #legend-right>
                        <mce-legend>
                            <mce-legend-group title="Asset Allocation">
                                <mce-legend-item
                                    v-for="(row, i) in fullData"
                                    :key="row.id"
                                    :label="row.label"
                                    :value="row.value | format"
                                    :color="chartColors[i]"
                                    key-type="donut"
                                >
                                </mce-legend-item>
                            </mce-legend-group>
                        </mce-legend>
                    </template>

                    <!-- Main plot (default slot) -->
                    <mce-donut-plot
                        :color="chartColors"
                        :data="chartData"
                        :height="plotHeight"
                        :width="plotWidth"
                    >
                    </mce-donut-plot>
                </mce-layout>
            </div>
        </mds-section>
    </div>
</div>

Provide an Accessible Alternative for Screen Readers

Let's add a table for our chart data to provide an accessible alternative to the visualization. We'll use an MDS Data Table for consistent styling and add it after our <mds-section>:

<div>
    <!-- ... -->
    <mds-section>
        <!-- ... -->
    </mds-section>

    <!-- provide an accessible alternative for screen-readers -->
    <mds-table class="table">
        <div class="accessibly-hidden">
            {{
                chartAriaDescription
            }}
        </div>
        <mds-thead>
            <mds-th>Asset Class</mds-th>
            <mds-th right-aligned>Allocation %</mds-th>
        </mds-thead>
        <mds-tbody>
            <mds-tr v-for="row in fullData" :key="row.id">
                <mds-td> {{ row.label }} </mds-td>
                <mds-td right-aligned> {{ row.value | format }} </mds-td>
            </mds-tr>
        </mds-tbody>
    </mds-table>
</div>

At this point we should have a simple donut chart with a legend and a table:



You can visit the CodePen for MCE Donut with Hover Flag Example 1 to see/edit a live example.

Part 2

Focused Data

Let's enhance the interactivity of our chart by adding a focus effect to the hovered slice. We'll add a new property, focusedData, to our data model. Then we'll hook the mousemove-plot and mouseleave-plot events on our <mce-donut-plot> element.

The mousemove-plot event will fire repeatedly as the mouse moves over the plot and mouseleave-plot will fire when the mouse leaves the boundaries of the plot.

The event emitted on mouse move will provide us with the chart data that maps to the underlying slice we're hovering. We'll use it to get access to the hovered data item's _itemId which we'll use to drive our focus behavior.

The onMousemovePlot and onMousleavePlot methods will serve as our event handlers while focusedData will eventually be a single item Array to track the _itemId of our hovered slice:

data() {
    return {
        // ...
        focusedData: null,
    };
},
methods: {
    onMousemovePlot(e) {
        const { data } = e;

        this.focusedData = [data._itemId];
    },
    onMouseleavePlot() {
        this.focusedData = null;
    }
}

We'll update our <mce-donut-plot> in the template to wire up the events and provide our focused data via the data-focused prop:

<mce-donut-plot
    :color="chartColors"
    :data="chartData"
    :data-focused="focusedData"
    :height="plotHeight"
    :width="plotWidth"
    @mousemove-plot="onMousemovePlot"
    @mouseleave-plot="onMouseleavePlot"
>
</mce-donut-plot>

Hovering a slice should now lower the opacity of the others to provide a focused data effect:

Initial Donut Chart with Focused Data.



You can visit the CodePen for MCE Donut with Hover Flag Example 2 to see/edit a live example.

Part 3

Adding the Hover Flag

An MCE Hover Flag creates the container shown on hover of a chart plot.

First, we'll add a Hover Flag with some basic content to our template, then we'll eventually link it to the mouse position.

We need to keep our Hover Flag adjacent to the <mce-layout> element, so we'll place it right before:

<!-- ... -->
<mce-hover-flag
    :x="0"
    :y="0"
>
    Hello from the Hover Flag
</mce-hover-flag>
<mce-layout
    :plot-width="plotWidth"
    :plot-height="plotHeight"
    :legend-right-width="legendRightWidth"
>
    <!-- ... -->
</mce-layout>

We should now see a static Hover Flag fixed at the top left (0, 0) of our plot:

Initial Donut Chart with fixed Hover Flag.

Linking the Hover Flag to Mouse Position

To allow the Hover Flag to follow the position of the cursor, we'll need to track some additional mouse information. We can add a new property, mousePosition to our data model and modify our onMousemovePlot handler to update it.

data() {
    return {
        // ...
        focusedData: null,
        mousePosition: {},
    };
},
methods: {
    onMousemovePlot(e) {
        const { data, position } = e;

        this.mousePosition = position;
        this.focusedData = [data._itemId];
    },
    onMouseleavePlot() {
        this.focusedData = null;
    }
}

Our Hover Flag should only be visible when we have focused data. Let's add a v-if to our <mce-hover-flag>:

<mce-hover-flag
    v-if="focusedData"
    :x="0"
    :y="0"
>
    Hello from the Hover Flag
</mce-hover-flag>

We can then update the x and y props of the Hover Flag using our mousePosition to allow it to follow us around:

<mce-hover-flag
    v-if="focusedData"
    :x="mousePosition.pageX"
    :y="mousePosition.pageY"
>
    Hello from the Hover Flag
</mce-hover-flag>

We now have a Hover Flag that follows our mouse around when we're over the plot:

Initial Donut Chart with basic Hover Flag.



You can visit the CodePen for MCE Donut with Hover Flag Example 3 to see/edit a live example.

Part 4

Adding Useful Information to the Hover Flag

We have our fancy Hover Flag but it isn't doing much yet. Let's enhance it to show us information relevant to the actual slice of the donut being hovered.

We can add a Legend Element inside our Hover Flag to give us a nice visual. We'll also add a new property, mouseData to our data model and modify our onMousemovePlot handler to update it:

data() {
    return {
        // ...
        focusedData: null,
        mouseData: {},
        mousePosition: {},
    };
},
methods: {
    onMousemovePlot(e) {
        const { data, position } = e;

        this.mouseData = data;
        this.mousePosition = position;
        this.focusedData = [data._itemId];
    },
}

mouseData will provide us with the chart data that maps to the slice we're hovering. We'll get access to the data item's label, type, and value, as well as the index of the item as rendered in the plot via _dataIndex.

Now that we have some mouseData, we'll add <mce-legend> and <mce-legend-item> elements to our Hover Flag.

<mce-hover-flag
    v-if="focusedData"
    :x="mousePosition.pageX"
    :y="mousePosition.pageY"
>
    <mce-legend
        hide-top-border
        hide-bottom-border
    >
        <mce-legend-item
            :color="chartColors[mouseData._dataIndex]"
            :label="mouseData.label"
            :value="mouseData.value | format"
            key-type="donut"
        >
        </mce-legend-item>
    </mce-legend>
</mce-hover-flag>

At this point, our Hover Flag should update as we hover the donut slices and show a nice Legend Item relevant to the underlying data:

Donut Chart with Hover Flag.



You can visit the CodePen for MCE Hover Flag Final to see/edit a live example.

Wrapping Up

Chart elements can be combined in many ways to created new visualizations for use in Morningstar applications. Be sure to try out Morningstar Chart Elements and see what you can create!