| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| <title>Events</title> |
| <style> |
| :root { |
| color-scheme: light; |
| } |
| body { |
| margin: 0; |
| padding: 20px; |
| font-family: Segoe UI, Arial, sans-serif; |
| background: #f5f7fb; |
| color: #1f2a37; |
| } |
| h1 { |
| margin: 0 0 10px 0; |
| font-size: 20px; |
| font-weight: 600; |
| } |
| #legend-container { |
| margin-bottom: 8px; |
| } |
| #chart-wrap { |
| position: relative; |
| width: min(1100px, 100%); |
| height: 520px; |
| background: #ffffff; |
| border: 1px solid #e2e8f0; |
| border-radius: 10px; |
| padding: 12px; |
| box-sizing: border-box; |
| } |
| #chart { |
| width: 100%; |
| height: 100%; |
| } |
| </style> |
| <script src="./luxon.min.js"></script> |
| <script src="./chart.umd.min.js"></script> |
| <script src="./chartjs-adapter-luxon.umd.min.js"></script> |
| </head> |
| <body> |
| <h1>Events</h1> |
| <div id="legend-container"></div> |
| <div id="chart-wrap"> |
| <canvas id="chart"></canvas> |
| </div> |
| <script> |
| |
| const components = { Tooltip: Chart.Tooltip }; |
| |
| const Utils = (() => { |
| let _seed = 1; |
| |
| const valueOrDefault = (value, defaultValue) => (value === undefined ? defaultValue : value); |
| const MONTHS = [ |
| 'January', 'February', 'March', 'April', 'May', 'June', |
| 'July', 'August', 'September', 'October', 'November', 'December' |
| ]; |
| |
| const COLORS = [ |
| '#4dc9f6', '#f67019', '#f53794', '#537bc4', |
| '#acc236', '#166a8f', '#00a950', '#58595b', '#8549ba' |
| ]; |
| |
| const CHART_COLORS = { |
| red: 'rgb(255, 99, 132)', |
| orange: 'rgb(255, 159, 64)', |
| yellow: 'rgb(255, 205, 86)', |
| green: 'rgb(75, 192, 192)', |
| blue: 'rgb(54, 162, 235)', |
| purple: 'rgb(153, 102, 255)', |
| grey: 'rgb(201, 203, 207)' |
| }; |
| |
| const NAMED_COLORS = [ |
| CHART_COLORS.red, |
| CHART_COLORS.orange, |
| CHART_COLORS.yellow, |
| CHART_COLORS.green, |
| CHART_COLORS.blue, |
| CHART_COLORS.purple, |
| CHART_COLORS.grey |
| ]; |
| |
| function srand(seedValue) { |
| const normalized = Number(seedValue); |
| _seed = Number.isFinite(normalized) ? (normalized >>> 0) : 1; |
| } |
| |
| function rand(min, max) { |
| const low = valueOrDefault(min, 0); |
| const high = valueOrDefault(max, 0); |
| _seed = (_seed * 9301 + 49297) % 233280; |
| return low + (_seed / 233280) * (high - low); |
| } |
| |
| function numbers(config) { |
| const cfg = config || {}; |
| const min = valueOrDefault(cfg.min, 0); |
| const max = valueOrDefault(cfg.max, 100); |
| const from = valueOrDefault(cfg.from, []); |
| const count = valueOrDefault(cfg.count, 8); |
| const decimals = valueOrDefault(cfg.decimals, 8); |
| const continuity = valueOrDefault(cfg.continuity, 1); |
| const dfactor = Math.pow(10, decimals) || 0; |
| const data = []; |
| |
| for (let i = 0; i < count; ++i) { |
| const value = (from[i] || 0) + rand(min, max); |
| if (rand(0, 1) <= continuity) { |
| data.push(Math.round(dfactor * value) / dfactor); |
| } else { |
| data.push(null); |
| } |
| } |
| |
| return data; |
| } |
| |
| function points(config) { |
| const xs = numbers(config); |
| const ys = numbers(config); |
| return xs.map((x, i) => ({x, y: ys[i]})); |
| } |
| |
| function bubbles(config) { |
| const cfg = config || {}; |
| return points(cfg).map((pt) => ({...pt, r: rand(cfg.rmin, cfg.rmax)})); |
| } |
| |
| function labels(config) { |
| const cfg = config || {}; |
| const min = valueOrDefault(cfg.min, 0); |
| const max = valueOrDefault(cfg.max, 100); |
| const count = valueOrDefault(cfg.count, 8); |
| const decimals = valueOrDefault(cfg.decimals, 8); |
| const prefix = valueOrDefault(cfg.prefix, ''); |
| const step = (max - min) / count; |
| const dfactor = Math.pow(10, decimals) || 0; |
| const values = []; |
| |
| for (let i = min; i < max; i += step) { |
| values.push(prefix + Math.round(dfactor * i) / dfactor); |
| } |
| |
| return values; |
| } |
| |
| function months(config) { |
| const cfg = config || {}; |
| const count = valueOrDefault(cfg.count, 12); |
| const section = cfg.section; |
| const values = []; |
| for (let i = 0; i < count; ++i) { |
| const value = MONTHS[Math.ceil(i) % 12]; |
| values.push(value.substring(0, section)); |
| } |
| return values; |
| } |
| |
| function color(index) { |
| return COLORS[index % COLORS.length]; |
| } |
| |
| function parseColorToRgb(input) { |
| const value = String(input || '').trim(); |
| |
| const hex3 = /^#([0-9a-f]{3})$/i.exec(value); |
| if (hex3) { |
| const r = parseInt(hex3[1][0] + hex3[1][0], 16); |
| const g = parseInt(hex3[1][1] + hex3[1][1], 16); |
| const b = parseInt(hex3[1][2] + hex3[1][2], 16); |
| return [r, g, b]; |
| } |
| |
| const hex6 = /^#([0-9a-f]{6})$/i.exec(value); |
| if (hex6) { |
| const intValue = parseInt(hex6[1], 16); |
| const r = (intValue >> 16) & 255; |
| const g = (intValue >> 8) & 255; |
| const b = intValue & 255; |
| return [r, g, b]; |
| } |
| |
| const rgb = /^rgba?\(([^)]+)\)$/i.exec(value); |
| if (rgb) { |
| const parts = rgb[1].split(',').map((part) => Number(part.trim())); |
| if (parts.length >= 3) { |
| return [parts[0] || 0, parts[1] || 0, parts[2] || 0]; |
| } |
| } |
| |
| const helper = document.createElement('canvas').getContext('2d'); |
| helper.fillStyle = value || '#000000'; |
| const normalized = helper.fillStyle; |
| const fallback = /^rgba?\(([^)]+)\)$/i.exec(normalized); |
| if (fallback) { |
| const parts = fallback[1].split(',').map((part) => Number(part.trim())); |
| if (parts.length >= 3) { |
| return [parts[0] || 0, parts[1] || 0, parts[2] || 0]; |
| } |
| } |
| |
| return [0, 0, 0]; |
| } |
| |
| function transparentize(value, opacity) { |
| const alpha = opacity === undefined ? 0.5 : 1 - opacity; |
| const [r, g, b] = parseColorToRgb(value); |
| return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')'; |
| } |
| |
| const BASE_DATE_MS = Date.UTC(2021, 0, 1, 0, 0, 0, 0); |
| function newDate(days) { |
| return new Date(BASE_DATE_MS + (Number(days) || 0) * 24 * 60 * 60 * 1000); |
| } |
| |
| function newDateString(days) { |
| return newDate(days).toISOString(); |
| } |
| |
| function parseISODate(str) { |
| const millis = Date.parse(str); |
| return { |
| toMillis() { |
| return millis; |
| } |
| }; |
| } |
| |
| return { |
| srand, |
| rand, |
| numbers, |
| points, |
| bubbles, |
| labels, |
| months, |
| color, |
| transparentize, |
| CHART_COLORS, |
| namedColor(index) { |
| return NAMED_COLORS[index % NAMED_COLORS.length]; |
| }, |
| newDate, |
| newDateString, |
| parseISODate |
| }; |
| })(); |
| |
| window.components = components; |
| window.Utils = Utils; |
| Utils.srand(101024); |
| |
| |
| const data = { |
| labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], |
| datasets: [{ |
| label: '# of Votes', |
| data: [12, 19, 3, 5, 2, 3], |
| borderWidth: 1, |
| backgroundColor: ['#CB4335', '#1F618D', '#F1C40F', '#27AE60', '#884EA0', '#D35400'], |
| }] |
| }; |
| |
| |
| function handleHover(evt, item, legend) { |
| legend.chart.data.datasets[0].backgroundColor.forEach((color, index, colors) => { |
| colors[index] = index === item.index || color.length === 9 ? color : color + '4D'; |
| }); |
| legend.chart.update(); |
| } |
| |
| |
| function handleLeave(evt, item, legend) { |
| legend.chart.data.datasets[0].backgroundColor.forEach((color, index, colors) => { |
| colors[index] = color.length === 9 ? color.slice(0, -2) : color; |
| }); |
| legend.chart.update(); |
| } |
| |
| const config = { |
| type: 'pie', |
| data: data, |
| options: { |
| plugins: { |
| legend: { |
| onHover: handleHover, |
| onLeave: handleLeave |
| } |
| } |
| } |
| }; |
| |
| function forceEnableHoverTooltip(chartConfig) { |
| if (!chartConfig || typeof chartConfig !== 'object') { |
| return; |
| } |
| |
| if (!chartConfig.options || typeof chartConfig.options !== 'object') { |
| chartConfig.options = {}; |
| } |
| |
| const options = chartConfig.options; |
| if (!options.plugins || typeof options.plugins !== 'object') { |
| options.plugins = {}; |
| } |
| |
| const plugins = options.plugins; |
| if (plugins.tooltip === false) { |
| plugins.tooltip = {}; |
| } |
| |
| if (!plugins.tooltip || typeof plugins.tooltip !== 'object') { |
| plugins.tooltip = {}; |
| } |
| |
| const hasExternalTooltip = typeof plugins.tooltip.external === 'function'; |
| if (!hasExternalTooltip) { |
| plugins.tooltip.enabled = true; |
| } |
| } |
| |
| forceEnableHoverTooltip(config); |
| |
| const sampleCanvas = document.getElementById('chart'); |
| const sampleChart = new Chart(sampleCanvas, config); |
| </script> |
| </body> |
| </html> |
|
|