Skip to content

Visualize your customer feedback

Graphs and charts are a way to display data to the user on a third-party system. It is important that they are easily consumable by the user. Below are different examples of charts and graphs.

Time Span

When working with data it is relevant to be able to choose a time span of when you want to view data.

Read more about how to implement Time Span here.

Time span

Speedometer

This is a simple way of showing data like NPS score and NPS rating to the user.

Read more about implementing a speedometer here.

Speedometer

Trend line

A trend line graph is mostly used to see data over time. In this case it can be used to see the development of the avg. rating or NPS score over time.

Trend line

This graph shows a customizable timeline for responses given from a respondent. It is also divided into promoters, passives, and detractors. The coloured bullet points that are being shown, represent the average rating for the respondent at that particular time. If no bullet point is being shown on a date then there were no responses given at the time. There are 5 different timelines the user can choose from:

  • All Time shows the timeline from the very first response to the last. The labels in the x-axis are of type months
  • Last Six Months shows the timeline from the first response six months ago to today. The labels in the x-axis are of type months
  • Last Three Months show the timeline from the very first response three months ago. The labels in the x-axis are of type week numbers
  • Last Month shows the timeline from the very first day this month. The labels in the x-axis are of type days
  • Last week shows the timeline from the very first day this week (day 1 is a Monday) to today. The labels in the x-axis are of type days.
Trend line CSS
        ul li {
        display: inline;
    }
    li {
        font-size: 9px;
    }

    li:nth-child(1)::before {
        content: "\2022";
        font-size: 40px;
        vertical-align: middle;
        color: rgb(214, 39, 37, 1);
    }
    li:nth-child(2)::before {
        content: "\2022";
        font-size: 40px;
        vertical-align: middle;
        color: rgb(253, 198, 25, 1);
    }
    li:nth-child(3)::before {
        content: "\2022";
        font-size: 40px;
        vertical-align: middle;
        color: rgb(0, 164, 110, 1);
    }

    .slds-m-vertical_medium{
        position:absolute;
        margin-top: -2vh;
    }
Trend line HTML
<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="trendingsv2.css">
</head>
<body>
    <div  class="slds-theme_default" style="height:26vh;">
        <div style="position:relative;height:23vh;width:30vw;top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);">
            <div style="position: absolute; right: 0; margin-top: -3vh">
            <ul>
                <li >(0-6)detractors</li>
                <li>(7-8)passives</li>
                <li>(9-10)promoters</li>
            </ul>
            </div>
            <canvas lwc:dom="manual" id="myChart"></canvas>
            <button onclick="lastWeek()">last week</button>
            <button onclick="lastMonth()">last month</button>
            <button onclick="lastThreeMonths()">last three months</button>
            <button onclick="lastSixMonths()">last six months</button>
            <button onclick="allTime()">all time</button>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
    <script src='trendingsv2.js'></script>
</body>
</html>
Trend line JavaScript
var chart;

_buildChart = (responses, labelsIndicator)=>{
    //let canvas = this.template.querySelector("canvas");
    let context = document.getElementById('myChart').getContext('2d');
    const data = [];
    let everyMonth2YearsLabels = [];
    let dSet = {};
    if (labelsIndicator === "month") {
    dSet = calculateAverageRatingForMonths(
        responses,
        data,
        everyMonth2YearsLabels
    );
    }
    if (labelsIndicator === "week") {
    dSet = calculateAverageRatingForWeeks(
        responses,
        data,
        everyMonth2YearsLabels
    );
    }
    if (labelsIndicator === "day") {
    dSet = calculateAverageRatingForDays(
        responses,
        data,
        everyMonth2YearsLabels
    );
    }
    const dSetData = dSet[0].data.map((d) => {
    if (d === null) {
        return "null";
    }
    return 10;
    });
    const before = [];
    const after = dSet[0].data.map(() => {
    return "null";
    });
    for (let i = 0; i < dSetData.length; i++) {
    if (!dSetData.includes("null")) {
        break;
    }
    const num = dSetData[i];
    if (num === "null") {
        before.push(10);
    } else {
        before.push(10);
        break;
    }
    }
    for (let i = after.length - 1; i >= 0; i--) {
    if (!dSetData.includes("null")) {
        break;
    }
    const num = dSetData[i];
    if (num === "null") {
        after[i] = 10;
    } else {
        after[i] = 10;
        break;
    }
    }
    chart = new Chart(context, {
    type: "line",
    data: {
        labels: everyMonth2YearsLabels,
        datasets: [
        dSet[0],
        {
            label: "Values",
            backgroundColor: "rgba(255, 255, 255,0)",
            borderColor: "transparent",
            data: dSet[0].data.map((d) => {
            if (d === null) {
                return "null";
            }
            return 10;
            }),
            fill: true,
            spanGaps: true,
            pointRadius: 0
        },
        {
            label: "Before",
            backgroundColor: "rgba(211,211,211,0.15)",
            borderColor: "transparent",
            data: before,
            fill: true,
            pointRadius: 0
        },
        {
            label: "After",
            backgroundColor: "rgba(211,211,211,0.15)",
            borderColor: "transparent",
            data: after,
            fill: true,
            pointRadius: 0
        }
        ]
    },
    options: {
        legend: { display: false },
        title: { display: true, text: "NPS Rating Trend" },
        responsive: true,
        maintainAspectRatio: false,
        scales: {
        yAxes: [
            {
            ticks: {
                max: 10,
                min: 0,
                stepSize: 1
            }
            }
        ]
        }
    }
    })}

