├── README.md ├── artificial-chart.css ├── artificial-chart.js ├── demo.html ├── demo.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # artificial-chart 2 | 3 | The SVG D3.js based charting solution used for the Jamstack Community Survey. 4 | 5 | ## [Demo](https://zachleat.github.io/artificial-chart/demo.html) 6 | 7 | All of the charts are viewable on https://jamstack.org/survey/2021/ 8 | 9 | ## Features 10 | 11 | * Responsive Web Design friendly charts 12 | * Progressive-enhancement friendly, draws data from HTML tables. Warning: does not include a No-JS visual view. 13 | * Horizontal and vertical bar chart type 14 | * Supports grouped data visualized side by side or stacked. 15 | * Supports proportional data (scaling to 100%) 16 | * Option to show inline labels inside or outside of the bars. 17 | * Bubble chart type 18 | * Styles nicely with gradients 19 | * Customizable margins 20 | * Wrapping on axis labels 21 | * Supports customized label precision 22 | * Supports auto-generated HTML legends (outside of the SVG). 23 | * Uses IntersectionObserver and ResizeObserver (when available) for better performance on initialization and resize. 24 | 25 | ## Changelog 26 | 27 | * `v2.0.0` swap to ESM 28 | 29 | ## Usage 30 | 31 | ```js 32 | import "https://d3js.org/d3.v7.min.js"; 33 | 34 | import { HorizontalBar, VerticalBar, Bubble } from "./artificial-chart.js"; 35 | 36 | new VerticalBar("chart-id", "source-table-id", { 37 | showInlineBarValues: "outside", 38 | }); 39 | ``` -------------------------------------------------------------------------------- /artificial-chart.css: -------------------------------------------------------------------------------- 1 | .artfc-placeholder { 2 | width: 100%; 3 | height: 450px; 4 | } 5 | .artfc-placeholder-large { 6 | height: 660px; 7 | } 8 | .artfc-placeholder-xl { 9 | height: 1000px; 10 | } 11 | .artfc-legend-placeholder { 12 | min-height: 1.71875em; /* 27.5px /16 */ 13 | } 14 | 15 | .artfc { 16 | position: relative; 17 | } 18 | .artfc > .artfc-legend { 19 | position: absolute; 20 | top: 0; 21 | right: 0; 22 | } 23 | 24 | .artfc-legend { 25 | display: flex; 26 | justify-content: flex-end; 27 | gap: .5em; 28 | font-size: 0.8125em; /* 13px /16 */ 29 | font-weight: 600; 30 | } 31 | .artfc-legend button { 32 | font-weight: inherit; 33 | } 34 | .artfc-legend-wrap .artfc-legend { 35 | flex-wrap: wrap; 36 | justify-content: center; 37 | margin-left: auto; 38 | margin-right: auto; 39 | } 40 | .artfc + .artfc-legend-placeholder .artfc-legend { 41 | justify-content: center; 42 | } 43 | .artfc-legend > * { 44 | border-radius: .25em; 45 | padding: .25em .5em; 46 | } 47 | 48 | .artfc .tick text { 49 | font-size: 1.3em; /* 13px /10 */ 50 | } 51 | .artfc .tick line { 52 | shape-rendering: crispEdges; 53 | stroke: rgba(255,255,255,.15); 54 | } 55 | .artfc-bubble .tick:nth-child(2n) line { 56 | stroke: rgba(255,255,255,.22); 57 | } 58 | .artfc-bubble .tick:nth-child(2n+1) line { 59 | stroke: rgba(255,255,255,.1); 60 | } 61 | .artfc-bubble .artfc-xaxis :first-child line, 62 | .artfc-bubble .artfc-yaxis .tick:last-child line { 63 | stroke: #737680; 64 | stroke-width: 2px; 65 | } 66 | .artfc-hbar .artfc-xaxis .tick:first-child line, 67 | .artfc-vbar .artfc-yaxis .tick:first-child line { 68 | stroke: #c5c5c9; 69 | stroke-width: 2px; 70 | } 71 | .artfc-hbar .artfc-xaxis .tick:first-child line { 72 | transform: translateX(-1px); 73 | } 74 | .artfc-vbar .artfc-yaxis .tick:first-child line { 75 | transform: translateY(1px); 76 | } 77 | .artfc-vbar .artfc-xaxis text, 78 | .artfc-hbar .artfc-yaxis text { 79 | --artfc-label-clamp: 2vw; 80 | font-size: 12px; 81 | font-size: clamp(12px, var(--artfc-label-clamp), 14px); 82 | font-weight: 600; 83 | } 84 | 85 | 86 | .artfc-inlinebarvalue { 87 | --artfc-label-clamp: 2vw; 88 | font-size: 12px; 89 | font-size: clamp(11px, var(--artfc-label-clamp), 16px); 90 | font-weight: 600; 91 | text-anchor: middle; 92 | } 93 | .artfc-inlinebarvalue-h { 94 | font-size: 16px; 95 | font-weight: 600; 96 | text-anchor: start; 97 | dominant-baseline: central; 98 | alignment-baseline: middle; 99 | } 100 | .artfc-inlinebarvalue-h.inside { 101 | text-anchor: end; 102 | } 103 | .artfc-inlinebarvalue-h.inside-offset { 104 | font-size: 14px; 105 | text-anchor: end; 106 | } 107 | 108 | /* Wrapped labels */ 109 | .artfc-yaxis text.artfc-label-wrapped { 110 | font-size: 13px; 111 | } 112 | 113 | /* Axis labels */ 114 | .artfc-axislabel { 115 | fill: #fff; 116 | text-anchor: end; 117 | font-weight: 700; 118 | } 119 | .artfc-axislabel-center { 120 | text-anchor: middle; 121 | } 122 | 123 | /* Bubble charts */ 124 | .artfc-bubblelabel { 125 | text-anchor: middle; 126 | dominant-baseline: central; 127 | font-size: 12px; 128 | font-weight: 600; 129 | } 130 | .artfc-bubblelabel.offset-l, 131 | .artfc-bubblelabel.offset-r { 132 | font-weight: 700; 133 | text-shadow: none; 134 | } 135 | .artfc-bubblelabel.offset-l { 136 | text-anchor: end; 137 | } 138 | .artfc-bubblelabel.offset-r { 139 | text-anchor: start; 140 | } 141 | .artfc-bubblelabelbg.offset { 142 | background-color: rgba(255,255,255,.4); 143 | } 144 | .artfc-bubble circle { 145 | fill-opacity: .85; 146 | } 147 | .artfc-bubble-active .artfc-bubblelabel, 148 | .artfc-bubble-active .artfc-bubblecircle { 149 | fill-opacity: .15; 150 | } 151 | .artfc-bubble-active .artfc-bubblelabel.active, 152 | .artfc-bubble-active .artfc-bubblecircle.active { 153 | fill-opacity: 1; 154 | } 155 | .artfc-bubble .artfc-yaxis .tick:last-child text { 156 | display: none; 157 | } 158 | .artfc-bubble .artfc-xaxis .tick text { 159 | transform: translateY(2px); 160 | } 161 | 162 | /* Color gradients */ 163 | .artfc-color-0 { 164 | fill: url(#gradient-sunrise-v); 165 | } 166 | .artfc-hbar .artfc-color-0 { 167 | fill: url(#gradient-sunrise-h); 168 | } 169 | .artfc-color-1 { 170 | fill: url(#gradient-blue-v); 171 | } 172 | .artfc-hbar .artfc-color-1 { 173 | fill: url(#gradient-blue-h); 174 | } 175 | .artfc-color-2 { 176 | fill: url(#gradient-sun-v); 177 | } 178 | .artfc-hbar .artfc-color-2 { 179 | fill: url(#gradient-sun-h); 180 | } 181 | .artfc-color-3 { 182 | fill: url(#gradient-seamist-v); 183 | } 184 | .artfc-hbar .artfc-color-3 { 185 | fill: url(#gradient-seamist-h); 186 | } 187 | .artfc-color-4 { 188 | fill: url(#gradient-hallows-v); 189 | } 190 | .artfc-hbar .artfc-color-4 { 191 | fill: url(#gradient-hallows-h); 192 | } 193 | .artfc-color-5 { 194 | fill: url(#gradient-bubblegum-v); 195 | } 196 | .artfc-hbar .artfc-color-5 { 197 | fill: url(#gradient-bubblegum-h); 198 | } 199 | .artfc-color-6 { 200 | fill: url(#gradient-purple-v); 201 | } 202 | .artfc-hbar .artfc-color-6 { 203 | fill: url(#gradient-purple-h); 204 | } 205 | .artfc-color-7 { 206 | fill: url(#gradient-air-v); 207 | } 208 | .artfc-hbar .artfc-color-7 { 209 | fill: url(#gradient-air-h); 210 | } 211 | .artfc-color-8 { 212 | fill: url(#gradient-pink-v); 213 | } 214 | .artfc-hbar .artfc-color-8 { 215 | fill: url(#gradient-pink-h); 216 | } 217 | .artfc-color-9 { 218 | fill: url(#gradient-leaves-v); 219 | } 220 | .artfc-hbar .artfc-color-9 { 221 | fill: url(#gradient-leaves-h); 222 | } 223 | .artfc-color-10 { 224 | fill: url(#gradient-haze-v); 225 | } 226 | .artfc-hbar .artfc-color-10 { 227 | fill: url(#gradient-haze-h); 228 | } 229 | .artfc-color-11 { 230 | fill: url(#gradient-gnat-v); 231 | } 232 | .artfc-hbar .artfc-color-11 { 233 | fill: url(#gradient-gnat-h); 234 | } 235 | .artfc-color-12 { 236 | fill: url(#gradient-fire-v); 237 | } 238 | .artfc-hbar .artfc-color-12 { 239 | fill: url(#gradient-fire-h); 240 | } 241 | .artfc-color-13 { 242 | fill: url(#gradient-ocean-v); 243 | } 244 | .artfc-hbar .artfc-color-13 { 245 | fill: url(#gradient-ocean-h); 246 | } 247 | .artfc-color-14 { 248 | fill: url(#gradient-night-v); 249 | } 250 | .artfc-hbar .artfc-color-14 { 251 | fill: url(#gradient-night-h); 252 | } 253 | .artfc-color-15 { 254 | fill: url(#gradient-dusk-v); 255 | } 256 | .artfc-hbar .artfc-color-15 { 257 | fill: url(#gradient-dusk-h); 258 | } 259 | 260 | /* Legend gradients */ 261 | .artfc-legend-0 { 262 | color: #fff; 263 | background: linear-gradient(352.65deg, #F0047F 1.39%, #FC814A 82.63%); 264 | } 265 | .artfc-legend-1 { 266 | color: #000; 267 | background: linear-gradient(47.9deg, #0090CA 6.17%, #00BFAD 79.63%); 268 | } 269 | .artfc-legend-2 { 270 | color: #000; 271 | background: linear-gradient(180deg, #FFC803 0%, #FC814A 100%); 272 | } 273 | .artfc-legend-3 { 274 | color: #000; 275 | background: linear-gradient(180deg, #78ECC2 0%, #00FFB2 100%); 276 | } 277 | .artfc-legend-4 { 278 | color: #000; 279 | background: linear-gradient(108.82deg, #DF4A1F 0%, #FFA278 90.74%); 280 | } 281 | .artfc-legend-5 { 282 | color: #000; 283 | background: linear-gradient(108.82deg, #6B38FB 0%, #CCB4FF 90.74%); 284 | } 285 | .artfc-legend-6 { 286 | color: #000; 287 | background: linear-gradient(108.82deg, #FD98BC 32.87%, #FFCCDE 90.74%); 288 | } 289 | .artfc-legend-7 { 290 | color: #000; 291 | background: linear-gradient(108.82deg, #03D0D0 0%, #B5FFF8 90.74%); 292 | } 293 | .artfc-legend-8 { 294 | color: #fff; 295 | background: linear-gradient(108.82deg, #C40468 0%, #FC2796 90.74%); 296 | } 297 | .artfc-legend-9 { 298 | color: #000; 299 | background: linear-gradient(180deg, #78F19A 0%, #13B110 100%); 300 | } 301 | .artfc-legend-10 { 302 | color: #000; 303 | background: linear-gradient(108.82deg, #91A5EE 37.71%, #D6DEFF 90.74%); 304 | } 305 | .artfc-legend-11 { 306 | color: #000; 307 | background: linear-gradient(108.82deg, #02C6B3 40.13%, #59F7E7 90.74%); 308 | } 309 | .artfc-legend-12 { 310 | color: #fff; 311 | background: linear-gradient(108.82deg, #FF0F00 0%, #FF928A 90.74%); 312 | } 313 | .artfc-legend-13 { 314 | color: #000; 315 | background: linear-gradient(180deg, #003EDD 0%, #6CDCFF 100%); 316 | } 317 | .artfc-legend-14 { 318 | color: #000; 319 | background: linear-gradient(108.82deg, #02465F 3.38%, #6AD7FF 90.74%); 320 | } 321 | .artfc-legend-15 { 322 | color: #fff; 323 | background: linear-gradient(108.82deg, #960000 0%, #E94242 92.82%); 324 | } 325 | .artfc-legend-16 { 326 | color: #fff; 327 | background: linear-gradient(108.82deg, #FF72CF 0%, #C92ECC 90.74%); 328 | } 329 | 330 | 331 | 332 | /* Overrides */ -------------------------------------------------------------------------------- /artificial-chart.js: -------------------------------------------------------------------------------- 1 | class ArtificialChart { 2 | constructor(targetId, options, className) { 3 | this.targetId = targetId; 4 | this.className = className; 5 | 6 | this.options = Object.assign({ 7 | showInlineBarValues: "inside", // inside, inside-offset, and outside supported 8 | showLegend: true, 9 | showAxisLabels: false, 10 | margin: {}, 11 | colors: [ 12 | "#F0047F", 13 | "#00BFAD", 14 | "#FFC803", 15 | "#78ECC2", 16 | "#DF4A1F", 17 | "#6B38FB", 18 | "#FD98BC", 19 | "#03D0D0", 20 | "#C40468", 21 | "#78F19A", 22 | "#91A5EE", 23 | "#02C6B3", 24 | "#FF0F00", 25 | "#003EDD", 26 | "#02465F", 27 | "#960000", 28 | "#FF72CF", 29 | ], 30 | // only applies when `showInlineBarValues: "inside"` 31 | labelColors: [ 32 | "#fff", 33 | "#000", 34 | "#000", 35 | "#000", 36 | "#000", 37 | "#000", 38 | "#000", 39 | "#000", 40 | "#fff", 41 | "#000", 42 | "#000", 43 | "#000", 44 | "#000", 45 | "#000", 46 | "#000", 47 | "#fff", 48 | "#fff", 49 | ], 50 | colorMod: 0, 51 | inlineLabelPad: 5, 52 | labelPrecision: 0, 53 | // TODO make this automatic by parsing `%` signs 54 | valueType: ["percentage"], 55 | sortLegend: false, 56 | highlightElementsFromLegend: false 57 | }, options); 58 | 59 | this.options.colors = this.normalizeColors(this.options.colors, this.options.colorMod); 60 | this.options.labelColors = this.normalizeColors(this.options.labelColors, this.options.colorMod); 61 | } 62 | 63 | onResize(callback) { 64 | if (!("ResizeObserver" in window)) { 65 | window.addEventListener("resize", () => { 66 | callback.call(this); 67 | }); 68 | return; 69 | } 70 | 71 | let resizeObserver = new ResizeObserver(entries => { 72 | for (let entry of entries) { 73 | // console.log( "resizing", this.target ); 74 | callback.call(this); 75 | } 76 | }); 77 | 78 | resizeObserver.observe(this.target); 79 | } 80 | 81 | onDeferInit(callback) { 82 | if (!('IntersectionObserver' in window)) { 83 | callback.call(this); 84 | return; 85 | } 86 | 87 | let observer = new IntersectionObserver((entries, observer) => { 88 | entries.forEach(entry => { 89 | if (entry.isIntersecting) { 90 | // console.log( "initing", this.target ); 91 | callback.call(this); 92 | observer.unobserve(entry.target); 93 | } 94 | }); 95 | }, { 96 | threshold: .1 97 | }); 98 | 99 | observer.observe(this.target); 100 | } 101 | 102 | normalizeColors(colors = [], mod = 0) { 103 | if(mod) { 104 | let c = []; 105 | let len = colors.length; 106 | let k = len + mod; 107 | for(let j = mod || 0; j < k; j++) { 108 | c.push(colors[j % len]); 109 | } 110 | return c; 111 | } 112 | 113 | return colors; 114 | } 115 | 116 | get margin() { 117 | let m = Object.assign({ 118 | top: 30, 119 | right: 10, 120 | bottom: 25, 121 | left: 40, 122 | }, this.options.margin); 123 | 124 | return m; 125 | } 126 | 127 | get dimensions() { 128 | let target = this.target; 129 | return { 130 | container: { 131 | width: target.clientWidth, 132 | height: target.clientHeight, 133 | }, 134 | min: { 135 | width: 300, 136 | height: 450 137 | }, 138 | max: { 139 | height: 1000 140 | }, 141 | }; 142 | } 143 | 144 | get width() { 145 | return Math.max(this.dimensions.container.width, this.dimensions.min.width); 146 | } 147 | 148 | get height() { 149 | return Math.max(Math.min(this.dimensions.container.height, this.dimensions.max.height) - this.margin.bottom, this.dimensions.min.height); 150 | } 151 | 152 | get svg() { 153 | return d3.create("svg") 154 | .attr("height", this.height) 155 | .attr("viewBox", [0, 0, this.width, this.height]); 156 | } 157 | 158 | get colors() { 159 | return d3.scaleOrdinal().range(this.options.colors); 160 | } 161 | 162 | get labelColors() { 163 | return d3.scaleOrdinal().range(this.options.labelColors); 164 | } 165 | 166 | get target() { 167 | return document.getElementById(this.targetId); 168 | } 169 | 170 | reset(svg) { 171 | let target = this.target; 172 | target.classList.add("artfc"); 173 | if(this.className) { 174 | target.classList.add(this.className); 175 | } 176 | 177 | for(let child of target.children) { 178 | if(child.tagName.toLowerCase() === "svg") { 179 | child.remove(); 180 | } 181 | } 182 | 183 | let node = svg.node(); 184 | target.appendChild(node); 185 | } 186 | 187 | // Thanks https://bl.ocks.org/mbostock/7555321 188 | static wrapText(text, width) { 189 | text.each(function() { 190 | var text = d3.select(this), 191 | words = text.text().split(/\s+/).reverse(), 192 | word, 193 | line = [], 194 | lineNumber = 0, 195 | lineHeight = 1.01, // ems 196 | y = text.attr("y"), 197 | dy = parseFloat(text.attr("dy")), 198 | tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y), 199 | firstTspan = tspan; 200 | 201 | let wrapCount = 0; 202 | while (word = words.pop()) { 203 | line.push(word); 204 | tspan.text(line.join(" ")); 205 | if (tspan.node().getComputedTextLength() > width) { 206 | wrapCount++; 207 | line.pop(); 208 | tspan.text(line.join(" ")); 209 | line = [word]; 210 | tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", lineHeight + dy + "em").text(word); 211 | } 212 | } 213 | 214 | if(wrapCount) { 215 | text.attr("dy", 0).attr("class", "artfc-label-wrapped"); 216 | firstTspan.attr("dy", (-0.3 * wrapCount * lineHeight) + "em") 217 | } 218 | }); 219 | } 220 | 221 | static parseDataToCsv(tableId, reverse) { 222 | let table = document.getElementById(tableId); 223 | let headerCells = table.querySelectorAll(":scope thead th"); 224 | let bodyRows = table.querySelectorAll(":scope tbody tr"); 225 | 226 | let headerOutput = []; 227 | for(let th of headerCells) { 228 | headerOutput.push(th.textContent); 229 | } 230 | 231 | let output = []; 232 | for(let tr of bodyRows) { 233 | let row = []; 234 | for(let child of tr.children) { 235 | let value = child.textContent; 236 | if(value.endsWith("%")) { 237 | value = parseFloat(value) / 100; 238 | } 239 | row.push(value); 240 | } 241 | output.push(row.join(",")); 242 | } 243 | if(reverse) { 244 | return [headerOutput.join(","), ...output.reverse()].join("\n"); 245 | } 246 | return [headerOutput.join(","), ...output].join("\n"); 247 | 248 | } 249 | 250 | retrieveLabelId(label) { 251 | let match = label.match(/^(\d*)\./); 252 | if(match && match[1]) { 253 | return parseInt(match[1], 10); 254 | } 255 | } 256 | 257 | slugify(slug, prefix) { 258 | return `${prefix}${slug.toLowerCase().replace(/[\s\.]/g, "")}`; 259 | } 260 | 261 | generateLegend(labels = []) { 262 | let container = document.createElement("div"); 263 | container.classList.add("artfc-legend"); 264 | 265 | let entries = []; 266 | for(let j = 0; j < labels.length; j++) { 267 | let tag = "div"; 268 | let attrs = ""; 269 | if(this.options.highlightElementsFromLegend) { 270 | tag = "button"; 271 | attrs = " type='button'" 272 | } 273 | 274 | entries.push({ 275 | label: labels[j], 276 | html: `<${tag}${attrs} class="artfc-legend-entry artfc-legend-${j + this.options.colorMod}">${labels[j] || ""}` 277 | }); 278 | } 279 | 280 | if(this.options.sortLegend) { 281 | entries = entries.sort((a, b) => { 282 | let idA = this.retrieveLabelId(a.label); 283 | let idB = this.retrieveLabelId(b.label); 284 | if(idA && idB) { 285 | return idA - idB; 286 | } 287 | if(a.label < b.label) { 288 | return -1; 289 | } else if(b.label < a.label) { 290 | return 1; 291 | } 292 | return 0; 293 | }); 294 | } 295 | 296 | let html = []; 297 | for(let entry of entries) { 298 | html.push(entry.html); 299 | } 300 | container.innerHTML = html.join(""); 301 | return container; 302 | } 303 | 304 | getKeys(data) { 305 | return data.columns.slice(1); 306 | } 307 | 308 | highlightElements(target, method) { 309 | // TODO this is specific to Bubble chart 310 | if(target.classList.contains("artfc-legend-entry")) { 311 | let circleSlug = this.slugify(target.innerHTML, `${this.targetId}-bubblecircle-`); 312 | let labelSlug = this.slugify(target.innerHTML, `${this.targetId}-bubblelabel-`); 313 | 314 | let circle = document.getElementById(circleSlug); 315 | let label = document.getElementById(labelSlug); 316 | 317 | circle.classList[method]("active"); 318 | label.classList[method]("active"); 319 | 320 | circle.closest("svg").classList[method]("artfc-bubble-active"); 321 | } 322 | } 323 | 324 | renderLegend(data) { 325 | if(!this.options.showLegend) { 326 | return; 327 | } 328 | 329 | let keys = this.getKeys(data); 330 | let legend = this.generateLegend(keys, this.options.colors); 331 | 332 | if(this.options.highlightElementsFromLegend) { 333 | legend.addEventListener("mouseover", e => { 334 | this.highlightElements(e.target, "add"); 335 | }); 336 | legend.addEventListener("mouseout", e => { 337 | this.highlightElements(e.target, "remove"); 338 | }); 339 | legend.addEventListener("focusin", e => { 340 | this.highlightElements(e.target, "add"); 341 | }); 342 | legend.addEventListener("focusout", e => { 343 | this.highlightElements(e.target, "remove"); 344 | }); 345 | } 346 | 347 | let legendPlaceholderClass = "artfc-legend-placeholder"; 348 | let childSelector = `:scope .${legendPlaceholderClass}`; 349 | 350 | let previousEl = this.target.previousElementSibling; 351 | let legendAnchorBefore; 352 | if(previousEl && previousEl.classList.contains(legendPlaceholderClass)) { 353 | legendAnchorBefore = previousEl; 354 | } else { 355 | legendAnchorBefore = previousEl ? previousEl.querySelector(childSelector) : null; 356 | } 357 | 358 | let nextEl = this.target.nextElementSibling; 359 | let legendAnchorAfter; 360 | if(nextEl && nextEl.classList.contains(legendPlaceholderClass)) { 361 | legendAnchorAfter = nextEl; 362 | } else { 363 | legendAnchorAfter = nextEl ? nextEl.querySelector(childSelector) : null; 364 | } 365 | 366 | if(legendAnchorBefore || legendAnchorAfter) { 367 | (legendAnchorBefore || legendAnchorAfter).appendChild(legend); 368 | } else { 369 | // inside 370 | this.target.appendChild(legend); 371 | } 372 | } 373 | 374 | roundValue(num, valueType = "percentage") { 375 | if(valueType !== "percentage") { 376 | return num; 377 | } 378 | 379 | let d0 = (num * 100).toFixed(0); 380 | if(this.options.labelPrecision === 0) { 381 | return d0; 382 | } 383 | 384 | let d1 = (num * 100).toFixed(1); 385 | if(d1.endsWith(".0")) { 386 | return d0; 387 | } 388 | return d1; 389 | } 390 | } 391 | 392 | class VerticalBar extends ArtificialChart { 393 | constructor(target, tableId, optionOverrides = {}) { 394 | let chart = super(target, optionOverrides, "artfc-vbar"); 395 | 396 | let csvData = ArtificialChart.parseDataToCsv(tableId); 397 | let dataSplit = csvData.split("\n"); 398 | this.axisLabels = [dataSplit[0].split(",")[0]]; 399 | 400 | let data = Object.assign(d3.csvParse(csvData, d3.autoType)); 401 | 402 | 403 | this.onDeferInit(function() { 404 | this.render(chart, data); 405 | this.renderLegend(data); 406 | 407 | this.onResize(function() { 408 | this.render(chart, data); 409 | }) 410 | }); 411 | } 412 | 413 | render(chart, data) { 414 | let { 415 | options, 416 | margin, 417 | width, 418 | height, 419 | dimensions, 420 | svg, 421 | colors, 422 | labelColors, 423 | } = chart; 424 | 425 | let keys = this.getKeys(data); 426 | let groupKey = data.columns[0]; 427 | let groups = data.map(d => d[groupKey]); 428 | 429 | let y = d3.scaleLinear() 430 | .domain([ 431 | 0, 432 | d3.max(data, d => { 433 | if(options.mode === "stacked") { 434 | let sum = 0; 435 | for(let key of keys) { 436 | sum += d[key]; 437 | } 438 | return sum; 439 | } 440 | 441 | return d3.max(keys, key => d[key]) 442 | }) 443 | ]).nice() 444 | .rangeRound([height - margin.bottom, margin.top]); 445 | 446 | let x0 = d3.scaleBand() 447 | .domain(groups) 448 | .rangeRound([margin.left, width - margin.right]) 449 | .paddingInner(.2); 450 | 451 | let x1 = d3.scaleBand() 452 | .domain(keys) 453 | .rangeRound([0, x0.bandwidth()]) 454 | .padding(0.05); 455 | 456 | let yAxis = g => g 457 | .attr("transform", `translate(${margin.left},0)`) 458 | .attr("class", "artfc-yaxis") 459 | .call(d3 460 | .axisLeft(y) 461 | .ticks(null, options.valueType[0] === "percentage" ? "%" : "") 462 | .tickSize(-width + margin.left + margin.right) 463 | .tickFormat(d => options.valueType[0] === "percentage" ? `${(d*100).toFixed(0)}%` : d)) 464 | .call(g => g.select(".domain").remove()); 465 | 466 | let xAxis = g => g 467 | .attr("transform", `translate(0,${height - margin.bottom})`) 468 | .attr("class", "artfc-xaxis") 469 | .call(d3 470 | .axisBottom(x0) 471 | .tickSizeOuter(0)) 472 | .call(g => g.select(".domain").remove()); 473 | 474 | let dataMod = d => { 475 | let incrementer = 0; 476 | 477 | return keys.map(key => { 478 | let data = { 479 | key, 480 | value: d[key], 481 | width: x1.bandwidth(), 482 | height: y(0) - y(d[key]), 483 | left: x1(key), 484 | top: y(d[key]) 485 | }; 486 | 487 | if(options.mode === "stacked") { 488 | data.width = x0.bandwidth(); 489 | data.left = 0; 490 | data.top = y(d[key]) - incrementer; 491 | incrementer += data.height; 492 | } 493 | 494 | return data; 495 | }) 496 | }; 497 | 498 | svg.append("g").call(xAxis); 499 | svg.append("g").call(yAxis); 500 | 501 | svg.append("g") 502 | .selectAll("g") 503 | .data(data) 504 | .join("g") 505 | .attr("transform", d => `translate(${x0(d[groupKey])},0)`) 506 | .selectAll("rect") 507 | .data(dataMod) 508 | .join("rect") 509 | .attr("x", d => d.left) 510 | .attr("y", d => d.top) 511 | .attr("width", d => d.width) 512 | .attr("height", d => d.height) 513 | .attr("fill", d => colors(d.key)) 514 | .attr("class", (d, j) => `artfc-color-${j + options.colorMod}`); 515 | 516 | if(options.showInlineBarValues) { 517 | svg.append("g") 518 | .selectAll("g") 519 | .data(data) 520 | .join("g") 521 | .attr("transform", d => `translate(${x0(d[groupKey])},0)`) 522 | .selectAll("text") 523 | .data(dataMod) 524 | .join("text") 525 | .attr("x", d => d.left + d.width / 2) 526 | .attr("y", d => d.top - (options.showInlineBarValues === "outside" ? options.inlineLabelPad : (-15 - options.inlineLabelPad))) 527 | .attr("fill", d => options.showInlineBarValues === "inside" ? labelColors(d.key) : "currentColor") 528 | .attr("class", "artfc-inlinebarvalue") 529 | .text(d => this.roundValue(d.value, options.valueType[0]) + (options.valueType[0] === "percentage" ? "%" : "")); 530 | } 531 | 532 | // TODO for horizontal bar chart 533 | if(options.showAxisLabels) { 534 | svg.append("text") 535 | .attr("x", Math.round(width/2)) 536 | .attr("y", height - 6) 537 | .attr("class", "artfc-axislabel artfc-axislabel-center") 538 | .text(this.axisLabels[0]); 539 | } 540 | 541 | chart.reset(svg); 542 | } 543 | } 544 | 545 | class Line extends ArtificialChart { 546 | constructor(target, tableId, optionOverrides = {}) { 547 | let chart = super(target, optionOverrides, "artfc-line"); 548 | 549 | let csvData = ArtificialChart.parseDataToCsv(tableId); 550 | let dataSplit = csvData.split("\n"); 551 | this.axisLabels = [dataSplit[0].split(",")[0]]; 552 | 553 | let data = Object.assign(d3.csvParse(csvData, d3.autoType)); 554 | 555 | this.onDeferInit(function() { 556 | this.render(chart, data); 557 | 558 | this.onResize(function() { 559 | this.render(chart, data); 560 | }) 561 | }); 562 | } 563 | 564 | render(chart, data) { 565 | let { 566 | options, 567 | margin, 568 | width, 569 | height, 570 | dimensions, 571 | svg, 572 | colors, 573 | labelColors, 574 | } = chart; 575 | 576 | let keys = this.getKeys(data); 577 | let groupKey = data.columns[0]; 578 | 579 | // d3.scaleLog 580 | let y = d3.scaleLinear() 581 | .domain([ 582 | 0, 583 | Math.min(d3.max(data, d => { 584 | return d3.max(keys, key => d[key]) 585 | }), options.max && options.max.y || Infinity) 586 | ]).nice() 587 | .rangeRound([height - margin.bottom, margin.top]); 588 | 589 | let x = d3.scaleLinear() 590 | .domain([ 591 | Math.min(...keys), 592 | Math.max(...keys) * 1.15 593 | ]) 594 | .rangeRound([margin.left, width - margin.right]); 595 | 596 | let yAxis = g => g 597 | .attr("transform", `translate(${margin.left},0)`) 598 | .attr("class", "artfc-yaxis") 599 | .call(d3 600 | .axisLeft(y) 601 | .ticks(null, options.valueType[0] === "percentage" ? "%" : "") 602 | .tickSize(-width + margin.left + margin.right) 603 | .tickFormat(d => options.valueType[0] === "percentage" ? `${(d*100).toFixed(0)}%` : d)) 604 | .call(g => g.select(".domain").remove()); 605 | 606 | let xAxis = g => g 607 | .attr("transform", `translate(0,${height - margin.bottom})`) 608 | .attr("class", "artfc-xaxis") 609 | .call(d3 610 | .axisBottom(x) 611 | .ticks(keys.length) 612 | .tickSize(-width + margin.left + margin.right) 613 | .tickFormat(d => d)) 614 | .call(g => g.select(".domain").remove()); 615 | 616 | svg.append("g").call(xAxis); 617 | svg.append("g").call(yAxis); 618 | 619 | let dataMod = data.map((entry, index) => { 620 | return keys.map(key => { 621 | return { 622 | index, 623 | group: entry[groupKey], 624 | key: key, 625 | value: entry[key] 626 | }; 627 | }) 628 | }) 629 | 630 | for(let datum of dataMod) { 631 | let dims = {}; 632 | svg.append("path") 633 | .datum(datum) 634 | .attr("fill", "none") 635 | .attr("stroke", d => { 636 | return colors(d) 637 | }) 638 | .attr("stroke-width", 3) 639 | .attr("d", d3.line() 640 | .x(function(d) { 641 | let c = x(d.key); 642 | dims.x = c; 643 | return c; 644 | }) 645 | .y(function(d) { 646 | let c = y(d.value); 647 | dims.y = c; 648 | return c; 649 | }).curve(d3.curveBasis) 650 | ); 651 | 652 | svg.append("text") 653 | .attr("x", dims.x + 4) 654 | .attr("y", Math.max(dims.y + 4, 14)) // if the data goes off the top, keep the label visible 655 | .attr("font-size", "14") 656 | .attr("font-family", "sans-serif") 657 | .text(datum[0].group) 658 | } 659 | 660 | chart.reset(svg); 661 | } 662 | } 663 | 664 | class HorizontalBar extends ArtificialChart { 665 | constructor(target, tableId, optionOverrides = {}) { 666 | optionOverrides.margin = Object.assign({ 667 | top: 20, 668 | right: 50, 669 | bottom: 20, 670 | left: 120 671 | }, optionOverrides.margin); 672 | let chart = super(target, optionOverrides, "artfc-hbar"); 673 | let csvData = ArtificialChart.parseDataToCsv(tableId, true); 674 | let data = Object.assign(d3.csvParse(csvData, d3.autoType)); 675 | 676 | this.onDeferInit(function() { 677 | this.render(chart, data); 678 | this.renderLegend(data); 679 | 680 | this.onResize(function() { 681 | this.render(chart, data); 682 | }); 683 | }); 684 | } 685 | 686 | render(chart, data) { 687 | let { 688 | options, 689 | margin, 690 | width, 691 | height, 692 | dimensions, 693 | svg, 694 | colors, 695 | labelColors, 696 | } = chart; 697 | 698 | let keys = this.getKeys(data); 699 | let groupKey = data.columns[0]; 700 | let groups = data.map(d => d[groupKey]); 701 | 702 | let x = d3.scaleLinear() 703 | .domain([0, d3.max(data, d => { 704 | if(options.scale === "proportional") { 705 | return 1; 706 | } 707 | 708 | if(options.mode === "stacked") { 709 | let sum = 0; 710 | for(let key of keys) { 711 | sum += d[key]; 712 | } 713 | return sum; 714 | } 715 | 716 | return d3.max(keys, key => d[key]); 717 | })]).nice() 718 | .rangeRound([margin.left, width - margin.right]); 719 | 720 | let y0 = d3.scaleBand() 721 | .domain(groups) 722 | .rangeRound([height - margin.bottom - margin.top, margin.top]) 723 | .paddingInner(options.showInlineBarValues === "inside-offset" ? 0.25 : 0.15); 724 | 725 | let y1 = d3.scaleBand() 726 | .domain(keys) 727 | .rangeRound([0, y0.bandwidth()]) 728 | .padding(0.05); 729 | 730 | let xAxis = g => g 731 | .attr("transform", `translate(0, ${(margin.top + margin.bottom)/4})`) 732 | .attr("class", "artfc-xaxis") 733 | .call(d3 734 | .axisBottom(x) 735 | .ticks(5, options.valueType[0] === "percentage" ? "%" : "") 736 | .tickSize(height - margin.bottom - margin.top) 737 | .tickFormat(d => options.valueType[0] === "percentage" ? `${(d*100).toFixed(0)}%` : d)) 738 | .call(g => g.select(".domain").remove()); 739 | 740 | let yAxis = g => g 741 | .attr("transform", `translate(${margin.left - 6},0)`) 742 | .attr("class", "artfc-yaxis") 743 | .call(d3.axisLeft(y0).tickSize(0)) 744 | .call(g => g.select(".domain").remove()); 745 | 746 | let dataMod = d => { 747 | let incrementer = 0; 748 | let sum = 0; 749 | for(let key of keys) { 750 | sum += d[key]; 751 | } 752 | 753 | return keys.map(key => { 754 | let data = { 755 | key, 756 | value: d[key], 757 | sum, 758 | width: x(options.scale === "proportional" ? (d[key] / sum) : d[key]) - x(0), 759 | height: y1.bandwidth(), 760 | left: margin.left, 761 | top: y1(key) 762 | }; 763 | 764 | if(options.mode === "stacked") { 765 | data.top = 0; 766 | data.height = y0.bandwidth(); 767 | data.left = margin.left + incrementer; 768 | 769 | incrementer += data.width; 770 | } 771 | 772 | return data; 773 | }) 774 | }; 775 | 776 | svg.append("g").call(xAxis); 777 | svg.append("g").call(yAxis); 778 | 779 | svg.append("g") 780 | .selectAll("g") 781 | .data(data) 782 | .join("g") 783 | .attr("transform", d => `translate(0,${y0(d[groupKey])})`) 784 | .selectAll("rect") 785 | .data(dataMod) 786 | .join("rect") 787 | .attr("x", d => d.left) 788 | .attr("y", d => d.top) 789 | .attr("width", d => d.width) 790 | .attr("height", d => d.height) 791 | .attr("fill", d => colors(d.key)) 792 | .attr("class", (d, j) => `artfc-color-${j + options.colorMod}`); 793 | 794 | if(options.showInlineBarValues) { 795 | svg.append("g") 796 | .selectAll("g") 797 | .data(data) 798 | .join("g") 799 | .attr("transform", d => `translate(0,${y0(d[groupKey])})`) 800 | .selectAll("text") 801 | .data(dataMod) 802 | .join("text") 803 | .attr("x", d => { 804 | let offset = options.inlineLabelPad; 805 | if(options.showInlineBarValues.startsWith("inside")) { 806 | offset = -1 * offset; 807 | } 808 | if(options.showInlineBarValues === "inside-offset") { 809 | offset += 16; 810 | } 811 | return d.left + d.width + offset; 812 | }) 813 | .attr("y", d => { 814 | if(options.showInlineBarValues === "inside-offset") { 815 | return -10; 816 | } 817 | return d.top + Math.floor(d.height / 2) - 1; 818 | }) 819 | .attr("class", d => "artfc-inlinebarvalue-h" + (options.showInlineBarValues.length ? ` ${options.showInlineBarValues}` : "")) 820 | .attr("fill", d => options.showInlineBarValues === "inside" ? labelColors(d.key) : "currentColor") 821 | .text(d => this.roundValue(d.value, options.valueType[0]) + (options.valueType[0] === "percentage" ? "%" : "")); 822 | } 823 | 824 | chart.reset(svg); 825 | 826 | if(options.wrapAxisLabel && options.wrapAxisLabel.left) { 827 | ArtificialChart.wrapText(svg.selectAll(".artfc-yaxis .tick text"), margin.left - 6); 828 | } 829 | } 830 | } 831 | 832 | class Bubble extends ArtificialChart { 833 | constructor(target, tableId, optionOverrides = {}) { 834 | optionOverrides.margin = { 835 | top: 20, 836 | right: 20, 837 | bottom: 50, 838 | left: 65 839 | }; 840 | 841 | optionOverrides.sortLegend = true; 842 | optionOverrides.highlightElementsFromLegend = true; 843 | optionOverrides.showAxisLabels = true; 844 | 845 | if(!optionOverrides.valueType) { 846 | optionOverrides.valueType = ["percentage", "percentage"]; 847 | } 848 | 849 | let chart = super(target, optionOverrides, "artfc-bubble"); 850 | let csvData = ArtificialChart.parseDataToCsv(tableId); 851 | let dataSplit = csvData.split("\n"); 852 | this.axisLabels = dataSplit[0].split(",").slice(1); 853 | 854 | let data = dataSplit.slice(1).map((entry, id) => { 855 | let [name, x, y, r] = entry.split(","); 856 | return { 857 | name, 858 | id, 859 | x, 860 | y, 861 | r, 862 | }; 863 | }); 864 | 865 | // sort from smallest to largest circles to insert in order (to render in the right z-index) 866 | data = data.slice().sort((a, b) => { 867 | return b.r - a.r; 868 | }); 869 | 870 | this.onDeferInit(function() { 871 | this.render(chart, data); 872 | this.renderLegend(data); 873 | 874 | this.onResize(function() { 875 | this.render(chart, data); 876 | }); 877 | }); 878 | } 879 | 880 | getKeys(data) { 881 | let keys = []; 882 | for(let entry of data) { 883 | keys.push(entry.name); 884 | } 885 | return keys; 886 | } 887 | 888 | resolveLimit(data, key, valueType, mode) { 889 | let limit = d3[mode](data, d => parseFloat(d[key])); 890 | if(valueType !== "percentage") { 891 | if(mode === "max") { 892 | limit = Math.ceil(limit); 893 | } else if(mode === "min") { 894 | limit = Math.min(Math.floor(limit), 0); 895 | } 896 | } else { 897 | if(mode === "max") { 898 | if(limit > 1) { 899 | limit += .1; 900 | } else { 901 | // round up to at most 1 if percentage < 100% 902 | if(limit > .5) { 903 | limit = Math.min(limit + .1, 1); 904 | } else { 905 | limit = limit + .05, 1; 906 | } 907 | } 908 | } 909 | if(mode === "min") { 910 | if(limit <= 0) { 911 | limit -= .1; 912 | } else { 913 | // round up to at most 1 if percentage < 100% 914 | limit = Math.min(limit, 0); 915 | } 916 | } 917 | } 918 | 919 | return limit; 920 | } 921 | 922 | render(chart, data) { 923 | let { 924 | options, 925 | margin, 926 | width, 927 | height, 928 | dimensions, 929 | svg, 930 | colors, 931 | labelColors, 932 | } = chart; 933 | 934 | let targetId = this.targetId; 935 | 936 | let xAxisMin = this.resolveLimit(data, "x", options.valueType[0], "min"); 937 | let xAxisMax = this.resolveLimit(data, "x", options.valueType[0], "max"); 938 | let yAxisMin = this.resolveLimit(data, "y", options.valueType[1], "min"); 939 | let yAxisMax = this.resolveLimit(data, "y", options.valueType[1], "max"); 940 | 941 | let xScale = d3.scaleLinear() 942 | .domain([ 943 | xAxisMin, 944 | xAxisMax 945 | ]) 946 | .range([ 947 | margin.left, 948 | width - margin.right 949 | ]); 950 | 951 | let yScale = d3.scaleLinear() 952 | .domain([ 953 | yAxisMax, 954 | yAxisMin, 955 | ]) 956 | .range([ 957 | margin.top, 958 | height - margin.top - margin.bottom 959 | ]); 960 | 961 | let rScale = d3.scaleLinear() 962 | .range([7, 25]) 963 | .domain([ 964 | Math.min(d3.min(data, d => parseFloat(d.r)), 0), 965 | d3.max(data, d => parseFloat(d.r)) 966 | ]); 967 | 968 | let xAxis = d3.axisBottom() 969 | .scale(xScale) 970 | .ticks(null) 971 | .tickSize(-height + margin.bottom + margin.top) 972 | .tickFormat(d => options.valueType[0] === "percentage" ? `${(d*100).toFixed(0)}%` : d); 973 | 974 | svg.append("g") 975 | .attr("class", "artfc-xaxis") 976 | .attr("transform", function(){ 977 | return "translate(0," + (height - margin.bottom) + ")"; 978 | }) 979 | .call(xAxis) 980 | .call(g => g.select(".domain").remove()); 981 | 982 | let yAxis = d3.axisLeft() 983 | .scale(yScale) 984 | .ticks(null) 985 | .tickSize(-width + margin.right + margin.left) 986 | .tickFormat(d => options.valueType[1] === "percentage" ? `${(d*100).toFixed(0)}%` : d); 987 | 988 | svg.append("g") 989 | .attr("class", "artfc-yaxis") 990 | .attr("transform", function(){ 991 | return "translate(" + margin.left + "," + margin.top + ")"; 992 | }) 993 | .call(yAxis) 994 | .call(g => g.select(".domain").remove()); 995 | 996 | if(options.showAxisLabels) { 997 | // Axis labels 998 | svg.append("text") 999 | .attr("x", width - margin.right) 1000 | .attr("y", height - 6) 1001 | .attr("class", "artfc-axislabel") 1002 | .text(this.axisLabels[0]); 1003 | 1004 | svg.append("text") 1005 | .attr("x", -1 * margin.top) 1006 | .attr("y", 6) 1007 | .attr("dy", ".75em") 1008 | .attr("transform", "rotate(-90)") 1009 | .attr("class", "artfc-axislabel") 1010 | .text(this.axisLabels[1]); 1011 | } 1012 | 1013 | let group = svg.append("g"); 1014 | 1015 | let circles = group.selectAll("circle").data(data); 1016 | 1017 | // Text Labels 1018 | function isOffsetLabel(d) { 1019 | let range = rScale(d.r); 1020 | return range <= 10; 1021 | } 1022 | 1023 | circles 1024 | .enter() 1025 | .insert("circle") 1026 | .attr("cx", function (d) { 1027 | return xScale(d.x); 1028 | }) 1029 | .attr("cy", function (d) { 1030 | return yScale(d.y); 1031 | }) 1032 | .attr("r", function (d) { 1033 | return rScale(d.r); 1034 | }) 1035 | .attr("id", d => this.slugify(d.name, `${targetId}-bubblecircle-`)) 1036 | .attr("fill", d => colors(d)) 1037 | .attr("class", (d, j) => `artfc-bubblecircle artfc-color-${j + options.colorMod}`); 1038 | 1039 | circles 1040 | .enter() 1041 | .append("text") 1042 | .attr("id", d => this.slugify(d.name, `${targetId}-bubblelabel-`)) 1043 | .attr("filter", d => { 1044 | return isOffsetLabel(d) ? "url(#offset-label-bg)" : "" 1045 | }) 1046 | .attr("x", d => { 1047 | return xScale(d.x) - (isOffsetLabel(d) ? rScale(d.r) + 4 : 0); 1048 | }) 1049 | .attr("y", d => yScale(d.y)) 1050 | .attr("class", d => { 1051 | return "artfc-bubblelabel" + (isOffsetLabel(d) ? " offset-l" : ""); 1052 | }) 1053 | .attr("fill", d => isOffsetLabel(d) ? "currentColor" : labelColors(d)) 1054 | .text(d => { 1055 | let labelId = this.retrieveLabelId(d.name); 1056 | if(labelId) { 1057 | return labelId; 1058 | } 1059 | return d.name; 1060 | }) 1061 | .filter(d => isOffsetLabel(d)) 1062 | .lower(); 1063 | 1064 | chart.reset(svg); 1065 | } 1066 | } 1067 | 1068 | export { 1069 | ArtificialChart, 1070 | Bubble, 1071 | HorizontalBar, 1072 | VerticalBar, 1073 | Line 1074 | }; -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 |

