Upgrade to Pro — share decks privately, control downloads, hide ads and more …

2022: Template-first Data Visualizations

2022: Template-first Data Visualizations

Ember's template-first philosophy helps developers center markup in their work. This has benefits in semantic markup, accessibility, and generally using the web platform. Meanwhile, d3.js (the state of the art data visualization library for 10 years) is distinctly JavaScript first. It uses selections and joins to produce and manipulate markup.

We can use Ember and D3 in a harmonious way that retains a template-first approach while leveraging all of the utility of D3 to create uncompromising visualizations that are interactive, animated, and accessible.

EmberConf

April 19, 2022
Tweet

More Decks by EmberConf

Other Decks in Technology

Transcript

  1. Why now? In the year 2022, is a data viz

    talk relevant? Data visualization talks will continue to be relevant for as long as our frameworks, libraries, and the web platform we build upon continues to change.
  2. Let’s start with a bar chart The quintessential D3 bar

    chart Avg rating of popular addons by category Data from emberobserver.com
  3. Let’s start with a bar chart The quintessential D3 bar

    chart • Bars • X-Axis • Y-Axis • Gridlines • Label Avg rating of popular addons by category Data from emberobserver.com
  4. Here’s the code const X = d3.map(data, x); const Y

    = d3.map(data, y); if (xDomain === undefined) xDomain = X; if (yDomain === undefined) yDomain = [0, d3.max(Y)]; xDomain = new d3.InternSet(xDomain); const I = d3.range(X.length).filter(i => xDomain.has(X[i])); const xScale = d3.scaleBand(xDomain, xRange).padding(xPadding); const yScale = yType(yDomain, yRange); const xAxis = d3.axisBottom(xScale).tickSizeOuter(0); const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat); const svg = d3.create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [0, 0, width, height]) .attr("style", "max-width: 100%; height: auto; height: intrinsic;"); svg.append("g") .attr("transform", `translate(${marginLeft},0)`) .call(yAxis) .call(g => g.select(".domain").remove()) .call(g => g.selectAll(".tick line").clone() .attr("x2", width - marginLeft - marginRight) .attr("stroke-opacity", 0.1)) .call(g => g.append("text") .attr("x", -marginLeft) .attr("y", 10) .text(yLabel)); const bar = svg.append("g") .attr("fill", color) .selectAll("rect") .data(I) .join("rect") .attr("x", i => xScale(X[i])) .attr("y", i => yScale(Y[i])) .attr("height", i => yScale(0) - yScale(Y[i])) .attr("width", xScale.bandwidth()); svg.append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(xAxis);
  5. Here’s the code Transform data & create utilities const X

    = d3.map(data, x); const Y = d3.map(data, y); if (xDomain === undefined) xDomain = X; if (yDomain === undefined) yDomain = [0, d3.max(Y)]; xDomain = new d3.InternSet(xDomain); const I = d3.range(X.length).filter(i => xDomain.has(X[i])); const xScale = d3.scaleBand(xDomain, xRange).padding(xPadding); const yScale = yType(yDomain, yRange); const xAxis = d3.axisBottom(xScale).tickSizeOuter(0); const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat); const svg = d3.create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [0, 0, width, height]) .attr("style", "max-width: 100%; height: auto; height: intrinsic;"); svg.append("g") .attr("transform", `translate(${marginLeft},0)`) .call(yAxis) .call(g => g.select(".domain").remove()) .call(g => g.selectAll(".tick line").clone() .attr("x2", width - marginLeft - marginRight) .attr("stroke-opacity", 0.1)) .call(g => g.append("text") .attr("x", -marginLeft) .attr("y", 10) .text(yLabel)); const bar = svg.append("g") .attr("fill", color) .selectAll("rect") .data(I) .join("rect") .attr("x", i => xScale(X[i])) .attr("y", i => yScale(Y[i])) .attr("height", i => yScale(0) - yScale(Y[i])) .attr("width", xScale.bandwidth()); svg.append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(xAxis);
  6. Here’s the code Transform data & create utilities Create an

    SVG using data & utilities const X = d3.map(data, x); const Y = d3.map(data, y); if (xDomain === undefined) xDomain = X; if (yDomain === undefined) yDomain = [0, d3.max(Y)]; xDomain = new d3.InternSet(xDomain); const I = d3.range(X.length).filter(i => xDomain.has(X[i])); const xScale = d3.scaleBand(xDomain, xRange).padding(xPadding); const yScale = yType(yDomain, yRange); const xAxis = d3.axisBottom(xScale).tickSizeOuter(0); const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat); const svg = d3.create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [0, 0, width, height]) .attr("style", "max-width: 100%; height: auto; height: intrinsic;"); svg.append("g") .attr("transform", `translate(${marginLeft},0)`) .call(yAxis) .call(g => g.select(".domain").remove()) .call(g => g.selectAll(".tick line").clone() .attr("x2", width - marginLeft - marginRight) .attr("stroke-opacity", 0.1)) .call(g => g.append("text") .attr("x", -marginLeft) .attr("y", 10) .text(yLabel)); const bar = svg.append("g") .attr("fill", color) .selectAll("rect") .data(I) .join("rect") .attr("x", i => xScale(X[i])) .attr("y", i => yScale(Y[i])) .attr("height", i => yScale(0) - yScale(Y[i])) .attr("width", xScale.bandwidth()); svg.append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(xAxis);
  7. Here’s the code Transform data & create utilities Create an

    SVG using data & utilities With an API inspired by jQuery const X = d3.map(data, x); const Y = d3.map(data, y); if (xDomain === undefined) xDomain = X; if (yDomain === undefined) yDomain = [0, d3.max(Y)]; xDomain = new d3.InternSet(xDomain); const I = d3.range(X.length).filter(i => xDomain.has(X[i])); const xScale = d3.scaleBand(xDomain, xRange).padding(xPadding); const yScale = yType(yDomain, yRange); const xAxis = d3.axisBottom(xScale).tickSizeOuter(0); const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat); const svg = d3.create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [0, 0, width, height]) .attr("style", "max-width: 100%; height: auto; height: intrinsic;"); svg.append("g") .attr("transform", `translate(${marginLeft},0)`) .call(yAxis) .call(g => g.select(".domain").remove()) .call(g => g.selectAll(".tick line").clone() .attr("x2", width - marginLeft - marginRight) .attr("stroke-opacity", 0.1)) .call(g => g.append("text") .attr("x", -marginLeft) .attr("y", 10) .text(yLabel)); const bar = svg.append("g") .attr("fill", color) .selectAll("rect") .data(I) .join("rect") .attr("x", i => xScale(X[i])) .attr("y", i => yScale(Y[i])) .attr("height", i => yScale(0) - yScale(Y[i])) .attr("width", xScale.bandwidth()); svg.append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(xAxis);
  8. Here’s the code Transform data & create utilities Create an

    SVG using data & utilities With an API inspired by jQuery And imperative data- binding const X = d3.map(data, x); const Y = d3.map(data, y); if (xDomain === undefined) xDomain = X; if (yDomain === undefined) yDomain = [0, d3.max(Y)]; xDomain = new d3.InternSet(xDomain); const I = d3.range(X.length).filter(i => xDomain.has(X[i])); const xScale = d3.scaleBand(xDomain, xRange).padding(xPadding); const yScale = yType(yDomain, yRange); const xAxis = d3.axisBottom(xScale).tickSizeOuter(0); const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat); const svg = d3.create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [0, 0, width, height]) .attr("style", "max-width: 100%; height: auto; height: intrinsic;"); svg.append("g") .attr("transform", `translate(${marginLeft},0)`) .call(yAxis) .call(g => g.select(".domain").remove()) .call(g => g.selectAll(".tick line").clone() .attr("x2", width - marginLeft - marginRight) .attr("stroke-opacity", 0.1)) .call(g => g.append("text") .attr("x", -marginLeft) .attr("y", 10) .text(yLabel)); const bar = svg.append("g") .attr("fill", color) .selectAll("rect") .data(I) .join("rect") .attr("x", i => xScale(X[i])) .attr("y", i => yScale(Y[i])) .attr("height", i => yScale(0) - yScale(Y[i])) .attr("width", xScale.bandwidth()); svg.append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(xAxis); Bind data Join data and rect elements
  9. <svg width='900' height='400' viewBox='0,0,900,400' style='max-width:100%; height: auto; overflow: visible'> <g

    transform='translate(40,0)' fill='none' font-size='10' font-family='sans-serif' text-anchor='end'> <g class='tick' opacity='1' transform='translate(0,370)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>0</text> </g> <g class='tick' opacity='1' transform='translate(0,335)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>1</text> </g> <g class='tick' opacity='1' transform='translate(0,300)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>2</text> </g> <g class='tick' opacity='1' transform='translate(0,265)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>3</text> </g> <g class='tick' opacity='1' transform='translate(0,230)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>4</text> </g> <g class='tick' opacity='1' transform='translate(0,195)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>5</text> </g> <g class='tick' opacity='1' transform='translate(0,160)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>6</text> </g> <g class='tick' opacity='1' transform='translate(0,125.00000000000001)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>7</text> < <g class='tick' opacity='1' transform='translate(0,89.99999999999999)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>8</text> </ <g class='tick' opacity='1' transform='translate(0,54.99999999999999)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>9</text> </ <g class='tick' opacity='1' transform='translate(0,20)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>10</text> </g> <text x='-40' y='10' fill='currentColor' text-anchor='end'>Avg. Rating</text> </g> <g fill='#e74500'> <rect x='45' y='102.34210526315793' height='267.6578947368421' width='47.333333333333336'></rect> <rect x='102.33333333333334' y='137.97058823529414' height='232.02941176470586' width='47.333333333333336'></rect> <rect x='159.66666666666669' y='157.2777777777778' height='212.7222222222222' width='47.333333333333336'></rect> <rect x='217' y='158.59999999999997' height='211.40000000000003' width='47.333333333333336'></rect> <rect x='274.33333333333337' y='159.55555555555551' height='210.44444444444449' width='47.333333333333336'></rect> <rect x='331.6666666666667' y='172.7340425531914' height='197.2659574468086' width='47.333333333333336'></rect> <rect x='389' y='175.99418604651154' height='194.00581395348846' width='47.333333333333336'></rect> <rect x='446.33333333333337' y='176.85975609756088' height='193.14024390243912' width='47.333333333333336'></rect> <rect x='503.6666666666667' y='178.12288135593235' height='191.87711864406765' width='47.333333333333336'></rect> <rect x='561' y='178.76' height='191.24' width='47.333333333333336'></rect> <rect x='618.3333333333334' y='181.71794871794867' height='188.28205128205133' width='47.333333333333336'></rect> <rect x='675.6666666666667' y='182.27702702702692' height='187.72297297297308' width='47.333333333333336'></rect> <rect x='733' y='191.34615384615373' height='178.65384615384627' width='47.333333333333336'></rect> <rect x='790.3333333333334' y='192.20000000000005' height='177.79999999999995' width='47.333333333333336'></rect> <rect x='847.6666666666667' y='209.3017241379311' height='160.6982758620689' width='47.333333333333336'></rect> </g> <g transform='translate(0,370)' fill='none' font-size='10' font-family='sans-serif' text-anchor='middle'> <path class='domain' stroke='currentColor' d='M40,0H900'></path> <g class='tick' opacity='1' transform='translate(68.66666666666667,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Data</text> </g> <g class='tick' opacity='1' transform='translate(126.00000000000001,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Validations</text> </g> <g class='tick' opacity='1' transform='translate(183.33333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Modifiers</text> </g> <g class='tick' opacity='1' transform='translate(240.66666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Experiments</text> </g> <g class='tick' opacity='1' transform='translate(298.00000000000006,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Testing</text> </g> <g class='tick' opacity='1' transform='translate(355.33333333333337,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Template Helpers</text> </g> <g class='tick' opacity='1' transform='translate(412.6666666666667,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Components</text> </g> <g class='tick' opacity='1' transform='translate(470.00000000000006,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Dev tools</text> </g> <g class='tick' opacity='1' transform='translate(527.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Miscellaneous</text> </g> <g class='tick' opacity='1' transform='translate(584.6666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Ember Polyfills</text> </g> <g class='tick' opacity='1' transform='translate(642,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Styles</text> </g> <g class='tick' opacity='1' transform='translate(699.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Helpers and Utilities</text> </g> <g class='tick' opacity='1' transform='translate(756.6666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Build tools</text> </g> <g class='tick' opacity='1' transform='translate(814,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Internationalization</text> </g> <g class='tick' opacity='1' transform='translate(871.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Library wrappers</text> </g> </g> </svg> And here’s the SVG Notice how different it looks Also SVGs look like HTML
  10. <svg width='900' height='400' viewBox='0,0,900,400' style='max-width:100%; height: auto; overflow: visible'> <g

    transform='translate(40,0)' fill='none' font-size='10' font-family='sans-serif' text-anchor='end'> <g class='tick' opacity='1' transform='translate(0,370)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>0</text> </g> <g class='tick' opacity='1' transform='translate(0,335)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>1</text> </g> <g class='tick' opacity='1' transform='translate(0,300)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>2</text> </g> <g class='tick' opacity='1' transform='translate(0,265)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>3</text> </g> <g class='tick' opacity='1' transform='translate(0,230)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>4</text> </g> <g class='tick' opacity='1' transform='translate(0,195)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>5</text> </g> <g class='tick' opacity='1' transform='translate(0,160)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>6</text> </g> <g class='tick' opacity='1' transform='translate(0,125.00000000000001)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>7</text> < <g class='tick' opacity='1' transform='translate(0,89.99999999999999)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>8</text> </ <g class='tick' opacity='1' transform='translate(0,54.99999999999999)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>9</text> </ <g class='tick' opacity='1' transform='translate(0,20)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>10</text> </g> <text x='-40' y='10' fill='currentColor' text-anchor='end'>Avg. Rating</text> </g> <g fill='#e74500'> <rect x='45' y='102.34210526315793' height='267.6578947368421' width='47.333333333333336'></rect> <rect x='102.33333333333334' y='137.97058823529414' height='232.02941176470586' width='47.333333333333336'></rect> <rect x='159.66666666666669' y='157.2777777777778' height='212.7222222222222' width='47.333333333333336'></rect> <rect x='217' y='158.59999999999997' height='211.40000000000003' width='47.333333333333336'></rect> <rect x='274.33333333333337' y='159.55555555555551' height='210.44444444444449' width='47.333333333333336'></rect> <rect x='331.6666666666667' y='172.7340425531914' height='197.2659574468086' width='47.333333333333336'></rect> <rect x='389' y='175.99418604651154' height='194.00581395348846' width='47.333333333333336'></rect> <rect x='446.33333333333337' y='176.85975609756088' height='193.14024390243912' width='47.333333333333336'></rect> <rect x='503.6666666666667' y='178.12288135593235' height='191.87711864406765' width='47.333333333333336'></rect> <rect x='561' y='178.76' height='191.24' width='47.333333333333336'></rect> <rect x='618.3333333333334' y='181.71794871794867' height='188.28205128205133' width='47.333333333333336'></rect> <rect x='675.6666666666667' y='182.27702702702692' height='187.72297297297308' width='47.333333333333336'></rect> <rect x='733' y='191.34615384615373' height='178.65384615384627' width='47.333333333333336'></rect> <rect x='790.3333333333334' y='192.20000000000005' height='177.79999999999995' width='47.333333333333336'></rect> <rect x='847.6666666666667' y='209.3017241379311' height='160.6982758620689' width='47.333333333333336'></rect> </g> <g transform='translate(0,370)' fill='none' font-size='10' font-family='sans-serif' text-anchor='middle'> <path class='domain' stroke='currentColor' d='M40,0H900'></path> <g class='tick' opacity='1' transform='translate(68.66666666666667,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Data</text> </g> <g class='tick' opacity='1' transform='translate(126.00000000000001,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Validations</text> </g> <g class='tick' opacity='1' transform='translate(183.33333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Modifiers</text> </g> <g class='tick' opacity='1' transform='translate(240.66666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Experiments</text> </g> <g class='tick' opacity='1' transform='translate(298.00000000000006,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Testing</text> </g> <g class='tick' opacity='1' transform='translate(355.33333333333337,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Template Helpers</text> </g> <g class='tick' opacity='1' transform='translate(412.6666666666667,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Components</text> </g> <g class='tick' opacity='1' transform='translate(470.00000000000006,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Dev tools</text> </g> <g class='tick' opacity='1' transform='translate(527.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Miscellaneous</text> </g> <g class='tick' opacity='1' transform='translate(584.6666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Ember Polyfills</text> </g> <g class='tick' opacity='1' transform='translate(642,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Styles</text> </g> <g class='tick' opacity='1' transform='translate(699.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Helpers and Utilities</text> </g> <g class='tick' opacity='1' transform='translate(756.6666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Build tools</text> </g> <g class='tick' opacity='1' transform='translate(814,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Internationalization</text> </g> <g class='tick' opacity='1' transform='translate(871.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Library wrappers</text> </g> </g> </svg> And here’s the SVG Notice how different it looks Also SVGs look like HTML
  11. <svg width='900' height='400' viewBox='0,0,900,400' style='max-width:100%; height: auto; overflow: visible'> <g

    transform='translate(40,0)' fill='none' font-size='10' font-family='sans-serif' text-anchor='end'> <g class='tick' opacity='1' transform='translate(0,370)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>0</text> </g> <g class='tick' opacity='1' transform='translate(0,335)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>1</text> </g> <g class='tick' opacity='1' transform='translate(0,300)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>2</text> </g> <g class='tick' opacity='1' transform='translate(0,265)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>3</text> </g> <g class='tick' opacity='1' transform='translate(0,230)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>4</text> </g> <g class='tick' opacity='1' transform='translate(0,195)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>5</text> </g> <g class='tick' opacity='1' transform='translate(0,160)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>6</text> </g> <g class='tick' opacity='1' transform='translate(0,125.00000000000001)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>7</text> < <g class='tick' opacity='1' transform='translate(0,89.99999999999999)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>8</text> </ <g class='tick' opacity='1' transform='translate(0,54.99999999999999)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>9</text> </ <g class='tick' opacity='1' transform='translate(0,20)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>10</text> </g> <text x='-40' y='10' fill='currentColor' text-anchor='end'>Avg. Rating</text> </g> <g fill='#e74500'> <rect x='45' y='102.34210526315793' height='267.6578947368421' width='47.333333333333336'></rect> <rect x='102.33333333333334' y='137.97058823529414' height='232.02941176470586' width='47.333333333333336'></rect> <rect x='159.66666666666669' y='157.2777777777778' height='212.7222222222222' width='47.333333333333336'></rect> <rect x='217' y='158.59999999999997' height='211.40000000000003' width='47.333333333333336'></rect> <rect x='274.33333333333337' y='159.55555555555551' height='210.44444444444449' width='47.333333333333336'></rect> <rect x='331.6666666666667' y='172.7340425531914' height='197.2659574468086' width='47.333333333333336'></rect> <rect x='389' y='175.99418604651154' height='194.00581395348846' width='47.333333333333336'></rect> <rect x='446.33333333333337' y='176.85975609756088' height='193.14024390243912' width='47.333333333333336'></rect> <rect x='503.6666666666667' y='178.12288135593235' height='191.87711864406765' width='47.333333333333336'></rect> <rect x='561' y='178.76' height='191.24' width='47.333333333333336'></rect> <rect x='618.3333333333334' y='181.71794871794867' height='188.28205128205133' width='47.333333333333336'></rect> <rect x='675.6666666666667' y='182.27702702702692' height='187.72297297297308' width='47.333333333333336'></rect> <rect x='733' y='191.34615384615373' height='178.65384615384627' width='47.333333333333336'></rect> <rect x='790.3333333333334' y='192.20000000000005' height='177.79999999999995' width='47.333333333333336'></rect> <rect x='847.6666666666667' y='209.3017241379311' height='160.6982758620689' width='47.333333333333336'></rect> </g> <g transform='translate(0,370)' fill='none' font-size='10' font-family='sans-serif' text-anchor='middle'> <path class='domain' stroke='currentColor' d='M40,0H900'></path> <g class='tick' opacity='1' transform='translate(68.66666666666667,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Data</text> </g> <g class='tick' opacity='1' transform='translate(126.00000000000001,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Validations</text> </g> <g class='tick' opacity='1' transform='translate(183.33333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Modifiers</text> </g> <g class='tick' opacity='1' transform='translate(240.66666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Experiments</text> </g> <g class='tick' opacity='1' transform='translate(298.00000000000006,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Testing</text> </g> <g class='tick' opacity='1' transform='translate(355.33333333333337,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Template Helpers</text> </g> <g class='tick' opacity='1' transform='translate(412.6666666666667,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Components</text> </g> <g class='tick' opacity='1' transform='translate(470.00000000000006,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Dev tools</text> </g> <g class='tick' opacity='1' transform='translate(527.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Miscellaneous</text> </g> <g class='tick' opacity='1' transform='translate(584.6666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Ember Polyfills</text> </g> <g class='tick' opacity='1' transform='translate(642,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Styles</text> </g> <g class='tick' opacity='1' transform='translate(699.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Helpers and Utilities</text> </g> <g class='tick' opacity='1' transform='translate(756.6666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Build tools</text> </g> <g class='tick' opacity='1' transform='translate(814,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Internationalization</text> </g> <g class='tick' opacity='1' transform='translate(871.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Library wrappers</text> </g> </g> </svg> And here’s the SVG Notice how different it looks Also SVGs look like HTML
  12. <svg width='900' height='400' viewBox='0,0,900,400' style='max-width:100%; height: auto; overflow: visible'> <g

    transform='translate(40,0)' fill='none' font-size='10' font-family='sans-serif' text-anchor='end'> <g class='tick' opacity='1' transform='translate(0,370)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>0</text> </g> <g class='tick' opacity='1' transform='translate(0,335)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>1</text> </g> <g class='tick' opacity='1' transform='translate(0,300)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>2</text> </g> <g class='tick' opacity='1' transform='translate(0,265)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>3</text> </g> <g class='tick' opacity='1' transform='translate(0,230)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>4</text> </g> <g class='tick' opacity='1' transform='translate(0,195)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>5</text> </g> <g class='tick' opacity='1' transform='translate(0,160)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>6</text> </g> <g class='tick' opacity='1' transform='translate(0,125.00000000000001)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>7</text> < <g class='tick' opacity='1' transform='translate(0,89.99999999999999)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>8</text> </ <g class='tick' opacity='1' transform='translate(0,54.99999999999999)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>9</text> </ <g class='tick' opacity='1' transform='translate(0,20)'> <line stroke='currentColor' x2='-6'></line> <line stroke='currentColor' x2='860' stroke-opacity='0.1'></line> <text fill='currentColor' x='-9' dy='0.32em'>10</text> </g> <text x='-40' y='10' fill='currentColor' text-anchor='end'>Avg. Rating</text> </g> <g fill='#e74500'> <rect x='45' y='102.34210526315793' height='267.6578947368421' width='47.333333333333336'></rect> <rect x='102.33333333333334' y='137.97058823529414' height='232.02941176470586' width='47.333333333333336'></rect> <rect x='159.66666666666669' y='157.2777777777778' height='212.7222222222222' width='47.333333333333336'></rect> <rect x='217' y='158.59999999999997' height='211.40000000000003' width='47.333333333333336'></rect> <rect x='274.33333333333337' y='159.55555555555551' height='210.44444444444449' width='47.333333333333336'></rect> <rect x='331.6666666666667' y='172.7340425531914' height='197.2659574468086' width='47.333333333333336'></rect> <rect x='389' y='175.99418604651154' height='194.00581395348846' width='47.333333333333336'></rect> <rect x='446.33333333333337' y='176.85975609756088' height='193.14024390243912' width='47.333333333333336'></rect> <rect x='503.6666666666667' y='178.12288135593235' height='191.87711864406765' width='47.333333333333336'></rect> <rect x='561' y='178.76' height='191.24' width='47.333333333333336'></rect> <rect x='618.3333333333334' y='181.71794871794867' height='188.28205128205133' width='47.333333333333336'></rect> <rect x='675.6666666666667' y='182.27702702702692' height='187.72297297297308' width='47.333333333333336'></rect> <rect x='733' y='191.34615384615373' height='178.65384615384627' width='47.333333333333336'></rect> <rect x='790.3333333333334' y='192.20000000000005' height='177.79999999999995' width='47.333333333333336'></rect> <rect x='847.6666666666667' y='209.3017241379311' height='160.6982758620689' width='47.333333333333336'></rect> </g> <g transform='translate(0,370)' fill='none' font-size='10' font-family='sans-serif' text-anchor='middle'> <path class='domain' stroke='currentColor' d='M40,0H900'></path> <g class='tick' opacity='1' transform='translate(68.66666666666667,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Data</text> </g> <g class='tick' opacity='1' transform='translate(126.00000000000001,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Validations</text> </g> <g class='tick' opacity='1' transform='translate(183.33333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Modifiers</text> </g> <g class='tick' opacity='1' transform='translate(240.66666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Experiments</text> </g> <g class='tick' opacity='1' transform='translate(298.00000000000006,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Testing</text> </g> <g class='tick' opacity='1' transform='translate(355.33333333333337,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Template Helpers</text> </g> <g class='tick' opacity='1' transform='translate(412.6666666666667,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Components</text> </g> <g class='tick' opacity='1' transform='translate(470.00000000000006,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Dev tools</text> </g> <g class='tick' opacity='1' transform='translate(527.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Miscellaneous</text> </g> <g class='tick' opacity='1' transform='translate(584.6666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Ember Polyfills</text> </g> <g class='tick' opacity='1' transform='translate(642,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Styles</text> </g> <g class='tick' opacity='1' transform='translate(699.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Helpers and Utilities</text> </g> <g class='tick' opacity='1' transform='translate(756.6666666666666,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Build tools</text> </g> <g class='tick' opacity='1' transform='translate(814,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Internationalization</text> </g> <g class='tick' opacity='1' transform='translate(871.3333333333334,0)'> <line stroke='currentColor' y2='6'></line> <text fill='currentColor' y='9' dy='0.71em' text-anchor='end' transform='rotate(-45)'>Library wrappers</text> </g> </g> </svg> And here’s the SVG Notice how different it looks Also SVGs look like HTML
  13. A Glimmer Component Glimmer has amazing support for SVG <svg

    width={{this.width}} height={{this.height}} viewBox='0 0 {{this.width}} {{this.height}}' style='max-width:100%; height: auto; overflow: visible' > </svg>
  14. A Glimmer Component Glimmer has amazing support for SVG We

    can use an each to create our rect elements <svg width={{this.width}} height={{this.height}} viewBox='0 0 {{this.width}} {{this.height}}' style='max-width:100%; height: auto; overflow: visible' > <g fill='{{this.color}}'> {{#each this.computedData as |bar|}} <rect x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> </svg>
  15. A Glimmer Component Glimmer has amazing support for SVG We

    can use an each to create our rect elements And for axes, let’s stick with d3 <svg width={{this.width}} height={{this.height}} viewBox='0 0 {{this.width}} {{this.height}}' style='max-width:100%; height: auto; overflow: visible’ {{did-insert this.mountElements}} > <g class='y-axis' transform='translate({{this.marginLeft}},0)'> <text x='-{{this.marginLeft}}' y='10' fill='currentColor' text-anchor='end' >{{this.yLabel}}</text> </g> <g fill='{{this.color}}'> {{#each this.computedData as |bar|}} <rect x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g class='x-axis' transform='translate(0, {{this.xAxisOffset}})'></g> </svg>
  16. A Glimmer Component Glimmer has amazing support for SVG We

    can use an each to create our rect elements And for axes, let’s stick with d3 Which we can invoke using the did-insert modifier <svg width={{this.width}} height={{this.height}} viewBox='0 0 {{this.width}} {{this.height}}' style='max-width:100%; height: auto; overflow: visible' {{did-insert this.mountElements}} > <g class='y-axis' transform='translate({{this.marginLeft}},0)'> <text x='-{{this.marginLeft}}' y='10' fill='currentColor' text-anchor='end' >{{this.yLabel}}</text> </g> <g fill='{{this.color}}'> {{#each this.computedData as |bar|}} <rect x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g class='x-axis' transform='translate(0, {{this.xAxisOffset}})'></g> </svg>
  17. A Glimmer Component Glimmer has amazing support for SVG We

    can use an each to create our rect elements And for axes, let’s stick with d3 Which we can invoke using the did-insert modifier Now to define these properties… <svg width={{this.width}} height={{this.height}} viewBox='0 0 {{this.width}} {{this.height}}' style='max-width:100%; height: auto; overflow: visible' {{did-insert this.mountElements}} > <g class='y-axis' transform='translate({{this.marginLeft}},0)'> <text x='-{{this.marginLeft}}' y='10' fill='currentColor' text-anchor='end' >{{this.yLabel}}</text> </g> <g fill='{{this.color}}'> {{#each this.computedData as |bar|}} <rect x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g class='x-axis' transform='translate(0, {{this.xAxisOffset}})'></g> </svg>
  18. D3 modules that will help us The quintessential D3 bar

    chart Avg rating of popular addons by category Data from emberobserver.com
  19. D3 modules that will help us The quintessential D3 bar

    chart axisLeft Avg rating of popular addons by category Data from emberobserver.com
  20. D3 modules that will help us The quintessential D3 bar

    chart axisLeft scaleLinear Avg rating of popular addons by category Data from emberobserver.com
  21. D3 modules that will help us The quintessential D3 bar

    chart axisLeft scaleLinear axisBottom Avg rating of popular addons by category Data from emberobserver.com
  22. D3 modules that will help us The quintessential D3 bar

    chart axisLeft scaleLinear axisBottom scaleOrdinal Avg rating of popular addons by category Data from emberobserver.com
  23. D3 modules that will help us The quintessential D3 bar

    chart axisLeft scaleLinear axisBottom scaleOrdinal mean Avg rating of popular addons by category Data from emberobserver.com
  24. What does our data look like? An array of categories

    Each category has an array of addons
  25. What does our data look like? An array of categories

    Each category has an array of addons Each addon has a score property
  26. Get the average The @d3/array package has handy stats functions

    import Component from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; import { action } from '@ember/object'; import * as d3 from 'd3'; export default class BarChart extends Component { yLabel = 'Avg. Rating'; @tracked width = 900; @tracked height = 400; @tracked marginTop = 20; @tracked marginBottom = 30; @tracked marginLeft = 40; @tracked marginRight = 0; @tracked color = '#e74500'; @cached get xAxisOffset() { return this.height - this.marginBottom; } @cached get data() { // Only chart the top 15 categories by number of addons const data = this.args.data.sortBy('addons.length').reverse().slice(0, 15); // Compute average score data.forEach((d) => { d.avgScore = d3.mean(d.addons.mapBy('score')); }); // Sort by average score descending return data.sortBy('avgScore').reverse(); } // ... }
  27. Create data-to- pixel mappings The @d3/array package has handy stats

    functions The @d3/scale package can make a variety of scale functions from a variety of data types import Component from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; import { action } from '@ember/object'; import * as d3 from 'd3'; export default class BarChart extends Component { // ... @cached get yRange() { return [this.height - this.marginBottom, this.marginTop]; } @cached get yDomain() { return [0, 10]; } @cached get yScale() { return d3.scaleLinear(this.yDomain, this.yRange); } @cached get yAxis() { return d3.axisLeft(this.yScale); } @cached get xRange() { return [this.marginLeft, this.width - this.marginRight]; } @cached get xDomain() { return this.data.mapBy('category.name'); } @cached get xScale() { return d3.scaleBand().domain(this.xDomain).range(this.xRange); } @cached get xAxis() { return d3.axisBottom(this.xScale).tickSizeOuter(0); } // ... }
  28. Create data-to- pixel mappings The @d3/array package has handy stats

    functions The @d3/scale package can make a variety of scale functions from a variety of data types import Component from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; import { action } from '@ember/object'; import * as d3 from 'd3'; export default class BarChart extends Component { // ... @cached get yRange() { return [this.height - this.marginBottom, this.marginTop]; } @cached get yDomain() { return [0, 10]; } @cached get yScale() { return d3.scaleLinear(this.yDomain, this.yRange); } @cached get yAxis() { return d3.axisLeft(this.yScale); } @cached get xRange() { return [this.marginLeft, this.width - this.marginRight]; } @cached get xDomain() { return this.data.mapBy('category.name'); } @cached get xScale() { return d3.scaleBand().domain(this.xDomain).range(this.xRange); } @cached get xAxis() { return d3.axisBottom(this.xScale).tickSizeOuter(0); } // ... } Pixel-bounds
  29. Create data-to- pixel mappings The @d3/array package has handy stats

    functions The @d3/scale package can make a variety of scale functions from a variety of data types import Component from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; import { action } from '@ember/object'; import * as d3 from 'd3'; export default class BarChart extends Component { // ... @cached get yRange() { return [this.height - this.marginBottom, this.marginTop]; } @cached get yDomain() { return [0, 10]; } @cached get yScale() { return d3.scaleLinear(this.yDomain, this.yRange); } @cached get yAxis() { return d3.axisLeft(this.yScale); } @cached get xRange() { return [this.marginLeft, this.width - this.marginRight]; } @cached get xDomain() { return this.data.mapBy('category.name'); } @cached get xScale() { return d3.scaleBand().domain(this.xDomain).range(this.xRange); } @cached get xAxis() { return d3.axisBottom(this.xScale).tickSizeOuter(0); } // ... } Pixel-bounds Data bounds
  30. Create data-to- pixel mappings The @d3/array package has handy stats

    functions The @d3/scale package can make a variety of scale functions from a variety of data types import Component from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; import { action } from '@ember/object'; import * as d3 from 'd3'; export default class BarChart extends Component { // ... @cached get yRange() { return [this.height - this.marginBottom, this.marginTop]; } @cached get yDomain() { return [0, 10]; } @cached get yScale() { return d3.scaleLinear(this.yDomain, this.yRange); } @cached get yAxis() { return d3.axisLeft(this.yScale); } @cached get xRange() { return [this.marginLeft, this.width - this.marginRight]; } @cached get xDomain() { return this.data.mapBy('category.name'); } @cached get xScale() { return d3.scaleBand().domain(this.xDomain).range(this.xRange); } @cached get xAxis() { return d3.axisBottom(this.xScale).tickSizeOuter(0); } // ... } Pixel-bounds Data bounds Data bounds can be ordinal
  31. Create axes The @d3/array package has handy stats functions The

    @d3/scale package can make a variety of scale functions from a variety of data types The @d3/axis package will create the SVG elements for an axis based on scales import Component from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; import { action } from '@ember/object'; import * as d3 from 'd3'; export default class BarChart extends Component { // ... @cached get yRange() { return [this.height - this.marginBottom, this.marginTop]; } @cached get yDomain() { return [0, 10]; } @cached get yScale() { return d3.scaleLinear(this.yDomain, this.yRange); } @cached get yAxis() { return d3.axisLeft(this.yScale); } @cached get xRange() { return [this.marginLeft, this.width - this.marginRight]; } @cached get xDomain() { return this.data.mapBy('category.name'); } @cached get xScale() { return d3.scaleBand().domain(this.xDomain).range(this.xRange); } @cached get xAxis() { return d3.axisBottom(this.xScale).tickSizeOuter(0); } // ... }
  32. Decorate Data Use a cached getter and the familiar derived

    data pattern Use our d3 scale functions to map values to pixels They’re just functions! import Component from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; import { action } from '@ember/object'; import * as d3 from 'd3'; export default class BarChart extends Component { // ... // Apply geometric computations on the dataset for use in the template @cached get computedData() { return this.data.map((d) => ({ ...d, x: this.xScale(d.category.name) + 5, y: this.yScale(d.avgScore), height: this.yScale(0) - this.yScale(d.avgScore), width: this.xScale.bandwidth() - 10, })); } // ... }
  33. What about axes? They aren’t just data, they are SVG

    Nodes We can use a modifier to get a reference to the SVG to do DOM things in JS <svg width={{this.width}} height={{this.height}} viewBox='0 0 {{this.width}} {{this.height}}' style='max-width:100%; height: auto; overflow: visible' {{did-insert this.mountElements}} > <g class='y-axis' transform='translate({{this.marginLeft}},0)'> <text x='-{{this.marginLeft}}' y='10' fill='currentColor' text-anchor='end' >{{this.yLabel}}</text> </g> <g fill='{{this.color}}'> {{#each this.computedData as |bar|}} <rect x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g class='x-axis' transform='translate(0, {{this.xAxisOffset}})'></g> </svg>
  34. D3 selections Create a d3 wrapper around an element Select

    child nodes Use “call” to apply arbitrary functions, such as the axes import Component from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; import { action } from '@ember/object'; import * as d3 from 'd3'; export default class BarChart extends Component { // ... @action mountElements(el) { const $el = d3.select(el); $el .select('.y-axis') .call(this.yAxis) .call((g) => g.select('.domain').remove()) .call((g) => g .selectAll('.tick line') .clone() .attr('x2', this.width - this.marginLeft - this.marginRight) .attr('stroke-opacity', 0.1) ); $el .select('.x-axis') .call(this.xAxis) .call((g) => g .selectAll('.tick text') .attr('text-anchor', 'end') .attr('transform', 'rotate(-45)') ); } }
  35. Refactor re-cap How we used D3 • @d3/array: statistics helper

    functions (including d3.mean) • @d3/scale: mapper functions to translate from data-space to pixel-space • @d3/axis: DOM-emitting function that creates the marks for an axis pre- positioned according to a scale.
  36. Data Driven Documents Fetching and parsing data Transforming data (math,

    spatial analysis, color, and other utilities)
  37. Data Driven Documents Fetching and parsing data Transforming data (math,

    spatial analysis, color, and other utilities) Drawing things to the DOM (HTML, SVG, and Canvas)
  38. Data Driven Documents d3-dsv d3-fetch d3-array d3-color d3-contour d3-delaunay d3-dispatch

    d3-drag d3-ease d3-force d3-format d3-geo d3-hierarchy d3-interpolate d3-path d3-polygon d3-quadtree d3-random d3-scale d3-scale-chromatic d3-shape d3-time d3-time-format d3-timer d3-axis d3-brush d3-chord d3-selection d3-transition d3-zoom
  39. let data = [ { "Title": "Adaptation", "Distributor": "Sony Pictures",

    "Genre": "Comedy", "Worldwide_Gross": 22498520, "Rating": 91 }, { "Title": "Air Bud", "Distributor": "Walt Disney Pictures", "Genre": "Comedy", "Worldwide_Gross": 27555061, "Rating": 45 }, { "Title": "Air Force One", "Distributor": "Sony Pictures", "Genre": "Action", "Worldwide_Gross": 315268353, "Rating": 78 }, ... ]; { data: [undefined, Map(3)], children: [ { data: ["Sony Pictures", Map(3)], children: [...], depth: 1, height: 1, parent: {...} }. { data: ["Walt Disney Pictures", Map(2)], children: [...], depth: 1, height: 1, parent: {...} }. { data: ["Warner Bros.", Map(3)], children: [...], depth: 1, height: 1, parent: {...} } ], depth: 0, height: 2, parent: null } Data Driven Documents
  40. Data Driven Documents d3-dsv d3-fetch d3-array d3-color d3-contour d3-delaunay d3-dispatch

    d3-drag d3-ease d3-force d3-format d3-geo d3-hierarchy d3-interpolate d3-path d3-polygon d3-quadtree d3-random d3-scale d3-scale-chromatic d3-shape d3-time d3-time-format d3-timer d3-axis d3-brush d3-chord d3-selection d3-transition d3-zoom
  41. Data Driven Documents d3-dsv d3-fetch d3-array d3-color d3-contour d3-delaunay d3-dispatch

    d3-drag d3-ease d3-force d3-format d3-geo d3-hierarchy d3-interpolate d3-path d3-polygon d3-quadtree d3-random d3-scale d3-scale-chromatic d3-shape d3-time d3-time-format d3-timer d3-axis d3-brush d3-chord d3-selection d3-transition d3-zoom
  42. Data Driven Documents d3-dsv d3-fetch d3-array d3-color d3-contour d3-delaunay d3-dispatch

    d3-drag d3-ease d3-force d3-format d3-geo d3-hierarchy d3-interpolate d3-path d3-polygon d3-quadtree d3-random d3-scale d3-scale-chromatic d3-shape d3-time d3-time-format d3-timer d3-axis d3-brush d3-chord d3-selection d3-transition d3-zoom
  43. Data Driven Documents d3-dsv d3-fetch d3-array d3-color d3-contour d3-delaunay d3-dispatch

    d3-drag d3-ease d3-force d3-format d3-geo d3-hierarchy d3-interpolate d3-path d3-polygon d3-quadtree d3-random d3-scale d3-scale-chromatic d3-shape d3-time d3-time-format d3-timer d3-axis d3-brush d3-chord d3-selection d3-transition d3-zoom
  44. Refactoring Styles Inline-styles Presentation attributes <svg width={{this.width}} height={{this.height}} viewBox='0 0

    {{this.width}} {{this.height}}' style='max-width:100%; height: auto; overflow: visible' {{did-insert this.mountElements}} > <g class='y-axis' transform='translate({{this.marginLeft}},0)'> <text x='-{{this.marginLeft}}' y='10' fill='currentColor' text-anchor='end' >{{this.yLabel}}</text> </g> <g fill='{{this.color}}'> {{#each this.computedData as |bar|}} <rect x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g class='x-axis' transform='translate(0, {{this.xAxisOffset}})'></g> </svg>
  45. Refactoring Styles Inline-styles Presentation attributes Styling in JS import Component

    from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; import { action } from '@ember/object'; import * as d3 from 'd3'; export default class BarChart extends Component { // ... @action mountElements(el) { const $el = d3.select(el); $el .select('.y-axis') .call(this.yAxis) .call((g) => g.select('.domain').remove()) .call((g) => g .selectAll('.tick line') .clone() .attr('x2', this.width - this.marginLeft - this.marginRight) .attr('stroke-opacity', 0.1) ); $el .select('.x-axis') .call(this.xAxis) .call((g) => g .selectAll('.tick text') .attr('text-anchor', 'end') .attr('transform', 'rotate(-45)') ); } }
  46. Refactoring Styles Inline-styles Presentation attributes Styling in JS These can

    all be CSS .bar-chart { width: 100%; height: 100%; overflow: visible; } .bar-chart .x-axis text, .bar-chart .y-axis text { text-anchor: end; fill: currentColor; } .bar-chart .gridlines line { stroke-opacity: 0.1; } .bar-chart .y-axis .domain, .bar-chart .gridlines .domain { display: none; } .bar-chart .bars { fill: #e74500; } .bar-chart .x-axis text { transform: translate(-5px) rotate(-45deg); font-size: 16px; }
  47. Tada! A bar chart using Ember & D3 Avg rating

    of popular addons by category Data from emberobserver.com
  48. <svg {{did-insert this.mount}}></svg> import Component from '@glimmer/component'; import { action

    } from '@ember/object'; import * as d3 from 'd3'; export default class D3BarChart extends Component { yLabel = 'Avg. Rating'; @action mount(el) { // Only chart the top 15 categories by number of addons let data = this.args.data.sortBy('addons.length').reverse().slice(0, 15); // Compute average score data.forEach((d) => { d.avgScore = d3.mean(d.addons.mapBy('score')); }); // Sort by average score descending data = data.sortBy('avgScore').reverse(); // Aesthetic measurements const height = 400; const width = 900; const marginTop = 20; const marginBottom = 30; const marginLeft = 40; const marginRight = 0; const color = '#e74500'; // Y-axis derivation const yRange = [height - marginBottom, marginTop]; const yDomain = [0, 10]; const yScale = d3.scaleLinear(yDomain, yRange); const yAxis = d3.axisLeft(yScale); // X-axis derivation const xRange = [marginLeft, width - marginRight]; const xDomain = data.mapBy('category.name'); const xScale = d3.scaleBand().domain(xDomain).range(xRange); const xAxis = d3.axisBottom(xScale).tickSizeOuter(0); // Hydrate svg element & decorate const svg = d3 .select(el) .attr('width', width) .attr('height', height) .attr('viewBox', [0, 0, width, height]) .attr('style', 'max-width:100%; height: auto; overflow: visible'); // Add the y-axis svg .append('g') .attr('transform', `translate(${marginLeft},0)`) .call(yAxis) .call((g) => g.select('.domain').remove()) .call((g) => g .selectAll('.tick line') .clone() .attr('x2', width - marginLeft - marginRight) .attr('stroke-opacity', 0.1) ) .call((g) => g .append('text') .attr('x', -marginLeft) .attr('y', 10) .attr('fill', 'currentColor') .attr('text-anchor', 'end') .text(this.yLabel) ); // Add bars svg .append('g') .attr('fill', color) .selectAll('rect') .data(data) .join('rect') .attr('x', (d) => xScale(d.category.name) + 5) .attr('y', (d) => yScale(d.avgScore)) .attr('height', (d) => yScale(0) - yScale(d.avgScore)) .attr('width', xScale.bandwidth() - 10); // Add the x-axis svg .append('g') .attr('transform', `translate(0,${height - marginBottom})`) .call(xAxis) .call((g) => g .selectAll('.tick text') .attr('text-anchor', 'end') .attr('transform', 'rotate(-45)') ); } } <svg class='bar-chart bar-chart--deck' {{did-insert this.mountElements}}> <g class='y-axis' transform='translate({{this.marginLeft}},0)'> <text>{{this.yLabel}}</text> </g> <g class='gridlines' transform='translate({{this.marginLeft}},0)'></g> <g class='bars'> {{#each this.computedData as |bar|}} <rect x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g class='x-axis' transform='translate(0, {{this.xAxisOffset}})'></g> </svg> import Component from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; import { action } from '@ember/object'; import * as d3 from 'd3'; export default class BarChartCSS extends Component { yLabel = 'Avg. Rating'; @tracked height = 1; @tracked width = 1; @tracked marginTop = 20; @tracked marginBottom = 30; @tracked marginLeft = 40; @tracked marginRight = 0; @cached get xAxisOffset() { return Math.max(0, this.height - this.marginBottom); } @cached get data() { // Only chart the top 15 categories by number of addons const data = this.args.data.sortBy('addons.length').reverse().slice(0, 15); // Compute average score data.forEach((d) => { d.avgScore = d3.mean(d.addons.mapBy(‘score')); }); // Sort by average score descending return data.sortBy('avgScore').reverse(); } @cached get yRange() { return [this.height - this.marginBottom, this.marginTop]; } @cached get yDomain() { return [0, 10]; } @cached get yScale() { return d3.scaleLinear(this.yDomain, this.yRange);} @cached get yAxis() { return d3.axisLeft(this.yScale); } @cached get yGridlines() { return d3 .axisLeft(this.yScale) .tickSize(-this.width + this.marginLeft + this.marginRight) .tickFormat(''); } @cached get xRange() { return [this.marginLeft, this.width - this.marginRight]; } @cached get xDomain() { return this.data.mapBy(‘category.name'); } @cached get xScale() { return d3.scaleBand().domain(this.xDomain).range(this.xRange); } @cached get xAxis() { return d3.axisBottom(this.xScale).tickSizeOuter(0); } // Apply geometric computations on the dataset for use in the template @cached get computedData() { return this.data.map((d) => ({ ...d, x: this.xScale(d.category.name) + 5, y: this.yScale(d.avgScore), height: Math.max(0, this.yScale(0) - this.yScale(d.avgScore)), width: Math.max(0, this.xScale.bandwidth() - 10), })); } @action mountElements(el) { this.width = el.clientWidth; this.height = el.clientHeight; const $el = d3.select(el); $el.select('.y-axis').call(this.yAxis); $el.select('.gridlines').call(this.yGridlines); $el.select('.x-axis').call(this.xAxis); } } .bar-chart { width: 100%; height: 100%; overflow: visible; } .bar-chart .x-axis text, .bar-chart .y-axis text { text-anchor: end; fill: currentColor; } .bar-chart .gridlines line { stroke-opacity: 0.1; } .bar-chart .y-axis .domain, .bar-chart .gridlines .domain { display: none; } .bar-chart .bars { fill: #e74500; } .bar-chart .x-axis text { transform: translate(-5px) rotate(-45deg); font-size: 16px; } Just D3 Patterns D3 + Ember Patterns
  49. <svg {{did-insert this.mount}}></svg> import Component from '@glimmer/component'; import { action

    } from '@ember/object'; import * as d3 from 'd3'; export default class D3BarChart extends Component { yLabel = 'Avg. Rating'; @action mount(el) { // Only chart the top 15 categories by number of addons let data = this.args.data.sortBy('addons.length').reverse().slice(0, 15); // Compute average score data.forEach((d) => { d.avgScore = d3.mean(d.addons.mapBy('score')); }); // Sort by average score descending data = data.sortBy('avgScore').reverse(); // Aesthetic measurements const height = 400; const width = 900; const marginTop = 20; const marginBottom = 30; const marginLeft = 40; const marginRight = 0; const color = '#e74500'; // Y-axis derivation const yRange = [height - marginBottom, marginTop]; const yDomain = [0, 10]; const yScale = d3.scaleLinear(yDomain, yRange); const yAxis = d3.axisLeft(yScale); // X-axis derivation const xRange = [marginLeft, width - marginRight]; const xDomain = data.mapBy('category.name'); const xScale = d3.scaleBand().domain(xDomain).range(xRange); const xAxis = d3.axisBottom(xScale).tickSizeOuter(0); // Hydrate svg element & decorate const svg = d3 .select(el) .attr('width', width) .attr('height', height) .attr('viewBox', [0, 0, width, height]) .attr('style', 'max-width:100%; height: auto; overflow: visible'); // Add the y-axis svg .append('g') .attr('transform', `translate(${marginLeft},0)`) .call(yAxis) .call((g) => g.select('.domain').remove()) .call((g) => g .selectAll('.tick line') .clone() .attr('x2', width - marginLeft - marginRight) .attr('stroke-opacity', 0.1) ) .call((g) => g .append('text') .attr('x', -marginLeft) .attr('y', 10) .attr('fill', 'currentColor') .attr('text-anchor', 'end') .text(this.yLabel) ); // Add bars svg .append('g') .attr('fill', color) .selectAll('rect') .data(data) .join('rect') .attr('x', (d) => xScale(d.category.name) + 5) .attr('y', (d) => yScale(d.avgScore)) .attr('height', (d) => yScale(0) - yScale(d.avgScore)) .attr('width', xScale.bandwidth() - 10); // Add the x-axis svg .append('g') .attr('transform', `translate(0,${height - marginBottom})`) .call(xAxis) .call((g) => g .selectAll('.tick text') .attr('text-anchor', 'end') .attr('transform', 'rotate(-45)') ); } } <svg class='bar-chart bar-chart--deck' {{did-insert this.mountElements}}> <g class='y-axis' transform='translate({{this.marginLeft}},0)'> <text>{{this.yLabel}}</text> </g> <g class='gridlines' transform='translate({{this.marginLeft}},0)'></g> <g class='bars'> {{#each this.computedData as |bar|}} <rect x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g class='x-axis' transform='translate(0, {{this.xAxisOffset}})'></g> </svg> import Component from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; import { action } from '@ember/object'; import * as d3 from 'd3'; export default class BarChartCSS extends Component { yLabel = 'Avg. Rating'; @tracked height = 1; @tracked width = 1; @tracked marginTop = 20; @tracked marginBottom = 30; @tracked marginLeft = 40; @tracked marginRight = 0; @cached get xAxisOffset() { return Math.max(0, this.height - this.marginBottom); } @cached get data() { // Only chart the top 15 categories by number of addons const data = this.args.data.sortBy('addons.length').reverse().slice(0, 15); // Compute average score data.forEach((d) => { d.avgScore = d3.mean(d.addons.mapBy(‘score')); }); // Sort by average score descending return data.sortBy('avgScore').reverse(); } @cached get yRange() { return [this.height - this.marginBottom, this.marginTop]; } @cached get yDomain() { return [0, 10]; } @cached get yScale() { return d3.scaleLinear(this.yDomain, this.yRange);} @cached get yAxis() { return d3.axisLeft(this.yScale); } @cached get yGridlines() { return d3 .axisLeft(this.yScale) .tickSize(-this.width + this.marginLeft + this.marginRight) .tickFormat(''); } @cached get xRange() { return [this.marginLeft, this.width - this.marginRight]; } @cached get xDomain() { return this.data.mapBy(‘category.name'); } @cached get xScale() { return d3.scaleBand().domain(this.xDomain).range(this.xRange); } @cached get xAxis() { return d3.axisBottom(this.xScale).tickSizeOuter(0); } // Apply geometric computations on the dataset for use in the template @cached get computedData() { return this.data.map((d) => ({ ...d, x: this.xScale(d.category.name) + 5, y: this.yScale(d.avgScore), height: Math.max(0, this.yScale(0) - this.yScale(d.avgScore)), width: Math.max(0, this.xScale.bandwidth() - 10), })); } @action mountElements(el) { this.width = el.clientWidth; this.height = el.clientHeight; const $el = d3.select(el); $el.select('.y-axis').call(this.yAxis); $el.select('.gridlines').call(this.yGridlines); $el.select('.x-axis').call(this.xAxis); } } .bar-chart { width: 100%; height: 100%; overflow: visible; } .bar-chart .x-axis text, .bar-chart .y-axis text { text-anchor: end; fill: currentColor; } .bar-chart .gridlines line { stroke-opacity: 0.1; } .bar-chart .y-axis .domain, .bar-chart .gridlines .domain { display: none; } .bar-chart .bars { fill: #e74500; } .bar-chart .x-axis text { transform: translate(-5px) rotate(-45deg); font-size: 16px; } Just D3 Patterns D3 + Ember Patterns
  50. If you want to go fast, go alone do whatever

    you want. If you want to go far, go together separate concerns.
  51. A product designer walks by… Avg rating of popular addons

    by category Data from emberobserver.com
  52. A product designer walks by… Avg rating of popular addons

    by category Data from emberobserver.com
  53. A product designer walks by… Avg rating of popular addons

    by category Data from emberobserver.com
  54. An a11y specialist walks by… Making a data viz accessible

    is highly situational There are many approaches and criteria to consider <svg class='bar-chart bar-chart--deck' {{did-insert this.mountElements}}> <g class='y-axis' transform='translate({{this.marginLeft}},0)'> <text>{{this.yLabel}}</text> </g> <g class='gridlines' transform='translate({{this.marginLeft}},0)'></g> <g class='bars'> {{#each this.computedData as |bar|}} <rect x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g class='x-axis' transform='translate(0, {{this.xAxisOffset}})'></g> </svg>
  55. An a11y specialist walks by… Making a data viz accessible

    is highly situational There are many approaches and criteria to consider <svg role='group' class='bar-chart bar-chart--deck' {{did-insert this.mountElements}} > <title>Average score of popular addons by category</title> <g aria-hidden='true' class='y-axis' transform='translate({{this.marginLeft}},0)' > <text>{{this.yLabel}}</text> </g> <g class='gridlines' transform='translate({{this.marginLeft}},0)'></g> <g class='bars'> {{#each this.computedData as |bar|}} <rect role='cell' aria-label='{{bar.category.name}} {{bar.formattedAvgScore}}' x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g aria-hidden='true' class='x-axis' transform='translate(0, {{this.xAxisOffset}})' ></g> </svg>
  56. An a11y specialist walks by… Making a data viz accessible

    is highly situational There are many approaches and criteria to consider <svg role='group' class='bar-chart bar-chart--deck' {{did-insert this.mountElements}} > <title>Average score of popular addons by category</title> <g aria-hidden='true' class='y-axis' transform='translate({{this.marginLeft}},0)' > <text>{{this.yLabel}}</text> </g> <g class='gridlines' transform='translate({{this.marginLeft}},0)'></g> <g class='bars'> {{#each this.computedData as |bar|}} <rect role='cell' aria-label='{{bar.category.name}} {{bar.formattedAvgScore}}' x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g aria-hidden='true' class='x-axis' transform='translate(0, {{this.xAxisOffset}})' ></g> </svg> Give the svg a role so it can be traversed
  57. An a11y specialist walks by… Making a data viz accessible

    is highly situational There are many approaches and criteria to consider <svg role='group' class='bar-chart bar-chart--deck' {{did-insert this.mountElements}} > <title>Average score of popular addons by category</title> <g aria-hidden='true' class='y-axis' transform='translate({{this.marginLeft}},0)' > <text>{{this.yLabel}}</text> </g> <g class='gridlines' transform='translate({{this.marginLeft}},0)'></g> <g class='bars'> {{#each this.computedData as |bar|}} <rect role='cell' aria-label='{{bar.category.name}} {{bar.formattedAvgScore}}' x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g aria-hidden='true' class='x-axis' transform='translate(0, {{this.xAxisOffset}})' ></g> </svg> Give the svg a role so it can be traversed Give the svg a title/label
  58. An a11y specialist walks by… Making a data viz accessible

    is highly situational There are many approaches and criteria to consider <svg role='group' class='bar-chart bar-chart--deck' {{did-insert this.mountElements}} > <title>Average score of popular addons by category</title> <g aria-hidden='true' class='y-axis' transform='translate({{this.marginLeft}},0)' > <text>{{this.yLabel}}</text> </g> <g class='gridlines' transform='translate({{this.marginLeft}},0)'></g> <g class='bars'> {{#each this.computedData as |bar|}} <rect role='cell' aria-label='{{bar.category.name}} {{bar.formattedAvgScore}}' x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g aria-hidden='true' class='x-axis' transform='translate(0, {{this.xAxisOffset}})' ></g> </svg> Give the svg a role so it can be traversed Give the svg a title/label Prevent axes ticks from being announced
  59. An a11y specialist walks by… Making a data viz accessible

    is highly situational There are many approaches and criteria to consider <svg role='group' class='bar-chart bar-chart--deck' {{did-insert this.mountElements}} > <title>Average score of popular addons by category</title> <g aria-hidden='true' class='y-axis' transform='translate({{this.marginLeft}},0)' > <text>{{this.yLabel}}</text> </g> <g class='gridlines' transform='translate({{this.marginLeft}},0)'></g> <g class='bars'> {{#each this.computedData as |bar|}} <rect role='cell' aria-label='{{bar.category.name}} {{bar.formattedAvgScore}}' x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g aria-hidden='true' class='x-axis' transform='translate(0, {{this.xAxisOffset}})' ></g> </svg> Give the svg a role so it can be traversed Give the svg a title/label Prevent axes ticks from being announced Give rects a cell that minimizes super fl uous announcement
  60. An a11y specialist walks by… Making a data viz accessible

    is highly situational There are many approaches and criteria to consider <svg role='group' class='bar-chart bar-chart--deck' {{did-insert this.mountElements}} > <title>Average score of popular addons by category</title> <g aria-hidden='true' class='y-axis' transform='translate({{this.marginLeft}},0)' > <text>{{this.yLabel}}</text> </g> <g class='gridlines' transform='translate({{this.marginLeft}},0)'></g> <g class='bars'> {{#each this.computedData as |bar|}} <rect role='cell' aria-label='{{bar.category.name}} {{bar.formattedAvgScore}}' x={{bar.x}} y={{bar.y}} height={{bar.height}} width={{bar.width}} ></rect> {{/each}} </g> <g aria-hidden='true' class='x-axis' transform='translate(0, {{this.xAxisOffset}})' ></g> </svg> Give the svg a role so it can be traversed Give the svg a title/label Prevent axes ticks from being announced Give rects a cell that minimizes super fl uous announcement Make sure the aria label includes both the X and the Y values
  61. Contextual Components Common scales and transformations happen in the parent

    component <svg role='group' class='bar-chart bar-chart--deck' {{did-insert this.onInsert}} > <title>{{@title}}</title> <g aria-hidden='true' class='y-axis' transform='translate({{this.yAxisWidth}}, 0)' > <text>{{@yLabel}}</text> </g> <g class='gridlines' transform='translate({{this.yAxisWidth}}, 0)'></g> <g class='canvas'> {{yield (hash Bars=(component 'cartesian-chart/bars' data=this.data xScale=this.xScale yScale=this.yScale ) ) }} </g> <g aria-hidden='true' class='x-axis' transform='translate(0, {{this.xAxisOffset}})' ></g> </svg>
  62. Contextual Components Common scales and transformations happen in the parent

    component And then get partially applied to yielded components <svg role='group' class='bar-chart bar-chart--deck' {{did-insert this.onInsert}} > <title>{{@title}}</title> <g aria-hidden='true' class='y-axis' transform='translate({{this.yAxisWidth}}, 0)' > <text>{{@yLabel}}</text> </g> <g class='gridlines' transform='translate({{this.yAxisWidth}}, 0)'></g> <g class='canvas'> {{yield (hash Bars=(component 'cartesian-chart/bars' data=this.data xScale=this.xScale yScale=this.yScale ) ) }} </g> <g aria-hidden='true' class='x-axis' transform='translate(0, {{this.xAxisOffset}})' ></g> </svg>
  63. Contextual Components Common scales and transformations happen in the parent

    component And then get partially applied to yielded components Which makes constructing a chart much less involved <div style='width:900px; height:500px;'> <CartesianChart @data={{this.addonsByCategory}} @xProp='category.name' @xLimit={{15}} @yProp='addons.score' @yDomain={{array 0 10}} @yAgg='mean' as |C| > <C.Bars /> </CartesianChart> </div>
  64. A data viz platform Chart types plotted on a cartesian

    canvas https://datavizproject.com/
  65. Navigating the world of web-based data viz by Krist Wongsuphasawat

    Composed contextual components But why not use one of these?
  66. Navigating the world of web-based data viz by Krist Wongsuphasawat

    Composed contextual components But why not use one of these?
  67. That bar chart, one last time. So I’ve been thinking…

    Avg rating of popular addons by category Data from emberobserver.com
  68. Something like this? Yes! You’re such a pleasure to work

    with! Avg rating of popular addons by category Data from emberobserver.com
  69. Marimekko Still a cartesian base Marimekko specific arg @xSizeProp aggregation

    may be useful in other contexts <div style='width:1100px; height:500px;'> <CartesianChart @data={{this.addonsByCategory}} @xProp='category.name' @xLimit={{15}} @xSizeProp='addons.length' @yProp='addons.score' @yDomain={{array 0 10}} @yAgg='mean' as |C| > <C.Marimekko @noun='addons' /> </CartesianChart> </div>
  70. Marimekko Still a cartesian base Marimekko specific arg @xSizeProp aggregation

    may be useful in other contexts More utility from d3 <div style='width:1100px; height:500px;'> <CartesianChart @data={{this.addonsByCategory}} @xProp='category.name' @xLimit={{15}} @xSizeProp='addons.length' @yProp='addons.score' @yDomain={{array 0 10}} @yAgg='mean' as |C| > <C.Marimekko @noun='addons' /> </CartesianChart> </div> @cached get xDomainSum() { return [0, d3.sum(this.data, (d) => d.$size)]; } @cached get xScale() { return d3.scaleBand().domain(this.xDomain).range(this.xRange); } @cached get xScaleLinear() { return d3.scaleLinear().domain(this.xDomainSum).range(this.xRange); } @cached get xAxis() { const xScale = this.args.xSizeProp ? this.xScaleLinear : this.xScale; const axis = d3.axisBottom(xScale).tickSizeOuter(0); if (this.args.xSizeProp) { axis.tickValues([0, ...d3.cumsum(this.data, (d) => d.$size)]); } return axis; }
  71. Something like this? Yes! You’re such a pleasure to work

    with! Avg rating of popular addons by category Data from emberobserver.com Is it good?
  72. The benefits of CSS For animation • Just a couple

    lines of code • Durable system for cancelation & updates • Hardware accelerated (often) • Use the platform • CSS Variables make it more powerful than ever!
  73. D3 Transition Mimics @d3/selection Coordinates across selections Smoothly interpolates various

    data types const svg = d3.create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [0, 0, width, height]) .attr("style", "max-width: 100%; height: auto; height: intrinsic;"); const zx = x.copy(); const line = d3.line() .x(d => zx(d.date)) .y(d => y(d.close)); const path = svg.append("path") .attr("fill", "none") .attr("stroke", "steelblue") .attr("stroke-width", 1.5) .attr("stroke-miterlimit", 1) .attr("d", line(data)); const gx = svg.append("g") .call(xAxis, zx); const gy = svg.append("g") .call(yAxis, y); return Object.assign(svg.node(), { update(domain) { const t = svg.transition().duration(750); zx.domain(domain); gx.transition(t).call(xAxis, zx); path.transition(t).attr("d", line(data)); } });
  74. D3 Transition Mimics @d3/selection A) Smoothly interpolates various data types

    B) Coordinates across selections But who’s in charge here? const svg = d3.create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [0, 0, width, height]) .attr("style", "max-width: 100%; height: auto; height: intrinsic;"); const zx = x.copy(); const line = d3.line() .x(d => zx(d.date)) .y(d => y(d.close)); const path = svg.append("path") .attr("fill", "none") .attr("stroke", "steelblue") .attr("stroke-width", 1.5) .attr("stroke-miterlimit", 1) .attr("d", line(data)); const gx = svg.append("g") .call(xAxis, zx); const gy = svg.append("g") .call(yAxis, y); return Object.assign(svg.node(), { update(domain) { const t = svg.transition().duration(750); zx.domain(domain); gx.transition(t).call(xAxis, zx); path.transition(t).attr("d", line(data)); } });
  75. Data Driven Documents d3-dsv d3-fetch d3-array d3-color d3-contour d3-delaunay d3-dispatch

    d3-drag d3-ease d3-force d3-format d3-geo d3-hierarchy d3-interpolate d3-path d3-polygon d3-quadtree d3-random d3-scale d3-scale-chromatic d3-shape d3-time d3-time-format d3-timer d3-axis d3-brush d3-chord d3-selection d3-transition d3-zoom
  76. Coordination is a concurrency problem So how about Ember Concurrency?

    • Control data over time • Ember is already handling re-renders when data changes • Orchestrate sequences of behaviors/rules
  77. EC for animation Run a task until a timer runs

    out @task *animate(oldData, newData, duration = 500) { const start = window.performance.now(); let now = 0; while (now < duration) { now = window.performance.now() - start; yield window.requestAnimationFrame(); } }
  78. EC for animation Run a task until a timer runs

    out requestAnimationFrame isn’t promise-aware const raf = () => new Promise((resolve) => window.requestAnimationFrame(resolve)); @task *animate(oldData, newData, duration = 500) { const start = window.performance.now(); let now = 0; while (now < duration) { now = window.performance.now() - start; yield raf(); } }
  79. EC for animation Run a task until a timer runs

    out requestAnimationFrame isn’t promise-aware Interpolate data const raf = () => new Promise((resolve) => window.requestAnimationFrame(resolve)); @task *animate(oldData, newData, duration = 500) { const start = window.performance.now(); const interpolator = d3.interpolate(oldData, newData); let now = 0; while (now < duration) { now = window.performance.now() - start; this._data = interpolator(Math.min(1, now / duration)); this.mountElements(this.element); yield raf(); } }
  80. EC for animation Run a task until a timer runs

    out requestAnimationFrame isn’t promise-aware Interpolate data const raf = () => new Promise((resolve) => window.requestAnimationFrame(resolve)); @task *animate(oldData, newData, duration = 500) { const start = window.performance.now(); const interpolator = d3.interpolate(oldData, newData); let now = 0; while (now < duration) { now = window.performance.now() - start; this._data = interpolator(Math.min(1, now / duration)); this.mountElements(this.element); yield raf(); } } Setting a tracked property
  81. EC for animation Run a task until a timer runs

    out requestAnimationFrame isn’t promise-aware Interpolate data const raf = () => new Promise((resolve) => window.requestAnimationFrame(resolve)); @task *animate(oldData, newData, duration = 500) { const start = window.performance.now(); const interpolator = d3.interpolate(oldData, newData); let now = 0; while (now < duration) { now = window.performance.now() - start; this._data = interpolator(Math.min(1, now / duration)); this.mountElements(this.element); yield raf(); } } Setting a tracked property Re-mount d3 controlled elements that aren’t autotracking aware
  82. Other things to consider When animating elements • What happens

    when a new data point is added to a data set? • What happens when one is removed?
  83. Data Viz is its own deep and rich field of

    study Frameworks Data Viz