top of page

Timeline of related record

Updated: Dec 1, 2023


Github Link : https://github.com/AourLegacy/AourFactory/tree/Timeline-lwc





Step 2 : Creation d’un projet sur VSCodes avec la commande cmd + shift + p , SFDX : Create Project .

Step 3 : Cree un lightning web component : cmd + shift + p , SFDX : Create Lightning web component . avec les code suivant : Timeline.js :


import { LightningElement , api , wire } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import { loadScript } from 'lightning/platformResourceLoader';
import shortDateFormat from '@salesforce/i18n/dateTime.shortDateFormat';
import LOCALE from '@salesforce/i18n/locale';

import getTimelineData from '@salesforce/apex/TimelineService.getTimelineRecords';
import getTimelineTypes from '@salesforce/apex/TimelineService.getTimelineTypes';

import d3JS from '@salesforce/resourceUrl/d3minified';
import momentJS from '@salesforce/resourceUrl/momentminified';

import APEX from '@salesforce/label/c.Timeline_Error_Apex';
import SETUP from '@salesforce/label/c.Timeline_Error_Setup';
import NO_DATA_HEADER from '@salesforce/label/c.Timeline_Error_NoDataHeader';
import NO_DATA_SUBHEADER from '@salesforce/label/c.Timeline_Error_NoDataSubHeader';
import JAVASCRIPT_LOAD from '@salesforce/label/c.Timeline_Error_JavaScriptResources';
import UNHANDLED from '@salesforce/label/c.Timeline_Error_Unhandled';

import DAYS from '@salesforce/label/c.Timeline_Label_Days';
import SHOWING from '@salesforce/label/c.Timeline_Label_Showing';
import ITEMS from '@salesforce/label/c.Timeline_Label_Items';
import FILTERS from '@salesforce/label/c.Timeline_Label_Filters';
import TYPE_LEGEND from '@salesforce/label/c.Timeline_Label_Filter_Type_Legend';
import DATE_RANGE_LEGEND from '@salesforce/label/c.Timeline_Label_Date_Range_Legend';
import FILE_TYPE from '@salesforce/label/c.Timeline_Label_Files';
import ALL_TYPES from '@salesforce/label/c.Timeline_Label_Filter_All_Types';
import BUTTON_APPLY from '@salesforce/label/c.Timeline_Label_Apply';
import BUTTON_CANCEL from '@salesforce/label/c.Timeline_Label_Cancel';

export default class Timelinelwc extends NavigationMixin(LightningElement) {
//Adminstrator accessible attributes in app builder
    @api timelineTitle; //title for the lwc set as design attribute
    @api preferredHeight; //height of the timeline set as design attribute
    @api earliestRange; //How far back in time to go
    @api latestRange; //How far into the future to go
    @api zoomTo; //Zoom to current dat or latest activity
    @api daysToShow; //number of days to plot for the default zoom

     //Component calculated attributes
     @api recordId; //current record id of lead, case, opportunity, contact or account

     @api flexipageRegionWidth; //SMALL, MEDIUM and LARGE based on where the component is placed in App Builder templates
 
     timelineStart; //Calculated based on the earliestRange
     timelineEnd; //Calculated based on the latestRange
 
     zoomStartDate; //Start date of the current zoom
     zoomEndDate; //End date of the current zoom
 
     localisedZoomStartDate; //Start date of the current zoom
     localisedZoomEndDate; //End date of the current zoom
 
     totalTimelineRecords = 0; //Total number of records returned
     totalZoomedRecords = 0; //Total records in zoom
 
     noData = false; //Boolean when no data is returned
     noFilterData = false; //Boolean when no data is returned after filtering
     isLoaded = false; //Boolean when timeline data is loaded
     isError = false; //Boolean when there is an error
 
     isMouseOver = false; //Boolean when mouse over is detected
     mouseOverRecordId; //Current Id of the record being hovered over
     mouseOverObjectAPIName; //API Name for the object being hovered over
     mouseOverObjectNameLabel;
     mouseOverDetailLabel;
     mouseOverDetailValue;
     mouseOverFallbackField;
     mouseOverFallbackValue;
     mouseOverTime;
 
     filterValues = [];
     startingFilterValues = [];
     allFilterValues = [];
     objectFilter = [];
     isFilter;
     isFilterUpdated;
     isFilterLoaded = false;
 
     illustrationVisibility = 'illustration-hidden'; //Toggles the class to show and hide the illustration component
 
     illustrationHeader; //Header to display when an information box displays
     illustrationSubHeader; //Sub Header to display when an info box appears
     illustrationType; //Type of illustration to display, 'error' or 'no data'
 
     label = {
        DAYS,
        SHOWING,
        ITEMS,
        FILTERS,
        TYPE_LEGEND,
        DATE_RANGE_LEGEND,
        FILE_TYPE,
        ALL_TYPES,
        BUTTON_APPLY,
        BUTTON_CANCEL
    };

    error = {
        APEX,
        SETUP,
        NO_DATA_HEADER,
        NO_DATA_SUBHEADER,
        JAVASCRIPT_LOAD,
        UNHANDLED
    };


     _timelineData = null;
     _timelineHeight = null;
 
     //These are the objects holding individual instances of the timeline
     _d3timelineCanvas = null;
     _d3timelineCanvasAxis = null;
     _d3timelineCanvasMap = null;
     _d3timelineCanvasMapAxis = null;
     _d3brush = null;
 
     //These are the d3 selections that allow us to modify the DOM
     _d3timelineCanvasSVG = null;
     _d3timelineCanvasAxisSVG = null;
     _d3timelineMapSVG = null;
     _d3timelineMapAxisSVG = null;
     _d3timelineCanvasDIV = null;
     _d3timelineCanvasMapDIV = null;
 
     _d3LocalisedShortDateFormat = null;
     _d3Rendered = false;

     @wire(getTimelineTypes, { parentObjectId: '$recordId' })
     wiredResult(result) {
         if (result.data) {
             const timelineTs = result.data;
 
             for (let key in timelineTs) {
                 // eslint-disable-next-line no-prototype-builtins
                 if (timelineTs.hasOwnProperty(key)) {
                     this.filterValues.push(key);
                     let tempFilter = [];
 
                     tempFilter.label = timelineTs[key];
                     tempFilter.value = key;
 
                     this.objectFilter.push(tempFilter);
                     this.startingFilterValues.push(key);
                     this.allFilterValues.push(key);
                 }
             }
             this.isFilterLoaded = true;
         } else if (result.error) {
             let errorMessage = result.error.body.message;
             this.processError('Error', this.error.APEX, errorMessage);
         }
     }

     disconnectedCallback() {
        window.removeEventListener('resize', this.debounce);
    }

    connectedCallback() {
        this._timelineHeight = this.getPreferredHeight();
        this._d3LocalisedShortDateFormat = this.userDateFormat();
    }
    renderedCallback() {
        if (!this._d3Rendered) {
            //set the height of the component as the height is dynamic based on the attributes
            let timelineDIV = this.template.querySelector('div.timeline-canvas');

            timelineDIV.setAttribute('style', 'height:' + this._timelineHeight + 'px');

            Promise.all([loadScript(this, d3JS), loadScript(this, momentJS)])
                .then(() => {
                    //Setup d3 timeline by manipulating the DOM and do it once only as render gets called many times
                    this._d3timelineCanvasDIV = d3.select(this.template.querySelector('div.timeline-canvas'));
                    this._d3timelineCanvasMapDIV = d3.select(this.template.querySelector('div.timeline-canvas-map'));
                    this._d3timelineCanvasSVG = d3
                        .select(this.template.querySelector('div.timeline-canvas'))
                        .append('svg');
                    this._d3timelineCanvasAxisSVG = d3
                        .select(this.template.querySelector('div.timeline-canvas-axis'))
                        .append('svg');
                    this._d3timelineMapSVG = d3.select(this.template.querySelector('div.timeline-map')).append('svg');
                    this._d3timelineMapAxisSVG = d3
                        .select(this.template.querySelector('div.timeline-map-axis'))
                        .append('svg');

                    this.processTimeline();
                })
                .catch((error) => {
                    this.processError('Error', this.error.JAVASCRIPT_LOAD, error);
                });

            this._d3Rendered = true;
        }

        let timelineSummary = this.template.querySelectorAll('span.timeline-summary-verbose');

        if (timelineSummary !== undefined && timelineSummary !== null) {
            for (let i = 0; i < timelineSummary.length; i++) {
                timelineSummary[i].classList.add('timeline-summary-verbose-' + this.flexipageRegionWidth);
            }
        }
    }