const nullifyHoursMinutesFilterDuplicates = (dates) => {
    const format = dates
    .map((date) => {
        return date.getTime();
    })
    //filter so that only one
    .filter((date, i, array) => {
        return array.indexOf(date) === i;
    })
    .map((time) => {
        return new Date(time);
    });
    format.sort((a, b) => {
    var dateA = new Date(a),
        dateB = new Date(b);
    return dateA - dateB;
    });
    return format;
};

const calculateAverageRatingForMonths = (
    responses,
    data,
    monthLabels
) => {
    let months = [];
    responses.forEach((r) => {
    //insert sorted dates to set
    if (r.npstoday__RatingTime__c !== undefined) {
        const splitted = r.npstoday__RatingTime__c.split("-");
        const yearAndMonth = new Date(splitted[0], splitted[1], 0);
        if (!months.includes(yearAndMonth)) {
        months.push(yearAndMonth);
        }
    }
    });
    months = nullifyHoursMinutesFilterDuplicates(months);
    const monthsPrefixed = [
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec"
    ];
    responses.lastMonths.forEach((m) => {
    monthLabels.push(
        /*m.toLocaleString("default", { month: "long" }).substring(0, 3)*/ monthsPrefixed[
        m.month - 1
        ] +
        " " +
        m.year
    );
    const ratingsForMonth = [];
    responses.forEach((r) => {
        const rM = new Date(r.npstoday__RatingTime__c);
        if (m.month === rM.getMonth() + 1 && m.year === rM.getFullYear()) {
        ratingsForMonth.push(r.npstoday__Rating__c);
        }
    });
    if (ratingsForMonth.length !== 0) {
        const averageRatingForMonth = Math.round(
        ratingsForMonth.reduce((num1, num2) => {
            return num1 + num2;
        }, 0) / ratingsForMonth.length
        );
        data.push(averageRatingForMonth);
    } else {
        data.push(null);
    }
    });
    // eslint-disable-next-line no-use-before-define
    return constructDataSet(data);
};

const calculateAverageRatingForDays = (responses, data, dayLabels) => {
    let days = [];
    responses.forEach((r) => {
    if (r.npstoday__RatingTime__c !== undefined) {
        const splitted = r.npstoday__RatingTime__c.split("-");
        const yearAndMonth = new Date(
        splitted[0],
        splitted[1],
        splitted[2].split("T")[0]
        );
        if (!days.includes(yearAndMonth)) {
        days.push(yearAndMonth);
        }
    }
    });
    days = nullifyHoursMinutesFilterDuplicates(days);
    responses.lastDays.forEach((d) => {
    dayLabels.push(
        d.getDate() + "/" + (d.getMonth() + 1) + "-" + d.getFullYear()
    );

    const ratingsForDay = [];
    responses.forEach((r) => {
        const rM = new Date(r.npstoday__RatingTime__c);
        if (
        d.getMonth() === rM.getMonth() &&
        d.getFullYear() === rM.getFullYear() &&
        d.getDate() === rM.getDate()
        ) {
        ratingsForDay.push(r.npstoday__Rating__c);
        }
    });
    const averageRatingForDay = Math.round(
        ratingsForDay.reduce((num1, num2) => {
        return num1 + num2;
        }, 0) / ratingsForDay.length
    );
    if (ratingsForDay.length === 0) {
        data.push(null);
    } else {
        data.push(averageRatingForDay);
    }
    });
    // eslint-disable-next-line no-use-before-define
    return constructDataSet(data);
};

