Lead volume widget

ui/src/components/adminDashboard/widgets/LeadVolumeWidget

The main widget of the page is LeadVolumeWidget. Widget written with usage of LineChart component. Each line has its own linear gradient which is made with function createGradient:

const createGradient = (ctx, index) => {
        const gradient = ctx.createLinearGradient(0, 0, 0, 500);
        gradient.addColorStop(0.03, lineChartGradients[index].startColor); // start of the gradient
        gradient.addColorStop(0.4, lineChartGradients[index].stopColor); // end of the gradient
        return gradient;
    };

The widget also has a comparison mode, which can be activated by clicking on the switch. In comparison mode, we can compare two ranges for the selected businesses. When in comparison mode, the page's RangePicker will not function unless both ranges are selected in the LeadVolumeWidget. To compare 2 ranges widget will send request for each range and then combine results in one array but with different styles. Code example:

            datasets.push(
                ...Object.keys(firstDatasetData).map((key, index) => ({
                    label: key,
                    data: firstDatasetData[key],
                    borderColor: chartColors[index],
                    borderWidth: 2,
                    fill: 'start',
                    backgroundColor: context => {
                        const ctx = context.chart.ctx;
                        return createGradient(ctx, index);
                    },
                    cubicInterpolationMode: 'monotone',
                    tension: 0.3,
                    pointBackgroundColor: primitiveColors.gray0,
                }))
            );

            datasets.push(
                ...Object.keys(secondDatasetData).map((key, index) => ({
                    label: key,
                    data: secondDatasetData[key],
                    borderColor: chartColors[index],
                    borderWidth: 2,
                    borderDash: [5, 3],
                    fill: false,
                    cubicInterpolationMode: 'monotone',
                    tension: 0.3,
                    pointBackgroundColor: primitiveColors.gray0,
                }))
            );

In the LineChart component, we have specific logic that ensures the correct display of the tooltip, axis steps, and date formatting in the tooltip. Let's see how calculateOptimalStepSize works:

function calculateOptimalStepSize(maxDataValue) {
        const potentialSteps = [1, 2, 5, 10, 20, 25, 50, 100, 200, 500, 1000, 2000, 3000, 4000, 5000];
        const targetSteps = 6;

        for (let i = 0; i < potentialSteps.length; i++) {
            const step = potentialSteps[i];
            if (maxDataValue / step <= targetSteps) {
                return step;
            }
        }

        return potentialSteps[potentialSteps.length - 1];
    }

For the optimal layout according to the design, the chart should have exactly 6 steps. To achieve this, we need to find the optimal step size. In the function, we have an array of available steps. To find the optimal size, we divide the maximum data value on the chart by the step size, and the result should be equal to or less than the target step size.

To create a custom tooltip, use the getOrCreateTooltip function. If the tooltip element already exists, it will return it; if not, it will create the element with specified styles using vanilla JS methods. Code example:

function getOrCreateTooltip(chart) {
        let tooltipEl = chart.canvas.parentNode.querySelector('div.chartjs-tooltip');

        if (!tooltipEl) {
            tooltipEl = document.createElement('div');
            tooltipEl.classList.add('chartjs-tooltip');
            tooltipEl.innerHTML =
                mode === chartModesConstants.Analysis
                    ? `<div id='tooltip-root'></div>`
                    : `<div class=${classes['tooltip-root']} id='tooltip-root'></div>`;
            tooltipEl.style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
            tooltipEl.style.borderRadius = '10px';
            tooltipEl.style.boxShadow = '0 3px 6px rgba(0, 0, 0, 0.1)';
            tooltipEl.style.opacity = 1;
            tooltipEl.style.pointerEvents = 'none';
            tooltipEl.style.position = 'absolute';
            tooltipEl.style.transform = 'translate(-50%, 0)';
            tooltipEl.style.transition = 'all .1s ease';
            tooltipEl.style.zIndex = 1;
            tooltipEl.style.font = 'Inter';
            tooltipEl.style.padding = '12px';

            chart.canvas.parentNode.appendChild(tooltipEl);
        }

        return tooltipEl;
    }