    processTimeline() {
        const me = this;
        me.isError = false;
        me.isLoaded = false;

        me.illustrationVisibility = 'illustration-hidden';
        me.noData = false;

        const dateTimeFormat = new Intl.DateTimeFormat(LOCALE);
        me.timelineStart = dateTimeFormat.format(moment().subtract(me.earliestRange, 'years'));
        me.timelineEnd = dateTimeFormat.format(moment().add(me.latestRange, 'years'));

        me._d3timelineCanvasSVG.selectAll('*').remove();
        me._d3timelineCanvasAxisSVG.selectAll('*').remove();
        me._d3timelineMapSVG.selectAll('*').remove();
        me._d3timelineMapAxisSVG.selectAll('*').remove();

        getTimelineData({ parentObjectId: me.recordId, earliestRange: me.earliestRange, latestRange: me.latestRange })
            .then((result) => {
                try {
                    if (result.length > 0) {
                        me.totalTimelineRecords = result.length;

                        //Process timeline records
                        me._timelineData = me.getTimelineRecords(result);

                        //Process timeline canvas
                        me._d3timelineCanvas = me.timelineCanvas();

                        const axisDividerConfig = {
                            tickFormat: '%d %b %Y',
                            innerTickSize: -me._d3timelineCanvas.SVGHeight,
                            translate: [0, me._d3timelineCanvas.SVGHeight],
                            tickPadding: 0,
                            ticks: 6,
                            class: 'axis-ticks'
                        };

                        me._d3timelineCanvasAxis = me.axis(
                            axisDividerConfig,
                            me._d3timelineCanvasSVG,
                            me._d3timelineCanvas
                        );

                        const axisLabelConfig = {
                            tickFormat: me._d3LocalisedShortDateFormat,
                            innerTickSize: 0,
                            tickPadding: 2,
                            translate: [0, 5],
                            ticks: 6,
                            class: 'axis-label'
                        };

                        me._d3timelineCanvasAxisLabel = me.axis(
                            axisLabelConfig,
                            me._d3timelineCanvasAxisSVG,
                            me._d3timelineCanvas
                        );

                        //Process timeline map
                        me._d3timelineMap = me.timelineMap();
                        me._d3timelineMap.redraw();

                        const mapAxisConfig = {
                            tickFormat: me._d3LocalisedShortDateFormat,
                            innerTickSize: 4,
                            tickPadding: 4,
                            ticks: 6,
                            class: 'axis-label'
                        };

                        me._d3timelineMapAxis = me.axis(mapAxisConfig, me._d3timelineMapAxisSVG, me._d3timelineMap);

                        me._d3brush = me.brush();

                        window.addEventListener(
                            'resize',
                            me.debounce(() => {
                                try {
                                    if (me.template.querySelector('div.timeline-canvas').offsetWidth !== 0) {
                                        me._d3timelineCanvas.x.range([
                                            0,
                                            me.template.querySelector('div.timeline-canvas').offsetWidth
                                        ]);
                                        me._d3timelineMap.x.range([
                                            0,
                                            Math.max(me.template.querySelector('div.timeline-map').offsetWidth, 0)
                                        ]);
                                        me._d3timelineCanvasAxis.redraw();
                                        me._d3timelineCanvasAxisLabel.redraw();
                                        me._d3timelineMap.redraw();
                                        me._d3timelineMapAxis.redraw();
                                        me._d3brush.redraw();
                                    }
                                } catch (error) {
                                    //stay silent
                                }
                            }, 200)
                        );

                        me.isLoaded = true;
                    } else {
                        me.processError('No-Data', me.error.NO_DATA_HEADER, me.error.NO_DATA_SUBHEADER);
                    }
                } catch (error) {
                    me.processError('Error', me.error.UNHANDLED, error.message);
                }
            })
            .catch((error) => {
                let errorType = 'Error';
                let errorHeading,
                    errorMessage = '--';

                try {
                    errorMessage = error.body.message;
                    let customError = JSON.parse(errorMessage);
                    errorType = customError.type;
                    errorMessage = customError.message;
                    errorHeading = me.error.SETUP;
                } catch (error2) {
                    //fails to parse message so is a generic apex error
                    errorHeading = me.error.APEX;
                }

                me.processError(errorType, errorHeading, errorMessage);
            });
    }

    getTimelineRecords(result) {
        let timelineRecords = {};
        let timelineResult = [];
        let timelineTimes = [];

        result.forEach(function (record, index) {
            let recordCopy = {};

            recordCopy.recordId = record.objectId;
            recordCopy.id = index;
            recordCopy.label =
                record.detailField.length <= 30 ? record.detailField : record.detailField.slice(0, 30) + '...';
            recordCopy.time = moment(record.positionDateValue, 'YYYY-MM-DD HH:mm:ss').toDate();
            recordCopy.week = moment(record.positionDateValue, 'YYYY-MM-DD').startOf('week');
            recordCopy.objectName = record.objectName;
            recordCopy.objectNameLabel = record.objectNameLabel;

            recordCopy.positionDateField = record.positionDateField;
            recordCopy.detailField = record.detailField;
            recordCopy.detailFieldLabel = record.detailFieldLabel;

            recordCopy.fallbackTooltipField = record.fallbackTooltipField;
            recordCopy.fallbackTooltipValue = record.fallbackTooltipValue;

            recordCopy.tooltipId = record.tooltipId;
            recordCopy.tooltipObject = record.tooltipObject;
            recordCopy.drilldownId = record.drilldownId;

            recordCopy.type = record.type;
            recordCopy.icon = record.icon;
            recordCopy.iconBackground = record.iconBackground;

            timelineResult.push(recordCopy);
            timelineTimes.push(recordCopy.time);
        });

        timelineRecords.data = timelineResult;
        timelineRecords.minTime = d3.min(timelineTimes);
        timelineRecords.maxTime = d3.max(timelineTimes);
        timelineRecords.requestRange = [
            moment().subtract(this.earliestRange, 'years').toDate(),
            moment().add(this.latestRange, 'years').toDate()
        ];

        return timelineRecords;
    }

