A simple SVG-based radar or spider chart for mithril without any external dependencies, based on the source code found here.
Check out the example on Flems.
const columns = { | |
price: 'Price', | |
useful: 'Usefulness', | |
design: 'Design', | |
battery: 'Battery Capacity', | |
camera: 'Camera Quality' | |
} | |
export const data = [ | |
{ | |
// iphone | |
color: '#edc951', | |
price: 1, | |
battery: .7, | |
design: 1, | |
useful: .9, | |
camera: .9 | |
}, { | |
// galaxy | |
color: '#cc333f', | |
price: .8, | |
battery: 1, | |
design: .6, | |
useful: .8, | |
camera: 1 | |
}, { | |
// nexus | |
color: '#00a0b0', | |
price: .5, | |
battery: .8, | |
design: .7, | |
useful: .6, | |
camera: .6 | |
} | |
] | |
const RadarChart = () => { | |
return { | |
view: () => { | |
return m('div#demo', [ | |
m('svg', { | |
id: 'demo-target', | |
xlmns: 'http://www.w3.org/2000/svg', | |
version: 1, | |
viewBox: '0 0 130 130', | |
}, render(columns, data, { | |
shapeProps: (data) => ({ | |
className: 'shape', | |
fill: data.color, | |
}) | |
})) | |
]); | |
} | |
} | |
} | |
m.render(document.body, m(RadarChart)) |
/** | |
* @source: https://github.com/derhuerst/svg-radar-chart | |
*/ | |
const round = v => Math.round(v * 10000) / 10000 | |
const polarToX = (angle, distance) => Math.cos(angle - Math.PI / 2) * distance | |
const polarToY = (angle, distance) => Math.sin(angle - Math.PI / 2) * distance | |
const points = (points) => { | |
return points | |
.map(point => point[0].toFixed(4) + ',' + point[1].toFixed(4)) | |
.join(' ') | |
} | |
const noSmoothing = (points) => { | |
let d = 'M' + points[0][0].toFixed(4) + ',' + points[0][1].toFixed(4) | |
for (let i = 1; i < points.length; i++) { | |
d += 'L' + points[i][0].toFixed(4) + ',' + points[i][1].toFixed(4) | |
} | |
return d + 'z' | |
} | |
const axis = (opt) => (col) => { | |
return m('polyline', Object.assign(opt.axisProps(col), { | |
points: points([ | |
[0, 0], [ | |
polarToX(col.angle, opt.chartSize / 2), | |
polarToY(col.angle, opt.chartSize / 2) | |
] | |
]) | |
})) | |
} | |
const shape = (columns, opt) => (data, i) => { | |
return m('path', Object.assign(opt.shapeProps(data), { | |
d: opt.smoothing(columns.map((col) => { | |
const val = data[col.key] | |
if ('number' !== typeof val) { | |
throw new Error(`Data set ${i} is invalid.`) | |
} | |
return [ | |
polarToX(col.angle, val * opt.chartSize / 2), | |
polarToY(col.angle, val * opt.chartSize / 2) | |
] | |
})) | |
})) | |
} | |
const scale = (opt, value) => { | |
return m('circle', Object.assign(opt.scaleProps(value), { | |
cx: 0, cy: 0, r: value * opt.chartSize / 2 | |
})) | |
} | |
const caption = (opt) => (col) => { | |
return m('text', Object.assign(opt.captionProps(col), { | |
x: polarToX(col.angle, opt.size / 2 * .95).toFixed(4), | |
y: polarToY(col.angle, opt.size / 2 * .95).toFixed(4), | |
dy: (opt.captionProps(col).fontSize || 2) / 2 | |
}), col.caption) | |
} | |
const defaults = { | |
size: 100, // size of the chart (including captions) | |
axes: true, // show axes? | |
scales: 3, // show scale circles? | |
captions: true, // show captions? | |
captionsPosition: 1.2, // where on the axes are the captions? | |
smoothing: noSmoothing, // shape smoothing function | |
axisProps: () => ({className: 'axis'}), | |
scaleProps: () => ({className: 'scale', fill: 'none'}), | |
shapeProps: () => ({className: 'shape'}), | |
captionProps: () => ({ | |
className: 'caption', | |
textAnchor: 'middle', fontSize: 3, | |
fontFamily: 'sans-serif' | |
}) | |
} | |
const render = (columns, data, opt = {}) => { | |
if ('object' !== typeof columns || Array.isArray(columns)) { | |
throw new Error('columns must be an object') | |
} | |
if (!Array.isArray(data)) { | |
throw new Error('data must be an array') | |
} | |
opt = Object.assign({}, defaults, opt) | |
opt.chartSize = opt.size / opt.captionsPosition | |
columns = Object.keys(columns).map((key, i, all) => ({ | |
key, caption: columns[key], | |
angle: Math.PI * 2 * i / all.length | |
})) | |
const groups = [ | |
m('g', data.map(shape(columns, opt))) | |
] | |
if (opt.captions) groups.push(m('g', columns.map(caption(opt)))) | |
if (opt.axes) groups.unshift(m('g', columns.map(axis(opt)))) | |
if (opt.scales > 0) { | |
const scales = [] | |
for (let i = opt.scales; i > 0; i--) { | |
scales.push(scale(opt, i / opt.scales)) | |
} | |
groups.unshift(m('g', scales)) | |
} | |
const delta = (opt.size / 2).toFixed(4) | |
return m('g', { | |
transform: `translate(${delta},${delta})` | |
}, groups) | |
} |