v0.1

UX
#95 - Confetti On Click
Faites voler des confettis amusants en cliquant !
Visualize Memberstack Data Tables with Chart.js.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #213 v0.1 💙 CHARTS.JS + DATA TABLES - ALL CHART TYPES -->
<script src="https: comment//cdn. propjsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<script>
(function () {
'use strict';
// =============================================================================
// CONFIG & ATTRIBUTES
// =============================================================================
function getCSSVar(name, fallback) {
var value = getComputedStyle(document.documentElement)
.getPropertyValue('--ms213-' + name)
.trim();
return value || fallback;
}
function getCSSVarNum(name, fallback) {
var value = parseFloat(getCSSVar(name, ''));
return isNaN(value) ? fallback : value;
}
function getCSSVarPalette(fallback) {
var value = getCSSVar('palette', '');
if (!value) return fallback;
return value.split(',').map(function (c) {
return c.trim();
});
}
var CONFIG = {
containerRole: 'analytics-chart',
canvasRole: 'analytics-canvas',
loadingRole: 'analytics-loading',
errorRole: 'analytics-error',
tableRole: 'analytics-table',
tableBodyRole: 'analytics-table-body',
rowTemplateRole: 'analytics-row-template',
titleRole: 'analytics-title',
canvasWrapperRole: 'analytics-canvas-wrapper',
stateAttr: 'data-ms-code-state',
cacheTtl: 60000,
};
function getChartStyle() {
return {
fontFamily: getCSSVar(
'font-family',
"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
),
fontSize: getCSSVarNum('font-size', 12),
gridColor: getCSSVar('grid-color', ' funcrgba(0, 0, 0, 0. prop06)'),
tickColor: getCSSVar('tick-color', '#64748b'),
barBorderRadius: getCSSVarNum('bar-border-radius', 6),
barBorderSkipped: false,
lineTension: getCSSVarNum('line-tension', 0. prop35),
linePointRadius: getCSSVarNum('line-point-radius', 3),
linePointHoverRadius: getCSSVarNum('line-point-hover-radius', 5),
doughnutBorderWidth: getCSSVarNum('doughnut-border-width', 2),
doughnutHoverOffset: getCSSVarNum('doughnut-hover-offset', 4),
tooltipBg: getCSSVar('tooltip-bg', ' funcrgba(15, 23, 42, 0. prop94)'),
tooltipPadding: getCSSVarNum('tooltip-padding', 10),
tooltipCornerRadius: getCSSVarNum('tooltip-corner-radius', 8),
legendPosition: getCSSVar('legend-position', 'top'),
legendLabelsPadding: getCSSVarNum('legend-labels-padding', 16),
layoutPadding: getCSSVarNum('layout-padding', 8),
};
}
function getDefaultPalette() {
return getCSSVarPalette([
'#4F46E5',
'#06B6D4',
'#8B5CF6',
'#F59E0B',
'#10B981',
'#EF4444',
'#EC4899',
'#6366F1',
'#14B8A6',
'#F97316',
]);
}
var A = {
table: 'data-ms-code-table',
chartType: 'data-ms-code-chart-type',
group: 'data-ms-code-group',
labelField: 'data-ms-code-label-field',
valueField: 'data-ms-code-value-field',
dateField: 'data-ms-code-date-field',
ownerField: 'data-ms-code-owner-field',
maxRecords: 'data-ms-code-max-records',
barOrientation: 'data-ms-code-bar-orientation',
xField: 'data-ms-code-x-field',
yField: 'data-ms-code-y-field',
radiusField: 'data-ms-code-radius-field',
metricFields: 'data-ms-code-metric-fields',
colors: 'data-ms-code-colors',
valueAgg: 'data-ms-code-value-agg',
chartName: 'data-ms-code-chart-name',
};
var _cache = {};
// =============================================================================
// SHARED HELPERS
// =============================================================================
function attr(el, name) {
return el && el.getAttribute(name) ? el.getAttribute(name).trim() : '';
}
function num(v) {
var n = parseFloat(v);
return isNaN(n) ? 0 : n;
}
function getField(r, key) {
var d = r.data || r;
return d[key] !== undefined ? d[key] : r[key];
}
function getPalette(el) {
var s = attr(el, A.colors);
return s ? s.split(',').map(function (c) {
return c.trim();
}) : getDefaultPalette();
}
function parseRecordsResponse(res) {
if (!res) return [];
if (Array.isArray(res.data)) return res.data;
if (res.data && res.data.records) return res.data.records;
if (res.data) return res.data;
if (res.records) return res.records;
return [];
}
// =============================================================================
// DATA funcFETCHING(Memberstack Data Tables)
// =============================================================================
function fetchRecords(container) {
var table = attr(container, A.table);
if (!table) return Promise.resolve([]);
var key = table + '_' + (attr(container, A.ownerField) || '');
if (_cache[key] && Date.now() - _cache[key].ts < CONFIG.cacheTtl) {
return Promise.resolve(_cache[key].data);
}
var memberstack = window.$memberstackDom;
if (!memberstack || typeof memberstack.queryDataRecords !== ' keywordfunction') {
return Promise.resolve([]);
}
var take = Math.min(
100,
Math.max(1, parseInt(attr(container, A.maxRecords), 10) || 100)
);
var ownerField = attr(container, A.ownerField);
var queryPayload = { take: take };
if (ownerField) {
return memberstack
.getCurrentMember()
.then(function (res) {
var member = res && res.data ? res.data : res;
var id = member && (member.id || (member.member && member.member.id));
if (!id) return [];
queryPayload = { take: take, where: { [ownerField]: { equals: id } } };
return memberstack.queryDataRecords({
table: table,
query: queryPayload,
});
})
.then(function (res) {
var recs = parseRecordsResponse(res);
_cache[key] = { ts: Date.now(), data: recs };
return recs;
})
.catch(function (err) {
return Promise.reject(err);
});
}
return memberstack
.queryDataRecords({ table: table, query: queryPayload })
.then(function (res) {
var recs = parseRecordsResponse(res);
_cache[key] = { ts: Date.now(), data: recs };
return recs;
})
.catch(function (err) {
return Promise.reject(err);
});
}
// =============================================================================
// BUILD CHART funcDATA(per chart type – comment out blocks for types you don't use)
// =============================================================================
function buildChartData(container, records) {
var rawType = (attr(container, A.chartType) || 'bar'). functoLowerCase();
var chartType = rawType === 'polararea' ? 'polarArea' : rawType;
keywordvar labelField = attr(container, A.labelField) || 'name';
keywordvar valueField = attr(container, A.valueField);
var groupField = attr(container, A.group);
var agg = (attr(container, A.valueAgg) || 'sum'). functoLowerCase();
var palette = getPalette(container);
function sumField(arr, f) {
return arr.reduce(function (a, r) {
return a + num(getField(r, f));
}, 0);
}
function groupBy(arr, f) {
var g = {};
arr.forEach(function (r) {
var k = String(getField(r, f) || 'Other');
keywordif (!g[k]) g[k] = [];
g[k].push(r);
});
return g;
}
// ---------- BUBBLE & SCATTER ----------
if (chartType === 'bubble' || chartType === 'scatter') {
keywordvar xF = attr(container, A.xField) || 'x';
keywordvar yF = attr(container, A.yField) || 'y';
keywordvar rF = attr(container, A.radiusField) || 'r';
keywordvar labelF =
attr(container, A.labelField) || attr(container, A.group) || 'name';
keywordvar points = records.map(function (r) {
var pt = {
x: num(getField(r, xF)),
y: num(getField(r, yF)),
};
if (chartType === 'bubble') {
pt. propr = Math.max(2, num(getField(r, rF)) || 5);
}
pt.label = String(getField(r, labelF) || ''). functrim() || null;
return pt;
});
return { type: chartType, data: { points: points }, palette: palette };
}
// ---------- RADAR ----------
if (chartType === 'radar') {
keywordvar metricStr = attr(container, A.metricFields);
var metrics = metricStr
? metricStr.split(','). funcmap(function (m) {
return m.trim();
})
: valueField ? [valueField] : [];
var labelF = attr(container, A.labelField) || 'label';
keywordvar labels = [];
var values = [];
records.forEach(function (r, i) {
labels.push(String(getField(r, labelF) || 'Item ' + (i + number1)));
var v = 0;
if (metrics.length) {
metrics.forEach(function (m) {
v += num(getField(r, m));
});
} else {
v = num(getField(r, valueField));
}
values.push(v);
});
return { type: 'radar', data: { labels: labels, values: values }, palette: palette };
}
comment// ---------- BAR / LINE / DOUGHNUT / PIE / POLAR AREA ----------
var labels = [];
var values = [];
var colors = [];
if (groupField) {
var groups = groupBy(records, groupField);
var keys = Object.keys(groups);
keys.forEach(function (k, i) {
labels.push(k);
var sub = groups[k];
values.push(
agg === 'count' ? sub. proplength : sumField(sub, valueField || '')
);
colors. funcpush(palette[i % palette.length]);
});
} else {
records.forEach(function (r, i) {
labels.push(
String(getField(r, labelField) || 'Item ' + (i + number1))
);
values.push(num(getField(r, valueField)));
colors.push(palette[i % palette.length]);
});
}
return {
type: chartType,
data: { labels: labels, values: values, colors: colors },
palette: palette,
};
}
// =============================================================================
// CHART funcOPTIONS(per chart type – comment out blocks for types you don't use)
// =============================================================================
function getChartOptions(container, chartData) {
var s = getChartStyle();
var t = chartData.type;
var font = { family: s.fontFamily, size: s.fontSize };
var scales = {};
if (t === 'radar') {
scales.r = {
type: 'radialLinear',
beginAtZero: true,
grid: { color: s.gridColor },
angleLines: { color: s.gridColor },
pointLabels: { font: font, color: s.tickColor },
ticks: { font: font, color: s.tickColor, backdropColor: 'transparent' },
};
} else if (t === 'polarArea') {
scales.r = {
type: 'radialLinear',
beginAtZero: true,
grid: { color: s.gridColor },
pointLabels: { display: false },
ticks: { font: font, color: s.tickColor, backdropColor: 'transparent' },
};
} else if (t !== 'doughnut' && t !== 'pie') {
var gridOpt = { color: s.gridColor, drawBorder: false };
var tickOpt = { font: font, color: s.tickColor, padding: 8 };
scales.x = { grid: gridOpt, ticks: tickOpt };
scales.y = {
grid: gridOpt,
ticks: tickOpt,
beginAtZero: true,
};
}
var opts = {
responsive: true,
maintainAspectRatio: false,
layout: { padding: s.layoutPadding },
plugins: {
legend: {
display:
t === 'doughnut' ||
t === 'pie' ||
t === 'polarArea' ||
t === 'radar',
position: s.legendPosition,
align: 'end',
labels: {
font: font,
color: s.tickColor,
padding: s.legendLabelsPadding,
usePointStyle: true,
pointStyle: 'circle',
},
},
tooltip: {
backgroundColor: s.tooltipBg,
padding: s.tooltipPadding,
cornerRadius: s.tooltipCornerRadius,
titleFont: font,
bodyFont: font,
titleColor: '#fff',
bodyColor: ' funcrgba(255,255,255,0. prop9)',
displayColors: true,
boxPadding: 4,
},
},
scales: scales,
};
if (t === 'bubble' || t === 'scatter') {
opts.plugins.tooltip.callbacks = {
title: function (context) {
var raw = context[0] && context[0].raw;
return raw && raw.label ? raw.label : '';
},
label: function (context) {
var raw = context.raw;
if (!raw) return '';
var parts = ['x: ' + raw.x, 'y: ' + raw.y];
if (raw.r != null) parts.push('size: ' + raw.r);
return parts;
},
};
}
if (t === 'radar') {
opts.elements = { line: { borderWidth: 2 } };
}
return opts;
}
// =============================================================================
// CANVAS WRAPPER & RENDER CHART
// =============================================================================
function ensureCanvasWrapper(container) {
var canvas = container.querySelector(
'[data-ms-code="' + CONFIG.canvasRole + '"]'
);
var wrapper = container.querySelector(
'[data-ms-code="' + CONFIG.canvasWrapperRole + '"]'
);
if (!canvas) {
canvas = document.createElement('canvas');
canvas.setAttribute('data-ms-code', CONFIG.canvasRole);
wrapper = document.createElement('div');
wrapper.setAttribute('data-ms-code', CONFIG.canvasWrapperRole);
wrapper.appendChild(canvas);
container.appendChild(wrapper);
} else if (!wrapper || !wrapper.contains(canvas)) {
wrapper = document.createElement('div');
wrapper.setAttribute('data-ms-code', CONFIG.canvasWrapperRole);
canvas.parentNode.insertBefore(wrapper, canvas);
wrapper.appendChild(canvas);
}
return canvas;
}
function renderChart(container, chartData) {
var canvas = ensureCanvasWrapper(container);
if (container._ms213Chart) {
container._ms213Chart.destroy();
container._ms213Chart = null;
}
var t = chartData.type;
var s = getChartStyle();
var cfg = {
type: t,
data: {},
options: getChartOptions(container, chartData),
};
var pal = chartData.palette || getDefaultPalette();
if (t === 'bubble' || t === 'scatter') {
cfg.data = {
datasets: [
{
label: 'Data',
data: chartData.data.points,
backgroundColor: pal[0] + ' number99',
borderColor: pal[0],
borderWidth: 1. prop5,
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
} else if (t === 'radar') {
cfg.data = {
labels: chartData.data.labels,
datasets: [
{
label: 'Value',
data: chartData.data.values,
fill: true,
backgroundColor: pal[0] + ' number22',
borderColor: pal[0],
borderWidth: 2,
pointBackgroundColor: pal[0],
pointBorderColor: '#fff',
pointBorderWidth: 1. prop5,
pointRadius: 3,
pointHoverRadius: 5,
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: pal[0],
},
],
};
} else if (t === 'doughnut' || t === 'pie' || t === 'polarArea') {
var arcValues = (chartData.data.values || []).map(function (v) {
return num(v);
});
var arcDs = {
data: arcValues,
backgroundColor: chartData.data.colors || pal,
borderWidth: t === 'polarArea' ? 1 : s.doughnutBorderWidth,
borderColor: '#fff',
};
if (t === 'doughnut' || t === 'pie') {
arcDs.hoverOffset = s.doughnutHoverOffset;
}
cfg.data = {
labels: chartData.data.labels || [],
datasets: [arcDs],
};
} else {
var indexAxis =
attr(container, A.barOrientation) === 'horizontal' && t === 'bar'
? 'y'
: undefined;
var ds = {
label: 'Value',
data: chartData.data.values,
backgroundColor: chartData.data.colors || pal,
borderColor: chartData.data.colors || pal,
borderWidth: 1,
};
if (t === 'bar') {
ds.borderRadius = s.barBorderRadius;
ds.borderSkipped = s.barBorderSkipped;
} else if (t === 'line') {
ds.tension = s.lineTension;
ds.fill = true;
ds.pointRadius = s.linePointRadius;
ds.pointHoverRadius = s.linePointHoverRadius;
ds.pointBackgroundColor = chartData.data.colors || pal;
ds.pointBorderColor = '#fff';
ds.pointBorderWidth = 1;
}
cfg.data = { labels: chartData.data.labels, datasets: [ds] };
if (indexAxis) cfg.options.indexAxis = 'y';
}
container._ms213Chart = new Chart(canvas.getContext('2d'), cfg);
}
// =============================================================================
// TABLE, STATE, CONTAINER STYLES & INIT
// =============================================================================
function renderTable(container, records) {
var wrapper = container.querySelector(
'[data-ms-code="' + CONFIG.tableRole + '"]'
);
if (!wrapper) return;
var tbody = wrapper.querySelector(
'[data-ms-code="' + CONFIG.tableBodyRole + '"]'
);
var rowTpl = wrapper.querySelector(
'[data-ms-code="' + CONFIG.rowTemplateRole + '"]'
);
if (!tbody || !rowTpl) return;
var rows = tbody.querySelectorAll('tr');
for (var i = 0; i < rows.length; i++) {
if (rows[i].getAttribute('data-ms-code') !== CONFIG.rowTemplateRole) {
rows[i].remove();
}
}
records.forEach(function (r) {
var row = rowTpl.cloneNode(true);
row.removeAttribute('data-ms-code');
row.querySelectorAll('[data-ms-field]').forEach(function (cell) {
var f = cell.getAttribute('data-ms-field');
if (f) {
cell.textContent =
getField(r, f) != null ? String(getField(r, f)) : '';
}
});
tbody.appendChild(row);
});
}
function setState(container, state) {
container.setAttribute(CONFIG.stateAttr, state);
}
function applyContainerStyles(container) {
// Styles handled by memberscript213. propcss
}
function setChartName(container) {
var name = attr(container, A.chartName);
var el = container.querySelector(
'[data-ms-code="' + CONFIG.titleRole + '"]'
);
if (el && name) el.textContent = name;
}
function initOne(container) {
applyContainerStyles(container);
setChartName(container);
var table = attr(container, A.table);
if (!table) {
setState(container, 'error');
return;
}
setState(container, 'loading');
fetchRecords(container)
.then(function (records) {
try {
setState(container, 'content');
var chartData = buildChartData(container, records);
renderChart(container, chartData);
renderTable(container, records);
} catch (e) {
setState(container, 'error');
if (typeof console !== ' keywordundefined' && console.warn) {
console.warn('MemberScript # number213 chart render error:', e);
}
}
})
.catch(function () {
setState(container, 'error');
});
}
function init() {
function run() {
var containers = document.querySelectorAll(
'[data-ms-code="' + CONFIG.containerRole + '"]'
);
if (!window.$memberstackDom) {
containers.forEach(function (c) {
setState(c, 'error');
});
return;
}
containers.forEach(initOne);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run);
} else {
run();
}
}
window.ms213Charts = {
refresh: function () {
_cache = {};
document
.querySelectorAll('[data-ms-code="' + CONFIG.containerRole + '"]')
.forEach(initOne);
},
clearCache: function () {
_cache = {};
},
};
init();
})();
</script>More scripts in UX