    timelineCanvas() {
        const me = this;
        const timelineCanvasDIV = this.template.querySelector('div.timeline-canvas');
        const timelineCanvas = me._d3timelineCanvasSVG;
        const timelineData = me._timelineData;
        const timelineHeight = me._timelineHeight;

        const width = timelineCanvasDIV.offsetWidth;

        timelineCanvasDIV.setAttribute('style', 'max-height:' + timelineHeight + 'px');
        timelineCanvas.SVGHeight = timelineHeight;

        timelineCanvas.x = d3.scaleTime().domain(timelineData.requestRange).rangeRound([0, width]);

        timelineCanvas.y = function (swimlane) {
            return swimlane * 25 * 1 + (swimlane + 1) * 5;
        };

        timelineCanvas.width = width;
        timelineCanvas.height = timelineHeight;

        timelineCanvas.filter = function (d) {
            if (me.isFilterLoaded === false || me.filterValues.includes(d.objectName)) {
                return true;
            }
            return false;
        };

        timelineCanvas.redraw = function (domain) {
            var i = 0;
            var swimlane = 0;

            if (domain) {
                timelineCanvas.x.domain(domain);
            }

            let swimlanes = [];
            const unitInterval = (timelineCanvas.x.domain()[1] - timelineCanvas.x.domain()[0]) / timelineCanvas.width;

            let data = timelineData.data
                .filter(function (d) {
                    d.endTime = new Date(d.time.getTime() + unitInterval * (d.label.length * 6 + 80));
                    return timelineCanvas.x.domain()[0] < d.endTime && d.time < timelineCanvas.x.domain()[1];
                })
                .filter(timelineCanvas.filter);

            me.totalZoomedRecords = data.length;

            data.sort(me.sortByValue('time'));

            data.forEach(function (entry) {
                for (i = 0, swimlane = 0; i < swimlanes.length; i++, swimlane++) {
                    if (entry.time > swimlanes[i]) break;
                }
                entry.swimlane = swimlane;
                swimlanes[swimlane] = entry.endTime;
            });

            timelineCanvas.width = timelineCanvas.x.range()[1];
            timelineCanvas.attr('width', timelineCanvas.width);

            const svgHeight = Math.max(timelineCanvas.y(swimlanes.length), timelineHeight);
            timelineCanvas.height = timelineHeight;

            timelineCanvas.attr('height', svgHeight - 1);
            timelineCanvas.SVGHeight = svgHeight;

            timelineCanvas.data = timelineCanvas
                .selectAll('[class~=timeline-canvas-record]')
                .data(data, function (d) {
                    return d.id;
                })
                .attr('transform', function (d) {
                    return 'translate(' + timelineCanvas.x(d.time) + ', ' + timelineCanvas.y(d.swimlane) + ')';
                });

            timelineCanvas.records = timelineCanvas.data
                .enter()
                .append('g')
                .attr('class', 'timeline-canvas-record')
                .attr('transform', function (d) {
                    return 'translate(' + timelineCanvas.x(d.time) + ', ' + timelineCanvas.y(d.swimlane) + ')';
                });

            if (timelineCanvas.records.size() > 0) {
                timelineCanvas.records
                    .append('rect')
                    .attr('class', 'timeline-canvas-icon-wrap')
                    .attr('style', function (d) {
                        let iconColour = '';
                        switch (d.type) {
                            case 'Call':
                                iconColour = '#48C3CC';
                                break;
                            case 'Task':
                                iconColour = '#4ac076';
                                break;
                            case 'Event':
                                iconColour = '#eb7092';
                                break;
                            case 'Email':
                                iconColour = '#95AEC5';
                                break;
                            case 'SNOTE':
                                iconColour = '#E6D478';
                                break;
                            default:
                                iconColour = d.iconBackground;
                                break;
                        }
                        return 'fill: ' + iconColour;
                    })
                    .attr('x', 0)
                    .attr('y', 0)
                    .attr('width', 24)
                    .attr('height', 24)
                    .attr('rx', 3)
                    .attr('ry', 3);

                timelineCanvas.records
                    .append('image')
                    .attr('x', 1)
                    .attr('y', 1)
                    .attr('height', 22)
                    .attr('width', 22)
                    .attr('xlink:href', function (d) {
                        let iconImage = '';

                        switch (d.type) {
                            case 'Call':
                                iconImage = '/img/icon/t4v35/standard/log_a_call.svg';
                                break;
                            case 'Task':
                                iconImage = '/img/icon/t4v35/standard/task.svg';
                                break;
                            case 'Event':
                                iconImage = '/img/icon/t4v35/standard/event.svg';
                                break;
                            case 'Email':
                                iconImage = '/img/icon/t4v35/standard/email.svg';
                                break;
                            case 'SNOTE':
                                iconImage = '/img/icon/t4v35/standard/note.svg';
                                break;
                            default:
                                iconImage = d.icon;
                                break;
                        }
                        return iconImage;
                    });

                timelineCanvas.records
                    .append('rect')
                    .attr('class', 'timeline-canvas-record-wrap')
                    .attr('x', 24 + 8)
                    .attr('y', 0)
                    .attr('height', 24)
                    .attr('rx', 3)
                    .attr('ry', 3);
                timelineCanvas.records
                    .append('line')
                    .attr('class', 'timeline-canvas-record-line')
                    .attr('x1', 24)
                    .attr('y1', 12)
                    .attr('x2', 24 + 8)
                    .attr('y2', 12);
                timelineCanvas.records
                    .append('text')
                    .attr('class', 'timeline-canvas-record-label')
                    .attr('x', 24 + 10)
                    .attr('y', 16)
                    .attr('font-size', 12)
                    .on('click', function (d) {
                        let drilldownId = d.recordId;
                        if (d.drilldownId !== '') {
                            drilldownId = d.drilldownId;
                        }

                        switch (d.objectName) {
                            case 'ContentDocumentLink':
                                me[NavigationMixin.Navigate]({
                                    type: 'standard__namedPage',
                                    attributes: {
                                        pageName: 'filePreview'
                                    },
                                    state: {
                                        selectedRecordId: d.recordId
                                    }
                                });
                                break;
                            default:
                                me[NavigationMixin.Navigate]({
                                    type: 'standard__recordPage',
                                    attributes: {
                                        recordId: drilldownId,
                                        actionName: 'view'
                                    }
                                });
                                break;
                        }
                    })
                    .on('mouseover', function (d) {
                        let tooltipId = d.recordId;
                        let tooltipObject = d.objectName;

                        if (d.tooltipId !== '') {
                            tooltipId = d.tooltipId;
                            tooltipObject = d.tooltipObject;
                        }

                        me.mouseOverObjectAPIName = tooltipObject;
                        me.mouseOverObjectNameLabel = d.objectNameLabel;
                        me.mouseOverRecordId = tooltipId;

                        me.mouseOverFallbackField = d.fallbackTooltipField;
                        me.mouseOverFallbackValue = d.fallbackTooltipValue;

                        me.mouseOverDetailLabel = d.detailFieldLabel;
                        me.mouseOverDetailValue = d.detailField;

                        me.mouseOverTime = moment(d.time).format('DD.MM.YYYY HH:mm');

                        me.isMouseOver = true;
                        let tooltipDIV = me.template.querySelector('div.tooltip-panel');
                        tooltipDIV.setAttribute(
                            'style',
                            'top:' +
                                (this.getBoundingClientRect().top - 30) +
                                'px ;left:' +
                                (this.getBoundingClientRect().right + 15) +
                                'px ;visibility:visible'
                        );
                    })
                    .on('mouseout', function () {
                        let tooltipDIV = me.template.querySelector('div.tooltip-panel');
                        tooltipDIV.setAttribute('style', 'visibility: hidden');
                        me.isMouseOver = false;
                    })
                    .text(function (d) {
                        return d.label;
                    });
            }
            timelineCanvas.data.exit().remove();
        };
        return timelineCanvas;
    }

    axis(axisConfig, targetSVG, target) {
        const me = this;
        const timelineCanvas = me._d3timelineCanvas;

        targetSVG.attr('width', target.width);

        let x_axis = d3
            .axisBottom(target.x)
            .tickSizeInner(axisConfig.innerTickSize)
            .ticks(axisConfig.ticks)
            .tickFormat(d3.timeFormat(axisConfig.tickFormat))
            .tickPadding(axisConfig.tickPadding);

        const axis = targetSVG
            .insert('g', ':first-child')
            .attr('class', axisConfig.class + '-' + me.flexipageRegionWidth)
            .call(x_axis);

        if (typeof axisConfig.translate === 'object') {
            axis.attr('transform', function () {
                return 'translate(' + axisConfig.translate[0] + ', ' + axisConfig.translate[1] + ')';
            });
        }

        axis.redraw = function () {
            targetSVG.attr('width', target.width);

            if (axisConfig.class === 'axis-ticks') {
                axisConfig.innerTickSize = -timelineCanvas.SVGHeight;
                axisConfig.translate = [0, timelineCanvas.SVGHeight];
            }

            x_axis = x_axis.tickSizeInner(axisConfig.innerTickSize);
            x_axis = x_axis.tickValues(axisConfig.tickValues);

            if (typeof axisConfig.translate === 'object') {
                axis.attr('transform', function () {
                    return 'translate(' + axisConfig.translate[0] + ', ' + axisConfig.translate[1] + ')';
                });
            }
            axis.call(x_axis);
        };

        return axis;
    }

    processError(type, header, message) {
        if (this.illustrationVisibility !== 'illustration') {
            this.isLoaded = true;
            this.illustrationVisibility = 'illustration';
            this.illustrationHeader = header;
            this.illustrationSubHeader = message;

            switch (type) {
                case 'No-Data':
                    this.illustrationType = 'Desert';
                    this.isError = false;
                    this.noData = true;
                    break;
                case 'No-Filter-Data':
                    this.illustrationType = 'Desert';
                    this.isError = false;
                    this.noFilterData = true;
                    break;
                case 'Setup-Error':
                    this.illustrationType = 'Setup';
                    this.isError = true;
                    this.noFilterData = false;
                    this.noData = false;
                    break;
                default:
                    this.illustrationType = 'PageNotAvailable';
                    this.isError = true;
                    break;
            }
        }
    }

    getPreferredHeight() {
        let height;

        switch (this.preferredHeight) {
            case '1 - Smallest':
                height = 125;
                break;
            case '2 - Small':
                height = 200;
                break;
            case '3 - Default':
                height = 275;
                break;
            case '4 - Big':
                height = 350;
                break;
            case '5 - Biggest':
                height = 425;
                break;
            default:
                height = 275;
                break;
        }

        return height;
    }

    timelineMap() {
        const me = this;

        const timelineData = me._timelineData;
        const timelineMapSVG = me._d3timelineMapSVG;
        const timelineMap = timelineMapSVG;
        const timelineMapDIV = me.template.querySelector('div.timeline-map');

        timelineMap.x = d3.scaleTime().domain(timelineData.requestRange).range([0, timelineMapDIV.offsetWidth]);

        timelineMap.y = function (swimlane) {
            return Math.min(swimlane, 7) * 4 + 4;
        };

        timelineMap.filter = function (d) {
            if (me.isFilterLoaded === false || me.filterValues.includes(d.objectName)) {
                return true;
            }
            return false;
        };

        timelineMap.width = timelineMapDIV.offsetWidth;
        timelineMap.height = timelineMapDIV.offsetHeight;

        timelineMap.redraw = function () {
            var i = 0;
            var swimlane = 0;
            let swimlanes = [];
            const unitInterval = (timelineMap.x.domain()[1] - timelineMap.x.domain()[0]) / timelineMap.width;

            let data = timelineData.data
                .filter(function (d) {
                    d.endTime = new Date(d.time.getTime() + unitInterval * 10);
                    return true;
                })
                .filter(timelineMap.filter);

            data.sort(me.sortByValue('time'));

            // calculating vertical layout for displaying data
            data.forEach(function (entry) {
                for (i = 0, swimlane = 0; i < swimlanes.length; i++, swimlane++) {
                    if (entry.time > swimlanes[i]) break;
                }
                entry.swimlane = swimlane;
                swimlanes[swimlane] = entry.endTime;
            });

            data = data.filter(function (d) {
                if (d.swimlane < 8) {
                    return true;
                }
                return false;
            });

            timelineMap.width = timelineMap.x.range()[1];
            timelineMapSVG.attr('width', timelineMap.width);

            timelineMap.data = timelineMap
                .selectAll('[class~=timeline-map-record]')
                .data(data, function (d) {
                    return d.id;
                })
                .attr('transform', function (d) {
                    return 'translate(' + timelineMap.x(d.time) + ', ' + timelineMap.y(d.swimlane) + ')';
                });

            timelineMap.records = timelineMap.data
                .enter()
                .append('g')
                .attr('class', 'timeline-map-record')
                .attr('transform', function (d) {
                    return 'translate(' + timelineMap.x(d.time) + ', ' + timelineMap.y(d.swimlane) + ')';
                });

            timelineMap.records
                .append('rect')
                .attr('style', function () {
                    return 'fill: #98C3EE; stroke: #4B97E6';
                })
                .attr('width', 3)
                .attr('height', 2)
                .attr('rx', 0.2)
                .attr('ry', 0.2);

            if (data.length <= 0) {
                me.processError('No-Filter-Data', me.error.NO_DATA_HEADER, me.error.NO_DATA_SUBHEADER);
            }
        };
        return timelineMap;
    }
 