externalTooltipHandlerComparison

The externalTooltipHandlerComparison function is a custom tooltip handler that manages the display and formatting of an external tooltip element for a chart in comparison mode. It retrieves or creates a tooltip element, formats its contents, and styles it for visual clarity, particularly when comparing two data points or time periods.

Parameters

  • context: Contains information about the chart and the tooltip, including chart data, tooltip position, and tooltip content.

Function Overview

  1. Retrieve Tooltip Element: Uses getOrCreateTooltip(chart) to fetch an existing tooltip element or create a new one if none exists.

  2. Tooltip Visibility:

    • Sets the tooltip’s opacity to 0 if there’s no tooltip data to display, effectively hiding it.

    • If there is data, continues with formatting.

  3. Tooltip Content Setup:

    • Extracts the title and body content from the tooltip.

    • Titles are split and formatted with dayjs for dates, if applicable, and displayed with appropriate month formatting.

    • Creates two title elements, firstTitleWrapper and secondTitleWrapper, representing the two comparison data points or time periods.

  4. Tooltip Body Formatting:

    • The tooltip body content (bodyLines) is sorted alphabetically.

    • For each line of content, a wrapper element is created and styled, displaying a colored span for visual distinction.

    • Alternates between adding text to the left or right wrapper to organize comparison values.

  5. Content Wrapping:

    • Clears any existing content in tableRoot to ensure fresh data is displayed.

    • Appends newly created elements (leftWrapper and rightWrapper) to the tableRoot.

  6. Tooltip Positioning:

    • Positions the tooltip relative to the chart's offsetLeft and offsetTop, ensuring it follows the tooltip caret's location accurately on the chart.

Styling Notes

The function uses classes (e.g., tooltip-title-wrapper, tooltip-fit-content, tooltip-text-wrapper) for custom styling, ensuring that elements such as colored lines and text are properly formatted. The createSpan helper function is used to apply colors from chartColors.

Example Usage

This function is called automatically by the chart’s event system when a tooltip event is triggered, e.g., hovering over chart points in comparison mode.

Code example:

