|
| 1 | +<script lang="ts"> |
| 2 | + import { Chart, Axis, LinearGradient, Layer, Spline } from 'layerchart'; |
| 3 | + import { curveBumpX } from 'd3-shape'; |
| 4 | + import { scaleLinear, scalePoint } from 'd3-scale'; |
| 5 | + import { cls } from '@layerstack/tailwind'; |
| 6 | +
|
| 7 | + const states = [ |
| 8 | + { name: 'AK', ranks: [51, 51, 51, 51, 51, 51, 51, 50, 48, 47, 48] }, |
| 9 | + { name: 'AL', ranks: [18, 15, 17, 17, 19, 21, 22, 22, 23, 23, 24] }, |
| 10 | + { name: 'AR', ranks: [25, 25, 24, 31, 31, 32, 33, 33, 33, 32, 33] }, |
| 11 | + { name: 'AZ', ranks: [46, 44, 44, 38, 35, 33, 29, 24, 20, 16, 14] }, |
| 12 | + { name: 'CA', ranks: [8, 6, 4, 2, 2, 1, 1, 1, 1, 1, 1] }, |
| 13 | + { name: 'CO', ranks: [33, 33, 33, 34, 33, 30, 28, 26, 24, 22, 21] }, |
| 14 | + { name: 'CT', ranks: [29, 29, 31, 28, 25, 24, 25, 27, 29, 29, 29] }, |
| 15 | + { name: 'DC', ranks: [42, 41, 37, 36, 40, 41, 47, 48, 50, 50, 49] }, |
| 16 | + { name: 'DE', ranks: [48, 48, 48, 48, 47, 47, 48, 46, 45, 45, 45] }, |
| 17 | + { name: 'FL', ranks: [32, 31, 25, 20, 10, 9, 7, 4, 4, 4, 3] }, |
| 18 | + { name: 'GA', ranks: [12, 14, 14, 13, 16, 15, 13, 11, 10, 9, 8] }, |
| 19 | + { name: 'HI', ranks: [47, 46, 46, 46, 44, 40, 39, 40, 42, 40, 40] }, |
| 20 | + { name: 'IA', ranks: [17, 19, 20, 22, 24, 25, 27, 30, 30, 30, 31] }, |
| 21 | + { name: 'ID', ranks: [43, 43, 43, 44, 43, 43, 41, 42, 39, 39, 38] }, |
| 22 | + { name: 'IL', ranks: [3, 3, 3, 4, 4, 5, 5, 6, 5, 5, 6] }, |
| 23 | + { name: 'IN', ranks: [11, 11, 12, 11, 11, 11, 12, 14, 14, 15, 17] }, |
| 24 | + { name: 'KS', ranks: [24, 24, 29, 30, 28, 28, 32, 32, 32, 33, 35] }, |
| 25 | + { name: 'KY', ranks: [15, 16, 16, 19, 22, 23, 23, 23, 25, 26, 26] }, |
| 26 | + { name: 'LA', ranks: [22, 22, 21, 21, 20, 20, 19, 21, 22, 25, 25] }, |
| 27 | + { name: 'MA', ranks: [6, 8, 8, 9, 9, 10, 11, 13, 13, 14, 15] }, |
| 28 | + { name: 'MD', ranks: [28, 28, 28, 24, 21, 18, 18, 19, 19, 19, 18] }, |
| 29 | + { name: 'ME', ranks: [35, 35, 35, 35, 36, 38, 38, 38, 40, 41, 42] }, |
| 30 | + { name: 'MI', ranks: [7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 10] }, |
| 31 | + { name: 'MN', ranks: [16, 18, 18, 18, 18, 19, 21, 20, 21, 21, 22] }, |
| 32 | + { name: 'MO', ranks: [9, 10, 10, 12, 13, 13, 15, 15, 17, 18, 19] }, |
| 33 | + { name: 'MS', ranks: [23, 23, 23, 26, 29, 29, 31, 31, 31, 31, 34] }, |
| 34 | + { name: 'MT', ranks: [39, 39, 40, 43, 42, 44, 44, 44, 44, 44, 44] }, |
| 35 | + { name: 'NC', ranks: [14, 12, 11, 10, 12, 12, 10, 10, 11, 10, 9] }, |
| 36 | + { name: 'ND', ranks: [36, 38, 39, 42, 45, 46, 46, 47, 47, 48, 47] }, |
| 37 | + { name: 'NE', ranks: [31, 32, 32, 33, 34, 35, 35, 36, 38, 38, 37] }, |
| 38 | + { name: 'NH', ranks: [41, 42, 45, 45, 46, 42, 42, 41, 41, 42, 41] }, |
| 39 | + { name: 'NJ', ranks: [10, 9, 9, 8, 8, 8, 9, 9, 9, 11, 11] }, |
| 40 | + { name: 'NM', ranks: [44, 45, 42, 40, 37, 37, 37, 37, 36, 36, 36] }, |
| 41 | + { name: 'NV', ranks: [50, 50, 50, 50, 50, 48, 43, 39, 35, 35, 32] }, |
| 42 | + { name: 'NY', ranks: [1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 4] }, |
| 43 | + { name: 'OH', ranks: [4, 4, 5, 5, 5, 6, 6, 7, 7, 7, 7] }, |
| 44 | + { name: 'OK', ranks: [21, 21, 22, 25, 27, 27, 26, 28, 27, 28, 28] }, |
| 45 | + { name: 'OR', ranks: [34, 34, 34, 32, 32, 31, 30, 29, 28, 27, 27] }, |
| 46 | + { name: 'PA', ranks: [2, 2, 2, 3, 3, 3, 4, 5, 6, 6, 5] }, |
| 47 | + { name: 'RI', ranks: [38, 37, 36, 37, 39, 39, 40, 43, 43, 43, 43] }, |
| 48 | + { name: 'SC', ranks: [26, 26, 27, 27, 26, 26, 24, 25, 26, 24, 23] }, |
| 49 | + { name: 'SD', ranks: [37, 36, 38, 41, 41, 45, 45, 45, 46, 46, 46] }, |
| 50 | + { name: 'TN', ranks: [20, 17, 15, 15, 17, 17, 17, 18, 16, 17, 16] }, |
| 51 | + { name: 'TX', ranks: [5, 5, 6, 6, 6, 4, 3, 3, 2, 2, 2] }, |
| 52 | + { name: 'UT', ranks: [40, 40, 41, 39, 38, 36, 36, 35, 34, 34, 30] }, |
| 53 | + { name: 'VA', ranks: [19, 20, 19, 16, 14, 14, 14, 12, 12, 12, 12] }, |
| 54 | + { name: 'VT', ranks: [45, 47, 47, 47, 48, 49, 49, 49, 49, 49, 50] }, |
| 55 | + { name: 'WA', ranks: [30, 30, 30, 23, 23, 22, 20, 17, 15, 13, 13] }, |
| 56 | + { name: 'WI', ranks: [13, 13, 13, 14, 15, 16, 16, 16, 18, 20, 20] }, |
| 57 | + { name: 'WV', ranks: [27, 27, 26, 29, 30, 34, 34, 34, 37, 37, 39] }, |
| 58 | + { name: 'WY', ranks: [49, 49, 49, 49, 49, 50, 50, 51, 51, 51, 51] } |
| 59 | + ]; |
| 60 | +
|
| 61 | + const years = [ |
| 62 | + '1920', |
| 63 | + '1930', |
| 64 | + '1940', |
| 65 | + '1950', |
| 66 | + '1960', |
| 67 | + '1970', |
| 68 | + '1980', |
| 69 | + '1990', |
| 70 | + '2000', |
| 71 | + '2010', |
| 72 | + '2020' |
| 73 | + ]; |
| 74 | + const maxRank = 51; |
| 75 | + const rowHeight = 14; |
| 76 | +
|
| 77 | + const data = years.map((year, i) => { |
| 78 | + const row: Record<string, string | number> = { year }; |
| 79 | + for (const state of states) { |
| 80 | + row[state.name] = state.ranks[i]; |
| 81 | + } |
| 82 | + return row; |
| 83 | + }); |
| 84 | + export { data }; |
| 85 | +
|
| 86 | + const keys = states.map((s) => s.name); |
| 87 | +
|
| 88 | + let hoveredState = $state<string | null>(null); |
| 89 | +</script> |
| 90 | + |
| 91 | +<Chart |
| 92 | + {data} |
| 93 | + x="year" |
| 94 | + xScale={scalePoint()} |
| 95 | + y={keys} |
| 96 | + yScale={scaleLinear()} |
| 97 | + yDomain={[maxRank + 0.5, 0.5]} |
| 98 | + padding={{ top: 30, bottom: 30, left: 14, right: 18 }} |
| 99 | + height={maxRank * rowHeight + 60} |
| 100 | +> |
| 101 | + {#snippet children({ context })} |
| 102 | + <Layer> |
| 103 | + <LinearGradient |
| 104 | + id="gradient-improved" |
| 105 | + stops={['var(--color-success-700)', 'var(--color-success-300)']} |
| 106 | + /> |
| 107 | + <LinearGradient |
| 108 | + id="gradient-declined" |
| 109 | + stops={['var(--color-danger-300)', 'var(--color-danger-700)']} |
| 110 | + /> |
| 111 | + |
| 112 | + <Axis placement="top" rule={false} /> |
| 113 | + <Axis placement="bottom" rule={false} /> |
| 114 | + |
| 115 | + <!-- Lines (one Spline per state) --> |
| 116 | + {#each states as state (state.name)} |
| 117 | + {@const dimmed = hoveredState !== null && hoveredState !== state.name} |
| 118 | + <!-- svelte-ignore a11y_no_static_element_interactions --> |
| 119 | + <g |
| 120 | + onmouseenter={() => (hoveredState = state.name)} |
| 121 | + class={cls('transition-opacity duration-200', dimmed && 'opacity-[0.15]')} |
| 122 | + > |
| 123 | + <Spline |
| 124 | + y={state.name} |
| 125 | + curve={curveBumpX} |
| 126 | + stroke={(d, i, arr) => { |
| 127 | + if (i >= arr.length - 1) |
| 128 | + return 'color-mix(in srgb, var(--color-surface-content) 30%, transparent)'; |
| 129 | + const from = d[state.name]; |
| 130 | + const to = arr[i + 1][state.name]; |
| 131 | + return from > to |
| 132 | + ? 'url(#gradient-improved)' |
| 133 | + : from < to |
| 134 | + ? 'url(#gradient-declined)' |
| 135 | + : 'color-mix(in srgb, var(--color-surface-content) 30%, transparent)'; |
| 136 | + }} |
| 137 | + strokeWidth={4} |
| 138 | + /> |
| 139 | + </g> |
| 140 | + {/each} |
| 141 | + |
| 142 | + <!-- Labels at each point --> |
| 143 | + {#each states as state (state.name)} |
| 144 | + {@const dimmed = hoveredState !== null && hoveredState !== state.name} |
| 145 | + {#each data as point, i (point.year)} |
| 146 | + {@const x = context.xScale(point.year)} |
| 147 | + {@const y = context.yScale(point[state.name])} |
| 148 | + {@const from = state.ranks[i === 0 ? 0 : i - 1]} |
| 149 | + {@const to = state.ranks[i === 0 ? 1 : i]} |
| 150 | + <!-- svelte-ignore a11y_no_static_element_interactions --> |
| 151 | + <rect |
| 152 | + x={x - 12} |
| 153 | + y={y - rowHeight / 2} |
| 154 | + width={24} |
| 155 | + height={rowHeight} |
| 156 | + class={cls('fill-surface-200 transition-opacity duration-200', dimmed && '_opacity-10')} |
| 157 | + onmouseenter={() => (hoveredState = state.name)} |
| 158 | + onmouseleave={() => (hoveredState = null)} |
| 159 | + /> |
| 160 | + <text |
| 161 | + {x} |
| 162 | + {y} |
| 163 | + text-anchor="middle" |
| 164 | + dominant-baseline="central" |
| 165 | + pointer-events="none" |
| 166 | + class={cls( |
| 167 | + 'text-[12px] font-semibold font-[monospace] transition-opacity duration-200', |
| 168 | + from > to ? 'fill-success' : from < to ? 'fill-danger' : 'fill-surface-content/50', |
| 169 | + dimmed && 'opacity-10' |
| 170 | + )} |
| 171 | + > |
| 172 | + {state.name} |
| 173 | + </text> |
| 174 | + {/each} |
| 175 | + {/each} |
| 176 | + </Layer> |
| 177 | + {/snippet} |
| 178 | +</Chart> |
0 commit comments