    brush() {
        const me = this;
        const d3timeline = me._d3timelineCanvas;
        const timelineData = me._timelineData;
        const timelineAxis = me._d3timelineCanvasAxis;
        const timelineAxisLabel = me._d3timelineCanvasAxisLabel;
        const timelineMap = me._d3timelineMap;
        const timelineMapSVG = me._d3timelineMapSVG;
        const timelineMapLayoutA = timelineMapSVG.append('g');
        const timelineMapLayoutB = timelineMapLayoutA.append('g');
        let emptySelectionStart;
        let defaultZoomDate;
        let startBrush;
        let endBrush;

        switch (this.zoomTo) {
            //case 'Historical Date':
            //   TODO
            case 'Last Activity':
                defaultZoomDate = moment(timelineData.maxTime).toDate();
                break;
            default:
                defaultZoomDate = new Date().getTime();
                break;
        }

        if (me.zoomStartDate !== undefined) {
            startBrush = moment(me.zoomStartDate, 'DD MMM YYYY').format('DD MMM YYYY');
            endBrush = moment(me.zoomEndDate, 'DD MMM YYYY').format('DD MMM YYYY');
        } else {
            startBrush = moment(defaultZoomDate)
                .subtract(me.daysToShow / 2, 'days')
                .toDate();
            endBrush = moment(defaultZoomDate)
                .add(me.daysToShow / 2, 'days')
                .toDate();
        }

        timelineMapLayoutB.append('g').attr('class', 'brush').attr('transform', 'translate(0, -1)');

        const xBrush = d3.select(this.template.querySelector('div.timeline-map')).select('g.brush');

        let brush = d3
            .brushX()
            .extent([
                [0, 0],
                [timelineMap.width, timelineMap.height]
            ])
            .on('brush', brushed)
            .on('start', brushStart)
            .on('end', brushEnd);

        const handle = xBrush
            .selectAll('.handle--custom')
            .data([{ type: 'w' }, { type: 'e' }])
            .enter()
            .append('path')
            .attr('class', 'handle--custom')
            .attr('fill', '#4b97e6')
            .attr('fill-opacity', 0.8)
            .attr('stroke', '#000')
            .attr('height', 40)
            .attr('stroke-width', 1)
            .attr('cursor', 'ew-resize')
            .attr(
                'd',
                'M0,0 L75,0 L75,176 C75,184.284271 68.2842712,191 60,191 L15,191 C6.71572875,191 1.01453063e-15,184.284271 0,176 L0,0 L0,0 Z'
            );
        xBrush.call(brush).call(brush.move, [new Date(startBrush), new Date(endBrush)].map(timelineMap.x));

        brush.redraw = function () {
            brush = d3
                .brushX()
                .extent([
                    [0, 0],
                    [timelineMap.width, timelineMap.height]
                ])
                .on('brush', brushed)
                .on('start', brushStart)
                .on('end', brushEnd);

            startBrush = moment(me.zoomStartDate, 'DD MMM YYYY').format('DD MMM YYYY');
            endBrush = moment(me.zoomEndDate, 'DD MMM YYYY').format('DD MMM YYYY');

            xBrush.call(brush).call(brush.move, [new Date(startBrush), new Date(endBrush)].map(timelineMap.x));
        };

        function brushed() {
            const selection = d3.event.selection;
            const dommy = [];

            if (selection) {
                dommy.push(timelineMap.x.invert(selection[0]));
                dommy.push(timelineMap.x.invert(selection[1]));

                d3timeline.redraw(dommy);
                timelineAxis.redraw();
                timelineAxisLabel.redraw();

                handle.attr('transform', function (d, i) {
                    return 'translate(' + (selection[i] - 2) + ', ' + 0 + ') scale(0.05)';
                });

                me.daysToShow = moment(d3timeline.x.domain()[1]).diff(moment(d3timeline.x.domain()[0]), 'days');

                const dateTimeFormat = new Intl.DateTimeFormat(LOCALE);

                me.zoomStartDate = moment(timelineMap.x.invert(selection[0])).format('DD MMM YYYY');
                me.zoomEndDate = moment(timelineMap.x.invert(selection[1])).format('DD MMM YYYY');

                me.localisedZoomStartDate = dateTimeFormat.format(moment(timelineMap.x.invert(selection[0])));
                me.localisedZoomEndDate = dateTimeFormat.format(moment(timelineMap.x.invert(selection[1])));
            }
        }

        function brushStart() {
            const selection = d3.event.selection;

            if (selection) {
                emptySelectionStart = timelineMap.x.invert(selection[0]);
                handle.attr('transform', function (d, i) {
                    return 'translate(' + (selection[i] - 2) + ', ' + 0 + ') scale(0.05)';
                });
            }
        }

        function brushEnd() {
            const selection = d3.event.selection;

            if (selection === null) {
                me.zoomStartDate = moment(emptySelectionStart).toDate();
                me.zoomEndDate = moment(emptySelectionStart).add(14, 'days').toDate();

                me._d3brush.redraw();
            }
        }

        return brush;
    }
    debounce = (fn, time) => {
        let timeout;

        return function () {
            const functionCall = () => fn.apply(this, arguments);

            clearTimeout(timeout);
            // eslint-disable-next-line @lwc/lwc/no-async-operation
            timeout = setTimeout(functionCall, time);
        };
    };

    sortByValue(param) {
        return function (a, b) {
            return a[param] < b[param] ? -1 : a[param] > b[param] ? 1 : 0;
        };
    }

    userDateFormat() {
        const userShortDate = shortDateFormat;

        let d3DateFormat = userShortDate.replace(/dd/gi, 'd');
        d3DateFormat = d3DateFormat.replace(/d/gi, 'd');
        d3DateFormat = d3DateFormat.replace(/M/gi, 'm');
        d3DateFormat = d3DateFormat.replace(/MM/gi, 'm');
        d3DateFormat = d3DateFormat.replace(/YYYY/gi, 'y');
        d3DateFormat = d3DateFormat.replace(/YY/gi, 'y');

        d3DateFormat = d3DateFormat.replace(/d/gi, '%d');
        d3DateFormat = d3DateFormat.replace(/m/gi, '%m');
        d3DateFormat = d3DateFormat.replace(/y/gi, '%Y');

        return d3DateFormat;
    }

    get showSummary() {
        if (this.isError || this.noData || this.noFilterData || !this.isLoaded) {
            return false;
        }
        return true;
    }

    get showFallbackTooltip() {
        if (this.mouseOverFallbackField != null && this.mouseOverFallbackField !== '') {
            return true;
        }
        return false;
    }

    toggleFilter() {
        const filterPopover = this.template.querySelector('div.timeline-filter');
        const filterClasses = String(filterPopover.classList);
        const refreshButton = this.template.querySelector('lightning-button-icon.timeline-refresh');

        if (filterClasses.includes('slds-is-open')) {
            refreshButton.disabled = false;
            filterPopover.classList.remove('slds-is-open');
            this.isFilter = false;
        } else {
            refreshButton.disabled = true;
            filterPopover.classList.add('slds-is-open');
            this.isFilter = true;
        }
    }

    get filterOptions() {
        this.handleAllTypes();
        return this.objectFilter;
    }

    get selectedFilterValues() {
        return this.filterValues.join(', ');
    }

    handleFilterChange(e) {
        this.filterValues = e.detail.value;
        this.handleAllTypes();
        this.isFilterUpdated = false;
        if (JSON.stringify(this.filterValues) !== JSON.stringify(this.startingFilterValues)) {
            this.isFilterUpdated = true;
        }
    }

    handleAllTypesChange(e) {
        if (e.target.checked === true) {
            this.filterValues = this.allFilterValues;
        } else if (e.target.checked === false) {
            this.filterValues = [];
        }

        this.isFilterUpdated = false;
        if (JSON.stringify(this.filterValues) !== JSON.stringify(this.startingFilterValues)) {
            this.isFilterUpdated = true;
        }
    }

    handleAllTypes() {
        const allTypesCheckbox = this.template.querySelector('input.all-types-checkbox');
        const countAllValues = this.allFilterValues.length;
        const countSelectedValues = this.filterValues.length;

        if (countSelectedValues !== countAllValues && countSelectedValues > 0) {
            allTypesCheckbox.checked = false;
            allTypesCheckbox.indeterminate = true;
        }

        if (countSelectedValues === countAllValues) {
            allTypesCheckbox.indeterminate = false;
            allTypesCheckbox.checked = true;
        }

        if (countSelectedValues < 1) {
            allTypesCheckbox.indeterminate = false;
            allTypesCheckbox.checked = false;
        }
    }

    applyFilter() {
        this.refreshTimeline();
        this.isFilterUpdated = false;
        this.startingFilterValues = this.filterValues;
        this.toggleFilter();
    }

    cancelFilter() {
        this.filterValues = this.startingFilterValues;
        this.isFilterUpdated = false;
        this.toggleFilter();
    }