const calculateAverageRatingForWeeks = (responses, data, dayLabels) => {
    const weekAndYears = responses.weekAndYears;
    const weekDates = responses.responses;
    let days = [];

    weekDates.forEach((r) => {
    if (r.npstoday__RatingTime__c !== undefined) {
        const splitted = r.npstoday__RatingTime__c.split("-");
        const yearAndMonthAndDay = new Date(
        splitted[0],
        splitted[1] - 1, //-1 because months in js are index based (starting at zero)
        splitted[2].split("T")[0]
        );
        days.push(yearAndMonthAndDay);
    }
    });
    days = nullifyHoursMinutesFilterDuplicates(days);
    weekAndYears.forEach((d) => {
    const ratingsForWeek = [];
    let dataForWeek = false;
    weekDates.forEach((w) => {
        const wD = new Date(w.npstoday__RatingTime__c);
        // eslint-disable-next-line no-use-before-define
        if (d.week === getWeekNumber(wD)[1]) {
        dataForWeek = true;
        ratingsForWeek.push(w.npstoday__Rating__c);
        }
    });
    if (dataForWeek === true) {
        const averageRatingForWeek = Math.round(
        ratingsForWeek.reduce((num1, num2) => {
            return num1 + num2;
        }, 0) / ratingsForWeek.length
        );
        data.push(averageRatingForWeek);
    } else {
        data.push(null);
    }
    //if (dataForWeek === true) {
    dayLabels.push("week " + d.week + " " + d.year);
    //}
    });
    // eslint-disable-next-line no-use-before-define
    return constructDataSet(data);
};

const getWeekNumber = (d) => {
    d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
    d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
    const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
    const weekNo = Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
    return [d.getUTCFullYear(), weekNo];
};
const constructDataSet = (data) => {
    const dataset = [
    {
        radius: 4,
        backgroundColor: [],
        borderColor: "black",
        fill: false,
        spanGaps: true,
        data: []
    }
    ];
    data.forEach((d) => {
    if (d < 7) {
        dataset[0].data.push(d);
        dataset[0].backgroundColor.push("rgb(214,39,37,1");
    }
    if (d >= 7 && d <= 8) {
        dataset[0].data.push(d);
        dataset[0].backgroundColor.push("rgb(253,198,25,1");
    }
    if (d >= 9) {
        dataset[0].data.push(d);
        dataset[0].backgroundColor.push("rgb(0,164,110,1");
    }
    });
    return dataset;
};

lastWeek = (doNotDestroy) => {
    let now = new Date();
    const daysOfTheWeekUntilTodayArr = this.lastDaysByAmount(now.getDay());
    if(!doNotDestroy)this.chart.destroy();
    this._buildChart(daysOfTheWeekUntilTodayArr, "day");
};

lastMonth = (doNotDestroy) => {
    let now = new Date();
    const daysOfMonthUntilTodayArr = this.lastDaysByAmount(now.getDate());
    if(!doNotDestroy)this.chart.destroy();
    this._buildChart(daysOfMonthUntilTodayArr, "day");
};

lastThreeMonths = (doNotDestroy) => {
    const daysOfWeeksForThreeMonths = this.lastWeeksByAmount(3);
    if(!doNotDestroy)this.chart.destroy();
    this._buildChart(daysOfWeeksForThreeMonths, "week");
};

lastSixMonths = (doNotDestroy) => {
    const daysOfLastSixMonths = this.lastMonthsByAmount(
    5,Date.now()
    );
    if(!doNotDestroy)this.chart.destroy();
    this._buildChart(daysOfLastSixMonths, "month");
};

allTime = (doNotDestroy) => {
    let dates = responses.map((r) => {
    return new Date(r.npstoday__RatingTime__c);
    });
    dates = dates.filter((f) => {
    return f !== "null";
    });
    const oldest = dates.reduce((c, n) => (n < c ? n : c));
    const newest = dates.reduce((c, n) => (n > c ? n : c));
    const difference =
    newest.getMonth() -
    oldest.getMonth() +
    12 * (newest.getFullYear() - oldest.getFullYear());
    const daysOfLastSixMonths = this.lastMonthsByAmount(difference, newest);
    if(!doNotDestroy)this.chart.destroy();
    this._buildChart(daysOfLastSixMonths, "month");
};

lastDaysByAmount = (amount) => {
    const d = new Date();
    const lastDaysArr = [];
    for (let i = 0; i < amount; i++) {
    lastDaysArr.push(new Date(d));
    d.setDate(d.getDate() - 1);
    }
    const responseArr = responses.filter((r) => {
    const responseDate = new Date(r.npstoday__RatingTime__c);
    return lastDaysArr.find(
        (date) =>
        date.getDate() === responseDate.getDate() &&
        date.getMonth() === responseDate.getMonth() &&
        date.getYear() === responseDate.getYear()
    );
    });
    responseArr.lastDays = lastDaysArr.reverse();
    return responseArr;
};

doesDateExistForWantedMonths = (lastMonthsArr, d) => {
    return lastMonthsArr.some((md) => {
    return md.year === d.getFullYear() && md.month === d.getMonth() + 1;
    });
};