const externalTooltipHandlerComparison = context => {
        // Tooltip Element
        const { chart, tooltip } = context;
        const tooltipEl = getOrCreateTooltip(chart);

        // Hide if no tooltip
        if (tooltip.opacity === 0) {
            tooltipEl.style.opacity = 0;
            return;
        }

        // Set Text
        if (tooltip.body) {
            const titles = tooltip.title[0].split('#') || [];
            const bodyLines = tooltip.body.map(b => b.lines);

            const firstTitleWrapper = document.createElement('div');
            firstTitleWrapper.classList.add(classes['tooltip-title-wrapper']);
            firstTitleWrapper.appendChild(
                document.createTextNode(
                    titles[0].split('-').length === 2
                        ? `${dayjs(titles[0]).format('MMM')} ${titles[0].split('-')[0]}`
                        : titles[0]
                )
            );

            const secondTitleWrapper = document.createElement('div');
            secondTitleWrapper.classList.add(classes['tooltip-title-wrapper']);
            secondTitleWrapper.appendChild(
                document.createTextNode(
                    titles[1].split('-').length === 2
                        ? `${dayjs(titles[1]).format('MMM')} ${titles[1].split('-')[0]}`
                        : titles[1]
                )
            );

            const leftWrapper = document.createElement('div');
            leftWrapper.classList.add(classes['tooltip-fit-content']);

            const rightWrapper = document.createElement('div');
            rightWrapper.classList.add(classes['tooltip-fit-content']);

            leftWrapper.appendChild(firstTitleWrapper);
            rightWrapper.appendChild(secondTitleWrapper);
            bodyLines.sort((a, b) => a[0].localeCompare(b[0]));

            bodyLines.forEach((body, i) => {
                const labelText = body[0].split(/:\s*/);

                const textWrapper = document.createElement('div');
                textWrapper.classList.add(classes['tooltip-text-wrapper']);

                const wrapper = document.createElement('div');
                wrapper.classList.add(classes['tooltip-wrapper']);

                const contentWrapper = document.createElement('div');
                contentWrapper.classList.add(classes['tooltip-content-wrapper']);

                const textElement = document.createElement('span');
                textElement.classList.add(classes['tooltip-text-element']);
                textElement.appendChild(document.createTextNode(`${labelText[0]}:`));

                const elementValue = document.createElement('span');
                elementValue.classList.add(classes['tooltip-element-value']);
                elementValue.textContent = labelText[1];

                if (i % 2 === 0) {
                    wrapper.appendChild(createSpan(chartColors[Math.floor(i / 2)]));
                } else {
                    const dashedLine = document.createElement('div');
                    dashedLine.classList.add(classes['tooltip-dashed-line']);
                    dashedLine.appendChild(createSpan(chartColors[Math.floor(i / 2)]));
                    dashedLine.appendChild(createSpan(chartColors[Math.floor(i / 2)]));
                    wrapper.appendChild(dashedLine);
                }

                wrapper.appendChild(textElement);
                textWrapper.appendChild(wrapper);
                textWrapper.appendChild(elementValue);

                if (i % 2 === 0) {
                    leftWrapper.appendChild(textWrapper, contentWrapper);
                } else {
                    rightWrapper.appendChild(textWrapper, contentWrapper);
                }
            });

            const tableRoot = tooltipEl.querySelector('#tooltip-root');

            // Remove old children
            while (tableRoot.firstChild) {
                tableRoot.firstChild.remove();
            }

            // Add new children
            tableRoot.appendChild(leftWrapper);
            tableRoot.appendChild(rightWrapper);
        }

        const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas;

        // Display, position
        tooltipEl.style.opacity = 1;
        tooltipEl.style.left = positionX + tooltip.caretX + 'px';
        tooltipEl.style.top = positionY + tooltip.caretY + 'px';
    };

It is also fucntion for generating tooltip for default mode but it is almost same but with different layout for one dataset.

formatDateToNumber

The formatDateToNumber function formats date labels based on the input date granularity (month, day, or week) and chart data label length.

  • Parameters:

    • label: A date string, either in "YYYY-MM" (month), "DD-MM-YYYY" (day), or "DD/MM" (week) format.

  • Function Logic:

    1. Monthly Format (YYYY-MM):

      • For labels with a monthly format and up to 9 items, returns the month abbreviation (e.g., "May").

      • For over 9 labels, returns the quarter and year (e.g., "Q2/2024") based on the month. The label is stored in quartalsLabels to prevent duplicates.

    2. Daily Format (DD-MM-YYYY):

      • Extracts and returns the day as a string (e.g., "19").

    3. Default:

      • Returns the original label (e.g., for weekly formats).

This function optimizes labels for various chart views, ensuring a simplified display depending on the date range.

Example Usage

Pass that function as a callback to your scales.ticks key in chartOptions:

ticks: {
    autoSkip: true,
    maxRotation: 0,
    color: primitiveColors.gray500,
    font: {
        size: 12,
        family: 'Inter',
        weight: 400,
    },
    callback: function (value, index, values) {
        return formatDateToNumber(chartData.labels[index]);
    },
},

Plugin for drawing vertical line on mouse enter to any point

The provided plugin is a custom plugin for a Chart.js Line chart that draws a vertical dashed line across the chart whenever a tooltip is active.

Implementation

  • Location: This plugin is added to the plugins array of the <Line /> component.

Plugin Functionality

  • Lifecycle Method: afterDraw

    • Executes after the chart is drawn, allowing for modifications to the canvas based on the current chart state.

Parameters

  • chart: The chart instance, providing access to its properties and methods.

