JavaScript/jQuery Chart - Drill-Down Chart

A drill-down chart visualizes data on several hierarchical levels. Data is usually generalized on the first level, but it becomes more detailed with each level. This article describes the main steps you need to take to implement a drill-down chart using the Chart UI component.

View Demo

Provide Data

Although a drill-down chart visualizes hierarchical data, the data source should be an array of plain objects, for example:

JavaScript
var population = [
    { arg: "North America", val: 493603615, parentID: "" },
    { arg: "South America", val: 331126555, parentID: "" },

    { arg: "United States", val: 325310275, parentID: "North America" },
    { arg: "Mexico", val: 121005815, parentID: "North America" },
    { arg: "Canada", val: 36048521, parentID: "North America" },
    { arg: "Cuba", val: 11239004, parentID: "North America" },

    { arg: "Brazil", val: 205737996, parentID: "South America" },
    { arg: "Colombia", val: 48400388, parentID: "South America" },
    { arg: "Venezuela", val: 30761000, parentID: "South America" },
    { arg: "Peru", val: 28220764, parentID: "South America" }
];

The main idea is to filter the data source by the parentID for different drill-down views. You can create a function that does the filtering:

jQuery
JavaScript
var population = [
    // ...
];

function filterData(name) {
    return population.filter(function (item) {
        return item.parentID === name;
    });
}

$(function() {
    $("#chartContainer").dxChart({
        dataSource: filterData(""),
        series: [{
            argumentField: "arg",
            valueField: "val",
            type: "bar"
        }]
    });
});
Angular
TypeScript
import { Injectable } from "@angular/core";

export class DataItem {
    arg: string;
    val: number;
    parentID: string;
}

let population: DataItem[] = [
    // ...
];

@Injectable()
export class Service {
    filterData(name): DataItem[] {
        return population.filter(function (item) {
            return item.parentID === name;
        });
    }
}
HTML
TypeScript
<dx-chart
    [dataSource]="dataSource">
    <dxi-series argumentField="arg" valueField="val" type="bar"></dxi-series>
</dx-chart>
import { DxChartModule } from "devextreme-angular";
import { Service, DataItem } from "./app.service";
@Component({
    // ...
    providers: [Service]
})
export class AppComponent {
    dataSource: DataItem[];
    service: Service;
    constructor(service: Service) {
        this.dataSource = service.filterData("");
        this.service = service;
    }
}
@NgModule({
    imports: [
        // ...
        DxChartModule
    ],
    // ...
})
Vue
App.vue
<template> 
    <DxChart ...
        :data-source="dataSource">
        <DxSeries
            argument-field="arg"
            value-field="val"
            type="bar"
        />
    </DxChart>
</template>

<script>
import DxChart, {
    DxSeries
} from 'devextreme-vue/chart';

const population = [
    // ...
];

export default {
    components: {
        DxChart,
        DxSeries
    },
    data() {
        return {
            dataSource: this.filterData('')
        };
    },
    methods: {
        filterData(name) {
            return population.filter(item => item.parentID === name);
        }
    }
}
</script>
React
App.js
import React from 'react';
import Chart, {
    Series
} from 'devextreme-react/chart';

const population = [
    // ...
];

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = { dataSource: this.filterData('') };
    }

    render() {
        return (
            <Chart ...
                dataSource={this.state.dataSource}>
                <Series
                    argumentField="arg"
                    valueField="val"
                    type="bar"
                />
            </Chart>
        );
    }

    filterData(name) {
        return population.filter(item => item.parentID === name);
    }
}

... or employ the DevExtreme DataSource object that provides an API for filtering:

jQuery
JavaScript
var population = [
    // ...
];

var dxDataSource = new DevExpress.data.DataSource({
    store: {
        type: "array",
        data: population
    },
    filter: ["parentID", "=", ""]
});

$(function() {
    $("#chartContainer").dxChart({
        dataSource: dxDataSource,
        series: [{
            argumentField: "arg",
            valueField: "val",
            type: "bar"
        }]
    });
});
Angular
HTML
TypeScript
<dx-chart
    [dataSource]="dxDataSource">
    <dxi-series argumentField="arg" valueField="val" type="bar"></dxi-series>
