MFChartColumn / beta / js / mf-chart-column.js
file last updated : 2023-08-30
/*! MFChartColumn.js (@ https://methodfish.com/Projects/MFChartColumn)
 * Version: 1.0
 * Licence: MIT
 */

const mfColumnChart = (chartContainer) => {

    const getTickStep = (minV, maxV) => {
        const stepValues = [1, 2, 2.5, 5];
        let range = maxV - minV;

        if (range <= 0) {
            return 1; // Invalid range
        }

        let bestStep = null;
        let bestNumTicks = Number.POSITIVE_INFINITY;

        for (const stepValue of stepValues) {
            for (let multiplier = -10; multiplier <= 10; multiplier++) {
                let step = stepValue * Math.pow(10, multiplier);
                let numTicks = Math.ceil(range / step);

                if (numTicks >=chartSettings.yAxisTickCountMin && numTicks <= chartSettings.yAxisTickCountMax && (bestStep === null || numTicks < bestNumTicks)) {
                    bestNumTicks = numTicks;
                    bestStep = step;
                }
            }
        }


        return bestStep;
    };


    // ----------------------------------------------------------
    const handleMouseMove = (event) => {
        const rect = canvas.getBoundingClientRect();
        const x = parseInt(event.clientX - rect.left);
        const y = parseInt(event.clientY - rect.top);

        let hover = false;


        // Loop through each column and check if the mouse pointer is over it
        for (let d = 0; d < dateLadder.length; d++) {
            const date = dateLadder[d];
            const value = getValueForDate(plotData, date);

            if ( value ) {
                const column       = columnRect[d];
                const columnLeft      = column.x;
                const columnRight  = column.x + column.w;
                const columnTop    = Math.min(column.y, column.y + column.columnHeight);
                const columnBottom = Math.max(column.y, column.y + column.columnHeight)

                if (x >= columnLeft && x <= columnRight && y >= columnTop && y <= columnBottom) {
                    //console.log(`mouse move ${x}/${y} - considering column ${date}=${value} x/y=${columnLeft}/${columnTop} to x/y=${columnRight}/${columnBottom} - hit ${d}`);

                    if (hoverColumnN !== d) {
                        if (hoverColumnN != -1) drawChart(chartContainer);
                        hoverColumnN = d;
                        hoverValueShow(date, value, columnLeft - marginLeft / 2 + column.w / 2, column.y, plotWidth);
                    }

                    hover = true;
                    break;
                }
            }
        }

        if (!hover) {
            hoverValueHide();
        }
    };


    // ----------------------------------------------------------
    const hoverValueShow = (date, v, x, y, w) => {
        // Clear the previous hover text and cancel the previous timeout
        //clearTimeout(hoverTimeoutId);

        ctx.font = chartSettings.columnValueHoverFont;

        // Get the hover text lines

        let formattedValue;
        if ( v ) formattedValue= v.toLocaleString();
        else formattedValue='null';
        let hoverText = `${date}\n${axisPrefix}${formattedValue}`;
        if ( axisUnits ) hoverText += ' '+axisUnits;
        const parts = hoverText.split("\n");

        // Calculate the width and height of the hover text box
        let maxWidth = 0;
        for (let p = 0; p < parts.length; p++) {
            const textWidth = ctx.measureText(parts[p]).width;
            maxWidth = Math.max(maxWidth, textWidth);
        }

        const lineHeight = 22;
        const padding = 9;
        const rectWidth = parseInt(maxWidth + 2 * padding + 10);
        const rectHeight = parseInt(parts.length * lineHeight + 2 * padding);

        y = Math.max(y - rectHeight, 12);

        if ( x+rectWidth > w ) x = w-rectWidth - 10;

        let y2= y - 10 + rectHeight;
        if ( y2 > chartHeight ) y = y - (y2 - chartHeight) - 4;


        // Draw the shadowed border
        ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
        ctx.fillRect(x, y - 10, rectWidth, rectHeight);


        // Draw the white background
        ctx.fillStyle = 'white';
        ctx.fillRect(x + 2, y - 8, rectWidth - 4, rectHeight - 4);

        ctx.fillStyle = chartSettings.columnValueHoverFontColorCd;
        // Draw the hover text lines
        for (let p = 0; p < parts.length; p++) {
            let x2=parseInt(x+padding);
            let y2= parseInt(y + 5 + lineHeight * p + padding);
            ctx.fillText(parts[p], x2, y2); // Adjust y-coordinate for hover text
        }
    };


    // ----------------------------------------------------------
    const hoverValueHide = () => {
        // Clear the hover text by redrawing the chart
        if (hoverColumnN !== -1) {
            hoverColumnN = -1;
            drawChart(chartContainer);
        }
    };


    // ----------------------------------------------------------
    const getValueForDate = (data, date) => {
        const dataEntry = data.find(item => item.date === date);
        return dataEntry ? dataEntry.value : null;
    };


    // ----------------------------------------------------------
    const getDateLabel = (date, i, len, width, showAnyway) => {
        const d = new Date(date).getUTCDate();
        const m = new Date(date).getUTCMonth() + 1;
        const yr = new Date(date).getUTCFullYear();
        dateLabelFormat = '-';
        var r=width/len*100;

        if ( r>10000 ) dateLabelFormat="d-MMM";
        if ( r>800 && d === 1 || i == 0)  dateLabelFormat="d-MMM";
        else if ( r>300 && d === 1 || i == 0)  dateLabelFormat="MMM";
        else if ( r>100 && d === 1 || i == 0 )  dateLabelFormat="M";

        if ( 0 ) {
            if (len < 60 && width > 450) dateLabelFormat = "d";
            if (width > 1500 || showAnyway) {
                if (i % 7 == 0) dateLabelFormat = 'd-MMM';
            }
            else if (width > 300) {
                if (d === 1 || i == 0) dateLabelFormat = 'MMM';
            }
            else if (width > 100) {
                if (d === 1 || i == 0) dateLabelFormat = 'M';
            }
            else {
                if ((m === 1 && d === 1) || i == 0) dateLabelFormat = 'MM';
            }
        }
        if (i === 0 || yr != lastYearLabel) {
            if ( dateLabelFormat=='-' ) dateLabelFormat='d-MMM';
            dateLabelFormat += '\nyyyy';
            lastYearLabel=yr;
        }
        return dateLabelFormat;
    };

    function getMaxDecimalPlaces(tickStep, plotValueMin, plotValueMax) {
        const absoluteDifference = Math.abs(plotValueMax - plotValueMin);
        const iterations = absoluteDifference / tickStep;

        // Convert the tickStep to a string to analyze its decimal places
        const tickStepStr = tickStep.toString();
        const decimalIndexTickStep = tickStepStr.indexOf('.');

        // Calculate the decimal places in the tickStep
        const tickStepDecimalPlaces = decimalIndexTickStep !== -1 ? tickStepStr.length - decimalIndexTickStep - 1 : 0;

        // Calculate the decimal places in the calculated iterations
        const iterationsDecimalPlaces = Math.max(0, tickStepDecimalPlaces - Math.floor(Math.log10(iterations)));

        return Math.max(tickStepDecimalPlaces, iterationsDecimalPlaces);
    }


    function getAxisLabel(value, rangeV, dp) {
        if ( !isNaN(value) ) {
            let label;
            let scale       = chartSettings.yAxisScale;
            let scaleTx     = chartSettings.yAxisScaleTx;
            let scalePrefix = chartSettings.yAxisScalePrefix || '';
            if (scale == 'AUTO') {
                const m1   = 1000 * 1000;
                const t100 = 100 * 1000;
                const t10  = 10 * 1000;
                scale = 1;
                if (rangeV > m1) {
                    scale   = m1;
                    scaleTx = 'm';
                } else if (rangeV > 1000) {
                    scale   = 1000;
                    scaleTx = 'k';
                }
            }
            if ( dp ) label = (value / scale).toFixed(dp);
            else label = value/scale;
            if (scaleTx) label += scaleTx;
            if ( label =='NaN' ) console.log('err');
            return scalePrefix + ' ' + label;
        }
    }


    // ----------------------------------------------------------
    function drawChart(chartContainer) {
        canvas.width  = chartContainer.clientWidth;
        canvas.height = chartContainer.clientHeight;
        plotWidth0 =chartContainer.clientWidth;
        chartHeight =chartContainer.clientHeight;
        // ----------------------------------------------------------
        plotWidth = plotWidth0 - plotLeft - marginRight;
        plotRight = plotLeft + plotWidth;
        plotTop = marginTop;
        plotBottom = canvas.height - marginBottom; //  - 35;// - marginBottom;
        if (chartSettings.trace== 'T') console.log(`${chartContainer.id} - plotBottom=${plotBottom}`);
        columnWidth = ((plotWidth-plotPaddingLeft-plotPaddingRight) / dateLadder.length);
        naturalColumnWidth = columnWidth;

        if ( chartSettings.columnWidthMin ) columnWidth = Math.max(chartSettings.columnWidthMin, columnWidth);
        if ( chartSettings.columnWidthMax ) columnWidth = Math.min(chartSettings.columnWidthMax, columnWidth);


        // Compute if columnValues are presented and if they affect the plot area
        let adjBottom= 0;
        ctx.font = chartSettings.columnValuesFont;
        let columnValuesFontHeight = ctx.measureText("M").width * fontWHRatio;


        ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear canvas and draw the chart
        if (chartSettings.trace== 'T') console.log(`${chartContainer.id} - canvas set up w=${plotWidth} / h=${canvas.height} (${getAxisLabel(plotValueMax, plotValueRange, yAxisTickDP)} = ${maxLabelWidth})`);


        if ( chartSettings.chartAreaColorBg ) {
            if (chartSettings.trace== 'T') console.log(`${chartContainer.id} - Adding plot area background`);
            ctx.fillStyle = chartSettings.chartAreaColorBg;
            ctx.fillRect(0, 0, canvas.width, canvas.height);
        }


        // Compute number of date label values will be shown
        let maxLabelLines=1;
        if ( 1 ) {
            for (let d = 0; d < dateLadder.length; d++) {
                const date  = dateLadder[d];

                dateLabelFormat     = getDateLabel(date, d, dateLadder.length, plotWidth, showAnyway);
                const formattedDate = new Date(date);
                if (dateLabelFormat !== '-') {
                    let dateLabel = dateLabelFormat.replace('yyyy', formattedDate.getUTCFullYear());

                    dateLabel = dateLabel.replace('MMM', formattedDate.toLocaleString(undefined, {month: 'short'}));
                    dateLabel = dateLabel.replace('MM', formattedDate.toLocaleString(undefined, {month: '2-digit'}));
                    dateLabel = dateLabel.replace('M', formattedDate.toLocaleString(undefined, {month: 'short'}).charAt(0));

                    dateLabel = dateLabel.replace('dd', formattedDate.getUTCDate().toString().padStart(2, '0'));
                    dateLabel = dateLabel.replace('d', formattedDate.getUTCDate().toString());

                    let parts = dateLabel.split('\n');
                    if (maxLabelLines < parts.length) maxLabelLines = parts.length;
                }
            }
            //maxLabelLines++;
            ctx.font        = chartSettings.xAxisValueFont;
            xAxisFontHeight = ctx.measureText("M").width * fontWHRatio;
            xAxisLabelY     = plotBottom - 1 * xAxisFontHeight + (adjBottom * columnValuesFontHeight);
            plotBottom      = plotBottom - maxLabelLines * xAxisFontHeight;
            plotHeight      = plotBottom - plotTop;
            if (chartSettings.trace== 'T') console.log(`${chartContainer.id} - range=${plotValueRange}; plotBottom=${plotBottom} from ${maxLabelLines} * ${xAxisFontHeight}`);
        }


        plotScaleY = plotHeight / plotValueRange;


        if ( chartSettings.plotAreaColorBg ) {
            if (chartSettings.trace== 'T') console.log(`${chartContainer.id} - Adding plot area background`);
            ctx.fillStyle = chartSettings.plotAreaColorBg;
            ctx.fillRect(plotLeft, plotTop, plotWidth, plotHeight);
        }


        if ( chartSettings.plotAreaBox=='T' ) {
            ctx.strokeStyle = chartSettings.plotAreaBoxColorCd;
            ctx.lineWidth=parseInt(chartSettings.plotAreaBoxLineWidth);
            ctx.beginPath();
            ctx.moveTo(plotLeft, plotTop);
            ctx.lineTo(plotRight, plotTop);
            ctx.stroke();

            ctx.beginPath();
            ctx.moveTo(plotLeft, plotTop);
            ctx.lineTo(plotLeft,  plotBottom);
            ctx.stroke();

            ctx.beginPath();
            ctx.moveTo(plotLeft, plotBottom);
            ctx.lineTo(plotRight, plotBottom);
            ctx.stroke();

            ctx.beginPath();
            ctx.moveTo(plotRight, plotTop);
            ctx.lineTo(plotRight, plotBottom);
            ctx.stroke();
        }


        // Draw vertical/Y-axis ticks
        if ( chartSettings.yAxisGridLines === 'T') {
            for (let value = plotValueMin; value <= plotValueMax; value += tickStep) {
                const y = parseInt(plotBottom - (Math.abs(plotValueMin) + value) * plotScaleY);

                ctx.beginPath();
                ctx.strokeStyle = chartSettings.yAxisGridColorCd;
                ctx.lineWidth=1;
                ctx.moveTo(plotLeft, y);
                ctx.lineTo(plotRight, y);
                ctx.stroke();


                ctx.beginPath();
                ctx.strokeStyle = defaultColorCd3;
                ctx.lineWidth=1;
                ctx.moveTo(plotLeft-yAxisTickWidth, y);
                ctx.lineTo(plotLeft-1, y);
                ctx.stroke();

                if ( chartSettings.trace=='T' ) console.log(`${chartContainer.id} - vertical grid added at ${y}`);
            }
        }


        if ( chartSettings.trace=='T' ) console.log(`${chartContainer.id} - min value = ${plotValueMin}`);


        // plot columns and column values....
        for (let d = 0; d < dateLadder.length; d++) {
            const date  = dateLadder[d];
            const value = getValueForDate(plotData, date);

            const x        = parseInt(plotPaddingLeft + plotLeft + d * naturalColumnWidth);
            const y        = parseInt(plotBottom - (Math.abs(plotValueMin) + value) * plotScaleY);
            const columnHeight= parseInt(value * plotScaleY);

            if (value) {
                if (chartSettings.trace== 'T') console.log(`${chartContainer.id} - date=${date} value=${value} column rect from ${x}/${y} with columnHeight=${columnHeight}`);
                ctx.fillStyle = chartSettings.columnColorCd;
                let w= Math.max(3, columnWidth - 2);
                ctx.fillRect(x, y, w, columnHeight); // Paint column
                if ( chartSettings.trace=='T' ) console.log(`${chartContainer.id} - fillRect ${x}/${y} to ${x+w}/${y+columnHeight}`);
                columnRect[d] = {
                    x: x,
                    y: y,
                    w: w,
                    columnHeight: columnHeight
                };


                ctx.strokeStyle = chartSettings.plotAreaBoxColorCd;
                ctx.lineWidth=1;
                ctx.beginPath();
                ctx.moveTo(x, y);
                ctx.lineTo(x, y+columnHeight);
                if ( chartSettings.trace=='T' ) console.log(`${chartContainer.id} - drawLine ${x}/${y} to ${x}/${y+columnHeight}`);
                ctx.stroke();
            }


            dateLabelFormat     = getDateLabel(date, d, dateLadder.length, plotWidth, showAnyway);
            const formattedDate = new Date(date);


            if (dateLabelFormat !== '-') {
                let dateLabel = dateLabelFormat.replace('yyyy', formattedDate.getUTCFullYear());

                dateLabel = dateLabel.replace('MMM', formattedDate.toLocaleString(undefined, {month: 'short'}));
                dateLabel = dateLabel.replace('MM', formattedDate.toLocaleString(undefined, {month: '2-digit'}));
                dateLabel = dateLabel.replace('M', formattedDate.toLocaleString(undefined, {month: 'short'}).charAt(0));

                dateLabel = dateLabel.replace('dd', formattedDate.getUTCDate().toString().padStart(2, '0'));
                dateLabel = dateLabel.replace('d', formattedDate.getUTCDate().toString());

                let parts = dateLabel.split('\n');
                for (let p = 0; p < parts.length; p++) {
                    const columnCenterX = x + columnWidth / 2;
                    const labelWidth    = ctx.measureText(parts[p]).width;
                    const labelX        = columnCenterX - labelWidth / 2;

                    // Draw date labels
                    ctx.fillStyle = chartSettings.xAxisValueColorCd;
                    ctx.font      = chartSettings.xAxisValueFont;
                    ctx.fillText(parts[p], labelX, xAxisLabelY + (p) * xAxisFontHeight); // date labels
                }
            }
        }


        // Draw vertical/Y-axis ticks and labels - overlaying the columns
        ctx.beginPath();
        for (let value = plotValueMin; value <= plotValueMax; value += tickStep) {
            const y= parseInt(plotBottom - (Math.abs(plotValueMin) + value) * plotScaleY);
            if ( chartSettings.yAxisGridLinesOverlay=='T' ) {
                if (chartSettings.yAxisGridLines=== 'T') {
                    ctx.font         = chartSettings.yAxisFont ;
                    ctx.strokeStyle = chartSettings.yAxisGridColorCd;
                    ctx.moveTo(plotLeft, y);
                    ctx.lineTo(plotRight, y);
                    ctx.stroke();
                }
            }


            let label = getAxisLabel(value, plotValueRange, yAxisTickDP);
            ctx.fillStyle = chartSettings.yAxisTickValueColorCd;
            ctx.font      = chartSettings.yAxisFont;
            let x= parseInt(plotLeft - ctx.measureText(label).width - 5 - parseFloat(chartSettings.yAxisLabelPaddingRight));
            ctx.fillText(label, x, y + 3); // yaxis vertical label
            if (chartSettings.trace== 'T') console.log(`${chartContainer.id} - yaxis2 tick label [${label}] at ${x}/${y}`);
        }



        if ( chartSettings.xAxisLine=='T' ) {
            ctx.beginPath();
            let y = parseInt(plotBottom - (Math.abs(plotValueMin)) * plotScaleY);

            ctx.strokeStyle = chartSettings.xAxisLineColorCd;
            ctx.lineWidth=1;
            ctx.moveTo(plotLeft-1, y);
            ctx.lineTo(plotRight, y);
            ctx.stroke();
        }


        if ( chartSettings.yAxisLine=='T' ) {
            ctx.beginPath();
            let y = parseInt(plotBottom - (Math.abs(plotValueMin)) * plotScaleY);

            ctx.strokeStyle = chartSettings.xAxisLineColorCd;
            ctx.lineWidth=1;
            ctx.moveTo(plotLeft, plotTop);
            ctx.lineTo(plotLeft, plotBottom);
            ctx.stroke();
        }
        const xAxisLineY = chartHeight;




        if ( chartSettings.trace=='T' ) console.log(`${chartContainer.id} - width = ${chartContainer.clientWidth} vs ${chartSettings.columnValuesChartMinWidth}`);
        if ( chartSettings.columnValues=='T' && chartContainer.clientWidth>chartSettings.columnValuesChartMinWidth ) {
            for (let d = 0; d < dateLadder.length; d++) {
                const date  = dateLadder[d];
                const value = getValueForDate(plotData, date);

                const x = parseInt(plotLeft + plotPaddingLeft + d * naturalColumnWidth);
                const y = parseInt(plotBottom - (Math.abs(plotValueMin) + value) * plotScaleY);

                // Draw column value labels (right-aligned)
                if (chartSettings.columnValues=='T' && value) {
                    const valueLabel = value.toString();
                    const labelX     = x + columnWidth / 2 - ctx.measureText(valueLabel).width / 2;
                    ctx.fillStyle    = chartSettings.columnValuesColorCd || chartSettings.columnColorCd || defaultColorCd3;
                    ctx.font         = chartSettings.columnValuesFont;
                    if (value > 0) ctx.fillText(valueLabel, labelX, y - parseInt(chartSettings.columnValuesLabelYOffset));
                    else ctx.fillText(valueLabel, labelX, y + 15);
                }
            }
        }


        if ( chartSettings.xAxisTickMarks == 'T' ) {
            for (let d = 0; d < dateLadder.length; d++) {
                const date  = dateLadder[d];
                const value = getValueForDate(plotData, date);
                const x = parseInt(plotLeft + plotPaddingLeft + d * naturalColumnWidth);
                const y = parseInt(plotBottom - (Math.abs(plotValueMin) + 0) * plotScaleY);
                if (firstDaysOfMonth.includes(d)) {
                    ctx.beginPath();
                    ctx.strokeStyle = chartSettings.xAxisTickMarksColorCd ;
                    ctx.lineWidth=1;
                    ctx.moveTo(x, y);
                    ctx.lineTo(x, y+5);
                    ctx.stroke();
                }
            }
        }

    }
    // ----------------------------------------------------------

    var defaultColorCd1= '#aeaec3';
    var defaultColorCd2= '#eaeaef';
    var defaultColorCd3 = 'black';
    var defaultColorCd4 = 'white';
    var defaultFont = '12px Arial';

    var fontWHRatio=1.5;

    var axisPrefix= chartContainer.getAttribute('data-prefix')||"";
    var axisUnits= chartContainer.getAttribute('data-units');
    var dateLadder = [];
    var plotData=[];
    var plotTop, plotBottom;
    var plotLeft, plotRight;
    var plotWidth0,plotWidth, plotHeight;
    var xAxisLabelY, xAxisFontHeight;
    var chartHeight;
    var columnWidth;
    var plotValueRange;
    var plotValueMin, plotValueMax, hoverColumnN=-1;
    if ( axisPrefix ) axisPrefix+=' ';


    var canvasArr = chartContainer.getElementsByTagName('canvas');
    var canvas;
    if ( canvasArr.length==0 ) {
        var canvas = document.createElement('canvas');
        chartContainer.appendChild(canvas);
    }
    else canvas = canvasArr[0];
    var ctx=canvas.getContext("2d");

    let controls = chartContainer.getAttribute('data-controls');
    controls=controls+';';

    var validControls = [
        { key: 'trace', valueType: 'T/F', defaultV: 'F', descr: 'Turns on tracing seen in the browser console' },
        { key: 'help', valueType:'T/F', defaultV: '', descr:'Dumps the valid controls to the browser console'},
        { key: 'chartAreaColorBg', valueType:'color code', defaultV: '=none, e.g. red, #fff, green, etc', descr:'Set the background color of the chart canvas (e.g. red, #ff0000, etc)' },
        { key: 'columnColorCd', valueType:'color code', defaultV: defaultColorCd1, descr:'Set the color of the chart columns (e.g. red, #ff0000, etc)'},
        { key: 'columnValues', valueType:'T/F', defaultV:'F', descr:'Enables column values shown at the top of the columns (or bottom if the value is negative)' },
        { key: 'columnValuesChartMinWidth', valueType:'integer', defaultV: '0', descr:'Set the minimum width of chart that supports chart values; charts thinner than this will not show chart values at the end of the plotted column' },
        { key: 'columnValuesColorCd', valueType:'color code', defaultV: '=columnColorCd, e.g. red, #fff, green, etc\'', descr:'Set the color of column values found at the top/bottom of each column if columnValues=T' },
        { key: 'columnValuesFont', valueType:'font-size and name', defaultV: defaultFont, descr:'Set the font for column values if columnValues=T' },
        { key: 'columnValueHoverFont', valueType:'font-size and name', defaultV: defaultFont, descr: 'Set the font name and size of the text in a column value hover' },
        { key: 'columnValueHoverFontColorCd', valueType:'color code', defaultV: defaultColorCd3, descr: 'Set the color the text in a column value hover' },
        { key: 'columnValuesLabelYOffset', valueType: 'integer', defaultV: '10', descr: 'Y position adjustment for column value display if columnValues=T' },
        { key: 'columnWidthMax', valueType: 'integer', defaultV: '=natural column width', descr: 'Overrides the maximum width of columns on the chart' },
        { key: 'columnWidthMin', valueType: 'integer', defaultV: '=natural column width', descr: 'Overrides the minimum width of columns on the chart; warning: this settings can cause columns of overlap.' },
        { key: 'hoverColumnNTips', valueType: 'T/F', defaultV: 'T', descr: 'Enable mouse over column hover display boxes to show date and value; Also use data-prefix and data-units attributes on the chart-container to adjust the value being shown' },
        { key: 'marginLeft', valueType: 'integer', defaultV: '15', descr: 'margin left for the chart' },
        { key: 'marginRight', valueType: 'integer', defaultV: '5', descr: 'margin right for the chart' },
        { key: 'marginBottom', valueType: 'integer', defaultV: '4', descr: 'margin bottom for the chart' },
        { key: 'marginTop', valueType: 'integer', defaultV: '=approx the axis font height/2', descr: 'margin top for the chart (note, the axis labels will want to nudge the top here, so the font size is used to account for this)' },
        { key: 'plotAreaBox', valueType: 'T/F', defaultV: 'F', descr: 'Add a box around the plot area (the area the grid lines can be found)' },
        { key: 'plotAreaBoxColorCd', valueType: 'color code', defaultV: defaultColorCd4, descr: 'Adjust the plot area box color' },
        { key: 'plotAreaBoxLineWidth', valueType: 'integer', defaultV: '1', descr: 'Adjust the plot area box line width' },
        { key: 'plotAreaColorBg', valueType: 'color code', defaultV: '=none, e.g. red, #fff, green, etc\'', descr: 'Adjust the color of the plot area background' },
        { key: 'plotPaddingLeft', valueType: 'integer', defaultV: '20', descr: 'Adjust the left/start x-position of the plot columns' },
        { key: 'plotPaddingRight', valueType: 'integer', defaultV: '20', descr: 'Adjust the right/last x-position of the plot columns' },
        { key: 'xAxisLine', valueType: 'T/F', defaultV: 'T', descr: 'Enable the display of the horizontal x-axis line at the y position of 0' },
        { key: 'xAxisLineColorCd', valueType: 'color code', defaultV: defaultColorCd3, descr: 'Adjust the x-axis line color' },
        { key: 'xAxisTickMarks', valueType: 'T/F', defaultV: 'T', descr: 'Enable the display of tick marks on the horizontal x-axis showing the column dates' },
        { key: 'xAxisTickMarksColorCd', valueType: 'color code', defaultV: defaultColorCd3, descr: 'Adjust the color of the tick marks on the horizontal x-axis showing the column dates' },
        { key: 'xAxisValueColorCd', valueType: 'color code', defaultV: defaultColorCd3, descr: 'Adjust the color of the date values on the horizontal x-axis' },
        { key: 'xAxisValueFont', valueType: 'font-size and name', defaultV: defaultFont, descr: 'Set the font used on the date axis values' },
        { key: 'yAxisFont', valueType: 'font-size and name', defaultV: defaultFont, descr: 'Set the font used on the vertical value axis values' },
        { key: 'yAxisGridColorCd', valueType: 'color code', defaultV: defaultColorCd2, descr: 'Adjust the color of the vertical values on the y-axis' },
        { key: 'yAxisGridLines', valueType: 'T/F', defaultV: 'T', descr: 'Enable the display of the horizontal grid lines for each y-axis value; gridlines sit behind the columns unless yAxisGridLinesOverlay=T' },
        { key: 'yAxisGridLinesOverlay', valueType: 'T/F', defaultV: 'F', descr: 'Make gridlines sit on top of the columns' },
        { key: 'yAxisLabelPaddingLeft', valueType: 'integer', defaultV: '0', descr: 'Pad the left of the y-axis value labels on the left of the chart' },
        { key: 'yAxisLabelPaddingRight', valueType: 'integer', defaultV: '0', descr: 'Pad the right of the y-axis value labels on the left of the chart - moves these labels away from the y-axis line' },
        { key: 'yAxisLine', valueType: 'T/F', defaultV: 'T', descr: 'Enable the display of the vertical y-axis line at the left of the chart' },
        { key: 'yAxisScale', valueType: 'integer', defaultV: '1', descr: 'Divide the values shown on the leftside y-axis by this, e.g. yAxisScale=1000000 and yAxisScaleTx=m will divide all numbers by 1,000,000 and add "m" to the label so that "1,500,000" would become "1.5m"; set to "AUTO" to have the library compute the most appropriate values to be shown (k or m)' },
        { key: 'yAxisScaleTx', valueType: 'text', defaultV: '', descr: 'Add text to the right of each y-axis value, e.g. "k" or "m"' },
        { key: 'yAxisScalePrefix', valueType: 'text', defaultV: '', descr: 'Add text to the left of each y-axis value, e.g. "GBP"' },
        { key: 'yAxisTickCountMin', valueType: 'integer', defaultV:'3', descr:'Set the minimum number of ticks that the logic should use to find an appropriate y-axis tick step' },
        { key: 'yAxisTickCountMax', valueType: 'integer', defaultV:'6', descr:'Set the maximum number of ticks that the logic should use to find an appropriate y-axis tick step' },
        { key: 'yAxisTickValueColorCd', valueType: 'color code', defaultV: defaultColorCd3, descr: 'Set the color of the vertical y-axis values on the left of the chart' },
        { key: 'yAxisTickWidth', valueType: 'integer', defaultV: '4', descr: 'Set the width of small line-ticks found to the left of the yAxisLine' }
    ];


    var chartSettings = controls.split(';').reduce((obj, control) => {
        if (control.indexOf('=') > -1) {
            const [controlKey, controlValue] = control.trim().split('=');
            const validControl = validControls.find(vc => vc.key === controlKey.trim());
            if (validControl) {
                const { key: validcontrolKey, valueType, defaultV, descr } = validControl;
                if (valueType === 'integer' && controlValue.trim()!='AUTO' ) {
                    obj[controlKey] = parseInt(controlValue.trim(), 10);
                } else {
                    obj[controlKey] = controlValue.trim();
                }
            } else {
                console.error(`Invalid control: ${controlKey}`);
            }
        }
        return obj;
    }, {});


    // Apply default values for controls not explicitly set
    validControls.forEach(validControl => {
        if (!chartSettings.hasOwnProperty(validControl.key)) {
            const { key, defaultV, valueType, descr } = validControl;
            if (!defaultV.startsWith('=')) {
                if (valueType === 'integer') {
                    chartSettings[key] = parseInt(defaultV, 10);
                } else {
                    chartSettings[key] = defaultV;
                }
            }
        }
    });


    if (chartSettings.trace == 'T') console.log(`-----------------------\n${chartContainer.id} - controls = ${controls}`);


    if ( chartSettings.help ) {
        let helpTx= '';
        validControls.forEach(validControl => {
            const { key, defaultV, valueType, descr } = validControl;
            let defaultTx='';
            if ( defaultV.startsWith('=none')) defaultTx+=', '+defaultV.replace(/=none, /, '');
            else if ( defaultV.startsWith('=') ) {}
            else if ( defaultV!='' ) defaultTx+=`; default=${defaultV}`;
            helpTx+='\n'+chartSettings.help+'|`'+key+'`|('+valueType+') '+descr+' '+defaultTx+'|';
        });
        console.log(helpTx);
    }


    if (chartSettings.trace == 'T') console.log(`${chartContainer.id} - canvas init w=${chartContainer.clientWidth} / h=${chartContainer.clientHeight}`);

    chartSettings.yAxisMin = 0;


    var naturalColumnWidth;
    var columnRect=[];

    var plotDataTx= chartContainer.getAttribute('data');
    var plotDate= chartContainer.getAttribute('data-start');
    let inputType = 1; // yyyy-mm-dd=n,yyyy-mm-dd=n, ...
    if ( plotDate!=null ) inputType=2; // n,n,n,n,....
    var partsArr= plotDataTx.replace(/;/g, ',').split(',');
    if ( inputType==1 ) partsArr.sort();
    for(var p= 0; p<partsArr.length; p++) {
        if ( inputType==1 ) {
            if ( partsArr[p]!='' ) {
                var x       = partsArr[p].split('=');
                plotData[p] = {"date": x[0], "value": parseFloat(x[1])};
            }
        }
        else {
            plotData[p] = {"date": plotDate, "value": parseFloat(partsArr[p])};
            let dateObject = new Date(plotDate);
            dateObject.setDate(dateObject.getDate() + 1);
            plotDate = dateObject.toISOString().split('T')[0];
        }
    }


    var v;
    if ( plotData.length==0 ) plotValueMin=plotValueMax=0;
    else {
        for (let i = 0; i < plotData.length; i++) {
            v=plotData[i].value;
            if ( !plotValueMin || v<plotValueMin ) plotValueMin=v;
            if ( !plotValueMax || v>plotValueMax ) plotValueMax=v;
        }
    }


    if ( plotValueMin>chartSettings.yAxisMin ) plotValueMin=chartSettings.yAxisMin;
    const tickStep = getTickStep(plotValueMin, plotValueMax);
    if (chartSettings.trace== 'T') console.log(`${chartContainer.id} - min = ${plotValueMin}; max= ${plotValueMax}`);


    if ( chartSettings.columnValues == 'T' ) {
        plotValueMax += tickStep;
        plotValueMin -= tickStep;
    }


    plotValueMin = Math.floor(plotValueMin / tickStep) * tickStep;
    plotValueMax = Math.ceil(plotValueMax / tickStep) * tickStep;
    if ( plotValueMin==plotValueMax ) {
        plotValueMin--;
        plotValueMax++;
    }
    plotValueRange = plotValueMax - plotValueMin;
    if (chartSettings.trace== 'T') console.log(`${chartContainer.id} - values adjusted min = ${plotValueMin}; max= ${plotValueMax}`);
    const yAxisTickDP = getMaxDecimalPlaces(tickStep, plotValueMin, plotValueMax);


    ctx.font         = chartSettings.yAxisFont;
    const yAxisFontHeight = ctx.measureText("M").width*fontWHRatio;
    const maxLabelWidthMax = parseInt(ctx.measureText(getAxisLabel(plotValueMax, plotValueRange, yAxisTickDP)).width + parseFloat(chartSettings.yAxisLabelPaddingRight));
    const maxLabelWidthMin = parseInt(ctx.measureText(getAxisLabel(plotValueMin, plotValueRange, yAxisTickDP)).width + parseFloat(chartSettings.yAxisLabelPaddingRight));
    const maxLabelWidth = Math.max(maxLabelWidthMax, maxLabelWidthMin);

    var marginLeft = parseInt(chartSettings.marginLeft);
    var marginRight = parseInt(chartSettings.marginRight);
    var marginBottom = parseInt(chartSettings.marginBottom);
    var marginTop = parseInt(chartSettings.marginTop||yAxisFontHeight/2+4);
    var plotPaddingLeft = parseInt(chartSettings.plotPaddingLeft);
    var plotPaddingRight = parseInt(chartSettings.plotPaddingRight);
    var yAxisTickWidth = parseInt(chartSettings.yAxisTickWidth);

    let plotScaleY;

    const startDate = new Date(plotData[0].date);
    const endDate   = new Date(plotData[plotData.length - 1].date);
    endDate.setDate(endDate.getDate() + 1); // Add one day to the endDate
    let currentDate  = new Date(startDate);
    let showAnyway   = false;
    while (currentDate < endDate) {
        dateLadder.push(currentDate.toISOString().slice(0, 10));
        currentDate.setDate(currentDate.getDate() + 1);
    }


    const firstDaysOfMonth = [];
    const lastDaysOfMonth = [];
    if ( 1 ) {
        let prevMonth = null;
        for (let d = 0; d < dateLadder.length; d++) {
            const currentDate  = new Date(dateLadder[d]);
            const currentMonth = currentDate.getUTCMonth();
            if (prevMonth === null || currentMonth !== prevMonth) {
                prevMonth = currentMonth;
                firstDaysOfMonth.push(d);
                if (lastDaysOfMonth.length > 0) {
                    lastDaysOfMonth[lastDaysOfMonth.length - 1] = d - 1;
                }
            }
        }
        lastDaysOfMonth.push(dateLadder.length - 1);
    }


    let dateLabelFormat = '';
    const leftPadding   = maxLabelWidth + parseFloat(chartSettings.yAxisLabelPaddingLeft);
    plotLeft = marginLeft + leftPadding;


    // Call the drawChart function
    drawChart(chartContainer);

    // Add event listener to handle mousemove event
    if ( chartSettings.hoverColumnNTips == 'T') {
        canvas.addEventListener('mousemove', handleMouseMove);
    }

    window.addEventListener('resize', function() {
        drawChart(chartContainer);
    });
};

window.addEventListener("load", function() {
    var elems=document.getElementsByClassName("mf-column-chart");
    for(var i=0; i<elems.length; i++) mfColumnChart(elems[i]);
})

About

License

Latest Release

Version 1.012024-05-08