    refreshTimeline() {
        this.isError = false;
        this.noFilterData = false;

        if (this.totalTimelineRecords > 0) {
            this.illustrationVisibility = 'illustration-hidden';
            this._d3timelineMapSVG.selectAll('[class~=timeline-map-record]').remove();
            this._d3timelineMap.redraw();
            this._d3brush.redraw();
        }
    }
}

Timeline.css


.stencil-label {
    background-color: #e4e0e0;
    height: 0.4rem;
    border-radius: 2rem;
    width: 10rem;
}

.stencil-value {
    background-color: #c5c2c2;
    height: 0.4rem;
    border-radius: 2rem;
    margin-top: 0.5rem;
    width: 10rem;
}

.stencil-timeline {
    text-align: center;
    height: 70%;
    position: relative;
    top: 5%;
}

.stencil-timeline-spinner {
    text-align: center;
}

.illustration {
    padding: 10px 5px 5px 5px;
    background-color: #fff;
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0;
    left: 0;
}

.illustration-hidden {
    display: none;
}

.timeline-container {
    position: relative;
    padding: 10px 5px 5px 5px;
    background-color: #fff;
    border-radius: 5px 5px 5px 5px;
    border: rgb(221, 219, 218);
    border-width: 1px;
    border-style: solid;
}

.timeline-container-hidden {
    padding: 10px 5px 5px 5px;
    background-color: #fff;
    border-radius: 5px 5px 5px 5px;
    display: none;
}

.timeline-filter {
    position: absolute;
    top: 0;
    right: 0;
}

.timeline-canvas-icon-wrap {
    fill: #fff;
    stroke: #d5dbe4;
    stroke-width: 1;
}

.timeline-map-wrapper {
    background-color: #f8fcff;
    padding: 10px;
    overflow: hidden;
}

.timeline-canvas-record-line {
    stroke: #d5dbe4;
    stroke-width: 1;
}

.timeline-canvas {
    overflow-y: auto;
    border-bottom: 1px solid #d5dbe4;
    overflow-x: hidden;
}

.timeline-canvas-axis {
    background-color: #fff;
    border-bottom: 1px solid #d5dbe4;
    height: 25px;
}

.timeline-canvas-axis line {
    stroke: #72a8e0;
}

.timeline-map {
    background-color: #fff;
    border: 1px solid #4b97e6;
    border-radius: 3px;
    height: 40px;
}

.timeline-map svg {
    height: 100%;
    width: 100%;
}

.timeline-map-axis {
    height: 20px;
}

.timeline-map-axis svg {
    overflow: visible;
}

/* axis labels */
.axis-ticks {
    font-family: Salesforce Sans, sans-serif;
    font-size: 10px;
}

.axis-ticks text {
    display: none;
}

.axis-label-LARGE text {
    color: rgb(62, 62, 60);
    font-size: 0.7rem;
}

.axis-label-MEDIUM text {
    color: rgb(62, 62, 60);
    font-size: 0.7rem;
}

.axis-label-SMALL text {
    color: rgb(62, 62, 60);
    font-size: 0.7rem;
}

/* axis tick marks */
.axis-ticks line {
    stroke-width: 1;
    stroke: rgb(221, 224, 228);
    stroke-dasharray: 9, 9;
    shape-rendering: crispEdges;
}

.axis-ticks-LARGE line {
    stroke-width: 1;
    stroke: rgb(221, 224, 228);
    stroke-dasharray: 9, 9;
    shape-rendering: crispEdges;
}

.axis-ticks-MEDIUM line {
    stroke-width: 1;
    stroke: rgb(221, 224, 228);
    stroke-dasharray: 9, 9;
    shape-rendering: crispEdges;
}

.axis-ticks-SMALL line {
    stroke-width: 1;
    stroke: rgb(221, 224, 228);
    stroke-dasharray: 9, 9;
    shape-rendering: crispEdges;
}

.timeline-map-axis line {
    stroke: #72a8e0;
}

.timeline-map-axis text {
    fill: #72a8e0;
    height: 10px;
    color: #92bce4;
    font-size: 0.7rem;
}

/* axis line */
.axis-ticks path {
    stroke: none;
}

.axis-label-LARGE path {
    stroke: none;
}

.axis-label-MEDIUM path {
    stroke: none;
}

.axis-label-SMALL path {
    stroke: none;
}

/* brush */
.brush .selection {
    stroke: #61adfc;
    fill: #61adfc;
    fill-opacity: 0.3;
}

.timeline-canvas-record {
    stroke-width: 6;
    cursor: default;
    pointer-events: true;
}

.timeline-canvas-record-label {
    fill: rgb(63, 66, 71);
    shape-rendering: crispEdges;
    color: #080707;
    font-size: 0.8125rem;
    cursor: pointer;
}

.timeline-wrapper {
    margin: 10px 10px 10px 10px;
    border: 1px solid #d5dbe4;
    border-radius: 3px;
    position: relative;
}

.slds-text-heading_medium,
.slds-text-heading--medium {
    font-weight: 500;
    font-size: 1rem;
    padding-left: 10px;
    line-height: 1.25;
}

.slds-popover__header {
    padding: 1rem 1rem 0.75rem;
}

.slds-popover {
    border-radius: 0.25rem;
    width: 25rem;
    min-height: 2rem;
    z-index: 98;
    text-align: left;
    background-color: #fff;
    display: inline-block;
    box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.16);
    border: 1px solid #dddbda;
}

.slds-is-relative.record-form-spinner-holder {
    min-height: 120px;
}

.tooltip-panel {
    background: rgb(243, 242, 242);
    color: #fff;
    position: fixed !important;
    border-radius: 4px;
    padding: 10px;
    font-size: 12px;
    z-index: 98;
    min-width: 400px;
    min-height: 150px;
    visibility: hidden;
}

.tooltip-header {
    color: var(--lwc-brandDark);
}

.tooltip-content {
    overflow: hidden;
    transition: 0.25s all ease;
    transition-delay: 0s;
}

@media only screen and (max-width: 900px) {
    .axis-label-LARGE .tick:nth-child(2n) text {
        visibility: hidden;
    }

    .timeline-summary-verbose {
        display: none;
    }
}

@media only screen and (max-width: 1200px) {
    .axis-label-MEDIUM .tick:nth-child(2n) text {
        visibility: hidden;
    }
}

.axis-label-SMALL .tick:nth-child(2n) text {
    visibility: hidden;
}

.timeline-summary-verbose-SMALL {
    display: none;
}

Timeline.html