</dx-chart>
import { DxChartModule } from "devextreme-angular";
import DataSource from "devextreme/data/data_source";
// ...
export class AppComponent {
    population = [
        // ...
    ];
    dxDataSource = new DataSource({
        store: {
            type: "array",
            data: this.population
        },
        filter: ["parentID", "=", ""]
    });
}
@NgModule({
    imports: [
        // ...
        DxChartModule
    ],
    // ...
})
Vue
App.vue
<template> 
    <DxChart ...
        :data-source="dxDataSource">
        <DxSeries
            argument-field="arg"
            value-field="val"
            type="bar"
        />
    </DxChart>
</template>

<script>
import DxChart, {
    DxSeries
} from 'devextreme-vue/chart';
import DataSource from 'devextreme/data/data_source';

const population = [
    // ...
];

export default {
    components: {
        DxChart,
        DxSeries
    },
    data() {
        return {
            dxDataSource: new DataSource({
                store: {
                    type: 'array',
                    data: population
                },
                filter: ['parentID', '=', '']
            })
        };
    }
}
</script>
React
App.js
import React from 'react';
import Chart, {
    Series
} from 'devextreme-react/chart';
import DataSource from 'devextreme/data/data_source';

const population = [
    // ...
];

class App extends React.Component {
    constructor(props) {
        super(props);

        this.dxDataSource = new DataSource({
            store: {
                type: 'array',
                data: population
            },
            filter: ['parentID', '=', '']
        });
    }

    render() {
        return (
            <Chart ...
                dataSource={this.dxDataSource}>
                <Series
                    argumentField="arg"
                    valueField="val"
                    type="bar"
                />
            </Chart>
        );
    }
}

Implement View Navigation

To navigate from the first to the second view, filter data by a different parentID in the Chart's onPointClick event handler. To navigate back, add the Button UI component and reset the filter in the onClick event handler. Distinguish between levels using the isFirstLevel flag.

jQuery
JavaScript
HTML
CSS
// ...
$(function() {
    var isFirstLevel = true;
    var chart = $("#chartContainer").dxChart({
        dataSource: filterData(""),
        series: [{
            argumentField: "arg",
            valueField: "val",
            type: "bar"
        }],
        onPointClick: function (e) {
            if (isFirstLevel) {
                isFirstLevel = false;
                chart.option("dataSource", filterData(e.target.originalArgument));
                backButton.option("visible", true);
            }
        }
    }).dxChart("instance");

    var backButton = $("#backButton").dxButton({
        text: "Back",
        icon: "chevronleft",
        visible: false,
        onClick: function (e) {
            if (!isFirstLevel) {
                isFirstLevel = true;
                chart.option("dataSource", filterData(""));
                backButton.option("visible", false);
            }
        }
    }).dxButton("instance");
});
<div id="chartContainer"></div>
<div id="backButton" class="button-container"></div>
.button-container {
    text-align: center;
    height: 40px;
    position: absolute;
    top: 7px;
    left: 0px;
}
Angular
HTML
TypeScript
CSS
<dx-chart
    [dataSource]="dataSource"
    (onPointClick)="onPointClick($event)">
    <dxi-series argumentField="arg" valueField="val" type="bar"></dxi-series>
</dx-chart>
<dx-button class="button-container"
    text="Back"
    icon="chevronleft"
    [visible]="!isFirstLevel"
    (onClick)="onButtonClick()">
