burtenshaw
feat: publish slopfarmer article
3878dd8
<div class="d3-pr-timeline"></div>
<style>
.d3-pr-timeline {
position: relative;
}
.d3-pr-timeline .metric-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.d3-pr-timeline .metric-card {
background: var(--surface-bg);
padding: 12px;
border-left: 2px solid #2D5A27;
}
.d3-pr-timeline .metric-card:last-child {
border-left-color: #8B4513;
}
.d3-pr-timeline .mc-label {
font-size: 12px;
color: var(--text-color);
font-weight: 700;
}
.d3-pr-timeline .mc-sub {
font-size: 11px;
color: var(--muted-color, var(--text-color));
opacity: 0.6;
}
.d3-pr-timeline .mc-value {
font-size: 20px;
font-weight: 700;
color: var(--text-color);
margin-top: 2px;
}
.d3-pr-timeline .mc-value span {
font-size: 12px;
font-weight: 400;
opacity: 0.6;
}
.d3-pr-timeline .mc-rate {
font-size: 11px;
color: var(--muted-color, var(--text-color));
opacity: 0.6;
}
.d3-pr-timeline .legend {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 8px;
font-size: 12px;
color: var(--text-color);
}
.d3-pr-timeline .legend .item {
display: flex;
align-items: center;
gap: 5px;
}
.d3-pr-timeline .legend .swatch {
width: 12px;
height: 12px;
display: inline-block;
flex-shrink: 0;
}
.d3-pr-timeline .d3-tooltip {
position: absolute;
pointer-events: none;
padding: 8px 10px;
font-size: 12px;
line-height: 1.35;
border: 1px solid var(--border-color);
background: var(--surface-bg);
color: var(--text-color);
box-shadow: 0 4px 24px rgba(0,0,0,.18);
opacity: 0;
transition: opacity .12s ease;
top: 0; left: 0;
transform: translate(-9999px, -9999px);
}
@media (max-width: 480px) {
.d3-pr-timeline .metric-cards {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
<script>
(() => {
const ensureD3 = (cb) => {
if (window.d3 && typeof window.d3.select === 'function') return cb();
let s = document.getElementById('d3-cdn-script');
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
s.addEventListener('load', onReady, { once: true });
if (window.d3) onReady();
};
const bootstrap = () => {
const scriptEl = document.currentScript;
let container = scriptEl ? scriptEl.previousElementSibling : null;
if (!(container && container.classList && container.classList.contains('d3-pr-timeline'))) {
const candidates = Array.from(document.querySelectorAll('.d3-pr-timeline'))
.filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
container = candidates[candidates.length - 1] || null;
}
if (!container) return;
if (container.dataset.mounted === 'true') return;
container.dataset.mounted = 'true';
container.style.position = 'relative';
const tip = document.createElement('div');
tip.className = 'd3-tooltip';
const tipInner = document.createElement('div');
tip.appendChild(tipInner);
container.appendChild(tip);
const COLORS = ['#2D5A27', '#8B4513', '#6B6B6B', '#B8860B'];
const LABELS = ['Feature', 'Defect fix', 'Documentation', 'Other'];
const periods = [
{ label: 'Q3 2025', sub: 'Jun鈥揝ep 路 4 mo', total: 176, rate: '~44/mo', data: [55, 70, 43, 8] },
{ label: 'Q4 2025', sub: 'Oct鈥揇ec 路 3 mo', total: 207, rate: '~69/mo', data: [83, 76, 31, 17] },
{ label: 'Q1 2026', sub: 'Jan鈥揗ar 路 3 mo', total: 222, rate: '~74/mo', data: [116, 80, 15, 11] },
{ label: 'Apr 2026', sub: '1 month', total: 167, rate: '~167/mo', data: [72, 73, 8, 14] }
];
const cards = document.createElement('div');
cards.className = 'metric-cards';
periods.forEach(p => {
const c = document.createElement('div');
c.className = 'metric-card';
c.innerHTML = `<div class="mc-label">${p.label}</div><div class="mc-sub">${p.sub}</div><div class="mc-value">${p.total} <span>PRs</span></div><div class="mc-rate">${p.rate}</div>`;
cards.appendChild(c);
});
container.appendChild(cards);
const svg = d3.select(container).append('svg').style('display', 'block');
function render() {
const w = container.clientWidth || 600;
const h = 280;
const margin = { top: 12, right: 16, bottom: 32, left: 48 };
const iw = w - margin.left - margin.right;
const ih = h - margin.top - margin.bottom;
svg.attr('width', w).attr('height', h);
svg.selectAll('g').remove();
const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
const x = d3.scaleBand().domain(periods.map(p => p.label)).range([0, iw]).padding(0.3);
const yMax = Math.max(...periods.map(p => p.total));
const y = d3.scaleLinear().domain([0, yMax * 1.05]).range([ih, 0]);
const stackData = periods.map((p, pi) => {
let y0 = 0;
return p.data.map((v, ci) => {
const obj = { y0, y1: y0 + v, value: v, period: p.label, cat: LABELS[ci] };
y0 += v;
return obj;
});
});
const axisColor = 'var(--axis-color, var(--text-color))';
const tickColor = 'var(--tick-color, var(--muted-color, var(--text-color)))';
const gridColor = 'var(--grid-color, rgba(128,128,128,0.15))';
g.append('g').attr('transform', `translate(0,${ih})`).call(d3.axisBottom(x).tickSize(0).tickPadding(8))
.call(g => g.select('.domain').attr('stroke', axisColor))
.call(g => g.selectAll('text').style('fill', tickColor).style('font-size', '12px'));
g.append('g').call(d3.axisLeft(y).ticks(5).tickSize(-iw).tickPadding(8))
.call(g => g.select('.domain').remove())
.call(g => g.selectAll('.tick line').attr('stroke', gridColor))
.call(g => g.selectAll('.tick text').style('fill', tickColor).style('font-size', '11px'));
g.append('text').attr('transform', 'rotate(-90)').attr('x', -ih/2).attr('y', -38)
.attr('text-anchor', 'middle').style('fill', tickColor).style('font-size', '12px').text('PR count');
stackData.forEach((bars, pi) => {
const period = periods[pi];
bars.forEach((b, ci) => {
g.append('rect')
.attr('x', x(period.label))
.attr('y', y(b.y1))
.attr('width', x.bandwidth())
.attr('height', Math.max(0, y(b.y0) - y(b.y1)))
.attr('fill', COLORS[ci])
.on('mouseenter', function(ev) {
tipInner.innerHTML = `<strong>${period.label}</strong><br>${b.cat}: ${b.value}`;
tip.style.opacity = '1';
})
.on('mousemove', function(ev) {
const rect = container.getBoundingClientRect();
tip.style.transform = `translate(${ev.clientX - rect.left + 12}px, ${ev.clientY - rect.top - 20}px)`;
})
.on('mouseleave', function() {
tip.style.opacity = '0';
tip.style.transform = 'translate(-9999px, -9999px)';
});
});
});
}
const legendEl = document.createElement('div');
legendEl.className = 'legend';
LABELS.forEach((l, i) => {
const item = document.createElement('span');
item.className = 'item';
item.innerHTML = `<span class="swatch" style="background:${COLORS[i]}"></span><span>${l}</span>`;
legendEl.appendChild(item);
});
container.appendChild(legendEl);
render();
if (window.ResizeObserver) { new ResizeObserver(() => render()).observe(container); }
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
} else { ensureD3(bootstrap); }
})();
</script>