MFChartColumn / 1.0 / 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;
if (rangeV > m1) {
scale = m1;
scaleTx = 'm';
} else if (rangeV > t10) {
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(',');
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]);
})
Also see
articles/Implementing-a-Monaco-Editorsquarearticles/iConnectionTestsquarearticles/javascript-camerasquarearticles/list-editsquarearticles/minifierssquarearticles/opayosquarearticles/table-drag-sortersquarearticles/typewatchsquareprojects/MFCalendarPopupsquareprojects/MFChartColumnsquareprojects/MFColorPickersquareprojects/MFColorPickerBasicsquareprojects/MFColumnGradientsquareprojects/MFFloatawayMsgsquareprojects/MFPanelssquareprojects/MFSelectorsquare
Comments
New Comment