</dx-button>
import { DxChartModule, DxButtonModule } from "devextreme-angular";
import { Service, DataItem } from "./app.service";
@Component({
    // ...
    providers: [Service]
})
export class AppComponent {
    dataSource: DataItem[];
    service: Service;
    isFirstLevel: boolean;
    constructor(service: Service) {
        this.dataSource = service.filterData("");
        this.service = service;
        this.isFirstLevel = true;
    }
    onPointClick(e) {
        if (this.isFirstLevel) {
            this.isFirstLevel = false;
            this.dataSource = this.service.filterData(e.target.originalArgument);
        }
    }
    onButtonClick() {
        if (!this.isFirstLevel) {
            this.isFirstLevel = true;
            this.dataSource = this.service.filterData("");
        }
    }
}
@NgModule({
    imports: [
        // ...
        DxChartModule
    ],
    // ...
})
.button-container {
    text-align: center;
    height: 40px;
    position: absolute;
    top: 7px;
    left: 0px;
}
Vue
App.vue
<template> 
    <DxChart ...
        :data-source="dataSource"
        @point-click="onPointClick">
        <DxSeries
            argument-field="arg"
            value-field="val"
            type="bar"
        />
    </DxChart>
    <DxButton
        :visible="!isFirstLevel"
        class="button-container"
        text="Back"
        icon="chevronleft"
        @click="onButtonClick"
    />
</template>

<script>
import DxChart, {
    DxSeries
} from 'devextreme-vue/chart';
import DxButton from 'devextreme-vue/button';
import service from './data.js';

export default {
    components: {
        DxChart,
        DxSeries,
        DxButton
    },
    data() {
        return {
            isFirstLevel: true,
            dataSource: service.filterData('')
        };
    },
    methods: {
        onPointClick({ target }) {
            if (this.isFirstLevel) {
                this.isFirstLevel = false;
                this.dataSource = service.filterData(target.originalArgument);
            }
        },
        onButtonClick() {
            if (!this.isFirstLevel) {
                this.isFirstLevel = true;
                this.dataSource = service.filterData("");
            }
        }
    }
}
</script>

<style>
.button-container {
    text-align: center;
    height: 40px;
    position: absolute;
    top: 7px;
    left: 0px;
}
</style>
React
App.js
CSS
import React from 'react';
import Chart, {
    Series
} from 'devextreme-react/chart';
import Button from 'devextreme-react/button';
import service from './data.js';

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            isFirstLevel: true,
            dataSorce: service.filterData('')
        };

        this.onPointClick = this.onPointClick.bind(this);
        this.onButtonClick = this.onButtonClick.bind(this);
    }

    render() {
        return (
            <Chart ...
                dataSource={this.state.dataSource}
                onPointClick={this.onPointClick}>
                <Series
                    argumentField="arg"
                    valueField="val"
                    type="bar"
                />
            </Chart>
            <Button className="button-container"
                text="Back"
                icon="chevronleft"
                visible={!this.state.isFirstLevel}
                onClick={this.onButtonClick}
            />
        );
    }

    onPointClick({ target }) {
        if(this.state.isFirstLevel) {
            this.setState({
                isFirstLevel: false,
                dataSource: service.filterData(target.originalArgument)
            });
        }
    }

    onButtonClick() {
        if(!this.state.isFirstLevel) {
            this.setState({
                isFirstLevel: true,
                dataSource: service.filterData('')
            });
        }
    }
}
.button-container {
    text-align: center;
    height: 40px;
    position: absolute;
    top: 7px;
    left: 0px;
}

The following code shows how to implement navigation when using the DevExtreme DataSource:

jQuery
JavaScript
HTML
CSS
var dxDataSource = new DevExpress.data.DataSource({
    store: {
        type: "array",
        data: population
    },
    filter: ["parentID", "=", ""]
});

$(function() {
    var isFirstLevel = true;
    var chart = $("#chartContainer").dxChart({
        dataSource: dxDataSource,
        series: [{
            argumentField: "arg",
            valueField: "val",
            type: "bar"
        }],
        onPointClick: function (e) {
            if (isFirstLevel) {
                isFirstLevel = false;
                dxDataSource.filter(["parentID", "=", e.target.originalArgument]);
                dxDataSource.load();
                backButton.option("visible", true);
            }
        }
    }).dxChart("instance");

    var backButton = $("#backButton").dxButton({
        text: "Back",
        icon: "chevronleft",
        visible: false,
        onClick: function (e) {
            if (!isFirstLevel) {
                isFirstLevel = true;
                dxDataSource.filter(["parentID", "=", ""]);
                dxDataSource.load();
                backButton.option("visible", false);
            }
        }
    }).dxButton("instance");
});
<div id="chartContainer"></div>
<div id="backButton" class="button-container"></div>
.button-container {
    text-align: center;
    height: 40px;
    position: absolute;
    top: 7px;
    left: 0px;
}
Angular
HTML
TypeScript
CSS
<dx-chart
    [dataSource]="dxDataSource"
    (onPointClick)="onPointClick($event)">
    <dxi-series argumentField="arg" valueField="val" type="bar"></dxi-series>
