|
570 | 570 | overlay: (overlay, restarter, chart, container) => {
|
571 | 571 | // If we don't have images, our work is done!
|
572 | 572 | if(!hasImage) { return; }
|
573 |
| - |
574 |
| - // if custom images are defined, use the image at that location |
575 |
| - // and overlay it atop each dot |
576 |
| - google.visualization.events.addListener(chart, 'ready', function () { |
577 |
| - // HACK(Emmanuel): |
578 |
| - // The only way to hijack marker events is to walk the DOM here |
579 |
| - // If Google changes the DOM, these lines will likely break |
580 |
| - const svgRoot = chart.container.querySelector('svg'); |
| 573 | + const svgRoot = chart.container.querySelector('svg'); |
581 | 574 |
|
582 |
| - // The order of SVG slices is *not* the order of the rows in the table!! |
583 |
| - // - 1 or 2 slices: drawn in reverse order |
584 |
| - // - More than 2 slices: the first row in the table is the first SVG |
585 |
| - // slice, but the rest are in reverse order |
586 |
| - let slices; |
587 |
| - if(table.length <= 2) { |
588 |
| - slices = Array.prototype.slice.call(svgRoot.children, 2, -1).reverse(); |
589 |
| - } else { |
590 |
| - slices = Array.prototype.slice.call(svgRoot.children, 3, -1).reverse(); |
591 |
| - slices.unshift(svgRoot.children[2]); |
592 |
| - } |
593 |
| - const defs = svgRoot.children[0]; |
594 |
| - const legendImgs = svgRoot.children[1].querySelectorAll('g[column-id]'); |
| 575 | + function customDraw() { |
595 | 576 |
|
596 | 577 | // remove any labels that have previously been drawn
|
597 | 578 | $('.__img_labels').each((idx, n) => $(n).remove());
|
598 | 579 |
|
599 |
| - // Render each slice under the old ones, using the image as a pattern |
600 |
| - table.forEach((row, i) => { |
601 |
| - const oldDot = legendImgs[i].querySelector('circle'); |
602 |
| - const oldSlice = slices[i]; |
603 |
| - |
604 |
| - // render the image to an img tag |
| 580 | + // HACK(Emmanuel): |
| 581 | + // The only way to hijack marker events is to walk the DOM here |
| 582 | + // If Google changes the DOM, these lines will likely break |
| 583 | + const svgRoot = chart.container.querySelector('svg'); |
| 584 | + const defs = svgRoot.children[0]; // invisible defs group |
| 585 | + const slices = [...svgRoot.children].slice(2, -1); // skip defs, legend & empty last elt |
| 586 | + |
| 587 | + // As we walk through the rows, keep track of what % of the total |
| 588 | + // we've seen AND how many slices we've walked clockwise |
| 589 | + const total = table.reduce((acc, row) => row[1]+acc, 0); |
| 590 | + let pctProcessed = 0; |
| 591 | + let clockwiseSlices = 0; |
| 592 | + |
| 593 | + // Walk through the table row by row, and the pie clockwise from 12pm |
| 594 | + // For each row, find the DOM node for the corresponding slice |
| 595 | + // Render a new, decorated slice under the old one using the image as a pattern |
| 596 | + table.forEach((row, i) => { |
| 597 | + pctProcessed += (row[1] / total); // update how much of the pie we've walked |
| 598 | + |
| 599 | + // Find the associated DOM node for the slice |
| 600 | + // if we've walked <50%, the nodes are in CW order |
| 601 | + // if we've walked >=50%, the node CCW. Jump to the last slice and count backwards |
| 602 | + let oldSlice; |
| 603 | + if(pctProcessed < .50) { |
| 604 | + oldSlice = slices[i]; |
| 605 | + clockwiseSlices++; |
| 606 | + } else { |
| 607 | + oldSlice = slices[(slices.length-1) - (i - clockwiseSlices)]; |
| 608 | + } |
| 609 | + |
| 610 | + // Render the image, and convert to a dataURL. |
605 | 611 | const imgDOM = row[3].val.toDomNode();
|
606 | 612 | row[3].val.render(imgDOM.getContext('2d'), 0, 0);
|
607 |
| - |
608 |
| - // make an SVGimage element from the img tag, and make it the size of the slice |
609 |
| - const sliceBox = oldSlice.getBoundingClientRect(); |
| 613 | + const imgURL = imgDOM.toDataURL() |
| 614 | + |
| 615 | + // Make an SVGimage element (same size as slice) from the imgURL |
| 616 | + const {width, height} = oldSlice.getBoundingClientRect(); |
610 | 617 | const imageElt = document.createElementNS("http://www.w3.org/2000/svg", 'image');
|
| 618 | + imageElt.setAttributeNS(null, 'href', imgURL); |
| 619 | + imageElt.setAttribute('width', Math.max(width, height)); |
611 | 620 | imageElt.classList.add('__img_labels'); // tag for later garbage collection
|
612 |
| - imageElt.setAttributeNS(null, 'href', imgDOM.toDataURL()); |
613 |
| - imageElt.setAttribute('width', Math.max(sliceBox.width, sliceBox.height)); |
614 | 621 |
|
615 | 622 | // create a pattern from that image
|
616 | 623 | const patternElt = document.createElementNS("http://www.w3.org/2000/svg", 'pattern');
|
|
619 | 626 | patternElt.setAttribute('width', 1);
|
620 | 627 | patternElt.setAttribute('height', 1);
|
621 | 628 | patternElt.setAttribute( 'id', 'pic'+i);
|
| 629 | + patternElt.classList.add('__img_labels'); // tag for later garbage collection |
| 630 | + |
| 631 | + // and add it to the (invisible) defs element |
| 632 | + patternElt.appendChild(imageElt); |
| 633 | + defs.append(patternElt); |
622 | 634 |
|
623 | 635 | // make a new slice, copy elements from the old slice, and fill with the pattern
|
624 | 636 | const newSlice = document.createElementNS("http://www.w3.org/2000/svg", 'path');
|
625 | 637 | Object.assign(newSlice, oldSlice); // we should probably not steal *everything*...
|
626 | 638 | newSlice.setAttribute( 'd', oldSlice.firstChild.getAttribute('d'));
|
627 | 639 | newSlice.setAttribute( 'fill', 'url(#pic'+i+')');
|
628 |
| - |
629 |
| - // add the image to the pattern and the pattern to the defs |
630 |
| - patternElt.appendChild(imageElt); |
631 |
| - defs.append(patternElt); |
| 640 | + newSlice.classList.add('__img_labels'); // tag for later garbage collection |
632 | 641 |
|
633 | 642 | // insert the new slice before the now-transparent old slice
|
634 | 643 | oldSlice.parentNode.insertBefore(newSlice, oldSlice)
|
635 |
| - |
636 |
| - // make a new dot, then set size and position of dot to replace the old dot |
637 |
| - const newDot = imageElt.cloneNode(true); |
638 |
| - const radius = oldDot.r.animVal.value; |
639 |
| - newDot.setAttribute('x', oldDot.cx.animVal.value - radius); |
640 |
| - newDot.setAttribute('y', oldDot.cy.animVal.value - radius); |
641 |
| - newDot.setAttribute('width', radius * 2); |
642 |
| - newDot.setAttribute('height', radius * 2); |
643 |
| - oldDot.parentNode.replaceChild(newDot, oldDot); |
644 | 644 | });
|
645 |
| - }); |
| 645 | + |
| 646 | + // After 100ms, check if each row is represented in the legend, and needs redrawing |
| 647 | + const delayedDrawLegend = () => setTimeout(() => { |
| 648 | + console.log('drawing legend'); |
| 649 | + const legend = svgRoot.children[1]; // legend group |
| 650 | + const legendEntries = [...legend.querySelectorAll('g[column-id]')]; |
| 651 | + table.forEach((row, i) => { |
| 652 | + const entry = legendEntries.find(e => e.getAttribute('column-id') == row[0]); |
| 653 | + if(entry) { |
| 654 | + const oldDot = entry.querySelector('circle'); |
| 655 | + oldDot.setAttribute( 'fill', 'url(#pic'+i+')'); |
| 656 | + } |
| 657 | + }) |
| 658 | + legend.addEventListener("click", delayedDrawLegend) |
| 659 | + }, 100); |
| 660 | + |
| 661 | + // force the legend to draw, the first time around |
| 662 | + delayedDrawLegend(); |
| 663 | + } |
| 664 | + |
| 665 | + // if custom images are defined, use the image at that location |
| 666 | + // and overlay it atop each dot |
| 667 | + google.visualization.events.addListener(chart, 'ready', customDraw); |
646 | 668 | }
|
647 | 669 | }
|
648 | 670 | }
|
|
0 commit comments