const svg = d3.select('svg') const margin = { top: 150, right: 60, bottom: 30, left: 60 } const width = +svg.attr('width') - margin.left - margin.right const height = +svg.attr('height') - margin.top - margin.bottom const x = d3 .scaleTime() .domain([new Date(2015, 4, 1), new Date(2016, 12, 31)]) .range([0, width]) const y = d3.scaleLinear().rangeRound([height, 0]) const g = svg .append('g') .attr('transform', `translate(${margin.left},${margin.top})`) const parseDate = d3.utcParse('%Y%B') d3.queue() .defer(d3.csv, 'data.csv') .defer(d3.json, 'annotations.json') .awaitAll(render) function render(err, response) { console.log('response', response) const data = response[0] const annotationsData = response[1] // format input data data.forEach(d => { d.unitsShipped = +d.unitsShipped d.launchDate = parseDate(`${d.launchYear}${d.launchMonth}`) d.imageXOffset = +d.imageXOffset d.imageYOffset = +d.imageYOffset }) annotationsData.forEach(d => { d.imageWidth = +d.imageWidth d.imageHeight = +d.imageHeight d.imageXOffset = +d.imageXOffset d.imageYOffset = +d.imageYOffset }) // x.domain(data.map(d => d.letter)); y.domain([0, d3.max(data, d => d.unitsShipped)]) const xAxis = d3 .axisBottom() .scale(x) // .ticks(d3.timeMonths) // .tickSize(16, 0) .tickSizeOuter(0) .tickFormat(d3.timeFormat('%B %Y')) const yAxis = d3 .axisLeft() .scale(y) .ticks(10, ',.0f') g.append('g') .attr('class', 'axis axis--x') .attr('transform', `translate(0,${height})`) .call(xAxis) g.append('g') .attr('class', 'axis axis--y') .call(yAxis) .append('text') .attr('transform', 'rotate(-90)') .attr('y', 6) .attr('dy', '0.71em') .attr('text-anchor', 'end') .text('Frequency') // style the x-axis path d3.select('.axis--x path').style('stroke-opacity', 0) const defs = svg.append('defs') // draw arrow for x-axis baseline defs .append('marker') .attr('id', 'arrow') .attr('markerWidth', '6') .attr('markerHeight', '6') .attr('viewbox', '-3 -3 6 6') .attr('refX', '-1') .attr('refY', '0') .attr('markerUnits', 'strokeWidth') .attr('orient', 'auto') .attr('overflow', 'visible') .append('polygon') .attr('points', '-1,0 -2,2 2,0 -2,-2') .attr('fill', 'black') // draw the baseline with an arrow svg .append('line') .attr('x1', margin.left) .attr('y1', height + margin.top) .attr('x2', width + margin.left) .attr('y2', height + margin.top) .style('stroke-width', 4) .style('stroke-opacity', 1.0) .style('stroke', 'black') .attr('transform', 'translate(0,3)') .attr('marker-end', 'url(#arrow)') // draw the bars g.selectAll('.bar') .data(data) .enter() .append('rect') .attr('class', 'bar') .attr('x', d => x(d.launchDate)) // + 1px y so that bar does not protrude // above the dot .attr('y', d => y(d.unitsShipped) + 1) .attr('width', 4) .attr('height', d => height - y(d.unitsShipped)) .style('fill', 'black') .on('mouseover', d => console.log(d)) // draw the circles at the top of the bars g.selectAll('.circle') .data(data) .enter() .append('circle') .attr('class', 'point') .attr('cx', d => x(d.launchDate) + 2) .attr('cy', d => y(d.unitsShipped) + 6) .attr('r', 6) .style('fill', 'black') .style('fill-opacity', 1.0) .on('mouseover', d => console.log(d)) // // draw head-mounted-display images // const imageScaleFactor = 8 const images = svg .selectAll('image') .data(data) .enter() .append('svg:image') .attr('xlink:href', (d, i) => annotationsData[i].imageFileName) .attr( 'x', (d, i) => x(d.launchDate) + margin.left + annotationsData[i].imageXOffset - annotationsData[i].imageWidth / imageScaleFactor / 2 ) .attr( 'y', (d, i) => y(d.unitsShipped) + margin.top + annotationsData[i].imageYOffset - annotationsData[i].imageHeight / imageScaleFactor ) .attr('width', (d, i) => annotationsData[i].imageWidth / imageScaleFactor) .attr('height', (d, i) => annotationsData[i].imageHeight / imageScaleFactor) .call( d3 .drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended) ) function dragstarted(d) { d3.select(this) .raise() .classed('active', true) } function dragged(d) { d3.select(this) .attr('x', (d.x = d3.event.x)) .attr('y', (d.y = d3.event.y)) } function dragended(d) { d3.select(this).classed('active', false) } // start with the axes hidden let axesVisible = false d3.selectAll('.axis').style('opacity', 0) d3.select('body').on('click', click) function click() { if (axesVisible) { d3.selectAll('.axis').style('opacity', 0) axesVisible = false } else { d3.selectAll('.axis').style('opacity', 1) axesVisible = true } } // // add labels // const format = d3.format('.2s') // collect annotations data // generate data-driven annotation positions // pad strings to achieve text alignment const annotations = [] data.forEach((d, i) => { let textXOffset = x(d.launchDate) + margin.left - 40 let textYOffset = y(d.unitsShipped) + margin.top - 100 if (typeof annotationsData[i].textXOffset !== 'undefined') { textXOffset = annotationsData[i].textXOffset } if (typeof annotationsData[i].textYOffset !== 'undefined') { textYOffset = annotationsData[i].textYOffset } if (typeof d.unitsSuffix === 'undefined') { d.unitsSuffix = '' } annotations.push({ path: 'M 610,143 A 81.322 81.322 0 0 1 564,221', text: [ `${annotationsData[i].textOffsetLine0}${d.company} ${d.product}`, `${annotationsData[i].textOffsetLine1}${d.launchMonth} ${d.launchYear}`, `${annotationsData[i].textOffsetLine2}${d.unitsPrefix}${format( d.unitsShipped )}${d.unitsSuffix} shipped` ], textOffset: [textXOffset, textYOffset] }) }) // draw the annotation layer const swoopy = d3 .swoopyDrag() .x(d => 0) .y(d => 0) .draggable(1) .annotations(annotations) const swoopySel = svg.append('g.swoopy').call(swoopy) // no circles for now swoopySel.selectAll('circle').remove() // no paths or arrowheads for now swoopySel.selectAll('path').remove() // .attr('marker-end', 'url(#arrow)') // svg.append('marker') // .attr('id', 'arrow') // .attr('viewBox', '-10 -10 20 20') // .attr('markerWidth', 20) // .attr('markerHeight', 20) // .attr('orient', 'auto') // .append('path') // .attr('d', 'M-6.75,-6.75 L 0,0 L -6.75,6.75') swoopySel.selectAll('text').each(function(d) { d3.select(this) .text('') .tspans(d.text) // d3.wordwrap(d.text, 22) }) swoopySel .selectAll('text') .style('font-size', 12) .style('font-family', 'Roboto') // d3.select('g.swoopy').selectAll('g') // .attr('transform', 'translate(0,-20)'); }