function getMatrix(data, {
dateMetrics=null,
visibleMetrics=null,
colorScale="interpolatePuBu",
sortMetricDefault=null
}={}) {
let sortedData = [...data]
let allMetrics = Object.keys(data[0])
if (visibleMetrics) allMetrics = visibleMetrics
const metricTypes = fromPairs(allMetrics.map(metric => [
metric, getMetricType(data.map(d => d[metric]), metric)
]))
let isSortDescending = false
const metrics = allMetrics.filter(metric => metricTypes[metric] != "text")
let sortMetric = sortMetricDefault || metrics[0]
const metricInfo = fromPairs(metrics.map(metric => {
const type = metricTypes[metric]
const accessor = getMetricAccessor(metricTypes[metric])
const values = data.map(d => accessor(d[metric]))
const uniqueValues = [...new Set(values)]
let scale = d3.scaleSequential(d3[colorScale])
.domain(d3.extent(uniqueValues))
if (type == "categorical") {
scale = d3.scaleOrdinal()
.domain([0, uniqueValues.length])
.range(d3.schemeTableau10)
}
const scaledAccessor = d => d ? (type == "categorical" ? scale(uniqueValues.indexOf(d)) : scale(d)) : "#fff"
const doubledColors = values.reduce((a,b) => [...a, scaledAccessor(b), scaledAccessor(b)], [])
const info = {metric, type, accessor, values, doubledColors, uniqueValues, scale, scaledAccessor}
return [metric, info]
}))
// set up chart
const rowHeight = 27
const brushHeight = 30
const brushMargin = 10
const tooltipWidth = 350
const dms = {
width,
height: metrics.length * rowHeight + brushMargin + brushHeight,
marginLeft: 100,
marginBottom: brushMargin + brushHeight
}
dms.boundedWidth = dms.width - dms.marginLeft
dms.boundedHeight = dms.height - dms.marginBottom
const {wrapper, svg, bounds} = drawChart(dms)
const tooltip = getTooltip(wrapper, tooltipWidth)
const tooltipText = tooltip.append("div")
.style("width", "100%")
.style("display", "flex")
.style("justify-contents", "space-between")
const tooltipChart = tooltip.append("div")
.style("position", "relative")
bounds.on("mouseleave", metric => {
tooltip.style("opacity", 0)
})
const defs = svg.append("defs")
let xScale = d3.scaleLinear()
.domain([0, data.length])
.range([0, dms.boundedWidth])
const allValues = flatten(
Object.values(metricInfo).map(({metric, values}) => (
values.map((d, i) => ([
metric,
d,
i,
]))
))
)
let tooltipMarker
let histogramScale
// draw the rows
bounds.selectAll(".row")
.data(metrics)
.join("rect")
.attr("class", "row")
.attr("fill", metric => `url(#${slugify(metric)})`)
.attr("height", rowHeight)
.attr("width", dms.boundedWidth)
.attr("y", (metric, i) => i * rowHeight)
// add interaction
.on("mouseenter", function(metric) {
const colorsMap = metricInfo[metric]["type"] == "categorical" && fromPairs(
metricInfo[metric]["uniqueValues"].map(value => [
value, metricInfo[metric]["scaledAccessor"](value)
])
)
tooltipChart.html("")
const miniChart = metricInfo[metric]["type"] == "categorical"
? getHorizontalBarChartOf(metricInfo[metric]["values"], colorsMap, tooltipWidth - 30)
: getHistogramOf(metricInfo[metric]["values"], d => d, tooltipWidth - 30, 100)
tooltipChart.node().append(miniChart)
if (metricInfo[metric]["type"] != "categorical") {
tooltipMarker = tooltipChart.append("div")
.style("position", "absolute")
.style("top", 0)
.style("bottom", "0")
.style("left", 0)
.style("border-left", "1px solid")
}
histogramScale = d3.scaleLinear()
.domain(d3.extent(metricInfo[metric]["values"]))
.range([30, tooltipWidth - 30])
})
.on("mousemove", function(metric) {
const [x, y] = d3.mouse(this)
let parsedX = x + dms.marginLeft
if (parsedX > dms.width - tooltipWidth - 10) parsedX = parsedX - tooltipWidth
const index = Math.floor(xScale.invert(x))
const value = sortedData[index][metric]
tooltip.style("opacity", 1)
tooltip.style("transform", `translate(${parsedX}px, calc(${y > dms.height - 200 ? -100 : 0}% + ${y}px))`)
const colorsMap = metricInfo[metric]["type"] == "categorical" && fromPairs(
metricInfo[metric]["uniqueValues"].map(value => [
value, metricInfo[metric]["scaledAccessor"](value)
])
)
tooltipText.html([
`<div style="margin-bottom: 1em"><strong>${ metric }</strong> (${metricInfo[metric]["type"]})</div>`,
`<div style="margin-left: auto; font-weight: 700; ${colorsMap ? `color: ${colorsMap[value]}` : ""};">${ value }</div>`,
].join(" "))
tooltipMarker.style("transform", `translateX(${histogramScale(value)}px)`)
})
const doubleValues = (arr) => arr.reduce((a,b) => [...a, b, b], [])
const update = () => {
const sortAccessor = d => metricInfo[sortMetric]["accessor"](d[sortMetric])
sortedData = [...data].sort((a, b) =>
(sortAccessor(a) > sortAccessor(b) ? -1 : 1)
* (isSortDescending ? -1 : 1)
)
const rowWidth = xScale(1) - xScale(0)
defs.selectAll("linearGradient")
.data(Object.values(metricInfo))
.join("linearGradient")
.attr("id", ({metric}) => slugify(metric))
.each(function(metricInfo) {
const xScaleRange = xScale.range()
const endOffset = (xScale(1) - xScale(0)) / (xScaleRange[1] - xScaleRange[0]) * 100
d3.select(this)
.selectAll("stop")
.data(doubleValues(sortedData.map(value => (
metricInfo["scaledAccessor"](value[metricInfo["metric"]])
))))
.join("stop")
.attr("stop-color", color => color)
.attr("offset", (color, i) => `${
i % 2 ? (xScale(Math.floor(i / 2))
/ (xScaleRange[1] - xScaleRange[0]) * 100) : 0
}%`)
})
svg.selectAll(".label")
.style("font-weight", d => d == sortMetric ? 700 : 500)
}
update()
svg.selectAll(".label")
.data(metrics)
.join("text")
.attr("class", "label")
.attr("x", () => 0)
.attr("y", (metric, i) => (i + 0.5) * rowHeight)
.text(d => d)
.style("font-family", "sans-serif")
.style("font-size", "12px")
.style("dominant-baseline", "middle")
// .style("pointer-events", "none")
.style("cursor", "pointer")
.on("click", function(metric) {
if (metric == sortMetric) isSortDescending = !isSortDescending
sortMetric = metric
update()
})
const brushXScale = d3.scaleLinear()
.domain([0, data.length])
.range([0, dms.boundedWidth])
const brush = d3.brushX()
.extent([
[0, dms.boundedHeight + brushMargin],
[dms.boundedWidth - 1, dms.boundedHeight + brushMargin + brushHeight - 1]
])
.on("brush", onBrush)
.on("end", onBrush)
function onBrush() {
const newXExtent = d3.event.selection
? d3.event.selection.map(brushXScale.invert, brushXScale)
: [0, data.length]
xScale.domain(newXExtent)
update()
}
const brushElement = bounds.append("g")
brushElement.append("rect")
.attr("x", 0)
.attr("y", dms.boundedHeight + brushMargin)
.attr("width", dms.boundedWidth - 1)
.attr("height", brushHeight - 1)
.attr("fill", "none")
.attr("stroke", "#cacacc")
brushElement.append("g")
.call(brush);
return wrapper.node();
}