<template>
    <!--main timeline component-->
    <div class="timeline-container">
        <!--header-->
        <div class="slds-grid slds-grid--align-spread">
            <div class="slds-col slds-shrink slds-align-middle">
                <template if:true={isLoaded}>
                    <h1 class="slds-text-heading--medium">{timelineTitle}</h1>
                </template>
                <template if:false={isLoaded}>
                    <div class="stencil-value" style="margin-left: 10px;"></div>
                </template>
            </div>
            <div class="slds-col slds-shrink-none slds-align-middle">
                <div class="slds-form--inline">
                    <div class="slds-form-element">
                        <template if:true={showSummary}>
                            <div class="timeline-summary">
                                <span class="timeline-summary-verbose">{label.SHOWING} </span>
                                <span>{localisedZoomStartDate} - {localisedZoomEndDate}</span>
                                <span class="timeline-summary-verbose">
                                    • {daysToShow} {label.DAYS} • {totalZoomedRecords} {label.ITEMS}</span
                                >
                            </div>
                        </template>
                        <template if:false={isLoaded}>
                            <div class="stencil-label"></div>
                        </template>
                    </div>
                    <div class="slds-form-element">
                        <lightning-button-group>
                            <lightning-button-icon
                                class="timeline-refresh"
                                icon-name="utility:refresh"
                                alternative-text="Refresh"
                                onclick={processTimeline}
                            ></lightning-button-icon>
                            <lightning-button-icon-stateful
                                icon-name="utility:filterList"
                                selected={isFilter}
                                onclick={toggleFilter}
                                alternative-text="Filter"
                            ></lightning-button-icon-stateful>
                        </lightning-button-group>
                    </div>
                </div>
            </div>
        </div>
        <!--end header-->

        <!--timeline body-->
        <div id="timeline-wrapper" class="timeline-wrapper">
            <div class="slds-col slds-size--1-of-1">
                <div class="timeline-canvas" id="timeline-canvas" lwc:dom="manual"></div>
                <div class="timeline-canvas-axis" id="timeline-canvas-axis" lwc:dom="manual"></div>
                <div class="timeline-map-wrapper">
                    <div class="timeline-map" id="timeline-map" lwc:dom="manual"></div>
                    <div class="timeline-map-axis" id="timeline-map-axis" lwc:dom="manual"></div>
                </div>
            </div>

            <!--illustration component when there is an error-->
            <div class={illustrationVisibility}>
                <div class="stencil-timeline">
                    <c-timeline-illustration
                        header={illustrationHeader}
                        sub-header={illustrationSubHeader}
                        type={illustrationType}
                    ></c-timeline-illustration>
                </div>
            </div>
            <!--end illustration component-->

            <div
                class="timeline-filter slds-float_right slds-panel slds-size_small slds-panel_docked slds-panel_docked-right"
                aria-hidden="false"
            >
                <div class="slds-panel__header" style="height: 50px;">
                    <template if:false={isFilterUpdated}>
                        <h2 class="slds-panel__header-title slds-text-heading_small slds-truncate" title="Panel Header">
                            {label.FILTERS}
                        </h2>
                        <button
                            class="slds-button slds-button_icon slds-button_icon slds-button_icon-small slds-float_right slds-panel__close"
                            title="Close dialog"
                            onclick={toggleFilter}
                        >
                            <lightning-icon
                                icon-name="utility:close"
                                alternative-text="Close"
                                size="xx-small"
                            ></lightning-icon>
                        </button>
                    </template>

                    <template if:true={isFilterUpdated}>
                        <div class="slds-grid" style="width: 100%;">
                            <div>
                                <lightning-button
                                    label={label.BUTTON_CANCEL}
                                    title="Cancel"
                                    onclick={cancelFilter}
                                ></lightning-button>
                            </div>
                            <div class="slds-grid slds-col slds-grid_align-end">
                                <lightning-button
                                    variant="brand"
                                    label={label.BUTTON_APPLY}
                                    title="Apply"
                                    onclick={applyFilter}
                                ></lightning-button>
                            </div>
                        </div>
                    </template>
                </div>

                <div class="slds-panel__body">
                    <legend class="slds-form-element__label slds-text-title--caps">{label.TYPE_LEGEND}</legend>
                    <div class="slds-form-element">
                        <div class="slds-form-element__control">
                            <div class="slds-checkbox">
                                <input
                                    type="checkbox"
                                    class="all-types-checkbox"
                                    name="options"
                                    id="all-filter"
                                    value={handleAllTypesLoad}
                                    onchange={handleAllTypesChange}
                                />
                                <label class="slds-checkbox__label" for="all-filter">
                                    <span class="slds-checkbox_faux"></span>
                                    <span class="slds-form-element__label">{label.ALL_TYPES}</span>
                                </label>
                            </div>
                        </div>
                    </div>

                    <template if:true={isFilter}>
                        <lightning-checkbox-group
                            name="Types Shown"
                            label=""
                            options={filterOptions}
                            value={filterValues}
                            onchange={handleFilterChange}
                        >
                        </lightning-checkbox-group>
                    </template>

                    <div style="padding-top: 10px;"></div>
                    <legend class="slds-form-element__label slds-text-title--caps">{label.DATE_RANGE_LEGEND}</legend>

                    <div class="slds-form-element__label" style="padding-top: 5px;">
                        {timelineStart} - {timelineEnd}
                    </div>
                </div>
            </div>
        </div>
        <!--end timeline body-->

        <!--spinner when component is loading-->
        <div class="stencil-timeline-spinner">
            <template if:false={isLoaded}>
                <lightning-spinner alternative-text="Loading" size="small"></lightning-spinner>
            </template>
        </div>
        <!--end spinner-->
    </div>

    <!--tooltips FALLBACK WHEN AN OBJECT IS NOT SUPPORTED-->
    <div aria-label="Tooltip popover" class="tooltip-panel slds-popover slds-popover_panel slds-nubbin_left-top">
        <div class="tooltip-content">
            <template if:true={isMouseOver}>
                <div class="tooltip-header slds-text-heading_medium">{mouseOverTime}<br/><b>{mouseOverObjectNameLabel}</b></div>
                <template if:false={showFallbackTooltip}>
                    <lightning-record-form
                        record-id={mouseOverRecordId}
                        object-api-name={mouseOverObjectAPIName}
                        layout-type="Compact"
                        columns="2"
                        mode="readonly"
                        class="tooltip-content"
                    >
                    </lightning-record-form>
                </template>

                <template if:true={showFallbackTooltip}>
                    <div class="slds-form" role="list">
                        <div class="slds-form__row">
                            <div class="slds-form__item" role="listitem">
                                <div class="slds-form-element slds-form-element_readonly slds-form-element_stacked">
                                    <span class="slds-form-element__label">{mouseOverDetailLabel} </span>
                                    <div class="slds-form-element__control">
                                        <div class="slds-form-element__static">{mouseOverDetailValue}</div>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div class="slds-form__row">
                            <div class="slds-form__item" role="listitem">
                                <div class="slds-form-element slds-form-element_readonly slds-form-element_stacked">
                                    <span class="slds-form-element__label">{mouseOverFallbackField} </span>
                                    <div class="slds-form-element__control">
                                        <div class="slds-form-element__static">{mouseOverFallbackValue}</div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </template>
            </template>
        </div>
    </div>
    <!--end tooltips-->
</template>

Timeline.js-meta.xml


<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Timeline</masterLabel>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightningCommunity__Page</target>
        <target>lightningCommunity__Default</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <objects>
                <object>Contact</object>
                <object>Lead</object>
                <object>Account</object>
                <object>Case</object>
                <object>Opportunity</object>
            </objects>

            <property name="timelineTitle" label="Title" default="Timeline" required="true" type="String" />
            <property name="preferredHeight" label="Height" default="3 - Default" required="true" type="String" datasource="1 - Smallest, 2 - Small, 3 - Default, 4 - Big, 5 - Biggest" />
            <property name="earliestRange" label="Historical Time Range (Years)" default="3" required="true" type="String" datasource="0.25, 0.5, 1, 2, 3, 5" description="Specify the start range for the timeline. As n minus current date."/>
            <property name="latestRange" label="Future Time Range (Years)" default="0.5" required="true" type="String" datasource="0.25, 0.5, 1" description="Specify the end range for the timeline. As n plus current date."/>
            <property name="zoomTo" label="Zoom Based On" default="Current Date" required="true" type="String" datasource="Current Date, Last Activity" description="Zoom to the last record found (even if it's in the past or future) OR zoom to the current date (even if there are no records)."/>
            <property name="daysToShow" label="Zoom Range (Days)" default="60" required="true" type="Integer" min="7" max="120" description="Specify how many days to zoom to by default. e.g. 60 days would show 30 days prior to and 30 days after the specified 'zoom based on' setting."/>

            <supportedFormFactors>
                <supportedFormFactor type="Large" />
            </supportedFormFactors>

        </targetConfig>
        <targetConfig targets="lightningCommunity__Default">
            <property name="timelineTitle" label="Title" default="Timeline" required="true" type="String" />
            <property name="preferredHeight" label="Height" default="3 - Default" required="true" type="String" datasource="1 - Smallest, 2 - Small, 3 - Default, 4 - Big, 5 - Biggest" />
            <property name="earliestRange" label="Historical Time Range (Years)" default="3" required="true" type="String" datasource="0.25, 0.5, 1, 2, 3, 5" description="Specify the start range for the timeline. As n minus current date."/>
            <property name="latestRange" label="Future Time Range (Years)" default="0.5" required="true" type="String" datasource="0.25, 0.5, 1" description="Specify the end range for the timeline. As n plus current date."/>
            <property name="zoomTo" label="Zoom Based On" default="Current Date" required="true" type="String" datasource="Current Date, Last Activity" description="Zoom to the last record found (even if it's in the past or future) OR zoom to the current date (even if there are no records)."/>
            <property name="daysToShow" label="Zoom Range (Days)" default="60" required="true" type="Integer" min="7" max="120" description="Specify how many days to zoom to by default. e.g. 60 days would show 30 days prior to and 30 days after the specified 'zoom based on' setting."/>
            <property name="recordId" label="Record Id" default="{!recordId}" required="true" type="String" datasource="{!recordId}" description="Automatically binds the page's record id to the component variable" />
            
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Step 4 : Cree un class Apex : cmd + shift + p : Create Apex Class Avec le code suivant :


/**
*
* https://github.com/deejay-hub/timeline-lwc with modifications
*
* @description Apex supporting methods for the timeline lightning web component
 */