Logic

  1. Check Active Tooltip:

    • If there is an active tooltip (i.e., chart.tooltip?._active?.length), the plugin retrieves the x-coordinate of the active tooltip's element.

  2. Draw Dashed Line:

    • It obtains the y-axis scale (yAxis) and the rendering context (ctx) from the chart.

    • The following drawing operations are performed:

      • Save Context: Saves the current canvas state.

      • Set Line Dash: Configures the line style to dashed with a pattern of 8 pixels on and 7 pixels off.

      • Begin Path: Starts a new path for drawing.

      • Move To: Moves the drawing cursor to the active tooltip's x-coordinate at the top of the y-axis.

      • Line To: Draws a line down to the bottom of the y-axis.

      • Set Line Width: Defines the line width to 1 pixel.

      • Set Stroke Style: Sets the stroke color to primitiveColors.gray100.

      • Stroke: Renders the line onto the canvas.

      • Restore Context: Restores the previous canvas state.

Backend

getLeadsPerBusinessesPerDay is responsible for getting datasets for LeadVolumeWidget.

The function will return different results based on the specified period:

  • For a period of less than 2 months: Labels will be daily dates, and the values will represent the leads created on those dates for each business.

  • For a period of more than 2 months and less than 4 months: Labels will be the starting days of the week (with the week beginning on Saturday) along with the month number, for example, 20/07. The values will represent the leads created during that week for each business.

  • For a period of more than 4 months: Labels will be the names of the months, and the values will represent the leads created during each month for each business.

(On the frontend, we also have a quarterly display that shows data by month but generates custom labels with quarters and years.)

The getLeadsPerBusinessesPerDay function retrieves the number of leads created per business over a specified date range. It organizes the data for visualization in a chart format, accounting for various conditions such as time periods and chart modes.

Parameters

  • mode (ChartMode): Determines the mode of the chart (e.g., analysis or comparison).

  • startDate (string): The start date of the period to analyze, formatted as a string.

  • endDate (string): The end date of the period to analyze, formatted as a string.

  • businessIds (number[]): An array of business IDs to filter the leads.

  • teamId (string): The ID of the team associated with the leads.

Returns

  • Promise<{ labels: string[], data: object, totalLeadsAmount: number, percentageChange: number }>:

    • labels: An array of date labels.

    • data: An object containing the lead counts for each business by date.

    • totalLeadsAmount: The total number of leads generated in the specified period.

    • percentageChange: The percentage change in leads compared to the previous period.

Description

  1. Date Validation:

    • The function checks if the provided start and end dates are valid. If not, it throws a BadRequestException.

  2. Date Range Calculation:

    • It calculates the duration of the period in days and prepares for querying data for the current and previous periods.

  3. Leads Query:

    • It retrieves leads data by joining the leads with businesses, grouping by business name and creation date, and counting the number of leads created.

  4. Business Names Retrieval:

    • Fetches the names of the businesses corresponding to the provided IDs for later reference.

  5. Data Structuring:

    • Organizes the leads data into a result object, formatted by business names and dates.

  6. Final Data Processing:

    • Handles different scenarios based on the duration of the date range:

      • If the range is less than two months, it fills in any missing dates with lead counts of zero.

      • If the range is two months or longer, it groups data by week or month.

  7. Comparison Mode Handling:

    • In comparison mode, it ensures that every business has a corresponding entry in the result, filling in zeros where necessary.

  8. Previous Period Calculation:

    • Calls a helper function to calculate the total number of leads for the previous period.

  9. Percentage Change Calculation:

    • Computes the percentage change in leads compared to the previous period, handling edge cases where the previous period's total may be zero.

  10. Final Sorting:

    • Sorts the final result by business name for better readability.

Error Handling

  • Throws a BadRequestException for invalid date formats and can impose business count limits if uncommented.

This function is essential for generating lead analytics, enabling teams to visualize their performance and track changes over time.

If there is no data for a specific date, we should manually set the lead count for that date key; otherwise, it will not appear in the result array and will cause incorrect chart display. Additionally, we should always sort the dates to ensure they appear in the correct order, and at the end, sort by business name to maintain consistent colors and order for the same businesses.

If it is a call for comparison mode, there is one additional cycle that handles the case when there is no data for a business and fills the array with zeroes for correct display on the chart.

Last updated