├── 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] || ""}${tag}>`
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 | Job Title |
28 | Percentage of Survey Participants |
29 |
30 |
31 |
32 |
33 | Developer: Front-end |
34 | 45% |
35 |
36 |
37 | Developer: Full Stack |
38 | 32% |
39 |
40 |
41 | Management |
42 | 6% |
43 |
44 |
45 | Other |
46 | 5% |
47 |
48 |
49 | Developer: Back-end |
50 | 5% |
51 |
52 |
53 | Designer |
54 | 4% |
55 |
56 |
57 | Content Producer |
58 | 2% |
59 |
60 |
61 | DevOps |
62 | 2% |
63 |
64 |
65 |
66 |
67 |
68 | Vertical Bar Chart
69 |
70 |
71 |
72 | Show Chart Data
73 |
74 |
75 |
76 | Years of Experience |
77 | Remote |
78 |
79 |
80 |
81 | < 1 | 17% |
82 | 1–2 | 32% |
83 | 3–4 | 40% |
84 | 5–6 | 40% |
85 | 7–8 | 40% |
86 | 9–10 | 41% |
87 | 11–12 | 42% |
88 | 13–14 | 41% |
89 | 15+ | 39% |
90 |
91 |
92 |
93 |
94 | Grouped Vertical bar chart
95 |
96 |
97 | Show Chart Data
98 |
99 |
100 |
101 | People |
102 | 2020 |
103 | 2021 |
104 |
105 |
106 |
107 |
108 | Developers |
109 | 79% |
110 | 81% |
111 |
112 |
113 | Non-developers |
114 | 21% |
115 | 19% |
116 |
117 |
118 |
119 |
120 |
121 | Grouped Horizontal Bar Chart
122 |
126 |
127 |
128 | Show Chart Data
129 |
130 |
131 |
132 | Employment Status |
133 | 2020 |
134 | 2021 |
135 |
136 |
137 |
138 |
139 | Full-time |
140 | 69% |
141 | 60% |
142 |
143 |
144 | Student |
145 | 9% |
146 | 16% |
147 |
148 |
149 | Contractor |
150 | 11% |
151 | 10% |
152 |
153 |
154 | Part-time |
155 | 4% |
156 | 5% |
157 |
158 |
159 | Unemployed |
160 | 4% |
161 | 5% |
162 |
163 |
164 | Between jobs |
165 | 4% |
166 | 3% |
167 |
168 |
169 | Retired |
170 | 0% |
171 | 1% |
172 |
173 |
174 |
175 |
176 |
177 |
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 |
--------------------------------------------------------------------------------