public without sharing class TimelineService {
   
    /**
     * @description Return all child record types for the parent record in context
     * @param parentObjectId Id of the parent object used as the basis for the query
     * @return A map of the API name and label or each child record type to plot on the timeline
     */
    @AuraEnabled(Cacheable=true)
    public static Map<String, String> getTimelineTypes( String parentObjectId ) {
        
        try {
            String parentObjectType = String.valueOf(Id.valueOf(parentObjectId).getSobjectType());
            

            String queryTimelineConfiguration = 'SELECT Active__c, '
                                            + 'Object_Name__c, '
                                            + 'ObjectNamePluralLabel__c, '
                                            + 'Tooltip_Object_Name__c, '
                                            + 'Sequence__c '
                                        + 'FROM Timeline_Configuration__mdt '
                                        + 'WHERE Active__c = true AND '
                                        + 'Parent_Object__c =:parentObjectType '
                                        + 'ORDER BY Sequence__c ASC '; //NOPMD

            List<Timeline_Configuration__mdt> listOfTimelineConfigurations = Database.query( queryTimelineConfiguration ); //NOPMD

            Map<String, String> mapOfTimelineTypes = new Map<String, String>();

            for ( Timeline_Configuration__mdt timelineType : listOfTimelineConfigurations ) {
                
                String objectLabel = ((SObject)(Type.forName('Schema.'+ String.valueOf(timelineType.Object_Name__c)).newInstance())).getSObjectType().getDescribe().getLabelPlural();

                if ( timelineType.Object_Name__c == 'ContentDocumentLink') {
                    objectLabel = System.Label.Timeline_Label_Files;
                }

                if ( timelineType.Tooltip_Object_Name__c != null && timelineType.Tooltip_Object_Name__c != '') {
                    objectLabel = ((SObject)(Type.forName('Schema.'+ String.valueOf(timelineType.Tooltip_Object_Name__c)).newInstance())).getSObjectType().getDescribe().getLabelPlural();
                }

                if (timelineType.ObjectNamePluralLabel__c != null && timelineType.ObjectNamePluralLabel__c != ''){
                    objectLabel = timelineType.ObjectNamePluralLabel__c;
                }

                mapOfTimelineTypes.put(timelineType.Object_Name__c, objectLabel);
            }

            return mapOfTimelineTypes;
        }
        catch(Exception e) {
            throw new AuraHandledException(e.getMessage() + ' : ' + e.getStackTraceString());
        }
    }

    @AuraEnabled
    /**
     * @description Return all child records for the parent record in context based on those active in Timeline_Configuration__mdt
     * @param parentObjectId The id of the parent record
     * @param earliestRange The number of historical years to include in the query
     * @param latestRange The number of years in the future to include in the query
     * @return A map of API Object Names and their corresponding translated labels
     */
    public static List<Map<String, String>> getTimelineRecords( String parentObjectId, String earliestRange, String latestRange ) {
        try {
            String parentObjectType = String.valueOf(Id.valueOf(parentObjectId).getSobjectType());
            String parentConfigType = parentObjectType;

          
            
            earliestRange = String.valueOf((Decimal.valueOf(earliestRange) * 12).intValue());
            latestRange = String.valueOf((Decimal.valueOf(latestRange) * 12).intValue());

            String queryTimelineConfiguration = 'SELECT Detail_Field__c, '
                                            + 'Detail_Field_to_Label__c, '
                                            + 'Relationship_Name__c, '
                                            + 'Active__c, '
                                            + 'Icon__c, '
                                            + 'Icon_Background_Colour__c, '
                                            + 'Position_Date_Field__c, '
                                            + 'Object_Name__c, '
                                            + 'Filter__c, '
                                            + 'Type_Field__c, '
                                            + 'Drilldown_Id_Field__c, '
                                            + 'Tooltip_Id_Field__c, '
                                            + 'Tooltip_Object_Name__c, '
                                            + 'Fallback_Tooltip_Field__c '
                                        + 'FROM Timeline_Configuration__mdt '
                                        + 'WHERE Active__c = true AND '
                                        + 'Parent_Object__c =:parentConfigType'; //NOPMD

            List<Timeline_Configuration__mdt> listOfTimelineConfigurations = Database.query( queryTimelineConfiguration ); //NOPMD

            if ( listOfTimelineConfigurations.size() < 1 ) {
                String errorMsg = 'No active records for parent entity  \'' + parentObjectType + '\' have been found in \'Timeline_Configuration__mdt\'. Ask an administrator for help.';
                throw new TimelineSetupException( '{"type": "Setup-Error", "message": "' + errorMsg + '"}' );
            }

            Map<String, TimelineRecord> mapOfTimelineConfigurationRecords = new Map<String, TimelineRecord>();
            Map<String, String> mapOfFields = new Map<String, String>();

            for ( Timeline_Configuration__mdt timelineConfigurationRecord : listOfTimelineConfigurations ) {

                TimelineRecord timelineRecord = new TimelineRecord();
                timelineRecord.relationshipName = timelineConfigurationRecord.Relationship_Name__c;
                timelineRecord.icon = timelineConfigurationRecord.Icon__c;
                timelineRecord.iconBackground = timelineConfigurationRecord.Icon_Background_Colour__c;
                timelineRecord.detailField = timelineConfigurationRecord.Detail_Field__c;
                timelineRecord.detailFieldToLabel = timelineConfigurationRecord.Detail_Field_to_Label__c;
                timelineRecord.objectName = timelineConfigurationRecord.Object_Name__c;
                timelineRecord.objectNameLabel = ((SObject)(Type.forName('Schema.'+ String.valueOf(timelineConfigurationRecord.Object_Name__c)).newInstance())).getSObjectType().getDescribe().getLabel();
                timelineRecord.filter = timelineConfigurationRecord.Filter__c;
                timelineRecord.type = timelineConfigurationRecord.Type_Field__c;
                timelineRecord.positionDateField = timelineConfigurationRecord.Position_Date_Field__c;
                timelineRecord.fallbackTooltipField = timelineConfigurationRecord.Fallback_Tooltip_Field__c;
                timelineRecord.tooltipIdField = timelineConfigurationRecord.Tooltip_Id_Field__c;
                timelineRecord.tooltipObject = timelineConfigurationRecord.Tooltip_Object_Name__c;
                timelineRecord.drilldownIdField = timelineConfigurationRecord.Drilldown_Id_Field__c;
 
                mapOfTimelineConfigurationRecords.put(timelineRecord.objectName + timelineRecord.relationshipName, timelineRecord);
                mapOfFields.put(timelineRecord.detailField, timelineRecord.objectName);
                mapOfFields.put(timelineRecord.positionDateField, timelineRecord.objectName);
                mapOfFields.put(timelineRecord.fallbackTooltipField, timelineRecord.objectName);
                mapOfFields.put(timelineRecord.tooltipIdField, timelineRecord.objectName);
                mapOfFields.put(timelineRecord.drilldownIdField, timelineRecord.objectName);
                mapOfFields.put(timelineRecord.type, timelineRecord.objectName);
            }

            Map<String, String> childObjects = getChildObjects(parentObjectType);
            Map<String, FieldMetadata> fieldAttributes = getFieldMetadata(mapOfFields, parentObjectType);

            String innerQuery = '';

            for (String eachObject : mapOfTimelineConfigurationRecords.keySet()) {

                TimelineRecord tcr = mapOfTimelineConfigurationRecords.get(eachObject);

                if (childObjects.containsKey(eachObject)) {

                    String objName = String.valueOf(tcr.objectName);
                    String tooltipField = String.valueOf(tcr.fallbackTooltipField);
                    String tooltipIdField = String.valueOf(tcr.tooltipIdField);
                    String drilldownIdField = String.valueOf(tcr.tooltipIdField);
                    String typeField = String.valueOf(tcr.type);
        
                    String selectStatement = '(SELECT Id, ' 
                                    + (tcr.detailFieldToLabel ? ('toLabel(' + tcr.detailField + '), ') : tcr.detailField + ', ') 
                                    + tcr.positionDateField + '';

                    if (typeField != null && typeField != '') {
                        selectStatement = selectStatement + ', '
                                    + tcr.type + '';
                    }

                    if ( objName == 'ContentDocumentLink' ) {
                        selectStatement = selectStatement + ', '
                                    + 'ContentDocumentId' + '';
                    }

                    if ( tooltipField != null && tooltipField != '' && tcr.detailField <> tcr.fallbackTooltipField) {
                        selectStatement = selectStatement + ', '
                                    + tcr.fallbackTooltipField + '';
                    }

                    if ( drilldownIdField != null && drilldownIdField != '' ) {
                        selectStatement = selectStatement + ', '
                                    + tcr.drilldownIdField + '';
                    }

                    if ( tooltipIdField != null && tooltipIdField != '' && tcr.drilldownIdField <> tcr.tooltipIdField) {
                        selectStatement = selectStatement + ', '
                                    + tcr.tooltipIdField + '';
                    }

                    String relationship = tcr.relationshipName;
                    if ( tcr.relationshipName.contains('Person') && !tcr.relationshipName.contains('__pr') ) {
                        relationship = tcr.relationshipName.substringAfter('Person');                       
                    }

                    innerQuery = innerQuery + 
                                selectStatement +
                                + ' FROM ' + relationship 
                                + ' WHERE ' + tcr.positionDateField + '>= LAST_N_MONTHS:' + earliestRange
                                + ' AND ' + tcr.positionDateField + ' <= NEXT_N_MONTHS:' + latestRange;

                    if( tcr.filter != null && tcr.filter != ''){
                        innerQuery = innerQuery + ' AND ' + tcr.filter;
                    }

                    innerQuery = innerQuery + '),';
                }
            }

            innerQuery = innerQuery.removeEnd(',');

            String queryRecords = 'SELECT Id, ' 
                                    + innerQuery 
                                + ' FROM ' + parentObjectType 
                                + ' WHERE Id =:parentObjectId'; //NOPMD

            List<SObject> listOfTimelineRecords = Database.query( queryRecords ); //NOPMD

            List<Map<String, String>> listOfTimelineData = new List<Map<String, String>>();

            for (SObject each : listOfTimelineRecords) {
                for (String eachObj : mapOfTimelineConfigurationRecords.keySet()) {
                    if (each.getSObjects(childObjects.get(eachObj)) != null) {
                        for (SObject eachCh : (List<SObject>)each.getSObjects(childObjects.get(eachObj))) {

                            Map<String, String> mapData = new Map<String, String>();

                            TimelineRecord tr = mapOfTimelineConfigurationRecords.get(eachObj );

                            if ( tr != null ) {
                                String myId = eachCh.Id;

                                Map<String, String> detailValues = getFieldValues(tr.detailField, eachCh, fieldAttributes);
                                Map<String, String> positionValues = getFieldValues(tr.positionDateField, eachCh, fieldAttributes);
                                Map<String, String> fallbackValues = getFieldValues(tr.fallbackTooltipField, eachCh, fieldAttributes);
                                Map<String, String> tooltipIdValues = getFieldValues(tr.tooltipIdField, eachCh, fieldAttributes);
                                Map<String, String> drilldownIdValues = getFieldValues(tr.drilldownIdField, eachCh, fieldAttributes);
                                Map<String, String> typeValues = getFieldValues(tr.type, eachCh, fieldAttributes);

                                if ( tr.objectName == 'ContentDocumentLink') { //NOPMD
                                    myId = String.valueOf(eachCh.get('ContentDocumentId'));
                                }

                                mapData.put('objectId', myId);
                                mapData.put('parentObject', parentObjectType);

                                if ( detailValues.get('value') == '' || detailValues.get('value') == null ) { //NOPMD
                                    mapData.put('detailField', '[' + detailValues.get('label') +']');
                                }
                                else {
                                    mapData.put('detailField', detailValues.get('value'));
                                }
                                
                                mapData.put('detailFieldLabel', detailValues.get('label'));
                                mapData.put('positionDateField', tr.positionDateField);
                                mapData.put('positionDateValue', positionValues.get('value'));
                                mapData.put('objectName', tr.objectName);
                                mapData.put('objectNameLabel', tr.objectNameLabel);
                                mapData.put('fallbackTooltipField', fallbackValues.get('label'));
                                mapData.put('fallbackTooltipValue', fallbackValues.get('value'));
                                mapData.put('drilldownId', drilldownIdValues.get('value'));
                                mapData.put('tooltipId', tooltipIdValues.get('value'));
                                mapData.put('tooltipObject', tr.tooltipObject);
                                mapData.put('fallbackTooltipValue', fallbackValues.get('value'));
                                mapData.put('type', typeValues.get('value'));
                                mapData.put('icon', tr.icon);
                                mapData.put('iconBackground', tr.iconBackground);

                                listOfTimelineData.add(mapData);
                            }
                        }
                    }
                }
            }
            return listOfTimelineData;
        }
        catch(Exception e) {
            throw new AuraHandledException(e.getMessage());
        }
    }

    private static Map<String, String> getChildObjects(String parentObject) {
        Map<String, String> childRelatedObjects = new Map<String, String>();
       
        List<Schema.ChildRelationship> objectRelationships = ((SObject)(Type.forName('Schema.'+ parentObject).newInstance())).getSObjectType().getDescribe().getChildRelationships();
        for (Schema.ChildRelationship eachRelationship : objectRelationships) {
            if (eachRelationship.getChildSObject().getDescribe().isAccessible()
                    && !eachRelationship.getChildSObject().getDescribe().getLabel().contains('Histories')
                    && eachRelationship.getRelationshipName() != null) {
                childRelatedObjects.put(String.valueOf(eachRelationship.getChildSObject() + String.valueOf(eachRelationship.getRelationshipName())), String.valueOf(eachRelationship.getRelationshipName()));
            }
        }
        return childRelatedObjects;
    }
    
    private static Map<String, FieldMetadata> getFieldMetadata(Map<String, String> fields, String baseObject) {
        
        String fieldLabel;
        Boolean fieldAccess;
        Map<String, FieldMetadata> mapOfFieldMetadata = new Map<String, FieldMetadata>();

        
        for (String field : fields.keySet() ) {
            

            if ( field != null && field != '' ) {
                String fieldObject = fields.get( field );
                Boolean isDotNotationUsed = field.contains('.');
                FieldMetadata fieldMetadata = new FieldMetadata();

                try {

                    if ( isDotNotationUsed == true ) {
                        String splitObject = field.substringBefore('.');
                        String splitField = field.substringAfter('.');
    
                        Schema.DescribeSObjectResult describeParentSobjects = ((SObject)(Type.forName('Schema.'+ String.valueOf(splitObject)).newInstance())).getSObjectType().getDescribe();
                        fieldMetadata.label = String.valueOf( describeParentSobjects.fields.getMap().get(splitField).getDescribe().getLabel() );
                        fieldMetadata.canAccess = Boolean.valueOf(describeParentSobjects.fields.getMap().get(splitField).getDescribe().isAccessible() );
                    }
                    else {
                        Schema.DescribeSObjectResult describeSobjects = ((SObject)(Type.forName('Schema.'+ fieldObject).newInstance())).getSObjectType().getDescribe();

                        fieldMetadata.type = describeSobjects.fields.getMap().get(field).getDescribe().getType();
                        fieldMetadata.label = String.valueOf( describeSobjects.fields.getMap().get(field).getDescribe().getLabel() );
                        fieldMetadata.canAccess = Boolean.valueOf(describeSobjects.fields.getMap().get(field).getDescribe().isAccessible() );
                    }
                }    
                catch(Exception e) {
                    String errorMsg = 'No such column \'' + field + '\' on entity \'' + fieldObject + '\'. If you are attempting to use a custom field, be sure to append the \'__c\' after the custom field name. ' + e.getMessage();
                    throw new TimelineSetupException( '{"type": "Setup-Error", "message": "' + errorMsg + '"}' );
                }

                mapOfFieldMetadata.put(field, fieldMetadata);
            }
        }
        
        return mapOfFieldMetadata;
    }

    private static Map<String, String> getFieldValues(String field, SObject records, Map<String, FieldMetadata> fieldAttributes) {

        Map<String, String> fieldDetails = new Map<String, String>();

        String fieldValue = '';
        String fieldLabel = '';
       
        if ( field == null || field == '' ) {
            fieldDetails.put('value' ,'');
            fieldDetails.put('label', '');
            return fieldDetails;
        }

        FieldMetadata fm = fieldAttributes.get(field );
        Boolean isDotNotationUsed = field.contains('.');

        fieldLabel = fm.label ;

        if (fm.canAccess) { 

            if ( isDotNotationUsed == true ) {
                String splitObject = field.substringBefore('.');
                String splitField = field.substringAfter('.');
                fieldValue = String.valueOf(records.getSObject(splitObject).get(splitField));
            }
            else {
                if (fm.type == Schema.DisplayType.DATETIME && records.get(field) != null){
                    fieldValue = String.valueOf(((Datetime) records.get(field)).format('YYYY-MM-dd HH:mm:ss'));
                } else {
                    fieldValue = String.valueOf(records.get(field));
                }
            }
        }

        if (fieldValue != null && fieldValue.length() > 255) {
            fieldValue = fieldValue.substring(0,251) + '...';
        }

        fieldDetails.put('value', fieldValue);
        fieldDetails.put('label', fieldLabel);

        return fieldDetails;
    }

    private static Boolean isPersonAccount(String recordId)
    {
        if ( Account.SObjectType.getDescribe().hasSubtypes ) {
            String queryPersonAccount = 'SELECT Id, IsPersonAccount FROM Account Where Id =:recordId';
            SObject acc = Database.query( queryPersonAccount );

            if ( acc.get('IsPersonAccount') == true ) {
                return true;
            }
        }
        return false;
    }

    private class TimelineRecord { //NOPMD
        private String relationshipName;
        private String parentObject;
        private String detailField;
        private Boolean detailFieldToLabel;
        private String detailFieldLabel;
        private String icon;
        private String iconBackground;
        private String positionDateField;
        private String positionDateValue;
        private String objectName;
        private String objectNameLabel;
        private String filter;
        private String type;
        private String tooltipIdField;
        private String tooltipObject;
        private String drilldownIdField;
        private String fallbackTooltipField;
        private String fallbackTooltipValue;
        private String fallbackNameField;
        private String fallbackNameValue;
        private Id recordId;
    }

    private class FieldMetadata { //NOPMD
        private Schema.DisplayType type;
        private Boolean canAccess;
        private String label;
    }

    private class TimelineSetupException extends Exception {}
    
}

