d3-svg-legend
Version:
A legend component for d3. Given a d3.scale it can create either a color legend, size legend, or symbol legend.
284 lines (247 loc) • 6.75 kB
JavaScript
import { select } from "d3-selection"
import { format, formatPrefix } from "d3-format"
const d3_identity = d => d
const d3_reverse = arr => {
const mirror = []
for (let i = 0, l = arr.length; i < l; i++) {
mirror[i] = arr[l - i - 1]
}
return mirror
}
//Text wrapping code adapted from Mike Bostock
const d3_textWrapping = (text, width) => {
text.each(function() {
var text = select(this),
words = text
.text()
.split(/\s+/)
.reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.2, //ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")) || 0,
tspan = text
.text(null)
.append("tspan")
.attr("x", 0)
.attr("dy", dy + "em")
while ((word = words.pop())) {
line.push(word)
tspan.text(line.join(" "))
if (tspan.node().getComputedTextLength() > width && line.length > 1) {
line.pop()
tspan.text(line.join(" "))
line = [word]
tspan = text
.append("tspan")
.attr("x", 0)
.attr("dy", lineHeight + dy + "em")
.text(word)
}
}
})
}
const d3_mergeLabels = (gen = [], labels, domain, range, labelDelimiter) => {
if (typeof labels === "object") {
if (labels.length === 0) return gen
let i = labels.length
for (; i < gen.length; i++) {
labels.push(gen[i])
}
return labels
} else if (typeof labels === "function") {
const customLabels = []
const genLength = gen.length
for (let i = 0; i < genLength; i++) {
customLabels.push(
labels({
i,
genLength,
generatedLabels: gen,
domain,
range,
labelDelimiter
})
)
}
return customLabels
}
return gen
}
const d3_linearLegend = (scale, cells, labelFormat) => {
let data = []
if (cells.length > 1) {
data = cells
} else {
const domain = scale.domain(),
increment = (domain[domain.length - 1] - domain[0]) / (cells - 1)
let i = 0
for (; i < cells; i++) {
data.push(domain[0] + i * increment)
}
}
const labels = data.map(labelFormat)
return {
data: data,
labels: labels,
feature: d => scale(d)
}
}
const d3_quantLegend = (scale, labelFormat, labelDelimiter) => {
const labels = scale.range().map(d => {
const invert = scale.invertExtent(d)
return (
labelFormat(invert[0]) +
" " +
labelDelimiter +
" " +
labelFormat(invert[1])
)
})
return {
data: scale.range(),
labels: labels,
feature: d3_identity
}
}
const d3_ordinalLegend = scale => ({
data: scale.domain(),
labels: scale.domain(),
feature: d => scale(d)
})
const d3_cellOver = (cellDispatcher, d, obj) => {
cellDispatcher.call("cellover", obj, d)
}
const d3_cellOut = (cellDispatcher, d, obj) => {
cellDispatcher.call("cellout", obj, d)
}
const d3_cellClick = (cellDispatcher, d, obj) => {
cellDispatcher.call("cellclick", obj, d)
}
export default {
d3_drawShapes: (
shape,
shapes,
shapeHeight,
shapeWidth,
shapeRadius,
path
) => {
if (shape === "rect") {
shapes.attr("height", shapeHeight).attr("width", shapeWidth)
} else if (shape === "circle") {
shapes.attr("r", shapeRadius)
} else if (shape === "line") {
shapes
.attr("x1", 0)
.attr("x2", shapeWidth)
.attr("y1", 0)
.attr("y2", 0)
} else if (shape === "path") {
shapes.attr("d", path)
}
},
d3_addText: function(svg, enter, labels, classPrefix, labelWidth) {
enter.append("text").attr("class", classPrefix + "label")
const text = svg
.selectAll(`g.${classPrefix}cell text.${classPrefix}label`)
.data(labels)
.text(d3_identity)
if (labelWidth) {
svg
.selectAll(`g.${classPrefix}cell text.${classPrefix}label`)
.call(d3_textWrapping, labelWidth)
}
return text
},
d3_calcType: function(
scale,
ascending,
cells,
labels,
labelFormat,
labelDelimiter
) {
const type = scale.invertExtent
? d3_quantLegend(scale, labelFormat, labelDelimiter)
: scale.ticks
? d3_linearLegend(scale, cells, labelFormat)
: d3_ordinalLegend(scale)
//for d3.scaleSequential that doesn't have a range function
const range = (scale.range && scale.range()) || scale.domain()
type.labels = d3_mergeLabels(
type.labels,
labels,
scale.domain(),
range,
labelDelimiter
)
if (ascending) {
type.labels = d3_reverse(type.labels)
type.data = d3_reverse(type.data)
}
return type
},
d3_filterCells: (type, cellFilter) => {
let filterCells = type.data
.map((d, i) => ({ data: d, label: type.labels[i] }))
.filter(cellFilter)
const dataValues = filterCells.map(d => d.data)
const labelValues = filterCells.map(d => d.label)
type.data = type.data.filter(d => dataValues.indexOf(d) !== -1)
type.labels = type.labels.filter(d => labelValues.indexOf(d) !== -1)
return type
},
d3_placement: (orient, cell, cellTrans, text, textTrans, labelAlign) => {
cell.attr("transform", cellTrans)
text.attr("transform", textTrans)
if (orient === "horizontal") {
text.style("text-anchor", labelAlign)
}
},
d3_addEvents: function(cells, dispatcher) {
cells
.on("mouseover.legend", function(d) {
d3_cellOver(dispatcher, d, this)
})
.on("mouseout.legend", function(d) {
d3_cellOut(dispatcher, d, this)
})
.on("click.legend", function(d) {
d3_cellClick(dispatcher, d, this)
})
},
d3_title: (svg, title, classPrefix, titleWidth) => {
if (title !== "") {
const titleText = svg.selectAll("text." + classPrefix + "legendTitle")
titleText
.data([title])
.enter()
.append("text")
.attr("class", classPrefix + "legendTitle")
svg.selectAll("text." + classPrefix + "legendTitle").text(title)
if (titleWidth) {
svg
.selectAll("text." + classPrefix + "legendTitle")
.call(d3_textWrapping, titleWidth)
}
const cellsSvg = svg.select("." + classPrefix + "legendCells")
const yOffset = svg
.select("." + classPrefix + "legendTitle")
.nodes()
.map(d => d.getBBox().height)[0],
xOffset = -cellsSvg.nodes().map(function(d) {
return d.getBBox().x
})[0]
cellsSvg.attr("transform", "translate(" + xOffset + "," + yOffset + ")")
}
},
d3_defaultLocale: {
format,
formatPrefix
},
d3_defaultFormatSpecifier: ".01f",
d3_defaultDelimiter: "to"
}