Artificial Chart

18 | 19 |

Horizontal Bar Chart

20 |
21 | 22 |
23 | Show Chart Data 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
Job TitlePercentage of Survey Participants
Developer: Front-end45%
Developer: Full Stack32%
Management6%
Other5%
Developer: Back-end5%
Designer4%
Content Producer2%
DevOps2%
66 |
67 | 68 |

Vertical Bar Chart

69 |
70 | 71 |
72 | Show Chart Data 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
Years of ExperienceRemote
< 117%
1–232%
3–440%
5–640%
7–840%
9–1041%
11–1242%
13–1441%
15+39%
92 |
93 | 94 |

Grouped Vertical bar chart

95 |
96 |
97 | Show Chart Data 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |
People20202021
Developers79%81%
Non-developers21%19%
119 |
120 | 121 |

Grouped Horizontal Bar Chart

122 |
123 |
124 |
125 |
126 | 127 |
128 | Show Chart Data 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 |
Employment Status20202021
Full-time69%60%
Student9%16%
Contractor11%10%
Part-time4%5%
Unemployed4%5%
Between jobs4%3%
Retired0%1%
175 |
176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | // Import d3 2 | import "https://d3js.org/d3.v7.min.js"; 3 | 4 | import { HorizontalBar, VerticalBar } from "./artificial-chart.js"; 5 | 6 | new VerticalBar("demographics-jobs-chart", "demographics-jobs-table", { 7 | showInlineBarValues: "outside", 8 | }); 9 | 10 | new HorizontalBar("demographics-jobtitle-chart", "demographics-jobtitle-table", { 11 | showInlineBarValues: "outside", 12 | showLegend: false, 13 | margin: { 14 | left: 150 15 | }, 16 | colorMod: 2 17 | }); 18 | 19 | new VerticalBar("experience-wentremote-chart", "experience-wentremote-table", { 20 | showInlineBarValues: "outside", 21 | colorMod: 2, 22 | showLegend: false, 23 | }); 24 | 25 | new HorizontalBar("demographics-employmentstatus-chart", "demographics-employmentstatus-table", { 26 | showInlineBarValues: "outside", 27 | margin: { 28 | left: 100 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "artificial-chart", 3 | "version": "2.0.3", 4 | "description": "The SVG D3.js based charting solution used for the Jamstack Community Survey.", 5 | "type": "module", 6 | "main": "artificial-chart.js", 7 | "style": "artificial-chart.css", 8 | "scripts": {}, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/zachleat/artificial-chart.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/zachleat/artificial-chart/issues" 18 | }, 19 | "homepage": "https://github.com/zachleat/artificial-chart#readme" 20 | } 21 | --------------------------------------------------------------------------------