diff --git a/.eleventy.js b/.eleventy.js index 41f980b1b..a6c739359 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -188,6 +188,7 @@ module.exports = function (eleventyConfig) { eleventyConfig.addPassthroughCopy("src/site/browserconfig.xml"); eleventyConfig.addPassthroughCopy("src/site/site.webmanifest"); eleventyConfig.addPassthroughCopy("src/site/survey/2021/community-survey-2021-methodology.pdf"); + eleventyConfig.addPassthroughCopy("src/site/survey/2022/community-survey-2022-methodology.pdf"); return { dir: { diff --git a/netlify.toml b/netlify.toml index 999220b04..b7b6c9618 100644 --- a/netlify.toml +++ b/netlify.toml @@ -67,7 +67,7 @@ [[redirects]] from = "/survey/" - to = "/survey/2021/" + to = "/survey/2022/" status = 302 [[redirects]] diff --git a/package.json b/package.json index 32878ed93..a449c29d1 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "autoprefixer": "^10.2.5", "cssnano": "^4.1.10", "d3": "^7.1.1", + "d3-textwrap": "^3.0.0", "dotenv": "^8.2.0", "fast-glob": "^3.2.5", "gray-matter": "^4.0.2", @@ -45,4 +46,4 @@ "spdx-correct": "^3.1.1", "tailwindcss": "^3.1.8" } -} \ No newline at end of file +} diff --git a/src/css/d3chart.css b/src/css/d3chart.css index 972dd619e..7be0aa43d 100644 --- a/src/css/d3chart.css +++ b/src/css/d3chart.css @@ -1,352 +1,528 @@ .d3chart-placeholder { - width: 100%; - height: 450px; + width: 100%; + height: 450px; } .d3chart-placeholder-large { - height: 660px; + height: 660px; } .d3chart-placeholder-xl { - height: 1000px; + height: 1000px; } .d3chart-legend-placeholder { - min-height: 1.71875em; /* 27.5px /16 */ + min-height: 1.71875em; /* 27.5px /16 */ } .d3chart { - position: relative; + position: relative; +} +.d3chart svg { + max-width: 100%; } .d3chart > .d3chart-legend { - position: absolute; - top: 0; - right: 0; + position: absolute; + top: 0; + right: 0; } .d3chart-legend { - display: flex; - justify-content: flex-end; - gap: .5em; - font-size: 0.8125em; /* 13px /16 */ - font-weight: 600; + display: flex; + justify-content: flex-end; + gap: 0.5em; + font-size: 0.8125em; /* 13px /16 */ + font-weight: 600; } .d3chart-legend button { - font-weight: inherit; + font-weight: inherit; } .d3chart-legend-wrap .d3chart-legend { - flex-wrap: wrap; - justify-content: center; - margin-left: auto; - margin-right: auto; + flex-wrap: wrap; + justify-content: center; + margin-left: auto; + margin-right: auto; } .d3chart + .d3chart-legend-placeholder .d3chart-legend { - justify-content: center; + justify-content: center; } .d3chart-legend > * { - border-radius: .25em; - padding: .25em .5em; + border-radius: 0.25em; + padding: 0.25em 0.5em; } .d3chart .tick text { - font-size: 1.3em; /* 13px /10 */ + font-size: 1.3em; /* 13px /10 */ } + .d3chart .tick line { - shape-rendering: crispEdges; + shape-rendering: crispEdges; } .d3chart .tick line { - stroke: rgba(22,26,42,.15); + stroke: rgba(22, 26, 42, 0.15); } .d3chart-bubble .tick:nth-child(2n) line { - stroke: rgba(22,26,42,.22); + stroke: rgba(22, 26, 42, 0.22); } -.d3chart-bubble .tick:nth-child(2n+1) line { - stroke: rgba(22,26,42,.1); +.d3chart-bubble .tick:nth-child(2n + 1) line { + stroke: rgba(22, 26, 42, 0.1); } .d3chart-bubble .d3chart-xaxis :first-child line, .d3chart-bubble .d3chart-yaxis .tick:last-child line { - stroke: #737680; - stroke-width: 2px; + stroke: #737680; + stroke-width: 2px; } .d3chart-hbar .d3chart-xaxis .tick:first-child line, .d3chart-vbar .d3chart-yaxis .tick:first-child line { - stroke: #c5c5c9; - stroke-width: 2px; + stroke: #c5c5c9; + stroke-width: 2px; +} +.d3chart-highlight-zero-axis .d3chart-xaxis :first-child line, +.d3chart-highlight-zero-axis .d3chart-yaxis .tick:last-child line { + stroke-width: 0; } .d3chart-hbar .d3chart-xaxis .tick:first-child line { - transform: translateX(-1px); + transform: translateX(-1px); } .d3chart-vbar .d3chart-yaxis .tick:first-child line { - transform: translateY(1px); + transform: translateY(1px); } .d3chart-vbar .d3chart-xaxis text, .d3chart-hbar .d3chart-yaxis text { - --d3chart-label-clamp: 2vw; - font-size: 12px; - font-size: clamp(12px, var(--d3chart-label-clamp), 14px); - font-weight: 600; + --d3chart-label-clamp: 2vw; + font-size: 12px; + font-size: clamp(12px, var(--d3chart-label-clamp), 14px); + font-weight: 600; } +.d3chart-bubble.d3chart-highlight-zero-axis [data-chart-value="0"] line { + stroke-width: 2px; + stroke: #737680; +} .d3chart-inlinebarvalue { - --d3chart-label-clamp: 2vw; - font-size: 12px; - font-size: clamp(11px, var(--d3chart-label-clamp), 16px); - font-weight: 600; - text-anchor: middle; + --d3chart-label-clamp: 2vw; + font-size: 12px; + font-size: clamp(11px, var(--d3chart-label-clamp), 16px); + font-weight: 600; + text-anchor: middle; } .d3chart-inlinebarvalue-h { - font-size: 16px; - font-weight: 600; - text-anchor: start; - dominant-baseline: central; - alignment-baseline: middle; + font-size: 16px; + font-weight: 600; + text-anchor: start; + dominant-baseline: central; + alignment-baseline: middle; } .d3chart-inlinebarvalue-h.inside { - text-anchor: end; + text-anchor: end; } .d3chart-inlinebarvalue-h.inside-offset { - font-size: 14px; - text-anchor: end; + font-size: 14px; + text-anchor: end; } /* Wrapped labels */ .d3chart-yaxis text.d3chart-label-wrapped { - font-size: 13px; + font-size: 13px; } /* Axis labels */ .d3chart-axislabel { - text-anchor: end; - font-weight: 700; + text-anchor: end; + font-weight: 700; } .d3chart-axislabel-center { - text-anchor: middle; + text-anchor: middle; } /* Bubble charts */ .d3chart-bubblelabel { - text-anchor: middle; - dominant-baseline: central; - font-size: 12px; - font-weight: 600; + text-anchor: middle; + dominant-baseline: central; + font-size: 12px; + font-weight: 600; } .d3chart-bubblelabel.offset-l, .d3chart-bubblelabel.offset-r { - font-weight: 700; - text-shadow: none; + font-weight: 700; + text-shadow: none; } .d3chart-bubblelabel.offset-l { - text-anchor: end; + text-anchor: end; } .d3chart-bubblelabel.offset-r { - text-anchor: start; + text-anchor: start; } .d3chart-bubblelabelbg.offset { - background-color: rgba(255,255,255,.4); + background-color: rgba(255, 255, 255, 0.4); } .d3chart-bubble circle { - fill-opacity: .85; -} -.d3chart-bubble-active .d3chart-bubblelabel, -.d3chart-bubble-active .d3chart-bubblecircle { - fill-opacity: .15; -} -.d3chart-bubble-active .d3chart-bubblelabel.active, -.d3chart-bubble-active .d3chart-bubblecircle.active { - fill-opacity: 1; + fill-opacity: 0.85; } .d3chart-bubble .d3chart-yaxis .tick:last-child text { - display: none; + display: none; } .d3chart-bubble .d3chart-xaxis .tick text { - transform: translateY(2px); + transform: translateY(2px); } /* Color gradients */ .d3chart-color-0 { - fill: url(#gradient-sunrise-v); + fill: url(#gradient-sunrise-v); } .d3chart-hbar .d3chart-color-0 { - fill: url(#gradient-sunrise-h); + fill: url(#gradient-sunrise-h); } .d3chart-color-1 { - fill: url(#gradient-blue-v); + fill: url(#gradient-blue-v); } .d3chart-hbar .d3chart-color-1 { - fill: url(#gradient-blue-h); + fill: url(#gradient-blue-h); } .d3chart-color-2 { - fill: url(#gradient-sun-v); + fill: url(#gradient-sun-v); } .d3chart-hbar .d3chart-color-2 { - fill: url(#gradient-sun-h); + fill: url(#gradient-sun-h); } .d3chart-color-3 { - fill: url(#gradient-seamist-v); + fill: url(#gradient-seamist-v); } .d3chart-hbar .d3chart-color-3 { - fill: url(#gradient-seamist-h); + fill: url(#gradient-seamist-h); } .d3chart-color-4 { - fill: url(#gradient-hallows-v); + fill: url(#gradient-hallows-v); } .d3chart-hbar .d3chart-color-4 { - fill: url(#gradient-hallows-h); + fill: url(#gradient-hallows-h); } .d3chart-color-5 { - fill: url(#gradient-bubblegum-v); + fill: url(#gradient-purple-v); } .d3chart-hbar .d3chart-color-5 { - fill: url(#gradient-bubblegum-h); + fill: url(#gradient-purple-h); } .d3chart-color-6 { - fill: url(#gradient-purple-v); + fill: url(#gradient-bubblegum-v); } .d3chart-hbar .d3chart-color-6 { - fill: url(#gradient-purple-h); + fill: url(#gradient-bubblegum-h); } .d3chart-color-7 { - fill: url(#gradient-air-v); + fill: url(#gradient-air-v); } .d3chart-hbar .d3chart-color-7 { - fill: url(#gradient-air-h); + fill: url(#gradient-air-h); } .d3chart-color-8 { - fill: url(#gradient-pink-v); + fill: url(#gradient-pink-v); } .d3chart-hbar .d3chart-color-8 { - fill: url(#gradient-pink-h); + fill: url(#gradient-pink-h); } .d3chart-color-9 { - fill: url(#gradient-leaves-v); + fill: url(#gradient-leaves-v); } .d3chart-hbar .d3chart-color-9 { - fill: url(#gradient-leaves-h); + fill: url(#gradient-leaves-h); } .d3chart-color-10 { - fill: url(#gradient-haze-v); + fill: url(#gradient-haze-v); } .d3chart-hbar .d3chart-color-10 { - fill: url(#gradient-haze-h); + fill: url(#gradient-haze-h); } .d3chart-color-11 { - fill: url(#gradient-gnat-v); + fill: url(#gradient-gnat-v); } .d3chart-hbar .d3chart-color-11 { - fill: url(#gradient-gnat-h); + fill: url(#gradient-gnat-h); } .d3chart-color-12 { - fill: url(#gradient-fire-v); + fill: url(#gradient-fire-v); } .d3chart-hbar .d3chart-color-12 { - fill: url(#gradient-fire-h); + fill: url(#gradient-fire-h); } .d3chart-color-13 { - fill: url(#gradient-ocean-v); + fill: url(#gradient-ocean-v); } .d3chart-hbar .d3chart-color-13 { - fill: url(#gradient-ocean-h); + fill: url(#gradient-ocean-h); } .d3chart-color-14 { - fill: url(#gradient-night-v); + fill: url(#gradient-night-v); } .d3chart-hbar .d3chart-color-14 { - fill: url(#gradient-night-h); + fill: url(#gradient-night-h); } .d3chart-color-15 { - fill: url(#gradient-dusk-v); + fill: url(#gradient-dusk-v); } .d3chart-hbar .d3chart-color-15 { - fill: url(#gradient-dusk-h); + fill: url(#gradient-dusk-h); +} + +.d3chart-color-stroke-0 { + stroke: url(#gradient-sunrise-h); +} + +.d3chart-color-stroke-1 { + stroke: url(#gradient-blue-h); +} + +.d3chart-color-stroke-2 { + stroke: url(#gradient-sun-h); +} + +.d3chart-color-stroke-3 { + stroke: url(#gradient-seamist-h); +} + +.d3chart-color-stroke-4 { + stroke: url(#gradient-hallows-h); +} + +.d3chart-color-stroke-5 { + stroke: url(#gradient-bubblegum-h); +} + +.d3chart-color-stroke-6 { + stroke: url(#gradient-purple-h); +} + +.d3chart-color-stroke-7 { + stroke: url(#gradient-air-h); +} + +.d3chart-color-stroke-8 { + stroke: url(#gradient-pink-h); +} + +.d3chart-color-stroke-9 { + stroke: url(#gradient-leaves-h); +} + +.d3chart-color-stroke-10 { + stroke: url(#gradient-haze-h); +} + +.d3chart-color-stroke-11 { + stroke: url(#gradient-gnat-h); +} + +.d3chart-color-stroke-12 { + stroke: url(#gradient-fire-h); +} + +.d3chart-color-stroke-13 { + stroke: url(#gradient-ocean-h); +} + +.d3chart-color-stroke-14 { + stroke: url(#gradient-night-h); +} + +.d3chart-color-stroke-15 { + stroke: url(#gradient-dusk-h); +} + +.d3chart-colors-extended .d3chart-legend-16 { + color: #000; + background-image: linear-gradient(108deg, #f0185d, #ff668f); +} +.d3chart-colors-extended .d3chart-color-16 { + fill: url(#gradient-extended-16-v); +} + +.d3chart-colors-extended .d3chart-legend-17 { + color: #000; + background-image: linear-gradient(108deg, #448bd0, #80c0ff); +} +.d3chart-colors-extended .d3chart-color-17 { + fill: url(#gradient-extended-17-v); +} + +.d3chart-colors-extended .d3chart-legend-18 { + color: #000; + background-image: linear-gradient(108deg, #dbd600, #ffff54); +} +.d3chart-colors-extended .d3chart-color-18 { + fill: url(#gradient-extended-18-v); +} + +.d3chart-colors-extended .d3chart-legend-19 { + color: #000; + background-image: linear-gradient(108deg, #63edd7, #a1ffff); +} +.d3chart-colors-extended .d3chart-color-19 { + fill: url(#gradient-extended-19-v); +} + +.d3chart-colors-extended .d3chart-legend-20 { + color: #000; + background-image: linear-gradient(108deg, #cb5f00, #ff932f); +} +.d3chart-colors-extended .d3chart-color-20 { + fill: url(#gradient-extended-20-v); +} + +.d3chart-colors-extended .d3chart-legend-21 { + color: #000; + background-image: linear-gradient(108deg, #ff98a8, #ffd0df); +} +.d3chart-colors-extended .d3chart-color-21 { + fill: url(#gradient-extended-21-v); +} + +.d3chart-colors-extended .d3chart-legend-22 { + color: #fff; + background-image: linear-gradient(108deg, #a800dc, #e449ff); +} +.d3chart-colors-extended .d3chart-color-22 { + fill: url(#gradient-extended-22-v); +} + +.d3chart-colors-extended .d3chart-legend-23 { + color: #000; + background-image: linear-gradient(108deg, #00cfe4, #6affff); +} +.d3chart-colors-extended .d3chart-color-23 { + fill: url(#gradient-extended-23-v); +} + +.d3chart-colors-extended .d3chart-legend-24 { + color: #fff; + background-image: linear-gradient(108deg, #c5114c, #ff5a7c); +} +.d3chart-colors-extended .d3chart-color-24 { + fill: url(#gradient-extended-24-v); +} + +.d3chart-colors-extended .d3chart-legend-25 { + color: #000; + background-image: linear-gradient(108deg, #4af4b5, #8effed); +} +.d3chart-colors-extended .d3chart-color-25 { + fill: url(#gradient-extended-25-v); +} + +.d3chart-colors-extended .d3chart-legend-26 { + color: #000; + background-image: linear-gradient(108deg, #aa9ee9, #e2d5ff); +} +.d3chart-colors-extended .d3chart-color-26 { + fill: url(#gradient-extended-26-v); +} + +.d3chart-colors-extended .d3chart-legend-27 { + color: #000; + background-image: linear-gradient(108deg, #00c6c9, #57ffff); +} +.d3chart-colors-extended .d3chart-color-27 { + fill: url(#gradient-extended-27-v); +} + +.d3chart-colors-extended .d3chart-legend-28 { + color: #000; + background-image: linear-gradient(108deg, #e64b00, #ff8300); +} +.d3chart-colors-extended .d3chart-color-28 { + fill: url(#gradient-extended-28-v); } /* Legend gradients */ .d3chart-legend-0 { - color: #fff; - background: linear-gradient(352.65deg, #F0047F 1.39%, #FC814A 82.63%); + color: #fff; + background: linear-gradient(352.65deg, #f0047f 1.39%, #fc814a 82.63%); } .d3chart-legend-1 { - color: #000; - background: linear-gradient(47.9deg, #0090CA 6.17%, #00BFAD 79.63%); + color: #000; + background: linear-gradient(47.9deg, #0090ca 6.17%, #00bfad 79.63%); } .d3chart-legend-2 { - color: #000; - background: linear-gradient(180deg, #FFC803 0%, #FC814A 100%); + color: #000; + background: linear-gradient(180deg, #fc814a 0%, #ffc803 100%); } .d3chart-legend-3 { - color: #000; - background: linear-gradient(180deg, #78ECC2 0%, #00FFB2 100%); + color: #000; + background: linear-gradient(180deg, #78ecc2 0%, #00ffb2 100%); } .d3chart-legend-4 { - color: #000; - background: linear-gradient(108.82deg, #DF4A1F 0%, #FFA278 90.74%); + color: #000; + background: linear-gradient(108.82deg, #df4a1f 0%, #ffa278 90.74%); } .d3chart-legend-5 { - color: #000; - background: linear-gradient(108.82deg, #FD98BC 32.87%, #FFCCDE 90.74%); + color: #000; + background: linear-gradient(108.82deg, #6b38fb 0%, #ccb4ff 90.74%); } .d3chart-legend-6 { - color: #000; - background: linear-gradient(108.82deg, #6B38FB 0%, #CCB4FF 90.74%); + color: #000; + background: linear-gradient(108.82deg, #fd98bc 32.87%, #ffccde 90.74%); } .d3chart-legend-7 { - color: #000; - background: linear-gradient(108.82deg, #03D0D0 0%, #B5FFF8 90.74%); + color: #000; + background: linear-gradient(108.82deg, #03d0d0 0%, #b5fff8 90.74%); } .d3chart-legend-8 { - color: #fff; - background: linear-gradient(108.82deg, #C40468 0%, #FC2796 90.74%); + color: #fff; + background: linear-gradient(108.82deg, #c40468 0%, #fc2796 90.74%); } .d3chart-legend-9 { - color: #000; - background: linear-gradient(180deg, #78F19A 0%, #13B110 100%); + color: #000; + background: linear-gradient(180deg, #78f19a 0%, #13b110 100%); } .d3chart-legend-10 { - color: #000; - background: linear-gradient(108.82deg, #91A5EE 37.71%, #D6DEFF 90.74%); + color: #000; + background: linear-gradient(108.82deg, #91a5ee 37.71%, #d6deff 90.74%); } .d3chart-legend-11 { - color: #000; - background: linear-gradient(108.82deg, #02C6B3 40.13%, #59F7E7 90.74%); + color: #000; + background: linear-gradient(108.82deg, #02c6b3 40.13%, #59f7e7 90.74%); } .d3chart-legend-12 { - color: #fff; - background: linear-gradient(108.82deg, #FF0F00 0%, #FF928A 90.74%); + color: #fff; + background: linear-gradient(108.82deg, #ff0f00 0%, #ff928a 90.74%); } .d3chart-legend-13 { - color: #000; - background: linear-gradient(180deg, #003EDD 0%, #6CDCFF 100%); + color: #000; + background: linear-gradient(180deg, #003edd 0%, #6cdcff 100%); } .d3chart-legend-14 { - color: #000; - background: linear-gradient(108.82deg, #02465F 3.38%, #6AD7FF 90.74%); + color: #000; + background: linear-gradient(108.82deg, #02465f 3.38%, #6ad7ff 90.74%); } .d3chart-legend-15 { - color: #fff; - background: linear-gradient(108.82deg, #960000 0%, #E94242 92.82%); + color: #fff; + background: linear-gradient(108.82deg, #960000 0%, #e94242 92.82%); } .d3chart-legend-16 { - color: #fff; - background: linear-gradient(108.82deg, #FF72CF 0%, #C92ECC 90.74%); + color: #fff; + background: linear-gradient(108.82deg, #ff72cf 0%, #c92ecc 90.74%); } /* Overrides */ /* Dark mode */ .dark .tick line { - stroke: rgba(255,255,255,.15); + stroke: rgba(255, 255, 255, 0.15); } .dark .d3chart-bubble .tick:nth-child(2n) line { - stroke: rgba(255,255,255,.22); + stroke: rgba(255, 255, 255, 0.22); } -.dark .d3chart-bubble .tick:nth-child(2n+1) line { - stroke: rgba(255,255,255,.1); +.dark .d3chart-bubble .tick:nth-child(2n + 1) line { + stroke: rgba(255, 255, 255, 0.1); +} + +.dark .d3chart-bubble.d3chart-highlight-zero-axis [data-chart-value="0"] line { + stroke: rgba(255, 255, 255, 0.75); } .dark .d3chart-axislabel { - fill: #fff; + fill: #fff; } .dark .d3chart-bubblelabel.offset-l { - filter: url(#offset-label-bg); + filter: url(#offset-label-bg); } diff --git a/src/css/tailwind.css b/src/css/tailwind.css index fa411455f..58aacb659 100644 --- a/src/css/tailwind.css +++ b/src/css/tailwind.css @@ -3,13 +3,14 @@ @import "../site/css/hubspot-form.css"; /* purgecss start ignore */ -@tailwind base; -@tailwind components; +@tailwind base; +@tailwind components; /* purgecss end ignore */ - @layer base { - h1, h2, h3 { + h1, + h2, + h3 { @apply text-blue-900 dark:text-white; } h1 { @@ -30,9 +31,6 @@ } } - - - /* Content sections conventions */ @@ -42,7 +40,6 @@ section { @apply mx-auto; } - :where(section a), :where(dd a) { @apply dark:text-white; @@ -55,11 +52,10 @@ dd a:hover, dd a:focus { @apply border-pink-500; } -p+p { +p + p { @apply mt-4; } - /* CTA links */ @@ -96,7 +92,6 @@ footer p a:focus { @apply border-red-500; } - /* left/right flip-flopping lists */ @@ -107,7 +102,6 @@ footer p a:focus { } } - /* Color theme selector */ .color-theme-selector-wrapper { --padding-inline: 0.5rem; @@ -127,7 +121,7 @@ footer p a:focus { } .color-theme-selector-wrapper::after { - position:absolute; + position: absolute; top: 50%; right: var(--padding-inline); transform: translateY(-50%); @@ -158,31 +152,32 @@ footer p a:focus { /* purgecss start ignore */ .bg-gradient-jams { - background: #D1036F linear-gradient(91.78deg, #D1036F 2.57%, #B6005F 96.33%); + background: #d1036f linear-gradient(91.78deg, #d1036f 2.57%, #b6005f 96.33%); } .bg-gradient-pink-orange, .hover\:bg-gradient-pink-orange:hover, .hover\:bg-gradient-pink-orange:focus { - background: #E7017A linear-gradient(91.78deg, #E7017A 2.57%, #DF4A1F 96.33%); + background: #e7017a linear-gradient(91.78deg, #e7017a 2.57%, #df4a1f 96.33%); } .bg-gradient-blue-green { - background: #0090CA linear-gradient(101.87deg, #0090CA 0%, #00BFAD 105.55%); + background: #0090ca linear-gradient(101.87deg, #0090ca 0%, #00bfad 105.55%); } .bg-gradient-card-sunrise { - --tw-gradient-stops: #F0047F 0%, #FC814A 100%; + --tw-gradient-stops: #f0047f 0%, #fc814a 100%; } .bg-gradient-card-blue { - --tw-gradient-stops: #04A2DD 0%, #4FF3EA 100%; + --tw-gradient-stops: #04a2dd 0%, #4ff3ea 100%; } .bg-gradient-card-seafoam { - --tw-gradient-stops: #88F9ED 0%, #00FFB2 100%; + --tw-gradient-stops: #88f9ed 0%, #00ffb2 100%; } .bg-gradient-card-gold { - --tw-gradient-stops: #FFC803 0%, #FC814A 100%; + --tw-gradient-stops: #ffc803 0%, #fc814a 100%; } .bg-gradient-card-blue-seafoam.bg-gradient-card-blue-seafoam { - background: linear-gradient(101.87deg, #0090CA 0%, #00BFAD 105.55%), linear-gradient(180deg, #009DDC 0%, #58FCEC 100%); + background: linear-gradient(101.87deg, #0090ca 0%, #00bfad 105.55%), + linear-gradient(180deg, #009ddc 0%, #58fcec 100%); } .card-shadow { @@ -310,12 +305,14 @@ details[open] .summary-swap-open { .hero-text { font-size: 2.5em; } -@media (min-width: 40em) { /* 640px */ +@media (min-width: 40em) { + /* 640px */ .hero-text { font-size: 3em; /* 48px /16 */ } } -@media (min-width: 64em) { /* 1024px */ +@media (min-width: 64em) { + /* 1024px */ .hero-text { font-size: 4em; /* 64px /16 */ } @@ -329,7 +326,7 @@ details[open] .summary-swap-open { .tool-content h1, .tool-content h2, .tool-content h3 { - margin: .5em 0; + margin: 0.5em 0; } .tool-content h1, .tool-content h2, @@ -357,7 +354,7 @@ details[open] .summary-swap-open { /* Filters */ .filter-form { - opacity: .4; + opacity: 0.4; pointer-events: none; } .filter-container--js .filter-form { @@ -403,7 +400,7 @@ details[open] .summary-swap-open { @supports (box-shadow: none) { .ais-SearchBox-input:focus { outline: none; - box-shadow: 0 0 1px 4px #E7017A; + box-shadow: 0 0 1px 4px #e7017a; } } @@ -433,8 +430,8 @@ details[open] .summary-swap-open { @apply font-bold; @apply text-blue-100; position: absolute; - right: .4em; - top: .4em; + right: 0.4em; + top: 0.4em; } .jamstacktv-time { display: inline-block; @@ -481,10 +478,10 @@ details[open] .summary-swap-open { @apply italic; } .jamstacktv-caption-quote:before { - content: "“" + content: "“"; } .jamstacktv-caption-quote:after { - content: "”" + content: "”"; } .jamstacktv-no-skip .jamstacktv-title { @apply text-2xl; @@ -515,6 +512,200 @@ details[open] .summary-swap-open { .chart-data-table-head { @apply bg-gray-200 dark:bg-gray-700 border-b-2; } + +.permalink-heading { + @apply flex items-center gap-x-2; +} + +.permalink-heading-anchor { + @apply border-0; +} + +.permalink-heading-icon { + @apply w-4; + @apply opacity-50; +} + +.permalink-heading-icon:hover { + @apply opacity-100; +} + +.survey-grid { + --gap: 2rem; + --full: minmax(var(--gap), 1fr); + --content: min(52rem, 100% - var(--gap) * 2); + --popout: minmax(0, 2rem); + --feature: minmax(0, 5rem); + + display: grid; + grid-template-columns: + [full-start] var(--full) + [feature-start] var(--feature) + [popout-start] var(--popout) + [content-start] var(--content) [content-end] + var(--popout) [popout-end] + var(--feature) [feature-end] + var(--full) [full-end]; +} + +.survey-grid > * { + grid-column: content; +} + +.stack > * + * { + margin-block-end: 0; + margin-block-start: var(--stack-space); +} + +:where(.survey) section { + padding: 0; + margin: 0; + max-width: none; +} + +:where(.survey) h1, +h2, +h3, +h4, +h5 { + margin: 0; +} + +.survey-intro-headings { + grid-column: popout; +} + +.survey-intro-heading { + max-width: 20ch; + @apply font-extrabold; +} + +.survey-intro-subheading { + margin-top: 1.5rem; + @apply leading-tight; +} + +.survey h3 { + @apply text-2xl font-semibold; +} + +.survey h4 { + @apply text-xl font-semibold; +} + +.survey h5 { + @apply text-lg font-semibold; +} + +.survey { + margin-block-start: 4.5rem; +} + +.survey > * + * { + margin-block-start: 6rem; +} + +.survey-toc, +.survey-toc + * { + margin-block-start: 3rem; +} + +.survey-section > * + * { + margin-block-start: 1.5rem; +} + +.survey-section:not(:first-of-type) { + padding-bottom: 6rem; +} + +.survey-section h3, +.survey-section h4, +.survey-section h5 { + margin-block-start: 3rem; +} + +.survey-chart h4, +.survey-chart h5 { + margin-block-start: 0; +} + +.survey-section .survey-chart, +.survey-section .survey-chart + * { + margin-block-start: 3rem; +} + +.survey-section .survey-chart-split, +.survey-section .survey-chart-split + * { + margin-block-start: 4.5rem; +} + +.survey-section ul { + list-style-position: inside; + list-style-type: disc; +} + +.survey-section ul > * + * { + margin-block-start: 0.375rem; +} + +.survey-chart > * + * { + margin-block-start: 1.5rem; +} + +.survey-chart > div:first-of-type { + column-gap: 1.5rem; +} + +.survey-section:first-of-type p:first-of-type { + margin-block-start: 4.5rem; +} + +.survey-chart-split { + display: flex; +} + +.survey-chart-split { + --min: 24rem; + --gap: 1.5rem; + + grid-column: feature; + display: grid; + grid-column-gap: var(--gap); + grid-row-gap: 4.5rem; + grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--min)), 1fr)); +} + +.survey-chart-split .survey-chart { + margin-block-start: 0; +} + +.survey-chart-respondents { + @apply text-sm text-right text-gray-600 dark:text-gray-300; +} + +.survey-chart-subheading { + @apply text-gray-600 dark:text-gray-300 text-sm; + margin-top: 0.375rem; +} + +.survey .final-heading { + @apply font-extrabold bg-gradient-pink-orange text-white; + grid-column: full; + text-align: center; + margin: 2em 0 1em 0; + font-size: clamp(1.5rem, 0.7857rem + 3.5714vw, 4rem); + padding: 0.5em 1em; + line-height: 1.125; + margin-inline: auto; +} + +.survey table { + display: block; + max-width: fit-content; + overflow-x: auto; + white-space: nowrap; +} + /* purgecss end ignore */ @tailwind utilities; diff --git a/src/site/_data/announcementbar.json b/src/site/_data/announcementbar.json index d769098a2..b16c357b5 100644 --- a/src/site/_data/announcementbar.json +++ b/src/site/_data/announcementbar.json @@ -1,5 +1,5 @@ { - "text": "Register for Jamstack Conf!", - "cta": "Join us in San Francisco or online, November 7-8.", - "url": "https://ntl.fyi/3K1iprI" + "text": "NEW! Jamstack Community Survey Results 2022 are here.", + "cta": "Explore the latest trends in web development", + "url": "/survey/2022/" } diff --git a/src/site/_includes/banner.njk b/src/site/_includes/banner.njk index 928e12e63..bb6a9f250 100644 --- a/src/site/_includes/banner.njk +++ b/src/site/_includes/banner.njk @@ -1,3 +1,3 @@ - {{ announcementbar.text }} {% if announcementbar.cta %}{{ announcementbar.cta }}{% endif %} + {{ announcementbar.text }} {% if announcementbar.cta %}{{ announcementbar.cta }}{% endif %} diff --git a/src/site/_includes/components/permalink-heading.njk b/src/site/_includes/components/permalink-heading.njk new file mode 100644 index 000000000..07d39ab55 --- /dev/null +++ b/src/site/_includes/components/permalink-heading.njk @@ -0,0 +1,22 @@ +{% macro render(level, text, headingClasses, id) %} + <{{level}} id="{% if id %}{{ id }}{% else %}{{ text | slugify }}{% endif %}" class="permalink-heading {{ headingClasses }}"> + {{ text }} + + Permalink + + + +{% endmacro %} diff --git a/src/site/_includes/header.njk b/src/site/_includes/header.njk index 9cd1a6bb8..e53e13ca7 100644 --- a/src/site/_includes/header.njk +++ b/src/site/_includes/header.njk @@ -7,12 +7,11 @@ { "url": "/generators/", "text": "Site Generators" }, { "url": "/headless-cms/", "text": "Headless CMS" }, { "url": "/community/", "text": "Community" }, - { "url": "/survey/2021/", "text": "Community Survey", "children": [ - { "url": "/survey/2021/#demographics", "text": "Demographics" }, - { "url": "/survey/2021/#adoption", "text": "Jamstack adoption" }, - { "url": "/survey/2021/#workflows", "text": "Workflows" }, - { "url": "/survey/2021/#choices", "text": "Technology choices" }, - { "url": "/survey/2021/#conclusion", "text": "Conclusion" } + { "url": "/survey/2022/", "text": "Community Survey", "children": [ + { "url": "/survey/2022/#whos-doing-the-building", "text": "Who’s doing the building?" }, + { "url": "/survey/2022/#what-is-the-jamstack-community-building", "text": "What is the Jamstack Community building?" }, + { "url": "/survey/2022/#how-are-we-building", "text": "How are we building?" }, + { "url": "/survey/2022/#emerging-trends-in-the-jamstack-community", "text": "Emerging Trends in the Jamstack Community" } ] } ] %} diff --git a/src/site/_includes/survey/adoption.njk b/src/site/_includes/survey/2021/adoption.njk similarity index 100% rename from src/site/_includes/survey/adoption.njk rename to src/site/_includes/survey/2021/adoption.njk diff --git a/src/site/_includes/survey/choices.njk b/src/site/_includes/survey/2021/choices.njk similarity index 100% rename from src/site/_includes/survey/choices.njk rename to src/site/_includes/survey/2021/choices.njk diff --git a/src/site/_includes/survey/conclusion.njk b/src/site/_includes/survey/2021/conclusion.njk similarity index 100% rename from src/site/_includes/survey/conclusion.njk rename to src/site/_includes/survey/2021/conclusion.njk diff --git a/src/site/_includes/survey/demographics.njk b/src/site/_includes/survey/2021/demographics.njk similarity index 100% rename from src/site/_includes/survey/demographics.njk rename to src/site/_includes/survey/2021/demographics.njk diff --git a/src/site/_includes/survey/experience.njk b/src/site/_includes/survey/2021/experience.njk similarity index 100% rename from src/site/_includes/survey/experience.njk rename to src/site/_includes/survey/2021/experience.njk diff --git a/src/site/_includes/survey/workflows.njk b/src/site/_includes/survey/2021/workflows.njk similarity index 100% rename from src/site/_includes/survey/workflows.njk rename to src/site/_includes/survey/2021/workflows.njk diff --git a/src/site/_includes/survey/2022/how-are-we-building/cms-usage-vs-satisfaction.njk b/src/site/_includes/survey/2022/how-are-we-building/cms-usage-vs-satisfaction.njk new file mode 100644 index 000000000..794a81414 --- /dev/null +++ b/src/site/_includes/survey/2022/how-are-we-building/cms-usage-vs-satisfaction.njk @@ -0,0 +1,125 @@ +
+
+ + {{ permalinkHeading.render('h4', "Content Management Systems") }} +
+ +
+ +
+
+
+ +
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CMSUsed on "some" or "many" projectsSatisfaction score
1. WordPress37%0.5
2. Notion26%2.3
3. WordPress (Headless)22%1.0
4. Contentful19%1.4
5. Strapi18%2.0
6. Sanity16%3.0
7. Drupal14%0.6
8. Wix13%0.6
9. Webflow12%1.0
10. Prismic11%1.8
11. Squarespace11%0.6
12. Ghost10%1.5
13. Storyblok9%2.0
14. Builder8%1.0
15. Forestry8%1.0
16. Agility CMS7%0.8
17. Weebly7%0.8
18. ButterCMS6%1.0
19. Contentstack6%1.0
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/how-are-we-building/frameworks-usage-vs-satisfaction-changes.njk b/src/site/_includes/survey/2022/how-are-we-building/frameworks-usage-vs-satisfaction-changes.njk new file mode 100644 index 000000000..fb14f6a99 --- /dev/null +++ b/src/site/_includes/survey/2022/how-are-we-building/frameworks-usage-vs-satisfaction-changes.njk @@ -0,0 +1,191 @@ +
+
+ {{ permalinkHeading.render('h4', 'Frameworks by 1-year change in usage and satisfaction') }} +
+ +
+ +
+
+
+ +
Source: Jamstack Community Survey 2021—2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FrameworkUsage change (%)Satisfaction changeUsage
1. React2.9%-1.471%
2. Express-2.3%-0.249%
3. Next.js3.8%-2.847%
4. jQuery-6.8%0.144%
5. Vue-6.4%-2.133%
6. Vite17.8%0.132%
7. Gatsby-8.9%-1.028%
8. Nuxt.js-2.8%-2.922%
9. Angular 2+0.1%-0.220%
10. 11ty1.6%-2.219%
11. Svelte4.6%-0.219%
12. SvelteKit6.9%-2.015%
13. Jekyll-2.5%-0.114%
14. Angular 1.x-1.3%0.114%
15. Hugo-1.8%-0.113%
16. Preact1.5%-0.712%
17. Remix7.7%0.910%
18. Nest0.2%-0.69%
19. VuePress-0.8%-0.78%
20. Gridsome-1.5%-0.97%
21. Docusaurus0.8%0.67%
22. Hapi0.4%-0.36%
23. Sapper-1.1%-0.55%
24. Stencil0.7%-0.35%
25. RedwoodJS-0.3%1.24%
26. Blitz.js0.7%1.04%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/how-are-we-building/frameworks-usage-vs-satisfaction.njk b/src/site/_includes/survey/2022/how-are-we-building/frameworks-usage-vs-satisfaction.njk new file mode 100644 index 000000000..392823a9b --- /dev/null +++ b/src/site/_includes/survey/2022/how-are-we-building/frameworks-usage-vs-satisfaction.njk @@ -0,0 +1,179 @@ +
+
+ {{ permalinkHeading.render('h4', 'Frameworks by usage and satisfaction') }} +
+ +
+ +
+
+
+ +
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageUsed on "some" or "many" projectsSatisfaction score
1. React71%2.9
2. Express49%1.7
3. Next.js47%4.2
4. jQuery44%0.3
5. Vue33%3.1
6. Vite32%9.7
7. Gatsby28%0.9
8. Nuxt.js22%2.7
9. Angular 2+20%0.7
10. 11ty19%3.8
11. Svelte19%5.3
12. SvelteKit15%4.0
13. Jekyll14%0.4
14. Angular 1.x14%0.3
15. Hugo13%1.2
16. Preact12%2.0
17. Astro11%4.5
18. Remix10%2.3
19. Nest9%2.0
20. VuePress8%1.7
21. Gridsome7%0.8
22. Docusaurus7%2.5
23. Hapi6%1.0
24. SolidJS6%2.0
25. Sapper5%0.7
26. Stencil5%1.5
27. Quasar4%1.0
28. RedwoodJS4%3.0
29. Blitz.js4%3.0
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/how-are-we-building/index.njk b/src/site/_includes/survey/2022/how-are-we-building/index.njk new file mode 100644 index 000000000..290136c48 --- /dev/null +++ b/src/site/_includes/survey/2022/how-are-we-building/index.njk @@ -0,0 +1,268 @@ +
+ {{ + permalinkHeading.render( + "h2", + "How are we building?", + "", + "how-are-we-building" + ) + }} + +

+ Our largest set of questions revolve around technical choices. It’s easy for + this kind of data to turn into a popularity contest, so we should be clear: + the most popular choice is not always the best choice for you. As we’ll see + shortly, your use case matters much more than total adoption of a + technology. However, within the bounds of a use case, popularity can help. + Open source technology benefits from more contributors: bugs are fixed + faster, documentation is better, rough edges are smoothed away more quickly, + and there will be more plugins and third-party integrations. +

+ + {{ + permalinkHeading.render( + "h3", + "A note on how to read Usage + Satisfaction graphs" + ) + }} + +

+ This section contains a number of graphs like the one below. On the + horizontal axis, we measure the usage of a technology, as measured by the + number of people who say they have used that technology in the last year on + “some projects” or “many projects”. We do not count people who say they use + a technology “rarely”, so we believe our “some+many” number represents real, + regular usage. +

+ +

+ At the same time as we ask people how often they use a technology, we ask + them whether they would like to use it more or less in the coming year. We + take the ratio of the “want to use it more” and the “want to use it less” + numbers to create our vertical axis, which we call the “Satisfaction score”. + A score of 1.0 or more means the technology’s users are on balance + enthusiastic about it, while under 1.0 it means they are not. In the three + years of our survey, a satisfaction score under 1.0 has been strongly (but + not perfectly) predictive of a loss in usage the following year, which high + satisfaction scores correlate well to growth in share. +

+ + {{ permalinkHeading.render("h3", "Content Management Systems (CMS)") }} + +

+ The decoupled nature of frontend and backend code in the Jamstack ecosystem + means that CMS are a big component of many of the websites we build. As + anyone who’s built a site with one knows, once a CMS has become embedded + into your company’s culture and workflows it can be hard to get it out + again, so this is a critical choice for many people. +

+ + + + {% include './cms-usage-vs-satisfaction.njk' %} + + {{ permalinkHeading.render("h3", "Programming languages") }} + +

+ There are not a lot of surprises in this year’s programming language data if + you have seen our previous surveys. One note: when we show programming + languages, we should be clear that this data is about their popularity + within the Jamstack community; in more general computing surveys Java is a + much more popular choice. +

+ + + + {% include './programming-language-usage-vs-satisfaction.njk' %} + + {{ permalinkHeading.render("h3", "Web frameworks") }} + +

+ Always our largest section, we tracked 29 frameworks this year, with a few + that we have tracked in previous years falling out of the survey (our + cut-off for frameworks that are not growing quickly is 4% share). +

+ + {{ permalinkHeading.render("h4", "React and Next.js") }} + +

+ The most obvious story in our framework data is the continued growth of + React. With high satisfaction scores last year, we predicted it would + continue to grow and that was borne out this year, hitting a new record of + 71% share, the highest of any framework we’ve tracked in all 3 years. While + there are many options for building a reactive web app, the enormous + ecosystem around React continues to make it an easy choice for many. +

+ +

+ Riding the tails of React’s popularity is Next.js, a full featured “kitchen + sink” framework based on React. This year 47%, or nearly 1 in 2 developers + say they used Next.js in some or many projects, and with a satisfaction + score over 4.0 we expect to see it continue to grow. +

+ + {{ permalinkHeading.render("h4", "Vite") }} + +

+ Although we have been tracking it in our frameworks data, Vite is more of a + bundler, competing with choices such as Webpack and Babel. It has been + adopted as the default bundler for several other frameworks including Nuxt + and SvelteKit, contributing to its high share, but its stellar satisfaction + score is all its own. +

+ + {% include './frameworks-usage-vs-satisfaction.njk' %} + + {{ permalinkHeading.render("h4", "Zooming in on smaller frameworks") }} + +

+ Looking at the crowded bottom-left corner of the overall frameworks graph + can hide some detail, so we take a closer look at frameworks at 10% share or + less. In here are some older frameworks such as Hapi and Gridsome, but also + some new entrants. +

+ + + + {% include './smaller-frameworks-usage-vs-satisfaction.njk' %} + + {{ permalinkHeading.render("h4", "Tracking usage and satisfaction changes") }} + +

+ We have found it instructive to look at how usage and satisfaction scores in + our survey have changed from year to year. Keep in mind that these are + changes; Next.js and Nuxt.js for example both have high + satisfaction scores overall, just lower than last year. We split this graph + into four quadrants. +

+ + {{ permalinkHeading.render("h5", "Bottom-right: regular growth") }} + +

+ A pattern we have seen every year is that frameworks that grow share usually + lose satisfaction score while doing so. This makes sense: as more people + adopt a technology, there are fewer enthusiastic early adopters, and more + people using the framework for use cases that are outside of its sweet spot. +

+ + + + {{ permalinkHeading.render("h5", "Top right: early adoption") }} + +

+ Technologies in the early phases of adoption tend to see rapid growth and + users who get happier year on year. +

+ + + + {{ permalinkHeading.render("h5", "Top left: core users") }} + +

+ Occupying a quadrant almost by itself is jQuery. Anyone still using jQuery + in 2022 is heavily invested in doing so and it shows. +

+ + {{ permalinkHeading.render("h5", "Bottom left: danger zone") }} + +

+ Losing usage share and satisfaction score at the same time is bad news for + project maintainers. +

+ + + + {% include './frameworks-usage-vs-satisfaction-changes.njk' %} +
diff --git a/src/site/_includes/survey/2022/how-are-we-building/programming-language-usage-vs-satisfaction.njk b/src/site/_includes/survey/2022/how-are-we-building/programming-language-usage-vs-satisfaction.njk new file mode 100644 index 000000000..dfc28aeba --- /dev/null +++ b/src/site/_includes/survey/2022/how-are-we-building/programming-language-usage-vs-satisfaction.njk @@ -0,0 +1,113 @@ +
+
+ {{ permalinkHeading.render('h4', 'Programming languages by usage and satisfaction') }} +
+ +
+ +
+
+
+ +
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageUsed on "some" or "many" projectsSatisfaction score
1. JavaScript96%3.0
2. TypeScript67%7.4
3. SQL64%1.8
4. Shell (Bash)53%1.5
5. Python42%2.2
6. PHP42%0.6
7. Java26%0.6
8. C#21%1.1
9. Ruby18%1.0
10. C/C++17%1.1
11. Go16%2.2
12. Rust12%3.0
13. Visual Basic10%0.7
14. Swift9%2.0
15. Objective-C6%0.5
16. Perl6%0.5
17. Elixir6%1.5
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/how-are-we-building/smaller-frameworks-usage-vs-satisfaction.njk b/src/site/_includes/survey/2022/how-are-we-building/smaller-frameworks-usage-vs-satisfaction.njk new file mode 100644 index 000000000..8407a6390 --- /dev/null +++ b/src/site/_includes/survey/2022/how-are-we-building/smaller-frameworks-usage-vs-satisfaction.njk @@ -0,0 +1,94 @@ +
+
+ {{ permalinkHeading.render('h4', 'Smaller frameworks by usage and satisfaction') }} +
+ +
+ +
+
+
+ +
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FrameworkUsed on "some" or "many" projectsSatisfaction score
1. Remix10%2.3
2. Nest9%2.0
3. VuePress8%1.7
4. Gridsome7%0.8
5. Docusaurus7%2.5
6. Hapi6%1.0
7. SolidJS6%2.0
8. Sapper5%0.7
9. Stencil5%1.5
10. Quasar4%1.0
11. RedwoodJS4%3.0
12. Blitz.js4%3.0
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/what-are-we-building/audience-sizes.njk b/src/site/_includes/survey/2022/what-are-we-building/audience-sizes.njk new file mode 100644 index 000000000..0321629d1 --- /dev/null +++ b/src/site/_includes/survey/2022/what-are-we-building/audience-sizes.njk @@ -0,0 +1,62 @@ +
+
+
+ {{ permalinkHeading.render('h4', "How many users are the websites you're building meant to serve?") }} +

Percentage of respondents

+
+
+ +
+ +
+
+
+ +
Source: Jamstack Community Survey 2020—2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
202020212022
10s of users63%65%64%
100s of users78%77%74%
1000s of users83%79%75%
100-000s of users58%55%55%
1-000-000s of users32%32%36%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/what-are-we-building/index.njk b/src/site/_includes/survey/2022/what-are-we-building/index.njk new file mode 100644 index 000000000..ea36e74f3 --- /dev/null +++ b/src/site/_includes/survey/2022/what-are-we-building/index.njk @@ -0,0 +1,84 @@ +
+ {{ + permalinkHeading.render( + "h2", + "What is the Jamstack Community building?", + "", + "what-is-the-jamstack-community-building" + ) + }} + +

Moving on from demographics, let’s look at what we’re building in 2022.

+ + {{ permalinkHeading.render("h3", "Purposes of websites built") }} + +

+ Most people build lots of sites in a year, so we allowed people to give + multiple answers to our question about what the sites they built were for. + The results were similar to last year: the single most common answer was + personal websites (such as blogs or resumes). Consumer software, B2B + software and e-commerce remained major areas of focus. +

+ + {% include './what-is-the-purpose-of-the-sites-you-built-in-2022.njk' %} + + {{ permalinkHeading.render("h3", "Application types") }} + +

+ Another question we repeated from last year was asking people what kinds of + websites they built. As was the case in 2021, Single Page Apps (SPAs) were + popular, but a majority were various levels of static websites – either + fully or mostly static. This is unsurprising, since the core of Jamstack has + always been progressive enhancement of static websites. +

+ +

+ Fully dynamic websites remain popular for some applications, and this time + we asked about a new category: edge-dynamic sites, which we’re defining here + as sites that are fully dynamic, and render all their content at the edge + (i.e. using serverless functions or edge functions). This is a pretty new + category and so it was also the smallest, but nearly half (47%) said they’d + built at least one website of this kind this year. This tracks the + growth in serverless + we saw in later questions. +

+ + {% include './types-of-sites-built-last-12-months.njk' %} + + {{ permalinkHeading.render("h3", "Target devices") }} + +

+ Another standard question we ask every year is about what devices your work + targets. We’ve used this previously to point out that while “mobile-first” + has been the mantra of the industry for a long time, desktop devices still + have a small edge in terms of being the most important target for our work, + with tablets third. +

+ +

+ However, over the last 3 years our “everything else” category, called + “device-specific browsers” (we suggested things like Internet of Things + devices, or smart watches) has been steadily growing and now fully one-third + of people say this somewhat poorly defined fourth category is at least + somewhat important. This was a surprise! We’ll be conducting follow-up + surveys to discover what exactly the folks who call these devices important + were talking about. +

+ + {% include './target-devices-by-type.njk' %} + + {{ permalinkHeading.render("h3", "Audience sizes") }} + +

+ Our final question about the goals of our websites in 2022 was about + audience sizes: how big is the audience your website serves? This is another + question where we have data from all 3 years of the survey and are able to + see a trend, although not much has changed. The most common type of website + remains one built for a relatively small audience – hundreds, or a few + thousand users. But more than a third of people say they’ve built websites + this year intended for audiences of millions, and this category grew in + 2022. +

+ + {% include './audience-sizes.njk' %} +
diff --git a/src/site/_includes/survey/2022/what-are-we-building/target-devices-by-type.njk b/src/site/_includes/survey/2022/what-are-we-building/target-devices-by-type.njk new file mode 100644 index 000000000..a2f09aa19 --- /dev/null +++ b/src/site/_includes/survey/2022/what-are-we-building/target-devices-by-type.njk @@ -0,0 +1,62 @@ +
+
+
+ {{ permalinkHeading.render('h4', "Target devices by type, 2020-2022", 'text-xl font-semibold') }} +

Percentage of respondents saying these targets were somewhat or very important

+
+
+ +
+ +
+
+
+ +
Source: Jamstack Community Survey 2020—2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type202020212022
Desktops99%98%97%
Phones95%94%94%
Tablets92%91%90%
Device-specific browsers18%25%34%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/what-are-we-building/types-of-sites-built-last-12-months.njk b/src/site/_includes/survey/2022/what-are-we-building/types-of-sites-built-last-12-months.njk new file mode 100644 index 000000000..88dcca2bb --- /dev/null +++ b/src/site/_includes/survey/2022/what-are-we-building/types-of-sites-built-last-12-months.njk @@ -0,0 +1,73 @@ +
+
+
+ {{ permalinkHeading.render('h4', "Types of websites built in the last 12 months") }} +

Percentage of respondents

+
+
+
+ +
+
+
+ +
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NoneA few projectsMany projectsMost projectsAll
SPA20%41%15%16%8%
Fully dynamic28%36%15%15%6%
Edge-dynamic53%30%9%6%3%
Mostly static26%43%17%11%3%
Fully static30%40%15%11%4%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/what-are-we-building/what-is-the-purpose-of-the-sites-you-built-in-2022.njk b/src/site/_includes/survey/2022/what-are-we-building/what-is-the-purpose-of-the-sites-you-built-in-2022.njk new file mode 100644 index 000000000..08341a4b6 --- /dev/null +++ b/src/site/_includes/survey/2022/what-are-we-building/what-is-the-purpose-of-the-sites-you-built-in-2022.njk @@ -0,0 +1,84 @@ +
+
+
+ {{ permalinkHeading.render('h4', "What is the purpose of the websites you built in 2022?") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PurposePercentage of Survey Participants
Personal websites45%
Consumer software40%
B2B software39%
E-commerce38%
Informational38%
Internal tools37%
Documentation29%
Lead capture29%
Enterprise software26%
News/Entertainment14%
Social media14%
Retail13%
Games11%
Streaming media9%
Politics/Activism5%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/where-are-we-going/index.njk b/src/site/_includes/survey/2022/where-are-we-going/index.njk new file mode 100644 index 000000000..9e802ea32 --- /dev/null +++ b/src/site/_includes/survey/2022/where-are-we-going/index.njk @@ -0,0 +1,179 @@ +
+ {{ + permalinkHeading.render( + "h2", + "Emerging Trends in the Jamstack Community", + "", + "emerging-trends-in-the-jamstack-community" + ) + }} + +

+ In addition to the current state of the Jamstack community, we also gathered + some data about emerging trends, and tried to use our data to make some + predictions about where we expect things will go in 2023. +

+ + {{ permalinkHeading.render("h3", "Trends in web frameworks") }} + +

+ The continued dominance of React in the web framework landscape seems set to + continue, and we expect further growth from React and its allied Next.js in + 2023. But React is only one of many possible ways to build a useful website. +

+ +

+ If you’re looking for interactivity with high performance and a low resource + footprint, such as if your user base is primarily mobile, you might want to + look at Astro or Sveltekit. +

+ +

+ As we mentioned already, if you’re building a static or nearly-static site, + we continue to think 11ty is an excellent choice given its growth relative + to other SSGs in the space. +

+ + {{ permalinkHeading.render("h3", "Is Web3 the future?") }} + +

+ We heard a great deal on social media in 2022 about Web3, so we included a + couple of specific questions about Web3 technologies in this year’s survey + (after running a small pre-survey, we did not include the Metaverse in our + definition of Web3, as a majority of respondents did not think of it as part + of Web3). +

+ +

+ Overall, only about 10% of respondents said they had tried out any of the + Web3 technologies we asked about. Applying the same “some or many projects” + standard that we do when counting web frameworks, Web3 technologies did not + cross 3% usage. +

+ + {% include './web3-usage.njk' %} + +

+ Low usage is to be expected in an early technology, so we also asked + sentiment questions. 13% of respondents did not know what Web3 was, while + another third were neutral towards it. Of those who expressed feelings about + Web3, those who were negative about it (31%) slightly outnumbered those who + were positive about it (28%). If we translate this into the satisfaction + score we use elsewhere in the survey, it would be 0.9, and we would expect + Web3 to lose usage share in the coming year. +

+ + {% include './web3-feelings.njk' %} + + {{ permalinkHeading.render("h3", "Web Components have arrived") }} + +

+ Browser-native Web Components were introduced 11 years ago but lacked + support from all major browsers until roughly + 2018. Since then, their adoption has accelerated notably, and while they are + still not in use by the majority of our respondents we believe we can call + them a solid choice in 2022. +

+ +

+ Using the same standards we apply to web frameworks, native Web Components + have usage of 32%. Even more positively, their Satisfaction Score is 4.3, so + we expect rapid growth in the adoption of web components in 2023. +

+ + {% include './web-components.njk' %} + + {{ permalinkHeading.render("h3", "Jamstack is Increasingly Serverless") }} + +

+ The final trend we covered was the growth in serverless technology, + sometimes also called edge computing. Last year we were taken somewhat by + surprise to learn that serverless adoption had hit 46%, so this year we made + sure to ask a more detailed question. +

+ +

+ Using the standard we used last year of any adoption at all, serverless + usage jumped from 46% to 71%. We expected growth, but that was much faster + than we predicted. Applying our usual standard of “some+many” projects we + use for web frameworks, serverless technology is at 35% adoption, which + relative to frameworks would make it bigger than Vue but smaller than + Next.js. +

+ +

+ We mentioned above that there was a big shift in the last year of people + describing themselves as “full stack” developers from “front end” + developers. We think the big jump in serverless adoption may be the + explanation: serverless lets front-end developers build full-stack + applications with a minimum of fuss, and the adoption has been so fast it’s + changing how we describe ourselves. +

+ +

+ Given the rapid growth since last year, we expect to see further growth in + adoption and especially users moving from the “few projects” category into + more serious usage. +

+ + {% include './serverless.njk' %} + +

+ Jamstack remains the standard architecture of the web +

+ +

+ The evolution of the web as a platform continues to be rapid and exciting, + with new technologies pushing the boundaries of what the web can do and how + quickly developers can ship. We’ve also learned more about our community as + human beings: where they are, who they are, and what motivates them. +

+ +

+ We hope giving you a sense of the community you’re part of and the + technologies that your peers use gives you a sense of place and some ideas + about where you should put your time and energy in the next year. +

+ +

+ Once again, we’d like to thank everybody who participated in the community + survey. +

+ + +
diff --git a/src/site/_includes/survey/2022/where-are-we-going/serverless.njk b/src/site/_includes/survey/2022/where-are-we-going/serverless.njk new file mode 100644 index 000000000..b0c42ca28 --- /dev/null +++ b/src/site/_includes/survey/2022/where-are-we-going/serverless.njk @@ -0,0 +1,45 @@ +
+
+
+ {{ permalinkHeading.render('h4', "How many websites you've built this year have used serverless functions?") }} +

Percentage of respondents

+
+
+
+ +
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Count
None30%
A few projects36%
Some projects18%
Many projects12%
All5%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/where-are-we-going/web-components.njk b/src/site/_includes/survey/2022/where-are-we-going/web-components.njk new file mode 100644 index 000000000..09fbf9448 --- /dev/null +++ b/src/site/_includes/survey/2022/where-are-we-going/web-components.njk @@ -0,0 +1,52 @@ +
+
+
+ {{ permalinkHeading.render('h4', "How much have you used Web Components in the last 12 months?") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Count
Not aware of them23%
Rarely and don't want to16%
Rarely but want more29%
Some and want fewer5%
Some and want more19%
Many and want fewer1%
Many and want more7%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/where-are-we-going/web3-feelings.njk b/src/site/_includes/survey/2022/where-are-we-going/web3-feelings.njk new file mode 100644 index 000000000..c3afe692b --- /dev/null +++ b/src/site/_includes/survey/2022/where-are-we-going/web3-feelings.njk @@ -0,0 +1,48 @@ +
+
+
+ {{ permalinkHeading.render('h4', "In general, how do you feel about Web3?") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
In general, how do you feel about Web3?Count
I don't know what it is13%
Strongly negative18%
Negative13%
Neutral29%
Positive20%
Strongly positive8%
+
+
diff --git a/src/site/_includes/survey/2022/where-are-we-going/web3-usage.njk b/src/site/_includes/survey/2022/where-are-we-going/web3-usage.njk new file mode 100644 index 000000000..6e579c002 --- /dev/null +++ b/src/site/_includes/survey/2022/where-are-we-going/web3-usage.njk @@ -0,0 +1,89 @@ +
+
+
+ {{ permalinkHeading.render('h4', "Which Web3 technologies did you use in the last 12 months?") }} +

Percentage of respondents

+
+
+
+ +
+
+
+ +
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NoneA few projectsMany projectsMost projectsAll
Bitcoin89%7%1%1%1%
Ethereum87%9%1%1%1%
Solana93%4%1%1%1%
Other blockchain89%7%1%1%1%
DAOs93%4%1%1%1%
Other dApps90%6%2%1%1%
NFTs86%10%2%1%1%
+
+
diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/employment-status.njk b/src/site/_includes/survey/2022/whos-doing-the-building/employment-status.njk new file mode 100644 index 000000000..18263c682 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/employment-status.njk @@ -0,0 +1,52 @@ +
+
+
+ {{ permalinkHeading.render('h4', "What's your employment status?") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Employment StatusPercentage of Survey Participants
Full-time50%
Student21%
Self-employed13%
Contractor6%
Part-time5%
Between jobs5%
Retired1%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/experience-by-region.njk b/src/site/_includes/survey/2022/whos-doing-the-building/experience-by-region.njk new file mode 100644 index 000000000..88238164d --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/experience-by-region.njk @@ -0,0 +1,155 @@ +
+
+
+ {{ permalinkHeading.render('h5', "Experience by region") }} +

Percentage of respondents

+
+
+
+ +
+
+
+ +
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Years of experienceAfricaAsia PacificCentral AmericaEastern AsiaEuropeMiddle EastNorth AmericaSouth AmericaSouthern AsiaCaribbean
< 19.3%21.1%0.5%3.6%21.7%2.1%21.7%7.2%12.9%0.0%
1-212.4%16.4%1.2%0.7%27.9%0.9%21.6%5.9%12.0%0.9%
3-48.4%13.1%1.3%2.2%37.4%2.2%24.5%4.5%5.4%1.1%
5-65.7%12.9%2.5%2.0%34.5%2.2%28.3%6.2%3.7%2.0%
7-83.7%6.7%0.7%1.9%39.6%0.7%37.0%3.0%5.6%1.1%
9-102.5%5.8%1.1%0.4%42.4%0.7%40.6%4.7%1.1%0.7%
11-123.8%5.0%0.6%1.3%51.9%1.3%32.5%3.1%0.6%0.0%
13-143.5%8.1%0.0%0.0%39.1%5.8%35.6%2.3%5.8%0.0%
15+0.7%8.0%0.5%1.1%40.3%1.5%44.1%2.0%1.3%0.5%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/experience-increasing-over-time.njk b/src/site/_includes/survey/2022/whos-doing-the-building/experience-increasing-over-time.njk new file mode 100644 index 000000000..34acd9653 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/experience-increasing-over-time.njk @@ -0,0 +1,94 @@ +
+
+
+ {{ permalinkHeading.render('h4', "Experience increasing over time") }} +

Years of experience relevant to current job, 2020-2022

+
+

Percentage of respondents

+
+ +
+ +
+
+
+ + +
Source: Jamstack Community Survey 2020—2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Years of experience202020212022
< 14%13%8%
1-213%19%16%
3-420%18%16%
5-615%12%14%
7-89%7%9%
9-1012%8%9%
11-128%5%5%
13-145%3%3%
15+14%14%19%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/have-you-changed-jobs-in-the-last-12-months.njk b/src/site/_includes/survey/2022/whos-doing-the-building/have-you-changed-jobs-in-the-last-12-months.njk new file mode 100644 index 000000000..48fd52739 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/have-you-changed-jobs-in-the-last-12-months.njk @@ -0,0 +1,32 @@ +
+
+
+ {{ permalinkHeading.render('h4', "Have you changed jobs in the last 12 months?") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + +
Have you changed jobs in the last 12 months?Count
No67%
Yes33%
+
+
diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/i-changed-jobs-to-work-remotely-more-often.njk b/src/site/_includes/survey/2022/whos-doing-the-building/i-changed-jobs-to-work-remotely-more-often.njk new file mode 100644 index 000000000..c1e4dceb6 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/i-changed-jobs-to-work-remotely-more-often.njk @@ -0,0 +1,44 @@ +
+
+
+ {{ permalinkHeading.render('h4', "I changed jobs to work remotely more often") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Percentage of Survey Participants
Strongly disagree23%
Somewhat disagree8%
Neither agree nor disagree34%
Somewhat agree12%
Strongly agree23%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/i-enjoy-remote-work.njk b/src/site/_includes/survey/2022/whos-doing-the-building/i-enjoy-remote-work.njk new file mode 100644 index 000000000..cabec9216 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/i-enjoy-remote-work.njk @@ -0,0 +1,44 @@ +
+
+
+ {{ permalinkHeading.render('h4', "I enjoy remote work") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Percentage of Survey Participants
Strongly disagree3%
Somewhat disagree4%
Neither agree nor disagree7%
Somewhat agree26%
Strongly agree61%
+
+
diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/i-would-like-to-work-remote-more-often.njk b/src/site/_includes/survey/2022/whos-doing-the-building/i-would-like-to-work-remote-more-often.njk new file mode 100644 index 000000000..a41891b56 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/i-would-like-to-work-remote-more-often.njk @@ -0,0 +1,44 @@ +
+
+
+ {{ permalinkHeading.render('h4', "I would like to work remotely more often") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Percentage of Survey Participants
Strongly disagree5%
Somewhat disagree8%
Neither agree nor disagree28%
Somewhat agree16%
Strongly agree43%
+
+
diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/i-would-quit-my-job-if-in-person-was-more-often.njk b/src/site/_includes/survey/2022/whos-doing-the-building/i-would-quit-my-job-if-in-person-was-more-often.njk new file mode 100644 index 000000000..dafbc7cec --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/i-would-quit-my-job-if-in-person-was-more-often.njk @@ -0,0 +1,44 @@ +
+
+
+ {{ permalinkHeading.render('h4', "I would quit my job if they made me work in person more often") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Percentage of Survey Participants
Strongly disagree12%
Somewhat disagree12%
Neither agree nor disagree20%
Somewhat agree27%
Strongly agree28%
+
+
diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/i-would-quit-my-job-if-remote-was-more-often.njk b/src/site/_includes/survey/2022/whos-doing-the-building/i-would-quit-my-job-if-remote-was-more-often.njk new file mode 100644 index 000000000..8132c1a68 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/i-would-quit-my-job-if-remote-was-more-often.njk @@ -0,0 +1,44 @@ +
+
+
+ {{ permalinkHeading.render('h4', "I would quit my job if they made me work remotely more often") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Percentage of Survey Participants
Strongly disagree65%
Somewhat disagree11%
Neither agree nor disagree13%
Somewhat agree5%
Strongly agree6%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/index.njk b/src/site/_includes/survey/2022/whos-doing-the-building/index.njk new file mode 100644 index 000000000..0305bd1a9 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/index.njk @@ -0,0 +1,235 @@ +
+ + {{ permalinkHeading.render("h2", "Who’s doing the building?", "", "whos-doing-the-building") }} + +

+ As usual, we kick off by looking at the demographics of our community. Who + are we, exactly? +

+ + + {{ permalinkHeading.render("h3", "Job titles") }} + +

+ There was not much change in the breakdown of reported job titles in our + survey this year: as usual, nearly everyone (84%) who responded considers + themselves to be an engineer of some kind. There was one curious change, + however: the number of people calling themselves “full stack” versus “front + end” has almost exactly flipped, from 32% full stack and 45% front end last + year to 44% full stack and 33% front end in the 2022 survey. None of the + other demographic markers we tracked changed very much, so we believe this + is a real shift in how the community thinks of itself. We have two theories + about why this might be the case, and we’ll discuss them in the sections on + job changes and serverless. +

+ + + {% include "./job-title-2021-vs-2022.njk" %} + + + {{ permalinkHeading.render("h3", "Employment status") }} + +

+ This year when asking about employment status we added a new category, + “self-employed”, which meant that the results are not totally comparable to + last year. A bunch of people who last year described themselves as + “full-time” switched to the “self-employed” category, which probably doesn’t + describe an actual change in status but more accurately describes what they + already were. Students continue to be the second-biggest group in the + community, at 21% of all respondents. As we said last year, this is a + solidly positive sign for a community: the Jamstack remains a popular way to + on-board students at bootcamps into deploying websites for the first time, + and becoming the “default” way to build a website means the Jamstack can + expect to enjoy growth for years to come. +

+ + + {% include "./employment-status.njk" %} + + {{ permalinkHeading.render("h3", "Working experience") }} + +

+ When asking about our community’s level of working experience, we saw a + continuing trend from 2020 and 2021: the community is slowly increasing in + experience. 2021 was our biggest year for new community members, and you can + see that cohort moving up by 1 year of experience in this chart. In 2022, + nearly 1 in 5 developers say they have been working in their current career + for 15 or more years. +

+ + + {% include "./experience-increasing-over-time.njk" %} + + + {{ permalinkHeading.render("h4", "Increasing geographical diversity") }} + +

+ Repeating a phenomenon we first noticed last year, the geographical + diversity of our respondents has a strong correlation to their level of + career experience. In the most experienced group, 84% of respondents come + from either North America or Europe. In our newest group, those with less + than a year of experience, that falls to just 43%. That means in 2022 for + the first time, more than half of people who joined the Jamstack community + came from outside of the two big regions! +

+ +

+ An explanation for this correlation that we find persuasive is that access + to technology is continuing to improve worldwide, leading to increased + geographical diversity. We think this is an encouraging trend, and hope that + it will lead to greater diversity in other dimensions as well. +

+ + + {% include "./experience-by-region.njk" %} + +

+ Every region outside of Europe and North America grew in share. The + fastest-growing region was Africa, which jumped from 4% of respondents to 8% + from 2021 to 2022. This author is also delighted to note that his home + region, the Caribbean, went from 0.5% to 1% in the same period. +

+ + + {% include "./respondents-by-region.njk" %} + + + {{ permalinkHeading.render("h3", "The Great Resignation") }} + +

+ A phenomenon that gained a great deal of attention in 2021 was a spike in + the number of people changing jobs, which has become known as The Great + Resignation. We were interested to get hard numbers on the reality of this + change, and we were not disappointed: fully one-third of our respondents + reported that they changed jobs in the last year, a huge shift. In our job + titles data we saw a big change in job titles, with 11% switching from + front-end to full-stack roles, a change that seems totally plausible in the + context of a community where 33% of people changed jobs. +

+ + + {% include "./have-you-changed-jobs-in-the-last-12-months.njk" %} + + + {{ permalinkHeading.render("h4", "Why people stay") }} + +

+ We had a second question about the great resignation asking people what + motivated their behavior – either why they stayed, or why they left. The + biggest reason people kept their jobs will be no surprise: people stay if + they like their team. Humans are social animals, and a team you love makes + work more bearable. +

+ +

+ A more surprising finding was that the number two reason, as measured by + those who called it “extremely important”, was remote work. People really, + really like working remotely. Money was important, but it was only the + fifth-biggest reason people stayed where they were. Career growth was also a + very important reason to stay. +

+ + + {% include "./what-influenced-staying.njk" %} + + + {{ permalinkHeading.render("h4", "Why people leave") }} + +

+ Why people left jobs was even heavier on remote work: being able to work + remotely at the new job was the number one reason people left their jobs in + our community, as measured by the number of people saying it was an + “extremely important” reason. Growing in your career came in second when + measured in this way, though if you include people who called things “very” + important in addition to “extremely” important it came first. Company + culture, bad teams, and not enough money came next. +

+ + + {% include "./what-influenced-leaving.njk" %} + + + {{ permalinkHeading.render("h3", "Remote work") }} + +

+ Given that one-third of respondents changed jobs in the last year and many + indicated that remote work was their primary reason for either staying or + leaving a company, our next finding makes sense: a startling 83% of our + respondents say they work remotely at least half of the time. Three in five + (62%) work remotely at least 90% of the time, which we’re going to call + “full time remote”. In last year’s survey about a third said their job had + gone full-time remote, and we know from earlier surveys (such as + GitHub’s Octoverse report) that about a third of people were already working remotely before the + pandemic, so this is roughly double the pre-pandemic numbers. +

+ + + {% include "./remote-frequency.njk" %} + + + {{ permalinkHeading.render("h4", "Changes in remote work") }} + +

+ Since a lot of remote work was driven by the pandemic and offices around the + world are still in the process of reopening, we thought it was fair to ask + whether or not this new state was going to be permanent, or whether people + were returning to offices, but slowly. +

+ +

+ The clear response was that remote work is here to stay. A solid majority + (76%) of respondents said their frequency of remote work had either stayed + the same or increased in the last year. Indeed the strongest signal is that + this is the new normal: 52% of people said nothing changed about their + remote working situation, and the ratio of those working remotely more often + versus less often was just 1.04, meaning only a small net change. +

+ + + {% include "./remote-changes.njk" %} + + + {{ permalinkHeading.render("h4", "Attitudes to remote work") }} + +

+ We also asked our community about their attitudes to various aspects of + remote work. 87% of respondents say they enjoy remote work, but only 71% say + their company has remote work “figured out”, which implies there’s 16% of + people enjoying remote work even though they believe their company doesn’t + do it very well. +

+ +
+ + {% include "./i-enjoy-remote-work.njk" %} + {% include "./my-company-has-remote-work-figured-out.njk" %} +
+ +

+ As we suspected from the job change data, the number of people who would + like to work remotely even more often than they currently do is high: 59%. + And the number saying they changed jobs specifically to be able to work + remotely more often is 35%. That is a huge amount of change, and a strong + motivator. +

+ +
+ + {% include "./i-would-like-to-work-remote-more-often.njk" %} + {% include "./i-changed-jobs-to-work-remotely-more-often.njk" %} +
+ +

+ Our final pair of questions about remote work determined two things: first, + we confirmed that it’s not just that people hate when their working + conditions change: asked if they would quit their jobs if asked to work + remotely more often, only 11% said they would, while 55% of respondents said + they would quit their jobs rather than work remotely less often. +

+ +
+ + {% include "./i-would-quit-my-job-if-remote-was-more-often.njk" %} + {% include "./i-would-quit-my-job-if-in-person-was-more-often.njk" %} +
+
diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/job-title-2021-vs-2022.njk b/src/site/_includes/survey/2022/whos-doing-the-building/job-title-2021-vs-2022.njk new file mode 100644 index 000000000..d1bd78961 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/job-title-2021-vs-2022.njk @@ -0,0 +1,68 @@ +
+
+
+ {{ permalinkHeading.render('h4', 'Job titles, 2021 vs. 2022') }} +

Percentage of respondents

+
+
+
+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Job Title20212022
Developer (full-stack)32%44%
Developer (front-end)45%33%
Developer (back-end)5%5%
Designer4%4%
Manager6%4%
Executive/Business owner4%
Content producer2%3%
DevOps2%2%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/my-company-has-remote-work-figured-out.njk b/src/site/_includes/survey/2022/whos-doing-the-building/my-company-has-remote-work-figured-out.njk new file mode 100644 index 000000000..2a412e928 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/my-company-has-remote-work-figured-out.njk @@ -0,0 +1,44 @@ +
+
+
+ {{ permalinkHeading.render('h4', "My company has remote work figured out") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Percentage of Survey Participants
Strongly disagree6%
Somewhat disagree9%
Neither agree nor disagree14%
Somewhat agree32%
Strongly agree39%
+
+
diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/remote-changes.njk b/src/site/_includes/survey/2022/whos-doing-the-building/remote-changes.njk new file mode 100644 index 000000000..4d812daa1 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/remote-changes.njk @@ -0,0 +1,44 @@ +
+
+
+ {{ permalinkHeading.render('h4', "Has your frequency of remote work changed in the last 12 months?") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FrequencyPercentage of Survey Participants
Lots more in office7%
Slightly more in office16%
No changes52%
Slighty more remote9%
Lots more remote15%
+
+
diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/remote-frequency.njk b/src/site/_includes/survey/2022/whos-doing-the-building/remote-frequency.njk new file mode 100644 index 000000000..e3ad238ee --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/remote-frequency.njk @@ -0,0 +1,56 @@ +
+
+
+ {{ permalinkHeading.render('h4', "What percentage of your time do you work remotely?") }} +

Percentage of respondents

+
+
+
+
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FrequencyPercentage of Survey Participants
0%3%
1-9%4%
10-24%5%
25-49%5%
50-74%9%
75-89%12%
90-99%23%
100%39%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/respondents-by-region.njk b/src/site/_includes/survey/2022/whos-doing-the-building/respondents-by-region.njk new file mode 100644 index 000000000..74ca67f4e --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/respondents-by-region.njk @@ -0,0 +1,82 @@ +
+
+
+ {{ permalinkHeading.render("h5", "Respondents by region") }} +

Percentage of respondents

+
+ +
+
+
+
Source: Jamstack Community Survey 2021—2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Employment Status20212022
Europe39%33%
North America31%28%
All Asia18%19%
Asia Pacific11%12%
Africa4%8%
Southern Asia6%8%
South America5%5%
Eastern Asia1%2%
Middle East1%2%
Central America1%1%
Caribbean1%1%
+
+
diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/what-influenced-leaving.njk b/src/site/_includes/survey/2022/whos-doing-the-building/what-influenced-leaving.njk new file mode 100644 index 000000000..94e15ccdd --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/what-influenced-leaving.njk @@ -0,0 +1,114 @@ +
+
+
+ {{ permalinkHeading.render('h5', "Why did you leave your job?") }} +

Percentage of respondents

+
+
+ +
+ +
+
+
+ +
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Not at all importantSlightly importantModerately importantVery importantExtremely important
Remote work6%6%18%30%41%
Career growth3%5%18%35%39%
Company culture4%6%21%38%31%
Team4%6%21%38%31%
Money4%5%20%40%30%
My manager6%9%24%34%26%
Corporate ethics6%9%25%36%25%
Technology choices4%7%25%42%22%
Environmental impact15%16%30%25%14%
Involuntary36%10%28%15%11%
+
+
\ No newline at end of file diff --git a/src/site/_includes/survey/2022/whos-doing-the-building/what-influenced-staying.njk b/src/site/_includes/survey/2022/whos-doing-the-building/what-influenced-staying.njk new file mode 100644 index 000000000..2e1377e18 --- /dev/null +++ b/src/site/_includes/survey/2022/whos-doing-the-building/what-influenced-staying.njk @@ -0,0 +1,114 @@ +
+
+
+ {{ permalinkHeading.render('h5', "Why did you stay in your job?") }} +

Percentage of respondents

+
+
+ +
+ +
+
+
+ +
Source: Jamstack Community Survey 2022
+ +
+ Show Chart Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Not at all importantSlightly importantModerately importantVery importantExtremely important
Team3%5%19%40%34%
Remote work5%9%22%32%32%
Career growth3%6%21%39%31%
Company culture4%8%21%38%29%
Money3%6%25%39%28%
Corporate ethics6%9%24%37%25%
My manager6%7%24%38%24%
Technology choices2%7%24%44%23%
Environmental impact14%16%30%26%14%
Involuntary31%10%34%15%10%
+
+
\ No newline at end of file diff --git a/src/site/img/og/jamstack-community-survey-2022-og.png b/src/site/img/og/jamstack-community-survey-2022-og.png new file mode 100644 index 000000000..cd1064b1f Binary files /dev/null and b/src/site/img/og/jamstack-community-survey-2022-og.png differ diff --git a/src/site/survey/2021.njk b/src/site/survey/2021.njk index 09df84104..f40a4367d 100644 --- a/src/site/survey/2021.njk +++ b/src/site/survey/2021.njk @@ -84,27 +84,27 @@ gradientColors:

Demographics

- {% include "survey/demographics.njk" %} + {% include "survey/2021/demographics.njk" %} - {% include "survey/experience.njk" %} + {% include "survey/2021/experience.njk" %}

Jamstack adoption

- {% include "survey/adoption.njk" %} + {% include "survey/2021/adoption.njk" %}

Workflows

- {% include "survey/workflows.njk" %} + {% include "survey/2021/workflows.njk" %}

Technology choices

- {% include "survey/choices.njk" %} + {% include "survey/2021/choices.njk" %}

Jamstack has become the standard architecture for the web.

- {% include "survey/conclusion.njk" %} + {% include "survey/2021/conclusion.njk" %}
\ No newline at end of file diff --git a/src/site/survey/2021/d3chart-survey.js b/src/site/survey/2021/d3chart-survey-2021.js similarity index 100% rename from src/site/survey/2021/d3chart-survey.js rename to src/site/survey/2021/d3chart-survey-2021.js diff --git a/src/site/survey/2021/d3chart.js b/src/site/survey/2021/d3chart.js deleted file mode 100644 index f7231c32f..000000000 --- a/src/site/survey/2021/d3chart.js +++ /dev/null @@ -1,931 +0,0 @@ -class D3Chart { - constructor(targetId, options, className) { - this.targetId = targetId; - this.className = className; - - this.options = Object.assign({ - showInlineBarValues: "inside", // inside, inside-offset, and outside supported - showLegend: true, - showAxisLabels: false, - margin: {}, - colors: [ - "#F0047F", - "#00BFAD", - "#FFC803", - "#78ECC2", - "#DF4A1F", - "#FD98BC", - "#6B38FB", - "#03D0D0", - "#C40468", - "#78F19A", - "#91A5EE", - "#02C6B3", - "#FF0F00", - "#003EDD", - "#02465F", - "#960000", - "#FF72CF", - ], - // only applies when `showInlineBarValues: "inside"` - labelColors: [ - "#fff", - "#000", - "#000", - "#000", - "#000", - "#000", - "#000", - "#000", - "#fff", - "#000", - "#000", - "#000", - "#000", - "#000", - "#000", - "#fff", - "#fff", - ], - colorMod: 0, - inlineLabelPad: 5, - labelPrecision: 0, - // TODO make this automatic by parsing `%` signs - valueType: ["percentage"], - sortLegend: false, - highlightElementsFromLegend: false - }, options); - - this.options.colors = this.normalizeColors(this.options.colors, this.options.colorMod); - this.options.labelColors = this.normalizeColors(this.options.labelColors, this.options.colorMod); - } - - onResize(callback) { - if (!("ResizeObserver" in window)) { - window.addEventListener("resize", () => { - callback.call(this); - }); - return; - } - - let resizeObserver = new ResizeObserver(entries => { - for (let entry of entries) { - // console.log( "resizing", this.target ); - callback.call(this); - } - }); - - resizeObserver.observe(this.target); - } - - onDeferInit(callback) { - if (!('IntersectionObserver' in window)) { - callback.call(this); - return; - } - - let observer = new IntersectionObserver((entries, observer) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - // console.log( "initing", this.target ); - callback.call(this); - observer.unobserve(entry.target); - } - }); - }, { - threshold: .1 - }); - - observer.observe(this.target); - } - - normalizeColors(colors = [], mod = 0) { - if(mod) { - let c = []; - let len = colors.length; - let k = len + mod; - for(let j = mod || 0; j < k; j++) { - c.push(colors[j % len]); - } - return c; - } - - return colors; - } - - get margin() { - let m = Object.assign({ - top: 30, - right: 10, - bottom: 25, - left: 40, - }, this.options.margin); - - return m; - } - - get dimensions() { - let target = this.target; - return { - container: { - width: target.clientWidth, - height: target.clientHeight, - }, - min: { - width: 300, - height: 450 - }, - max: { - height: 1000 - }, - }; - } - - get width() { - return Math.max(this.dimensions.container.width, this.dimensions.min.width); - } - - get height() { - return Math.max(Math.min(this.dimensions.container.height, this.dimensions.max.height) - this.margin.bottom, this.dimensions.min.height); - } - - get svg() { - return d3.create("svg") - .attr("height", this.height) - .attr("viewBox", [0, 0, this.width, this.height]); - } - - get colors() { - return d3.scaleOrdinal().range(this.options.colors); - } - - get labelColors() { - return d3.scaleOrdinal().range(this.options.labelColors); - } - - get target() { - return document.getElementById(this.targetId); - } - - reset(svg) { - let target = this.target; - target.classList.add("d3chart"); - if(this.className) { - target.classList.add(this.className); - } - - for(let child of target.children) { - if(child.tagName.toLowerCase() === "svg") { - child.remove(); - } - } - - let node = svg.node(); - target.appendChild(node); - } - - // Thanks https://bl.ocks.org/mbostock/7555321 - static wrapText(text, width) { - text.each(function() { - var text = d3.select(this), - words = text.text().split(/\s+/).reverse(), - word, - line = [], - lineNumber = 0, - lineHeight = 1.01, // ems - y = text.attr("y"), - dy = parseFloat(text.attr("dy")), - tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y), - firstTspan = tspan; - - let wrapCount = 0; - while (word = words.pop()) { - line.push(word); - tspan.text(line.join(" ")); - if (tspan.node().getComputedTextLength() > width) { - wrapCount++; - line.pop(); - tspan.text(line.join(" ")); - line = [word]; - tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", lineHeight + dy + "em").text(word); - } - } - - if(wrapCount) { - text.attr("dy", 0).attr("class", "d3chart-label-wrapped"); - firstTspan.attr("dy", (-0.3 * wrapCount * lineHeight) + "em") - } - }); - } - - parseDataToCsv(tableId, reverse) { - let table = document.getElementById(tableId); - let headerCells = table.querySelectorAll(":scope thead th"); - let bodyRows = table.querySelectorAll(":scope tbody tr"); - - let headerOutput = []; - for(let th of headerCells) { - headerOutput.push(th.textContent); - } - - let output = []; - for(let tr of bodyRows) { - let row = []; - for(let child of tr.children) { - let value = child.textContent; - if(value.endsWith("%")) { - value = parseFloat(value) / 100; - } - row.push(value); - } - output.push(row.join(",")); - } - if(reverse) { - return [headerOutput.join(","), ...output.reverse()].join("\n"); - } - return [headerOutput.join(","), ...output].join("\n"); - - } - - retrieveLabelId(label) { - let match = label.match(/^(\d*)\./); - if(match && match[1]) { - return parseInt(match[1], 10); - } - } - - slugify(slug, prefix) { - return `${prefix}${slug.toLowerCase().replace(/[\s\.]/g, "")}`; - } - - generateLegend(labels = []) { - let container = document.createElement("div"); - container.classList.add("d3chart-legend"); - - let entries = []; - for(let j = 0; j < labels.length; j++) { - let tag = "div"; - let attrs = ""; - if(this.options.highlightElementsFromLegend) { - tag = "button"; - attrs = " type='button'" - } - - entries.push({ - label: labels[j], - html: `<${tag}${attrs} class="d3chart-legend-entry d3chart-legend-${j + this.options.colorMod}">${labels[j] || ""}` - }); - } - - if(this.options.sortLegend) { - entries = entries.sort((a, b) => { - let idA = this.retrieveLabelId(a.label); - let idB = this.retrieveLabelId(b.label); - if(idA && idB) { - return idA - idB; - } - if(a.label < b.label) { - return -1; - } else if(b.label < a.label) { - return 1; - } - return 0; - }); - } - - let html = []; - for(let entry of entries) { - html.push(entry.html); - } - container.innerHTML = html.join(""); - return container; - } - - getKeys(data) { - return data.columns.slice(1); - } - - highlightElements(target, method) { - // TODO this is specific to Bubble chart - if(target.classList.contains("d3chart-legend-entry")) { - let circleSlug = this.slugify(target.innerHTML, `${this.targetId}-bubblecircle-`); - let labelSlug = this.slugify(target.innerHTML, `${this.targetId}-bubblelabel-`); - - let circle = document.getElementById(circleSlug); - let label = document.getElementById(labelSlug); - - circle.classList[method]("active"); - label.classList[method]("active"); - - circle.closest("svg").classList[method]("d3chart-bubble-active"); - } - } - - renderLegend(data) { - if(!this.options.showLegend) { - return; - } - - let keys = this.getKeys(data); - let legend = this.generateLegend(keys, this.options.colors); - - if(this.options.highlightElementsFromLegend) { - legend.addEventListener("mouseover", e => { - this.highlightElements(e.target, "add"); - }); - legend.addEventListener("mouseout", e => { - this.highlightElements(e.target, "remove"); - }); - legend.addEventListener("focusin", e => { - this.highlightElements(e.target, "add"); - }); - legend.addEventListener("focusout", e => { - this.highlightElements(e.target, "remove"); - }); - } - - let selector = ":scope .d3chart-legend-placeholder"; - - let previousEl = this.target.previousElementSibling; - let legendAnchorBefore = previousEl ? previousEl.querySelector(selector) : null; - - let nextEl = this.target.nextElementSibling; - let legendAnchorAfter = nextEl ? nextEl.querySelector(selector) : null; - - if(legendAnchorBefore || legendAnchorAfter) { - (legendAnchorBefore || legendAnchorAfter).appendChild(legend) - } else { - // inside - this.target.appendChild(legend); - } - } - - roundValue(num, valueType = "percentage") { - if(valueType !== "percentage") { - return num; - } - - let d0 = (num * 100).toFixed(0); - if(this.options.labelPrecision === 0) { - return d0; - } - - let d1 = (num * 100).toFixed(1); - if(d1.endsWith(".0")) { - return d0; - } - return d1; - } -} - -class D3VerticalBarChart extends D3Chart { - constructor(target, tableId, optionOverrides = {}) { - let chart = super(target, optionOverrides, "d3chart-vbar"); - - let csvData = chart.parseDataToCsv(tableId); - let dataSplit = csvData.split("\n"); - this.axisLabels = [dataSplit[0].split(",")[0]]; - - let data = Object.assign(d3.csvParse(csvData, d3.autoType)); - - - this.onDeferInit(function() { - this.render(chart, data); - this.renderLegend(data); - - this.onResize(function() { - this.render(chart, data); - }) - }); - } - - render(chart, data) { - let { - options, - margin, - width, - height, - dimensions, - svg, - colors, - labelColors, - } = chart; - - let keys = this.getKeys(data); - let groupKey = data.columns[0]; - let groups = data.map(d => d[groupKey]); - - let y = d3.scaleLinear() - .domain([ - 0, - d3.max(data, d => { - if(options.mode === "stacked") { - let sum = 0; - for(let key of keys) { - sum += d[key]; - } - return sum; - } - - return d3.max(keys, key => d[key]) - }) - ]).nice() - .rangeRound([height - margin.bottom, margin.top]); - - let x0 = d3.scaleBand() - .domain(groups) - .rangeRound([margin.left, width - margin.right]) - .paddingInner(.2); - - let x1 = d3.scaleBand() - .domain(keys) - .rangeRound([0, x0.bandwidth()]) - .padding(0.05); - - let yAxis = g => g - .attr("transform", `translate(${margin.left},0)`) - .attr("class", "d3chart-yaxis") - .call(d3 - .axisLeft(y) - .ticks(null, options.valueType[0] === "percentage" ? "%" : "") - .tickSize(-width + margin.left + margin.right)) - .call(g => g.select(".domain").remove()); - - let xAxis = g => g - .attr("transform", `translate(0,${height - margin.bottom})`) - .attr("class", "d3chart-xaxis") - .call(d3 - .axisBottom(x0) - .tickSizeOuter(0)) - .call(g => g.select(".domain").remove()); - - let dataMod = d => { - let incrementer = 0; - - return keys.map(key => { - let data = { - key, - value: d[key], - width: x1.bandwidth(), - height: y(0) - y(d[key]), - left: x1(key), - top: y(d[key]) - }; - - if(options.mode === "stacked") { - data.width = x0.bandwidth(); - data.left = 0; - data.top = y(d[key]) - incrementer; - incrementer += data.height; - } - - return data; - }) - }; - - svg.append("g").call(xAxis); - svg.append("g").call(yAxis); - - svg.append("g") - .selectAll("g") - .data(data) - .join("g") - .attr("transform", d => `translate(${x0(d[groupKey])},0)`) - .selectAll("rect") - .data(dataMod) - .join("rect") - .attr("x", d => d.left) - .attr("y", d => d.top) - .attr("width", d => d.width) - .attr("height", d => d.height) - .attr("fill", d => colors(d.key)) - .attr("class", (d, j) => `d3chart-color-${j + options.colorMod}`); - - if(options.showInlineBarValues) { - svg.append("g") - .selectAll("g") - .data(data) - .join("g") - .attr("transform", d => `translate(${x0(d[groupKey])},0)`) - .selectAll("text") - .data(dataMod) - .join("text") - .attr("x", d => d.left + d.width / 2) - .attr("y", d => d.top - (options.showInlineBarValues === "outside" ? options.inlineLabelPad : (-15 - options.inlineLabelPad))) - .attr("fill", d => options.showInlineBarValues === "inside" ? labelColors(d.key) : "currentColor") - .attr("class", "d3chart-inlinebarvalue") - .text(d => this.roundValue(d.value, options.valueType[0]) + (options.valueType[0] === "percentage" ? "%" : "")); - } - - // TODO for horizontal bar chart - if(options.showAxisLabels) { - svg.append("text") - .attr("x", Math.round(width/2)) - .attr("y", height - 6) - .attr("class", "d3chart-axislabel d3chart-axislabel-center") - .text(this.axisLabels[0]); - } - - chart.reset(svg); - } -} - -class D3HorizontalBarChart extends D3Chart { - constructor(target, tableId, optionOverrides = {}) { - optionOverrides.margin = Object.assign({ - top: 20, - right: 50, - bottom: 20, - left: 120 - }, optionOverrides.margin); - let chart = super(target, optionOverrides, "d3chart-hbar"); - let csvData = chart.parseDataToCsv(tableId, true); - let data = Object.assign(d3.csvParse(csvData, d3.autoType)); - - this.onDeferInit(function() { - this.render(chart, data); - this.renderLegend(data); - - this.onResize(function() { - this.render(chart, data); - }); - }); - } - - render(chart, data) { - let { - options, - margin, - width, - height, - dimensions, - svg, - colors, - labelColors, - } = chart; - - let keys = this.getKeys(data); - let groupKey = data.columns[0]; - let groups = data.map(d => d[groupKey]); - - let x = d3.scaleLinear() - .domain([0, d3.max(data, d => { - if(options.scale === "proportional") { - return 1; - } - - if(options.mode === "stacked") { - let sum = 0; - for(let key of keys) { - sum += d[key]; - } - return sum; - } - - return d3.max(keys, key => d[key]); - })]).nice() - .rangeRound([margin.left, width - margin.right]); - - let y0 = d3.scaleBand() - .domain(groups) - .rangeRound([height - margin.bottom - margin.top, margin.top]) - .paddingInner(options.showInlineBarValues === "inside-offset" ? 0.25 : 0.15); - - let y1 = d3.scaleBand() - .domain(keys) - .rangeRound([0, y0.bandwidth()]) - .padding(0.05); - - let xAxis = g => g - .attr("transform", `translate(0, ${(margin.top + margin.bottom)/4})`) - .attr("class", "d3chart-xaxis") - .call(d3 - .axisBottom(x) - .ticks(5, options.valueType[0] === "percentage" ? "%" : "") - .tickSize(height - margin.bottom - margin.top)) - .call(g => g.select(".domain").remove()); - - let yAxis = g => g - .attr("transform", `translate(${margin.left - 6},0)`) - .attr("class", "d3chart-yaxis") - .call(d3.axisLeft(y0).tickSize(0)) - .call(g => g.select(".domain").remove()); - - let dataMod = d => { - let incrementer = 0; - let sum = 0; - for(let key of keys) { - sum += d[key]; - } - - return keys.map(key => { - let data = { - key, - value: d[key], - sum, - width: x(options.scale === "proportional" ? (d[key] / sum) : d[key]) - x(0), - height: y1.bandwidth(), - left: margin.left, - top: y1(key) - }; - - if(options.mode === "stacked") { - data.top = 0; - data.height = y0.bandwidth(); - data.left = margin.left + incrementer; - - incrementer += data.width; - } - - return data; - }) - }; - - svg.append("g").call(xAxis); - svg.append("g").call(yAxis); - - svg.append("g") - .selectAll("g") - .data(data) - .join("g") - .attr("transform", d => `translate(0,${y0(d[groupKey])})`) - .selectAll("rect") - .data(dataMod) - .join("rect") - .attr("x", d => d.left) - .attr("y", d => d.top) - .attr("width", d => d.width) - .attr("height", d => d.height) - .attr("fill", d => colors(d.key)) - .attr("class", (d, j) => `d3chart-color-${j + options.colorMod}`); - - if(options.showInlineBarValues) { - svg.append("g") - .selectAll("g") - .data(data) - .join("g") - .attr("transform", d => `translate(0,${y0(d[groupKey])})`) - .selectAll("text") - .data(dataMod) - .join("text") - .attr("x", d => { - let offset = options.inlineLabelPad; - if(options.showInlineBarValues.startsWith("inside")) { - offset = -1 * offset; - } - if(options.showInlineBarValues === "inside-offset") { - offset += 16; - } - return d.left + d.width + offset; - }) - .attr("y", d => { - if(options.showInlineBarValues === "inside-offset") { - return -10; - } - return d.top + Math.floor(d.height / 2) - 1; - }) - .attr("class", d => "d3chart-inlinebarvalue-h" + (options.showInlineBarValues.length ? ` ${options.showInlineBarValues}` : "")) - .attr("fill", d => options.showInlineBarValues === "inside" ? labelColors(d.key) : "currentColor") - .text(d => this.roundValue(d.value, options.valueType[0]) + (options.valueType[0] === "percentage" ? "%" : "")); - } - - chart.reset(svg); - - if(options.wrapAxisLabel && options.wrapAxisLabel.left) { - D3Chart.wrapText(svg.selectAll(".d3chart-yaxis .tick text"), margin.left - 6); - } - } -} - -class D3BubbleChart extends D3Chart { - constructor(target, tableId, optionOverrides = {}) { - optionOverrides.margin = { - top: 20, - right: 20, - bottom: 50, - left: 65 - }; - - optionOverrides.sortLegend = true; - optionOverrides.highlightElementsFromLegend = true; - optionOverrides.showAxisLabels = true; - - if(!optionOverrides.valueType) { - optionOverrides.valueType = ["percentage", "percentage"]; - } - - let chart = super(target, optionOverrides, "d3chart-bubble"); - let csvData = chart.parseDataToCsv(tableId); - let dataSplit = csvData.split("\n"); - this.axisLabels = dataSplit[0].split(",").slice(1); - - let data = dataSplit.slice(1).map((entry, id) => { - let [name, x, y, r] = entry.split(","); - return { - name, - id, - x, - y, - r, - }; - }); - - // sort from smallest to largest circles to insert in order (to render in the right z-index) - data = data.slice().sort((a, b) => { - return b.r - a.r; - }); - - this.onDeferInit(function() { - this.render(chart, data); - this.renderLegend(data); - - this.onResize(function() { - this.render(chart, data); - }); - }); - } - - getKeys(data) { - let keys = []; - for(let entry of data) { - keys.push(entry.name); - } - return keys; - } - - resolveLimit(data, key, valueType, mode) { - let limit = d3[mode](data, d => parseFloat(d[key])); - if(valueType !== "percentage") { - if(mode === "max") { - limit = Math.ceil(limit); - } else if(mode === "min") { - limit = Math.min(Math.floor(limit), 0); - } - } else { - if(mode === "max") { - if(limit > 1) { - limit += .1; - } else { - // round up to at most 1 if percentage < 100% - if(limit > .5) { - limit = Math.min(limit + .1, 1); - } else { - limit = limit + .05, 1; - } - } - } - if(mode === "min") { - if(limit <= 0) { - limit -= .1; - } else { - // round up to at most 1 if percentage < 100% - limit = Math.min(limit, 0); - } - } - } - - return limit; - } - - render(chart, data) { - let { - options, - margin, - width, - height, - dimensions, - svg, - colors, - labelColors, - } = chart; - - let targetId = this.targetId; - - let xAxisMin = this.resolveLimit(data, "x", options.valueType[0], "min"); - let xAxisMax = this.resolveLimit(data, "x", options.valueType[0], "max"); - let yAxisMin = this.resolveLimit(data, "y", options.valueType[1], "min"); - let yAxisMax = this.resolveLimit(data, "y", options.valueType[1], "max"); - - let xScale = d3.scaleLinear() - .domain([ - xAxisMin, - xAxisMax - ]) - .range([ - margin.left, - width - margin.right - ]); - - let yScale = d3.scaleLinear() - .domain([ - yAxisMax, - yAxisMin, - ]) - .range([ - margin.top, - height - margin.top - margin.bottom - ]); - - let rScale = d3.scaleLinear() - .range([7, 25]) - .domain([ - Math.min(d3.min(data, d => parseFloat(d.r)), 0), - d3.max(data, d => parseFloat(d.r)) - ]); - - let xAxis = d3.axisBottom() - .scale(xScale) - .ticks(null) - .tickSize(-height + margin.bottom + margin.top) - .tickFormat(d => options.valueType[0] === "percentage" ? `${(d*100).toFixed(0)}%` : d); - - svg.append("g") - .attr("class", "d3chart-xaxis") - .attr("transform", function(){ - return "translate(0," + (height - margin.bottom) + ")"; - }) - .call(xAxis) - .call(g => g.select(".domain").remove()); - - let yAxis = d3.axisLeft() - .scale(yScale) - .ticks(null) - .tickSize(-width + margin.right + margin.left) - .tickFormat(d => options.valueType[1] === "percentage" ? `${(d*100).toFixed(0)}%` : d); - - svg.append("g") - .attr("class", "d3chart-yaxis") - .attr("transform", function(){ - return "translate(" + margin.left + "," + margin.top + ")"; - }) - .call(yAxis) - .call(g => g.select(".domain").remove()); - - if(options.showAxisLabels) { - // Axis labels - svg.append("text") - .attr("x", width - margin.right) - .attr("y", height - 6) - .attr("class", "d3chart-axislabel") - .text(this.axisLabels[0]); - - svg.append("text") - .attr("x", -1 * margin.top) - .attr("y", 6) - .attr("dy", ".75em") - .attr("transform", "rotate(-90)") - .attr("class", "d3chart-axislabel") - .text(this.axisLabels[1]); - } - - let group = svg.append("g"); - - let circles = group.selectAll("circle").data(data); - - // Text Labels - function isOffsetLabel(d) { - let range = rScale(d.r); - return range <= 10; - } - - circles - .enter() - .insert("circle") - .attr("cx", function (d) { - return xScale(d.x); - }) - .attr("cy", function (d) { - return yScale(d.y); - }) - .attr("r", function (d) { - return rScale(d.r); - }) - .attr("id", d => this.slugify(d.name, `${targetId}-bubblecircle-`)) - .attr("fill", d => colors(d)) - .attr("class", (d, j) => `d3chart-bubblecircle d3chart-color-${j + options.colorMod}`); - - circles - .enter() - .append("text") - .attr("id", d => this.slugify(d.name, `${targetId}-bubblelabel-`)) - .attr("x", d => { - return xScale(d.x) - (isOffsetLabel(d) ? rScale(d.r) + 4 : 0); - }) - .attr("y", d => yScale(d.y)) - .attr("class", d => { - return "d3chart-bubblelabel" + (isOffsetLabel(d) ? " offset-l" : ""); - }) - .attr("fill", d => isOffsetLabel(d) ? "currentColor" : labelColors(d)) - .text(d => { - let labelId = this.retrieveLabelId(d.name); - if(labelId) { - return labelId; - } - return d.name; - }) - .filter(d => isOffsetLabel(d)) - .lower(); - - chart.reset(svg); - } -} \ No newline at end of file diff --git a/src/site/survey/2021/js.njk b/src/site/survey/2021/js.njk index 27c8bf986..19ffb114c 100644 --- a/src/site/survey/2021/js.njk +++ b/src/site/survey/2021/js.njk @@ -1,6 +1,7 @@ --- permalink: /survey/2021/bundle.js --- + {% include "../../../../node_modules/d3/dist/d3.min.js" %} -{% include "./d3chart.js" %} -{% include "./d3chart-survey.js" %} \ No newline at end of file +{% include "../shared/d3chart.js" %} +{% include "./d3chart-survey-2021.js" %} diff --git a/src/site/survey/2022.njk b/src/site/survey/2022.njk new file mode 100644 index 000000000..24f00fe85 --- /dev/null +++ b/src/site/survey/2022.njk @@ -0,0 +1,201 @@ +--- +title: "Jamstack Community Survey Results 2022" +description: "The third annual Jamstack Survey conducted by Netlify reveals developer attitudes towards trends like remote work, Web3, serverless, edge and more." +layout: layouts/base.njk +ogimage: "/img/og/jamstack-community-survey-2022-og.png" +stylesheets: + - /css/d3chart.css +javascripts: + - /survey/2022/bundle.js +gradientColors: + sunrise: ["#F0047F", "#FC814A"] + blue: ["#0090c9", "#00c0ad"] + sun: ["#FC814A", "#FFC803"] + seamist: ["#78ECC2", "#00FFB2"] + hallows: ["#DF4A1F", "#FFA278"] + bubblegum: ["#FF98BC", "#FFCCDE"] + purple: ["#6B38FB", "#CCB4FF"] + air: ["#03d0d0", "#B5FFF8"] + pink: ["#c40468", "#fc2796"] + leaves: ["#78f19a", "#13b110"] + haze: ["#91A5EE", "#d6deff"] + gnat: ["#02C6B3", "#59F7E7"] + fire: ["#FF0F00", "#FF928A"] + ocean: ["#003EDD", "#6CDCFF"] + night: ["#02465F", "#6AD7FF"] + dusk: ["#960000", "#E94242"] + rain: ["#FF72CF", "#C92ECC"] +gradientColorsExtended: + "16": ["#f0185d", "#ff668f"] + "17": ["#448bd0", "#80c0ff"] + "18": ["#dbd600", "#ffff54"] + "19": ["#63edd7", "#a1ffff"] + "20": ["#cb5f00", "#ff932f"] + "21": ["#ff98a8", "#ffd0df"] + "22": ["#a800dc", "#e449ff"] + "23": ["#00cfe4", "#6affff"] + "24": ["#c5114c", "#ff5a7c"] + "25": ["#4af4b5", "#8effed"] + "26": ["#aa9ee9", "#e2d5ff"] + "27": ["#00c6c9", "#57ffff"] + "28": ["#e64b00", "#ff8300"] +--- + +{% import "components/permalink-heading.njk" as permalinkHeading %} + + + + + + + + + + + + {%- for key, entry in gradientColors %} + + + + + + + + + {%- endfor %} {%- for key, entry in gradientColorsExtended %} + + + + + + + + + {%- endfor %} + + + +
+
+
+

Jamstack gives developers full-stack powers

+

Findings from the Jamstack Community Survey 2022

+
+

+ The third year of the Jamstack Community Survey found a mix of things we + expected – indeed, things we predicted last year – as well as some big + surprises about the many diverse members of our community. Some key + takeaways include: +

+ +

+ Netlify sits at the + center of the Jamstack community, and we conduct our annual survey so we + can understand our community of developers. This helps us tailor our + products and services to our community. In sharing our survey results, we + also want to help developers better understand themselves and one another. + Working as a developer often means working in a vacuum, without a sense of + what’s happening in the broader community. Our survey data can help + provide a sense of best practices as well as an idea of what else is + happening in the community. +

+

+ In addition to our usual framework census and our questions about content + management systems, this year we asked about some emerging technologies + that have received a lot of attention. The fuzzy group of technologies + called “Web3” garnered mixed feelings despite a great deal of press in + 2021 and 2022. Browser-native web components, on the other hand, seem to + have finally reached mainstream adoption. +

+

+ As usual, our survey covers everyone we can reach: every kind of developer + responded to our survey from every region of the world, whether or not + they were Netlify users, and whether or not they considered themselves + Jamstack developers. Our survey this year received a little under 7,000 + responses. If you’re interested in the specifics of our methodology, we + have a + detailed writeup + of the demographics and margins of error in our survey. +

+

+ As usual, we want to thank the developers who took the time to contribute + to the survey. We have done our best to take the data you’ve given us and + turn it into useful, actionable insights for everyone in our community, + and we hope it helps you. +

+

This year, our results are split into four sections:

+ +
+ + + + {% include "survey/2022/whos-doing-the-building/index.njk" %} + + {% include "survey/2022/what-are-we-building/index.njk" %} + + {% include "survey/2022/how-are-we-building/index.njk" %} + + {% include "survey/2022/where-are-we-going/index.njk" %} +
diff --git a/src/site/survey/2022/community-survey-2022-methodology.pdf b/src/site/survey/2022/community-survey-2022-methodology.pdf new file mode 100644 index 000000000..33e636b24 Binary files /dev/null and b/src/site/survey/2022/community-survey-2022-methodology.pdf differ diff --git a/src/site/survey/2022/d3chart-survey-2022.js b/src/site/survey/2022/d3chart-survey-2022.js new file mode 100644 index 000000000..a6ebe7b29 --- /dev/null +++ b/src/site/survey/2022/d3chart-survey-2022.js @@ -0,0 +1,392 @@ +new D3HorizontalBarChart( + "job-titles-2021-2022-comparison-chart", + "job-titles-2021-2022-comparison-table", + { + showInlineBarValues: "outside", + margin: { + left: 188, + }, + scaleTicks: { + x: true, + }, + colorMod: 1, + interactive: true + } +); + +new D3HorizontalBarChart("employment-status-chart", "employment-status-table", { + showInlineBarValues: "outside", + showLegend: false, + margin: { + left: 128, + }, + colorMod: 0, + scaleTicks: { + x: true, + }, +}); + +new D3VerticalBarChart( + "experience-increasing-over-time-chart", + "experience-increasing-over-time-table", + { + showInlineBarValues: false, + interactive: true + } +); + +new D3HorizontalBarChart( + "experience-by-region-chart", + "experience-by-region-table", + { + mode: "stacked", + showInlineBarValues: false, + margin: { + left: 48, + right: 0, + }, + interactive: true + } +); + +new D3VerticalBarChart( + "respondents-by-region-chart", + "respondents-by-region-table", + { + showInlineBarValues: false, + margin: { + left: 32, + bottom: 88, + right: 32, + }, + colorMod: 1, + rotateXAxisLabels: true, + interactive: true + } +); + +new D3HorizontalBarChart( + "have-you-changed-jobs-in-the-last-12-months-chart", + "have-you-changed-jobs-in-the-last-12-months-table", + { + showInlineBarValues: "outside", + showLegend: false, + margin: { + left: 40, + }, + colorMod: 3, + } +); + +new D3HorizontalBarChart( + "what-influenced-staying-chart", + "what-influenced-staying-table", + { + mode: "stacked", + showInlineBarValues: false, + margin: { + left: 164, + right: 0, + }, + scaleTicks: { + x: true, + }, + interactive: true + } +); + +new D3HorizontalBarChart( + "what-influenced-leaving-chart", + "what-influenced-leaving-table", + { + mode: "stacked", + showInlineBarValues: false, + margin: { + left: 164, + right: 0, + }, + scaleTicks: { + x: true, + }, + interactive: true + } +); + +new D3HorizontalBarChart("remote-frequency-chart", "remote-frequency-table", { + showLegend: false, + showInlineBarValues: "outside", + margin: { + left: 64, + }, + colorMod: 0, + scaleTicks: { + x: true, + }, +}); + +new D3HorizontalBarChart("remote-changes-chart", "remote-changes-table", { + showLegend: false, + showInlineBarValues: "outside", + margin: { + left: 164, + }, + colorMod: 1, + scaleTicks: { + x: true, + }, +}); + +new D3VerticalBarChart( + "i-enjoy-remote-work-chart", + "i-enjoy-remote-work-table", + { + showLegend: false, + showInlineBarValues: "outside", + colorMod: 2, + wrapTicks: { + x: true, + }, + } +); + +new D3VerticalBarChart( + "my-company-has-remote-work-figured-out-chart", + "my-company-has-remote-work-figured-out-table", + { + showLegend: false, + showInlineBarValues: "outside", + colorMod: 2, + wrapTicks: { + x: true, + }, + } +); + +new D3VerticalBarChart( + "i-would-like-to-work-remote-more-often-chart", + "i-would-like-to-work-remote-more-often-table", + { + showLegend: false, + showInlineBarValues: "outside", + colorMod: 3, + wrapTicks: { + x: true, + }, + } +); + +new D3VerticalBarChart( + "i-would-like-to-work-remote-more-often-chart", + "i-would-like-to-work-remote-more-often-table", + { + showLegend: false, + showInlineBarValues: "outside", + colorMod: 3, + wrapTicks: { + x: true, + }, + } +); + +new D3VerticalBarChart( + "i-changed-jobs-to-work-remotely-more-often-chart", + "i-changed-jobs-to-work-remotely-more-often-table", + { + showLegend: false, + showInlineBarValues: "outside", + colorMod: 3, + wrapTicks: { + x: true, + }, + } +); + +new D3VerticalBarChart( + "i-would-quit-if-in-person-was-more-often-chart", + "i-would-quit-if-in-person-was-more-often-table", + { + showLegend: false, + showInlineBarValues: "outside", + colorMod: 0, + wrapTicks: { + x: true, + }, + } +); + +new D3VerticalBarChart( + "i-would-quit-my-job-if-remote-was-more-often-chart", + "i-would-quit-my-job-if-remote-was-more-often-table", + { + showLegend: false, + showInlineBarValues: "outside", + colorMod: 0, + wrapTicks: { + x: true, + }, + } +); + +new D3HorizontalBarChart( + "what-is-the-purpose-of-the-sites-you-built-in-2022-chart", + "what-is-the-purpose-of-the-sites-you-built-in-2022-table", + { + showInlineBarValues: "outside", + showLegend: false, + margin: { + left: 148, + }, + colorMod: 2, + scaleTicks: { + x: true, + }, + } +); + +new D3HorizontalBarChart( + "types-of-sites-built-last-12-months-chart", + "types-of-sites-built-last-12-months-table", + { + mode: "stacked", + showInlineBarValues: false, + margin: { + left: 128, + right: 0, + }, + scaleTicks: { + x: true, + }, + interactive: true + } +); + +new D3VerticalBarChart( + "target-devices-by-type-chart", + "target-devices-by-type-table", + { + showInlineBarValues: "outside", + wrapTicks: { + x: true, + }, + interactive: true + } +); + +new D3VerticalBarChart("audience-sizes-chart", "audience-sizes-table", { + showInlineBarValues: "outside", + wrapTicks: { + x: true, + }, + interactive: true +}); + +new D3BubbleChart( + "cms-usage-vs-satisfaction-chart", + "cms-usage-vs-satisfaction-table", + { + radiusColumn: 1, + valueType: ["percentage", "float"], + extendedColors: true, + scaleTicks: { + x: true, + }, + } +); + +new D3BubbleChart( + "programming-language-usage-vs-satisfaction-chart", + "programming-language-usage-vs-satisfaction-table", + { + radiusColumn: 1, + valueType: ["percentage", "float"], + scaleTicks: { + x: true, + }, + } +); + +new D3BubbleChart( + "frameworks-usage-vs-satisfaction-chart", + "frameworks-usage-vs-satisfaction-table", + { + radiusColumn: 1, + valueType: ["percentage", "float"], + extendedColors: true, + scaleTicks: { + x: true, + }, + } +); + +new D3BubbleChart( + "smaller-frameworks-usage-vs-satisfaction-chart", + "smaller-frameworks-usage-vs-satisfaction-table", + { + radiusColumn: 1, + valueType: ["percentage", "float"], + scaleTicks: { + x: true, + }, + } +); + +new D3BubbleChart( + "frameworks-usage-vs-satisfaction-changes-chart", + "frameworks-usage-vs-satisfaction-changes-table", + { + valueType: ["percentage", "float"], + extendedColors: true, + scaleTicks: { + x: true, + }, + } +); + +new D3HorizontalBarChart("web3-feelings-chart", "web3-feelings-table", { + showLegend: false, + showInlineBarValues: "outside", + margin: { + left: 164, + }, + colorMod: 1, + scaleTicks: { + x: true, + }, +}); + +new D3HorizontalBarChart("web3-usage-chart", "web3-usage-table", { + mode: "stacked", + colorMod: 2, + showInlineBarValues: false, + margin: { + left: 128, + scaleTicks: { + x: true, + }, + }, + interactive: true +}); + +new D3HorizontalBarChart("web-components-chart", "web-components-table", { + showLegend: false, + showInlineBarValues: "outside", + margin: { + left: 180, + }, + colorMod: 3, + scaleTicks: { + x: true, + }, +}); + +new D3VerticalBarChart("serverless-usage-chart", "serverless-usage-table", { + showLegend: false, + showInlineBarValues: "outside", + colorMod: 2, + wrapTicks: { + x: true, + }, + scaleTicks: { + x: true, + }, +}); diff --git a/src/site/survey/2022/js.njk b/src/site/survey/2022/js.njk new file mode 100644 index 000000000..d437aadfc --- /dev/null +++ b/src/site/survey/2022/js.njk @@ -0,0 +1,8 @@ +--- +permalink: /survey/2022/bundle.js +--- + +{% include "../../../../node_modules/d3/dist/d3.min.js" %} +{% include "../../../../node_modules/d3-textwrap/build/d3-textwrap.min.js" %} +{% include "../shared/d3chart.js" %} +{% include "./d3chart-survey-2022.js" %} diff --git a/src/site/survey/shared/d3chart.js b/src/site/survey/shared/d3chart.js new file mode 100644 index 000000000..4385125f6 --- /dev/null +++ b/src/site/survey/shared/d3chart.js @@ -0,0 +1,1559 @@ +const debounce = (callback, wait) => { + let timeoutId = null; + return (...args) => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => { + callback.apply(null, args); + }, wait); + }; +}; + +class D3Chart { + constructor(targetId, options, className) { + this.targetId = targetId; + this.className = className; + + this.options = Object.assign( + { + showInlineBarValues: "inside", // inside, inside-offset, and outside supported + showLegend: true, + showAxisLabels: false, + margin: {}, + colors: [ + "#F0047F", + "#00BFAD", + "#FFC803", + "#78ECC2", + "#DF4A1F", + "#FD98BC", + "#6B38FB", + "#03D0D0", + "#C40468", + "#78F19A", + "#91A5EE", + "#02C6B3", + "#FF0F00", + "#003EDD", + "#02465F", + "#960000", + "#FF72CF", + ], + // only applies when `showInlineBarValues: "inside"` + labelColors: [ + "#fff", + "#000", + "#000", + "#000", + "#000", + "#000", + "#000", + "#000", + "#fff", + "#000", + "#000", + "#000", + "#000", + "#000", + "#000", + "#fff", + "#fff", + "#000", + "#000", + "#000", + "#000", + "#000", + "#000", + "#fff", + "#000", + "#fff", + "#000", + "#000", + "#000", + "#000", + ], + colorMod: 0, + inlineLabelPad: 5, + labelPrecision: 0, + // TODO make this automatic by parsing `%` signs + valueType: ["percentage"], + sortLegend: false, + highlightElementsFromLegend: false, + extendedColors: false, + }, + options + ); + + this.options.colors = this.normalizeColors( + this.options.colors, + this.options.colorMod + ); + this.options.labelColors = this.normalizeColors( + this.options.labelColors, + this.options.colorMod + ); + } + + scaleTicksX(svg) { + const getTranslateX = (node) => + node.transform.baseVal.consolidate().matrix["e"]; + + const tickDistancesX = []; + const tickWidths = []; + + svg.selectAll(".d3chart-xaxis .tick").each(function () { + tickDistancesX.push(getTranslateX(d3.select(this).node())); + tickWidths.push(d3.select(this).node().getBBox().width); + }); + + const tickSize = (tickDistancesX.at(-1) - tickDistancesX.at(-2)) * 0.75; + const largestTickWidth = Math.max(...tickWidths); + const baseFontSize = 1.3; + + if (largestTickWidth >= tickSize) { + const scale = tickSize / largestTickWidth; + + svg + .selectAll(".d3chart-xaxis .tick text") + .style("font-size", `${baseFontSize * scale}em`) + } + } + + onResize(callback) { + if (!("ResizeObserver" in window)) { + window.addEventListener("resize", () => { + callback.call(this); + }); + return; + } + + let resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + // console.log( "resizing", this.target ); + callback.call(this); + } + }); + + resizeObserver.observe(this.target); + } + + onDeferInit(callback) { + if (!("IntersectionObserver" in window)) { + callback.call(this); + return; + } + + let observer = new IntersectionObserver( + (entries, observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + // console.log( "initing", this.target ); + callback.call(this); + observer.unobserve(entry.target); + } + }); + }, + { + threshold: 0.1, + } + ); + + observer.observe(this.target); + } + + normalizeColors(colors = [], mod = 0) { + if (mod) { + let c = []; + let len = colors.length; + let k = len + mod; + for (let j = mod || 0; j < k; j++) { + c.push(colors[j % len]); + } + return c; + } + + return colors; + } + + get margin() { + let m = Object.assign( + { + top: 30, + right: 10, + bottom: 25, + left: 40, + }, + this.options.margin + ); + + return m; + } + + get dimensions() { + let target = this.target; + return { + container: { + width: target.clientWidth, + height: target.clientHeight, + }, + min: { + width: 300, + height: 450, + }, + max: { + height: 1000, + }, + }; + } + + get width() { + return Math.max(this.dimensions.container.width, this.dimensions.min.width); + } + + get height() { + return Math.max( + Math.min(this.dimensions.container.height, this.dimensions.max.height) - + this.margin.bottom, + this.dimensions.min.height + ); + } + + get svg() { + return d3 + .create("svg") + .attr("height", this.height) + .attr("viewBox", [0, 0, this.width, this.height]); + } + + get colors() { + return d3.scaleOrdinal().range(this.options.colors); + } + + get labelColors() { + return d3.scaleOrdinal().range(this.options.labelColors); + } + + get target() { + return document.getElementById(this.targetId); + } + + reset(svg) { + let target = this.target; + target.classList.add("d3chart"); + if (this.className) { + target.classList.add(this.className); + } + + for (let child of target.children) { + if (child.tagName.toLowerCase() === "svg") { + child.remove(); + } + } + + let node = svg.node(); + target.appendChild(node); + } + + // Thanks https://bl.ocks.org/mbostock/7555321 + static wrapText(text, width) { + text.each(function () { + var text = d3.select(this), + words = text.text().split(/\s+/).reverse(), + word, + line = [], + lineNumber = 0, + lineHeight = 1.01, // ems + y = text.attr("y"), + dy = parseFloat(text.attr("dy")), + tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y), + firstTspan = tspan; + + let wrapCount = 0; + while ((word = words.pop())) { + line.push(word); + tspan.text(line.join(" ")); + if (tspan.node().getComputedTextLength() > width) { + wrapCount++; + line.pop(); + tspan.text(line.join(" ")); + line = [word]; + tspan = text + .append("tspan") + .attr("x", 0) + .attr("y", y) + .attr("dy", lineHeight + dy + "em") + .text(word); + } + } + + if (wrapCount) { + text.attr("dy", 0).attr("class", "d3chart-label-wrapped"); + firstTspan.attr("dy", -0.3 * wrapCount * lineHeight + "em"); + } + }); + } + + parseDataToCsv(tableId, reverse) { + let table = document.getElementById(tableId); + let headerCells = table.querySelectorAll(":scope thead th"); + let bodyRows = table.querySelectorAll(":scope tbody tr"); + + let headerOutput = []; + for (let th of headerCells) { + headerOutput.push(th.textContent.replace(/,/g, ",")); + } + + let output = []; + for (let tr of bodyRows) { + let row = []; + for (let child of tr.children) { + let value = child.textContent; + if ( + child.getAttribute("data-avoid-parse") === null && + value.endsWith("%") + ) { + value = parseFloat(value) / 100; + } + row.push(value); + } + output.push(row.join(",")); + } + + if (reverse) { + return [headerOutput.join(","), ...output.reverse()].join("\n"); + } + return [headerOutput.join(","), ...output].join("\n"); + } + + retrieveLabelId(label) { + let match = label.match(/^(\d*)\./); + if (match && match[1]) { + return parseInt(match[1], 10); + } + } + + slugify(slug, prefix) { + return `${prefix}${slug.toLowerCase().replace(/[\s\.]/g, "")}`; + } + + generateLegend(labels = []) { + let container = document.createElement("div"); + container.classList.add("d3chart-legend"); + + let entries = []; + for (let j = 0; j < labels.length; j++) { + let tag = "div"; + let attrs = ""; + if ( + this.options.highlightElementsFromLegend || + this.options.interactive + ) { + tag = "button"; + attrs = " type='button'"; + } + + attrs += ` data-item=${this.slugify(labels[j], "")} `; + + entries.push({ + label: labels[j], + html: `<${tag}${attrs} class="d3chart-legend-entry d3chart-legend-${ + j + this.options.colorMod + }">${labels[j] || ""}`, + }); + } + + if (this.options.sortLegend) { + entries = entries.sort((a, b) => { + let idA = this.retrieveLabelId(a.label); + let idB = this.retrieveLabelId(b.label); + if (idA && idB) { + return idA - idB; + } + if (a.label < b.label) { + return -1; + } else if (b.label < a.label) { + return 1; + } + return 0; + }); + } + + let html = []; + for (let entry of entries) { + html.push(entry.html); + } + container.innerHTML = html.join(""); + return container; + } + + getKeys(data) { + return data.columns.slice(1); + } + + highlightElements(target, method) { + // TODO this is specific to Bubble chart + if (target.classList.contains("d3chart-legend-entry")) { + let circleSlug = this.slugify( + target.innerHTML, + `${this.targetId}-bubblecircle-` + ); + let labelSlug = this.slugify( + target.innerHTML, + `${this.targetId}-bubblelabel-` + ); + + let circle = document.getElementById(circleSlug); + let label = document.getElementById(labelSlug); + + circle.classList[method]("active"); + label.classList[method]("active"); + + circle.closest("svg").classList[method]("d3chart-bubble-active"); + } + } + + renderLegend(data) { + if (!this.options.showLegend) { + return; + } + + let keys = this.getKeys(data); + let legend = this.generateLegend(keys, this.options.colors); + + legend.classList.add(`${this.targetId}-legend`); + + let selector = ":scope .d3chart-legend-placeholder"; + + let previousEl = this.target.previousElementSibling; + let legendAnchorBefore = previousEl + ? previousEl.querySelector(selector) + : null; + + let nextEl = this.target.nextElementSibling; + let legendAnchorAfter = nextEl ? nextEl.querySelector(selector) : null; + + if (legendAnchorBefore || legendAnchorAfter) { + (legendAnchorBefore || legendAnchorAfter).appendChild(legend); + } else { + // inside + this.target.appendChild(legend); + } + } + + roundValue(num, valueType = "percentage") { + if (valueType !== "percentage") { + return num; + } + + let d0 = (num * 100).toFixed(0); + if (this.options.labelPrecision === 0) { + return d0; + } + + let d1 = (num * 100).toFixed(1); + if (d1.endsWith(".0")) { + return d0; + } + return d1; + } +} + +class D3VerticalBarChart extends D3Chart { + constructor(target, tableId, optionOverrides = {}) { + if (!optionOverrides.rotateXAxisLabels) { + optionOverrides.rotateXAxisLabels = { + maxWidth: 0, + }; + } + + let chart = super(target, optionOverrides, "d3chart-vbar"); + + let csvData = chart.parseDataToCsv(tableId); + let dataSplit = csvData.split("\n"); + this.axisLabels = [dataSplit[0].split(",")[0]]; + + let data = Object.assign(d3.csvParse(csvData, d3.autoType)); + + this.onDeferInit(function () { + this.render(chart, data); + this.renderLegend(data); + + this.onResize(function () { + this.render(chart, data); + }); + }); + } + + render(chart, data) { + let { + options, + margin, + width, + height, + dimensions, + svg, + colors, + labelColors, + } = chart; + + let keys = this.getKeys(data); + let groupKey = data.columns[0]; + let groups = data.map((d) => d[groupKey]); + + let y = d3 + .scaleLinear() + .domain([ + 0, + d3.max(data, (d) => { + if (options.mode === "stacked") { + let sum = 0; + for (let key of keys) { + sum += d[key]; + } + return sum; + } + + return d3.max(keys, (key) => d[key]); + }), + ]) + .nice() + .rangeRound([height - margin.bottom, margin.top]); + + let x0 = d3 + .scaleBand() + .domain(groups) + .rangeRound([margin.left, width - margin.right]) + .paddingInner(0.2); + + let x1 = d3 + .scaleBand() + .domain(keys) + .rangeRound([0, x0.bandwidth()]) + .padding(0.1); + + let yAxis = (g) => + g + .attr("transform", `translate(${margin.left},0)`) + .attr("class", "d3chart-yaxis") + .call( + d3 + .axisLeft(y) + .ticks(null, options.valueType[0] === "percentage" ? "%" : "") + .tickSize(-width + margin.left + margin.right) + ) + .call((g) => g.select(".domain").remove()); + + let xAxis = (g) => + g + .attr("transform", `translate(0,${height - margin.bottom})`) + .attr("class", "d3chart-xaxis") + .call(d3.axisBottom(x0).tickSizeOuter(0)) + .call((g) => g.select(".domain").remove()); + + let dataMod = (d) => { + let incrementer = 0; + + return keys.map((key) => { + let data = { + key, + value: d[key], + width: x1.bandwidth(), + height: y(0) - y(d[key]), + left: x1(key), + top: y(d[key]), + slug: this.slugify(key, ""), + }; + + if (options.mode === "stacked") { + data.width = x0.bandwidth(); + data.left = 0; + data.top = y(d[key]) - incrementer; + incrementer += data.height; + } + + return data; + }); + }; + + svg.append("g").call(xAxis); + svg.append("g").call(yAxis); + + svg + .append("g") + .selectAll("g") + .data(data) + .join("g") + .attr("transform", (d) => `translate(${x0(d[groupKey])},0)`) + .selectAll("rect") + .data(dataMod) + .join("rect") + .attr("x", (d) => d.left) + .attr("y", (d) => d.top) + .attr("width", (d) => d.width) + .attr("height", (d) => (isNaN(d.height) ? 0 : d.height)) + .attr("fill", (d) => colors(d.key)) + .attr("data-item", (d) => d.key) + .attr("class", (d, j) => `d3chart-color-${j + options.colorMod}`) + .classed("d3chart-rect", true); + + if (options.showInlineBarValues) { + svg + .append("g") + .selectAll("g") + .data(data) + .join("g") + .attr("transform", (d) => `translate(${x0(d[groupKey])},0)`) + .selectAll("text") + .data(dataMod) + .join("text") + .attr("x", (d) => d.left + d.width / 2) + .attr("y", (d) => { + if (isNaN(d.height)) { + return 0; + } + + return ( + d.top - + (options.showInlineBarValues === "outside" + ? options.inlineLabelPad + : -15 - options.inlineLabelPad) + ); + }) + .attr("fill", (d) => + options.showInlineBarValues === "inside" + ? labelColors(d.key) + : "currentColor" + ) + .attr("class", "d3chart-inlinebarvalue") + .text((d) => { + if (d.value === null) { + return ""; + } + + return ( + this.roundValue(d.value, options.valueType[0]) + + (options.valueType[0] === "percentage" ? "%" : "") + ); + }); + } + + // TODO for horizontal bar chart + if (options.showAxisLabels) { + svg + .append("text") + .attr("x", Math.round(width / 2)) + .attr("y", height - 6) + .attr("class", "d3chart-axislabel d3chart-axislabel-center") + .text(this.axisLabels[0]); + } + + chart.reset(svg); + + if (options.wrapTicks && options.wrapTicks.x) { + const heights = []; + const wrap = d3.textwrap().bounds({ + height: margin.bottom, + width: x0.bandwidth() * 1 + keys.length + 2, + }); + + svg.selectAll(".d3chart-xaxis text").call(wrap); + svg + .selectAll("foreignObject") + .attr("x", function () { + return (-1 * +d3.select(this).attr("width")) / 2; + }) + .style("text-align", "center") + .style("font-weight", 600) + .attr("height", function () { + const height = d3 + .select(this) + .select("div") + .node() + .getBoundingClientRect().height; + heights.push(height); + return height; + }); + + svg.attr("overflow", "visible"); + svg.node().parentNode.style.marginBottom = `${Math.max(...heights)}px`; + } + + if (options.rotateXAxisLabels === true) { + svg + .select(".d3chart-xaxis") + .selectAll("text") + .attr("transform", "rotate(45)") + .style("text-anchor", "start"); + } + + if (options.interactive) { + this.setupInteractivity(svg); + } + } + + setupInteractivity(svg) { + const rectElements = svg.selectAll(".d3chart-rect"); + const labelElements = svg.selectAll(".d3chart-bubblelabel"); + + let resetTimeout; + + const legendItems = d3.selectAll( + `.${this.targetId}-legend .d3chart-legend-entry` + ); + + function knockBackOpacity() { + rectElements.style("fill-opacity", 0.15); + labelElements.style("fill-opacity", 0.15); + legendItems.style("opacity", 0.15); + } + + function resetOpacity() { + resetTimeout = setTimeout(() => { + labelElements.style("fill-opacity", 1); + rectElements.style("fill-opacity", 1); + + legendItems.style("opacity", 1); + }, 512); + } + + function handleLegendIteraction() { + clearTimeout(resetTimeout); + + knockBackOpacity(); + + const item = d3.select(this).attr("data-item"); + + const rect = rectElements.filter(function () { + return d3.select(this).attr("data-item") === item; + }); + + const label = labelElements.filter(function () { + return d3.select(this).attr("data-item") === item; + }); + + rect.style("fill-opacity", 1); + label.style("fill-opacity", 1); + + d3.select(this).style("opacity", 1); + } + + legendItems.on("mouseover", handleLegendIteraction); + legendItems.on("focus", handleLegendIteraction); + + legendItems.on("mouseout", function () { + resetOpacity(); + }); + + legendItems.on("focusout", function () { + resetOpacity(); + }); + + rectElements.on("mouseover", function (e, data) { + clearTimeout(resetTimeout); + + knockBackOpacity(); + + const label = svg.select( + `.d3chart-bubblelabel[data-item="${data.slug}"]` + ); + const legendItem = legendItems.filter(function () { + return d3.select(this).attr("data-item") === data.slug; + }); + + d3.select(this).style("fill-opacity", 1); + label.style("fill-opacity", 1); + legendItem.style("opacity", 1); + }); + + rectElements.on("mouseout", function () { + resetOpacity(); + }); + } +} + +class D3HorizontalBarChart extends D3Chart { + constructor(target, tableId, optionOverrides = {}) { + optionOverrides.margin = Object.assign( + { + top: 20, + right: 50, + bottom: 20, + left: 120, + }, + optionOverrides.margin + ); + let chart = super(target, optionOverrides, "d3chart-hbar"); + let csvData = chart.parseDataToCsv(tableId, true); + let data = Object.assign(d3.csvParse(csvData, d3.autoType)); + + this.onDeferInit(function () { + this.render(chart, data); + this.renderLegend(data); + + this.onResize(function () { + this.render(chart, data); + }); + }); + } + + render(chart, data) { + let { + options, + margin, + width, + height, + dimensions, + svg, + colors, + labelColors, + } = chart; + + let keys = this.getKeys(data); + let groupKey = data.columns[0]; + let groups = data.map((d) => d[groupKey]); + + let x = d3 + .scaleLinear() + .domain([ + 0, + d3.max(data, (d) => { + if (options.scale === "proportional") { + return 1; + } + + if (options.mode === "stacked") { + let sum = 0; + for (let key of keys) { + sum += d[key]; + } + return sum; + } + + return d3.max(keys, (key) => d[key]); + }), + ]) + .nice() + .rangeRound([margin.left, width - margin.right]); + + let y0 = d3 + .scaleBand() + .domain(groups) + .rangeRound([height - margin.bottom - margin.top, margin.top]) + .paddingInner( + options.showInlineBarValues === "inside-offset" ? 0.25 : 0.15 + ); + + let y1 = d3 + .scaleBand() + .domain(keys) + .rangeRound([0, y0.bandwidth()]) + .padding(0.05); + + let xAxis = (g) => + g + .attr("transform", `translate(0, ${(margin.top + margin.bottom) / 4})`) + .attr("class", "d3chart-xaxis") + .call( + d3 + .axisBottom(x) + .ticks(5, options.valueType[0] === "percentage" ? "%" : "") + .tickSize(height - margin.bottom - margin.top) + ) + .call((g) => g.select(".domain").remove()); + + let yAxis = (g) => + g + .attr("transform", `translate(${margin.left - 6},0)`) + .attr("class", "d3chart-yaxis") + .call(d3.axisLeft(y0).tickSize(0)) + .call((g) => g.select(".domain").remove()); + + let dataMod = (d) => { + let incrementer = 0; + let sum = 0; + for (let key of keys) { + sum += d[key]; + } + + return keys.map((key) => { + let data = { + key, + value: d[key], + sum, + width: + x(options.scale === "proportional" ? d[key] / sum : d[key]) - x(0), + height: y1.bandwidth(), + left: margin.left, + top: y1(key), + slug: this.slugify(key, ""), + }; + + if (options.mode === "stacked") { + data.top = 0; + data.height = y0.bandwidth(); + data.left = margin.left + incrementer; + + incrementer += data.width; + } + + return data; + }); + }; + + svg.append("g").call(xAxis); + svg.append("g").call(yAxis); + + svg + .append("g") + .selectAll("g") + .data(data) + .join("g") + .attr("transform", (d) => `translate(0,${y0(d[groupKey])})`) + .selectAll("rect") + .data(dataMod) + .join("rect") + .attr("x", (d) => d.left) + .attr("y", (d) => d.top) + .attr("width", (d) => (isNaN(d.width) ? 0 : d.width)) + .attr("height", (d) => d.height) + .attr("fill", (d) => colors(d.key)) + .attr("class", (d, j) => `d3chart-color-${j + options.colorMod}`) + .classed("d3chart-rect", true) + .attr("data-item", (d) => d.slug); + + if (options.showInlineBarValues) { + svg + .append("g") + .selectAll("g") + .data(data) + .join("g") + .attr("transform", (d) => `translate(0,${y0(d[groupKey])})`) + .selectAll("text") + .data(dataMod) + .join("text") + .attr("x", (d) => { + let offset = options.inlineLabelPad; + + if (isNaN(d.width)) { + return d.left + offset; + } + + if (options.showInlineBarValues.startsWith("inside")) { + offset = -1 * offset; + } + + if (options.showInlineBarValues === "inside-offset") { + offset += 16; + } + + return d.left + d.width + offset; + }) + .attr("y", (d) => { + if (options.showInlineBarValues === "inside-offset") { + return -10; + } + return d.top + Math.floor(d.height / 2) - 1; + }) + .attr( + "class", + (d) => + "d3chart-inlinebarvalue-h" + + (options.showInlineBarValues.length + ? ` ${options.showInlineBarValues}` + : "") + ) + .attr("fill", (d) => + options.showInlineBarValues === "inside" + ? labelColors(d.key) + : "currentColor" + ) + .text((d) => { + if (d.value === null) { + return ""; + } + + return ( + this.roundValue(d.value, options.valueType[0]) + + (options.valueType[0] === "percentage" ? "%" : "") + ); + }); + } + + chart.reset(svg); + + if (options.scaleTicks && options.scaleTicks.x) { + this.scaleTicksX(svg); + } + + if (options.wrapAxisLabel && options.wrapAxisLabel.left) { + D3Chart.wrapText( + svg.selectAll(".d3chart-yaxis .tick text"), + margin.left - 6 + ); + } + + if (options.interactive) { + this.setupInteractivity(svg); + } + } + + setupInteractivity(svg) { + const rectElements = svg.selectAll(".d3chart-rect"); + const labelElements = svg.selectAll(".d3chart-bubblelabel"); + + let resetTimeout; + + const legendItems = d3.selectAll( + `.${this.targetId}-legend .d3chart-legend-entry` + ); + + function knockBackOpacity() { + rectElements.style("fill-opacity", 0.15); + labelElements.style("fill-opacity", 0.15); + legendItems.style("opacity", 0.15); + } + + function resetOpacity() { + resetTimeout = setTimeout(() => { + labelElements.style("fill-opacity", 1); + rectElements.style("fill-opacity", 1); + + legendItems.style("opacity", 1); + }, 512); + } + + function handleLegendIteraction() { + clearTimeout(resetTimeout); + + knockBackOpacity(); + + const item = d3.select(this).attr("data-item"); + + const rect = rectElements.filter(function () { + return d3.select(this).attr("data-item") === item; + }); + + const label = labelElements.filter(function () { + return d3.select(this).attr("data-item") === item; + }); + + rect.style("fill-opacity", 1); + label.style("fill-opacity", 1); + + d3.select(this).style("opacity", 1); + } + + legendItems.on("mouseover", handleLegendIteraction); + legendItems.on("focus", handleLegendIteraction); + + legendItems.on("mouseout", function () { + resetOpacity(); + }); + + legendItems.on("focusout", function () { + resetOpacity(); + }); + + rectElements.on("mouseover", function (e, data) { + clearTimeout(resetTimeout); + + knockBackOpacity(); + + const label = svg.select( + `.d3chart-bubblelabel[data-item="${data.slug}"]` + ); + const legendItem = legendItems.filter(function () { + return d3.select(this).attr("data-item") === data.slug; + }); + + d3.select(this).style("fill-opacity", 1); + label.style("fill-opacity", 1); + legendItem.style("opacity", 1); + }); + + rectElements.on("mouseout", function () { + resetOpacity(); + }); + } +} + +class D3BubbleChart extends D3Chart { + constructor(target, tableId, optionOverrides = {}) { + optionOverrides.margin = { + top: 20, + right: 20, + bottom: 50, + left: 65, + }; + + optionOverrides.sortLegend = true; + optionOverrides.highlightElementsFromLegend = true; + optionOverrides.showAxisLabels = true; + + if (!optionOverrides.valueType) { + optionOverrides.valueType = ["percentage", "percentage"]; + } + + let chart = super(target, optionOverrides, "d3chart-bubble"); + let csvData = chart.parseDataToCsv(tableId); + let dataSplit = csvData.split("\n"); + this.axisLabels = dataSplit[0].split(",").slice(1); + + let data = dataSplit.slice(1).map((entry, id) => { + const columns = entry.split(","); + let [name, x, y, r] = columns; + return { + name, + id, + x, + y, + r: r ?? columns[optionOverrides.radiusColumn], + slug: this.slugify(name, ""), + }; + }); + + // sort from smallest to largest circles to insert in order (to render in the right z-index) + data = data.slice().sort((a, b) => { + return b.r - a.r; + }); + + this.onDeferInit(function () { + this.render(chart, data); + this.renderLegend(data); + + this.onResize(function () { + this.render(chart, data); + }); + }); + } + + getKeys(data) { + let keys = []; + for (let entry of data) { + keys.push(entry.name); + } + return keys; + } + + resolveLimit(data, key, valueType, mode) { + let limit = d3[mode](data, (d) => parseFloat(d[key])); + if (valueType !== "percentage") { + if (mode === "max") { + limit = Math.ceil(limit); + } else if (mode === "min") { + limit = Math.min(Math.floor(limit), 0); + } + } else { + if (mode === "max") { + if (limit > 1) { + limit += 0.1; + } else { + // round up to at most 1 if percentage < 100% + if (limit > 0.5) { + limit = Math.min(limit + 0.1, 1); + } else { + (limit = limit + 0.05), 1; + } + } + } + if (mode === "min") { + if (limit <= 0) { + limit -= 0.1; + } else { + // round up to at most 1 if percentage < 100% + limit = Math.min(limit, 0); + } + } + } + + return limit; + } + + render(chart, data) { + let { + options, + margin, + width, + height, + dimensions, + svg, + colors, + labelColors, + } = chart; + let xScale = d3.scaleLinear().range([margin.left, width - margin.right]); + let yScale = d3.scaleLinear().range([margin.top, height - margin.bottom]); + + let xAxisMin = this.resolveLimit(data, "x", options.valueType[0], "min"); + let xAxisMax = this.resolveLimit(data, "x", options.valueType[0], "max"); + let yAxisMin = this.resolveLimit(data, "y", options.valueType[1], "min"); + let yAxisMax = this.resolveLimit(data, "y", options.valueType[1], "max"); + + const yExtent = d3.extent([yAxisMin, yAxisMax]); + const yRange = yExtent[1] - yExtent[0]; + + xScale.domain([xAxisMin, xAxisMax]).nice(); + yScale + .domain([yExtent[1] + yRange * 0.05, yExtent[0] - yRange * 0.05]) + .nice(); + + let rScale = d3 + .scaleLinear() + .range([7, 25]) + .domain([ + Math.min( + d3.min(data, (d) => parseFloat(d.r)), + 0 + ), + d3.max(data, (d) => parseFloat(d.r)), + ]); + + let xAxis = d3 + .axisBottom() + .scale(xScale) + .ticks(null) + .tickSize(-height + margin.bottom + margin.top) + .tickFormat((d) => + options.valueType[0] === "percentage" ? `${(d * 100).toFixed(0)}%` : d + ); + + svg + .append("g") + .attr("class", "d3chart-xaxis") + .attr("transform", function () { + return "translate(0," + (height - margin.bottom) + ")"; + }) + .call(xAxis) + .call((g) => g.select(".domain").remove()); + + let yAxis = d3 + .axisLeft() + .scale(yScale) + .ticks(null) + .tickSize(-width + margin.right + margin.left) + .tickFormat((d) => + options.valueType[1] === "percentage" ? `${(d * 100).toFixed(0)}%` : d + ); + + svg + .append("g") + .attr("class", "d3chart-yaxis") + .attr("transform", function () { + return "translate(" + margin.left + "," + 0 + ")"; + }) + .call(yAxis) + .call((g) => g.select(".domain").remove()); + + svg.selectAll(".d3chart-xaxis .tick").attr("data-chart-value", (d) => d); + svg.selectAll(".d3chart-yaxis .tick").attr("data-chart-value", (d) => d); + + if (options.showAxisLabels) { + // Axis labels + svg + .append("text") + .attr("x", width - margin.right) + .attr("y", height - 6) + .attr("class", "d3chart-axislabel") + .text(this.axisLabels[0]); + + svg + .append("text") + .attr("x", -1 * margin.top) + .attr("y", 6) + .attr("dy", ".75em") + .attr("transform", "rotate(-90)") + .attr("class", "d3chart-axislabel") + .text(this.axisLabels[1]); + } + + let group = svg.append("g"); + + let circles = group.selectAll("circle").data(data); + + // Text Labels + function isOffsetLabel(d) { + let range = rScale(d.r); + return range <= 10; + } + + circles + .enter() + .insert("circle") + .attr("data-item", (d) => d.slug) + .attr("cx", function (d) { + return xScale(d.x); + }) + .attr("cy", function (d) { + return yScale(d.y); + }) + .attr("r", function (d) { + return rScale(d.r); + }) + .attr( + "class", + (d, j) => `d3chart-bubblecircle d3chart-color-${j + options.colorMod}` + ); + + circles + .enter() + .append("text") + .attr("data-item", (d) => d.slug) + .attr("x", (d) => { + return xScale(d.x) - (isOffsetLabel(d) ? rScale(d.r) + 4 : 0); + }) + .attr("y", (d) => yScale(d.y)) + .attr("class", (d) => { + return "d3chart-bubblelabel" + (isOffsetLabel(d) ? " offset-l" : ""); + }) + .attr("fill", (d) => (isOffsetLabel(d) ? "currentColor" : labelColors(d))) + .attr("pointer-events", "none") + .text((d) => { + let labelId = this.retrieveLabelId(d.name); + if (labelId) { + return labelId; + } + return d.name; + }) + .filter((d) => isOffsetLabel(d)) + .lower(); + + chart.reset(svg); + + if (options.scaleTicks && options.scaleTicks.x) { + this.scaleTicksX(svg); + } + + this.setupInteractivity(svg); + } + + setupInteractivity(svg) { + const circleElements = svg.selectAll(".d3chart-bubblecircle"); + const labelElements = svg.selectAll(".d3chart-bubblelabel"); + + let resetTimeout; + + const legendItems = d3.selectAll( + `.${this.targetId}-legend .d3chart-legend-entry` + ); + + function knockBackOpacity() { + circleElements.style("fill-opacity", 0.15); + labelElements.style("fill-opacity", 0.15); + legendItems.style("opacity", 0.15); + } + + function resetOpacity() { + resetTimeout = setTimeout(() => { + labelElements.style("fill-opacity", 1); + circleElements.style("fill-opacity", 0.85); + + legendItems.style("opacity", 1); + }, 512); + } + + function handleLegendIteraction() { + clearTimeout(resetTimeout); + + knockBackOpacity(); + + const item = d3.select(this).attr("data-item"); + + const circle = circleElements.filter(function () { + return d3.select(this).attr("data-item") === item; + }); + + const label = labelElements.filter(function () { + return d3.select(this).attr("data-item") === item; + }); + + circle.style("fill-opacity", 1); + label.style("fill-opacity", 1); + + d3.select(this).style("opacity", 1); + } + + legendItems.on("mouseover", handleLegendIteraction); + legendItems.on("focus", handleLegendIteraction); + + legendItems.on("mouseout", function () { + resetOpacity(); + }); + + legendItems.on("focusout", resetOpacity); + + circleElements.on("mouseover", function (e, data) { + clearTimeout(resetTimeout); + + knockBackOpacity(); + + const label = svg.select( + `.d3chart-bubblelabel[data-item="${data.slug}"]` + ); + const legendItem = legendItems.filter(function () { + return d3.select(this).attr("data-item") === data.slug; + }); + + d3.select(this).style("fill-opacity", 1); + label.style("fill-opacity", 1); + legendItem.style("opacity", 1); + }); + + circleElements.on("mouseout", function () { + resetOpacity(); + }); + } +} + +class D3LineChart extends D3Chart { + constructor(target, tableId, optionOverrides = {}) { + let chart = super(target, optionOverrides, "d3chart-hline"); + let csvData = chart.parseDataToCsv(tableId, true); + let data = Object.assign(d3.csvParse(csvData, d3.autoType)); + + this.onDeferInit(function () { + this.render(chart, data); + this.renderLegend(data); + + this.onResize(function () { + this.render(chart, data); + }); + }); + } + + render(chart, data) { + let { options, margin, width, height, dimensions, svg } = chart; + + const paddingX = dimensions.container.width / 16; + const paddingY = 0; + + const timeConv = d3.timeParse("%Y"); + + const slices = data.columns.slice(1).map((id) => { + return { + id, + values: data.map((d) => { + return { + date: timeConv(d.Date), + measurement: +d[id], + }; + }), + slug: this.slugify(id, ""), + }; + }); + + const xScale = d3 + .scaleTime() + .range([margin.left + paddingX, width - margin.right - paddingX]) + .domain(d3.extent(data, (d) => timeConv(d.Date))); + + const yScale = d3 + .scaleLinear() + .rangeRound([height - margin.bottom - paddingY, margin.top + paddingY]) + .domain([ + 0, + d3.max(slices, (c) => d3.max(c.values, (d) => d.measurement)), + ]) + .nice(); + + const yaxis = d3 + .axisLeft() + .tickFormat(d3.format(".0%")) + .tickSize(-width + margin.left + margin.right) + .scale(yScale); + const xaxis = d3.axisBottom().ticks(d3.timeYear.every(1)).scale(xScale); + + svg + .append("g") + .attr("transform", `translate(0, ${height - margin.bottom})`) + .call(xaxis) + .call((g) => g.select(".domain").remove()); + + svg + .append("g") + .attr("transform", `translate(${margin.left}, 0)`) + .call(yaxis) + .call((g) => g.select(".domain").remove()); + + const line = d3 + .line() + .x(function (d) { + return xScale(d.date); + }) + .y(function (d) { + return yScale(d.measurement); + }); + + const lines = svg.selectAll("lines").data(slices).enter().append("g"); + + lines + .append("path") + .attr("d", (d) => line(d.values)) + .attr("fill", "none") + .attr("stroke-width", 5) + .attr( + "class", + (d, j) => `d3chart-line d3chart-color-stroke-${j + options.colorMod}` + ) + .attr("data-item", (d) => d.slug); + + chart.reset(svg); + + this.setupInteractivity(svg); + } + + setupInteractivity(svg) { + const lineElements = svg.selectAll(".d3chart-line"); + const legendItems = d3.selectAll( + `.${this.targetId}-legend .d3chart-legend-entry` + ); + + let resetTimeout; + + function knockBackOpacity() { + lineElements.style("opacity", 0.15); + legendItems.style("opacity", 0.15); + } + + function resetOpacity() { + resetTimeout = setTimeout(() => { + lineElements.style("opacity", 1); + legendItems.style("opacity", 1); + }, 512); + } + + lineElements.on("mouseover", function (e, data) { + clearTimeout(resetTimeout); + + knockBackOpacity(); + + d3.select(this).style("opacity", 1); + + const legendItem = legendItems.filter(function () { + return d3.select(this).attr("data-item") === data.slug; + }); + + legendItem.style("opacity", 1); + }); + + lineElements.on("mouseout", function () { + resetOpacity(); + }); + + legendItems.on("mouseover", function (e, data) { + clearTimeout(resetTimeout); + knockBackOpacity(); + + const slug = d3.select(this).attr("data-item"); + + const line = lineElements.filter(function (d) { + return d.slug === slug; + }); + + line.style("opacity", 1); + + d3.select(this).style("opacity", 1); + }); + + legendItems.on("mouseout", function () { + resetOpacity(); + }); + } +}