lastWeeksByAmount = (amount) => {
    let d = new Date();
    const lastMonthsArr = [];
    let lastWeeksArr = [];
    let dDiff = new Date(d);
    for (let i = 0; i < amount; i++) {
    lastMonthsArr.push({
        year: dDiff.getFullYear(),
        month: dDiff.getMonth() + 1
    });
    dDiff.setMonth(dDiff.getMonth() - 1);
    }
    lastMonthsArr.forEach((md) => {
    const firstOfMonth = new Date(md.year, md.month - 1, 1);
    const lastOfMonth = new Date(md.year, md.month, 0);
    lastWeeksArr.push(getWeekNumber(firstOfMonth));
    if (d < lastOfMonth) {
        lastWeeksArr.push(getWeekNumber(d));
    } else {
        lastWeeksArr.push(getWeekNumber(lastOfMonth));
    }
    });
    let min = Math.min(...lastWeeksArr.map((b) => b[0] + "" + b[1]));
    const max = Math.max(...lastWeeksArr.map((a) => a[0] + "" + a[1]));
    lastWeeksArr = [];
    while (min <= max) {
    const year = parseInt(min.toString().substring(0, 4));
    const week = parseInt(min.toString().substring(4, min.toString().length));
    lastWeeksArr.push({ year: year, week: week });
    min++;
    }
    const weekAndYearNumForMonths = [];
    let found = this.doesDateExistForWantedMonths(lastMonthsArr, d);
    while (found) {
    const weekAndYear = getWeekNumber(d);
    if (
        !weekAndYearNumForMonths.some((w) => {
        return w.year === weekAndYear[0] && w.week === weekAndYear[1];
        })
    ) {
        weekAndYearNumForMonths.push({
        year: weekAndYear[0],
        week: weekAndYear[1]
        });
    }
    d = new Date(d.setDate(d.getDate() - 1));
    found = this.doesDateExistForWantedMonths(lastMonthsArr, d);
    }
    //now that we have week and year - it is then possible to find the corresponding responses
    const responseArr = responses.filter((r) => {
    const responseDate = new Date(r.npstoday__RatingTime__c);
    return lastMonthsArr.find(
        (date) =>
        date.month === responseDate.getMonth() + 1 &&
        date.year === responseDate.getFullYear()
    );
    });
    return {
    responses: responseArr,
    weekAndYears: lastWeeksArr
    };
};

lastMonthsByAmount = (amount, newest) => {
    let d = new Date(newest).setDate(1); //first day of the month. This is done so no bugs happen when substracting months.
    const lastMonthsArr = [];
    let dDiff = new Date(d);
    for (let i = 0; i <= amount; i++) {
    lastMonthsArr.push({
        year: dDiff.getFullYear(),
        month: dDiff.getMonth() + 1
    });
    dDiff.setMonth(dDiff.getMonth() - 1);
    }
    const responseArr = responses.filter((r) => {
    const responseDate = new Date(r.npstoday__RatingTime__c);
    return lastMonthsArr.find(
        (date) =>
        date.month === responseDate.getMonth() + 1 &&
        date.year === responseDate.getFullYear()
    );
    });
    responseArr.lastMonths = lastMonthsArr.reverse();
    return responseArr;
};

const responses = [
    {
        "npstoday__RatingTime__c": "2020-04-11T11:52:44.000Z",
        "npstoday__Rating__c" : 6
    },
    {
        "npstoday__RatingTime__c": "2020-07-11T11:52:44.000Z",
        "npstoday__Rating__c" : 7
    },
    {
        "npstoday__RatingTime__c": "2020-01-11T11:52:44.000Z",
        "npstoday__Rating__c" : 10
    },
    {
        "npstoday__RatingTime__c": "2020-03-11T11:52:44.000Z",
        "npstoday__Rating__c" : 9
    },
    {
        "npstoday__RatingTime__c": "2020-10-11T11:52:44.000Z",
        "npstoday__Rating__c" : 3
    },
    {
        "npstoday__RatingTime__c": "2020-12-11T11:52:44.000Z",
        "npstoday__Rating__c" : 10
    },
    {
        "npstoday__RatingTime__c": "2020-11-24T11:52:44.000Z",
        "npstoday__Rating__c" : 2
    },
    {
        "npstoday__RatingTime__c": "2020-11-23T11:52:44.000Z",
        "npstoday__Rating__c" : 9
    },
    {
        "npstoday__RatingTime__c": "2020-11-22T11:52:44.000Z",
        "npstoday__Rating__c" : 7
    },
    {
        "npstoday__RatingTime__c": "2020-11-01T11:52:44.000Z",
        "npstoday__Rating__c" : 10
    }
]
allTime(true);

Distribution

This distribution chart shows the distribution of the number of detractors (0-6 rating), passives (7-8), and promoters (9-10).

Distribution

Responses

Showing responses to the users is essential to provide value to the user. Having responses right at your hands as, for example, a sales- or service agent is crucial to utilize former customer experiences in your communication strategy.

Responses