</dx-chart>
<dx-button class="button-container"
    text="Back"
    icon="chevronleft"
    [visible]="!isFirstLevel"
    (onClick)="onButtonClick()">
</dx-button>
import { DxChartModule, DxButtonModule } from "devextreme-angular";
import DataSource from "devextreme/data/data_source";
// ...
export class AppComponent {
    // ...
    dxDataSource = new DataSource({
        store: {
            type: "array",
            data: this.population
        },
        filter: ["parentID", "=", ""]
    });
    isFirstLevel: boolean = true;
    onPointClick(e) {
        if (this.isFirstLevel) {
            this.isFirstLevel = false;
            this.dxDataSource.filter(["parentID", "=", e.target.originalArgument]);
            this.dxDataSource.load();
        }
    }
    onButtonClick() {
        if (!this.isFirstLevel) {
            this.isFirstLevel = true;
            this.dxDataSource.filter(["parentID", "=", ""]);
            this.dxDataSource.load();
        }
    }
}
@NgModule({
    imports: [
        // ...
        DxChartModule
    ],
    // ...
})
.button-container {
    text-align: center;
    height: 40px;
    position: absolute;
    top: 7px;
    left: 0px;
}
Vue
App.vue
<template> 
    <DxChart ...
        :data-source="dxDataSource"
        @point-click="onPointClick">
        <DxSeries
            argument-field="arg"
            value-field="val"
            type="bar"
        />
    </DxChart>
    <DxButton
        :visible="!isFirstLevel"
        class="button-container"
        text="Back"
        icon="chevronleft"
        @click="onButtonClick"
    />
</template>

<script>
import DxChart, {
    DxSeries
} from 'devextreme-vue/chart';
import DxButton from 'devextreme-vue/button';
import DataSource from "devextreme/data/data_source";

const population = [
    // ...
];

export default {
    components: {
        DxChart,
        DxSeries,
        DxButton
    },
    data() {
        return {
            isFirstLevel: true,
            dxDataSource: new DataSource({
                store: {
                    type: 'array',
                    data: population
                },
                filter: ['parentID', '=', '']
            })
        };
    },
    methods: {
       onPointClick({ target }) {
            if(this.isFirstLevel) {
                this.isFirstLevel = false;
                this.dxDataSource.filter(['parentID', '=', target.originalArgument]);
                this.dxDataSource.load();
            }
        },
        onButtonClick() {
            if(!this.isFirstLevel) {
                this.isFirstLevel = true;
                this.dxDataSource.filter(['parentID', '=', '']);
                this.dxDataSource.load();
            }
        }
    }
}
</script>

<style>
.button-container {
    text-align: center;
    height: 40px;
    position: absolute;
    top: 7px;
    left: 0px;
}
</style>
React
App.js
CSS
import React from 'react';
import Chart, {
    Series
} from 'devextreme-react/chart';
import Button from 'devextreme-react/button';
import DataSource from "devextreme/data/data_source";

const population = [
    // ...
];

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = { isFirstLevel: true };
        this.dxDataSource = new DataSource({
            store: {
                type: 'array',
                data: population
            },
            filter: ['parentID', '=', '']
        });

        this.onPointClick = this.onPointClick.bind(this);
        this.onButtonClick = this.onButtonClick.bind(this);
    }

    render() {
        return (
            <Chart ...
                dataSource={this.dxDataSource}
                onPointClick={this.onPointClick}>
                <Series
                    argumentField="arg"
                    valueField="val"
                    type="bar"
                />
            </Chart>
            <Button className="button-container"
                text="Back"
                icon="chevronleft"
                visible={!this.state.isFirstLevel}
                onClick={this.onButtonClick}
            />
        );
    }

    onPointClick({ target }) {
        if(this.state.isFirstLevel) {
            this.setState({ isFirstLevel: false });
            this.dxDataSource.filter(['parentID', '=', target.originalArgument]);
            this.dxDataSource.load();
        }
    }

    onButtonClick() {
        if(!this.state.isFirstLevel) {
            this.setState({ isFirstLevel: true });
            this.dxDataSource.filter(['parentID', '=', '']);
            this.dxDataSource.load();
        }
    }
}
.button-container {
    text-align: center;
    height: 40px;
    position: absolute;
    top: 7px;
    left: 0px;
}