Step 5 : Cree une autre LWC : timelineIllustration :

timelineIllustration.css


.illustration-img {
    width: 75%;
    height: 75%;
}

.slds-text-heading_medium {
    padding-top: 10px;
}

.slds-text-body_regular {
    padding-left: 2%;
    padding-right: 2%;
}

timelineIlustration.html


<template>
    <img class="illustration-img" src={illustration} alt="Illustration" />

    <div class="slds-text-longform">
        <h3 class="slds-text-heading_medium">{header}</h3>
        <p class="slds-text-body_regular">{subHeader}</p>
    </div>
</template>

timelineIllustration.js


import { LightningElement, api } from 'lwc';
import illustrations from '@salesforce/resourceUrl/timelineImages';

export default class timelineIllustration extends LightningElement {
    @api header;
    @api subHeader;

    illustration;
    illustrationType;

    @api
    get type() {
        return this.illustrationType;
    }

    set type(value) {
        this.illustrationType = value;

        if (value != null && value !== '') {
            this.illustration = illustrations + '/Illustrations/' + value + '.svg';
        }
    }
}

Step 6 : Ajouter les library Js : d3JS , momentJS et les icons dans les static resources et Ajouter les Labels aussi .



6 views0 comments

Recent Posts

See All

Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating
bottom of page