Customize the Appearance

The Chart provides the customizePoint and customizeLabel functions specifically for changing individual point and label properties. Any other UI component properties can be changed in the Chart's onPointClick event handler, but remember to change them back in the Button's onClick event handler.

jQuery
JavaScript
// ...
$(function() {
    var isFirstLevel = true;
    var originalTitle = "The Most Populated Countries by Continents";
    var chart = $("#chartContainer").dxChart({
        // ...
        title: originalTitle,
        onPointClick: function (e) {
            if (isFirstLevel) {
                // ...
                chart.option({
                    title: "The Most Populated Countries in " + e.target.originalArgument
                });
            }
        }
    }).dxChart("instance");

    var backButton = $("#backButton").dxButton({
        // ...
        onClick: function (e) {
            if (!isFirstLevel) {
                // ...
                chart.option({
                    title: originalTitle
                });
            }
        }
    }).dxButton("instance");
});
Angular
HTML
TypeScript
<dx-chart ...
    (onPointClick)="onPointClick($event)"
    [title]="currentTitle">
</dx-chart>
<dx-button ...
    (onClick)="onButtonClick()">
</dx-button>
// ...
export class AppComponent {
    currentTitle: string = "The Most Populated Countries by Continents"
    // ...
    onPointClick(e) {
        if (this.isFirstLevel) {
            // ...
            this.currentTitle = "The Most Populated Countries in " + e.target.originalArgument;
        }
    }
    onButtonClick() {
        if (!this.isFirstLevel) {
            // ...
            this.currentTitle = "The Most Populated Countries by Continents";
        }
    }
}
Vue
App.vue
<template> 
    <DxChart ...
        :title="currentTitle"
        @point-click="onPointClick">
    </DxChart>
    <DxButton ...
        @click="onButtonClick"
    />
</template>

<script>
import DxChart from 'devextreme-vue/chart';
import DxButton from 'devextreme-vue/button';
import service from './data.js';

export default {
    components: {
        DxChart,
        DxButton
    },
    data() {
        return {
            isFirstLevel: true,
            currentTitle: 'The Most Populated Countries by Continents'
        };
    },
    methods: {
        onPointClick({ target }) {
             if (this.isFirstLevel) {
                // ...
                this.currentTitle = `The Most Populated Countries in ${target.originalArgument}`;
            }
        },
        onButtonClick() {
            if (!this.isFirstLevel) {
                // ...
                this.currentTitle = 'The Most Populated Countries by Continents';
            }
        }
    }
}
</script>
React
App.js
import React from 'react';
import Chart from 'devextreme-react/chart';
import Button from 'devextreme-react/button';

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            isFirstLevel: true,
            currentTitle: 'The Most Populated Countries by Continents'
        };

        this.onPointClick = this.onPointClick.bind(this);
        this.onButtonClick = this.onButtonClick.bind(this);
    }

    render() {
        return (
            <Chart ...
                title={this.state.currentTitle}
                onPointClick={this.onPointClick}>
            </Chart>
            <Button ...
                onClick={this.onButtonClick}
            />
        );
    }

    onPointClick({ target }) {
        if(this.state.isFirstLevel) {
            this.setState({
                // ...
                currentTitle: `The Most Populated Countries in ${target.originalArgument}`
            });
        }
    }

    onButtonClick() {
        if(!this.state.isFirstLevel) {
            this.setState({
                // ...
                currentTitle: 'The Most Populated Countries by Continents'
            });
        }
    }
}

This article outlined the steps to implement a drill-down chart and provided code examples for each step. For the full code, refer to the Drill-Down Chart demo.

View Demo