autumnplot-gl 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,183 @@
1
+ ;
2
+ const createElement = (tagname, attributes, parent) => {
3
+ const elem = document.createElementNS('http://www.w3.org/2000/svg', tagname);
4
+ if (attributes !== undefined) {
5
+ Object.entries(attributes).forEach(([k, v]) => {
6
+ elem.setAttribute(k, v.toString());
7
+ });
8
+ }
9
+ if (parent !== undefined) {
10
+ parent.appendChild(elem);
11
+ }
12
+ return elem;
13
+ };
14
+ /**
15
+ * Make an SVG containing a color bar. The color bar can either be oriented horizontal or vertical, and a label can be provided.
16
+ * @param colormap - The color map to use
17
+ * @param opts - The options for creating the color bar
18
+ * @returns An SVGElement containing the color bar image.
19
+ * @example
20
+ * // Create the color bar
21
+ * const svg = makeColorBar(color_map, {label: 'Wind Speed (kts)', orientation: 'horizontal',
22
+ * fontface: 'Trebuchet MS'});
23
+ *
24
+ * // Add colorbar to the page
25
+ * document.getElementById('colorbar-container').appendChild(svg);
26
+ */
27
+ function makeColorBar(colormap, opts) {
28
+ const label = opts.label || "";
29
+ const ticks = opts.ticks || colormap.levels;
30
+ const orientation = opts.orientation || 'vertical';
31
+ const fontface = opts.fontface || 'sans-serif';
32
+ const tick_dir = opts.tick_direction || (orientation == 'vertical' ? 'left' : 'bottom');
33
+ if (orientation == 'vertical' && (tick_dir == 'top' || tick_dir == 'bottom') ||
34
+ orientation == 'horizontal' && (tick_dir == 'left' || tick_dir == 'right')) {
35
+ throw `tick_direction of '${tick_dir} doesn't match an orientation of ${orientation}`;
36
+ }
37
+ const getNChar = (n) => {
38
+ return n.toString().length;
39
+ };
40
+ const chars_left = getNChar(ticks[0]);
41
+ const chars_right = getNChar(ticks[ticks.length - 1]);
42
+ const bar_long_size = 600;
43
+ const bar_cross_size = bar_long_size / 9;
44
+ const bar_long_pad = orientation == 'horizontal' ? Math.max(chars_left, chars_right) * 6 : 5;
45
+ const bar_cross_pad = 3;
46
+ const bar_thickness = 10;
47
+ let height, width, bar_left, bar_top, bar_width, bar_height;
48
+ if (orientation == 'vertical') {
49
+ height = bar_long_size;
50
+ width = bar_cross_size;
51
+ bar_left = tick_dir == 'left' ? bar_cross_size - bar_cross_pad - bar_thickness : bar_cross_pad;
52
+ bar_top = bar_long_pad;
53
+ bar_width = bar_thickness;
54
+ bar_height = bar_long_size - 2 * bar_long_pad;
55
+ }
56
+ else {
57
+ width = bar_long_size;
58
+ height = bar_cross_size - 6;
59
+ bar_left = bar_long_pad;
60
+ bar_top = tick_dir == 'bottom' ? bar_cross_pad : bar_cross_size - 6 - bar_cross_pad - bar_thickness;
61
+ bar_height = bar_thickness;
62
+ bar_width = bar_long_size - 2 * bar_long_pad;
63
+ }
64
+ const n_colors = colormap.colors.length;
65
+ const root = createElement('svg', { width: width, height: height });
66
+ const gbar = createElement('g', {}, root);
67
+ let gtickattrs;
68
+ if (orientation == 'vertical') {
69
+ gtickattrs = tick_dir == 'left' ? { 'text-anchor': 'end', transform: `translate(${bar_left}, ${bar_top})` } :
70
+ { transform: `translate(${bar_left + bar_width}, ${bar_top})` };
71
+ }
72
+ else {
73
+ gtickattrs = tick_dir == 'bottom' ? { 'text-anchor': 'middle', transform: `translate(${bar_left}, ${bar_top + bar_height})` } :
74
+ { 'text-anchor': 'middle', transform: `translate(${bar_left}, ${bar_top})` };
75
+ }
76
+ const gticks = createElement('g', gtickattrs, root);
77
+ colormap.colors.forEach((color, icolor) => {
78
+ let attrs = {};
79
+ if (orientation == 'vertical') {
80
+ attrs = {
81
+ x: bar_left, y: bar_top + bar_height * (1 - (icolor + 1) / n_colors), width: bar_width, height: bar_height / n_colors
82
+ };
83
+ }
84
+ else {
85
+ attrs = { x: bar_left + bar_width * icolor / n_colors, y: bar_top, width: bar_width / n_colors, height: bar_height };
86
+ }
87
+ createElement('rect', { ...attrs, fill: color.color, opacity: color.opacity }, gbar);
88
+ });
89
+ ticks.forEach(level => {
90
+ const ilevel = colormap.levels.indexOf(level);
91
+ const tickattrs = orientation == 'vertical' ? { transform: `translate(0, ${bar_height * (1 - ilevel / n_colors)})` } :
92
+ { transform: `translate(${bar_width * ilevel / n_colors}, 0)` };
93
+ const gtick = createElement('g', tickattrs, gticks);
94
+ let lineattrs;
95
+ if (orientation == 'vertical') {
96
+ lineattrs = tick_dir == 'left' ? { x2: -6 } : { x2: 6 };
97
+ }
98
+ else {
99
+ lineattrs = tick_dir == 'bottom' ? { y2: 6 } : { y2: -6 };
100
+ }
101
+ createElement('line', { ...lineattrs, stroke: '#000000', 'stroke-width': 1.5 }, gtick);
102
+ let textattrs;
103
+ if (orientation == 'vertical') {
104
+ textattrs = tick_dir == 'left' ? { x: -9, dy: '0.32em' } : { x: 9, dy: '0.32em' };
105
+ }
106
+ else {
107
+ textattrs = tick_dir == 'bottom' ? { y: 9, dy: '0.8em' } : { y: -9, dy: '0em' };
108
+ }
109
+ const text = createElement('text', { ...textattrs, fill: '#000000', style: `font-family: ${fontface};` }, gtick);
110
+ text.textContent = level.toString();
111
+ });
112
+ const outline_attrs = {
113
+ x: bar_left,
114
+ y: bar_top,
115
+ width: bar_width,
116
+ height: bar_height,
117
+ stroke: '#000000',
118
+ 'stroke-width': 1.5,
119
+ fill: 'none'
120
+ };
121
+ createElement('rect', outline_attrs, root);
122
+ let labelattrs;
123
+ if (orientation == 'vertical') {
124
+ labelattrs = tick_dir == 'left' ? { transform: `translate(15, ${height / 2}) rotate(-90)` } : { transform: `translate(${width - 6}, ${height / 2}) rotate(-90)` };
125
+ }
126
+ else {
127
+ labelattrs = tick_dir == 'bottom' ? { transform: `translate(${width / 2}, ${height - 5})` } : { transform: `translate(${width / 2}, 15)` };
128
+ }
129
+ const label_elem = createElement('text', { ...labelattrs, fill: '#000000', 'text-anchor': 'middle', style: `font-family: ${fontface};` }, root);
130
+ label_elem.textContent = label;
131
+ return root;
132
+ }
133
+ /**
134
+ * Make an SVG containing a color key for a paintball plot. The key can be split over any number of columns.
135
+ * @param colors - A list of colors
136
+ * @param labels - The labels corresponding to each color
137
+ * @param opts - The options for creating the color key
138
+ * @returns An SVGElement containing the color bar image.
139
+ * @example
140
+ * // Create the color key
141
+ * const svg = makePaintballKey(colors, labels, {n_cols: 2, fontface: 'Trebuchet MS'});
142
+ *
143
+ * // Add the color key to the page
144
+ * document.getElementById('pb-key-container').appendChild(svg);
145
+ */
146
+ function makePaintballKey(colors, labels, opts) {
147
+ if (colors.length != labels.length) {
148
+ throw `Mismatch between the number of colors (${colors.length}) and the number of labels (${labels.length})`;
149
+ }
150
+ opts = opts === undefined ? {} : opts;
151
+ const n_cols = opts.n_cols === undefined ? 1 : opts.n_cols;
152
+ const fontface = opts.fontface === undefined ? 'sans-serif' : opts.fontface;
153
+ let height, width;
154
+ const swatch_width = 20;
155
+ const swatch_height = 20;
156
+ const swatch_text_pad = 3;
157
+ const swatch_text_space = 100;
158
+ const swatch_width_pad = 5;
159
+ const swatch_height_pad = 5;
160
+ const n_rows = Math.ceil(colors.length / n_cols);
161
+ width = (swatch_width + swatch_text_pad + swatch_text_space) * n_cols + swatch_width_pad * (n_cols + 1);
162
+ height = swatch_height * n_rows + swatch_height_pad * (n_rows + 1);
163
+ const root = createElement('svg', { width: width, height: height });
164
+ const gbar = createElement('g', {}, root);
165
+ colors.forEach((color, icolor) => {
166
+ const label = labels[icolor];
167
+ const irow = icolor % n_rows;
168
+ const icol = Math.floor(icolor / n_rows);
169
+ let opacity = 1.;
170
+ if (typeof color != 'string') {
171
+ opacity = color.opacity;
172
+ color = color.color;
173
+ }
174
+ const x = swatch_width_pad + icol * (swatch_width + swatch_text_pad + swatch_text_space + swatch_width_pad);
175
+ const y = swatch_height_pad + irow * (swatch_height + swatch_height_pad);
176
+ createElement('rect', { x: x, y: y, fill: color, opacity: opacity, width: swatch_width, height: swatch_height }, gbar);
177
+ let textattrs = { x: x + swatch_width + swatch_text_pad, y: y + swatch_height / 2, dy: '0.32em' };
178
+ const text = createElement('text', { ...textattrs, fill: '#000000', style: `font-size: 0.8em; font-family: ${fontface};` }, gbar);
179
+ text.textContent = label;
180
+ });
181
+ return root;
182
+ }
183
+ export { makeColorBar, makePaintballKey };
@@ -0,0 +1,70 @@
1
+ interface Color {
2
+ /** The color as a hex color string */
3
+ color: string;
4
+ /** The opacity as a number from 0 to 1 */
5
+ opacity: number;
6
+ }
7
+ /** A mapping from values to colors */
8
+ declare class ColorMap {
9
+ readonly levels: number[];
10
+ readonly colors: Color[];
11
+ /**
12
+ * Create a color map
13
+ * @param levels - The list of levels. The number of levels should always be one more than the number of colors.
14
+ * @param colors - A list of colors
15
+ */
16
+ constructor(levels: number[], colors: Color[] | string[]);
17
+ /**
18
+ * @returns an array of hex color strings
19
+ */
20
+ getColors(): string[];
21
+ /**
22
+ * @returns an array of opacities, one for each color in the color map
23
+ */
24
+ getOpacities(): number[];
25
+ /**
26
+ * Make a new color map with different opacities. The opacities are set by func.
27
+ * @param func - A function which takes the two levels associated with a color (an upper and lower bound) and returns an opacity in the range from 0 to 1.
28
+ * @returns A new color map
29
+ */
30
+ withOpacity(func: (level_lower: number, level_upper: number) => number): ColorMap;
31
+ /**
32
+ * Create a diverging color map using two input colors
33
+ * @param color1 - The color corresponding to the lowest value in the color map
34
+ * @param color2 - The color corresponding to the highest value in the color map
35
+ * @param level_min - The lowest value in the color map
36
+ * @param level_max - The highest value in the color map
37
+ * @param n_colors - The number of colors to use
38
+ * @returns a Colormap object
39
+ */
40
+ static diverging(color1: string, color2: string, level_min: number, level_max: number, n_colors: number): ColorMap;
41
+ }
42
+ declare const pw_speed500mb: ColorMap;
43
+ declare const pw_speed850mb: ColorMap;
44
+ declare const pw_cape: ColorMap;
45
+ declare const pw_t2m: ColorMap;
46
+ declare const pw_td2m: ColorMap;
47
+ /**
48
+ * Create a diverging red/blue colormap, where red corresponds to the lowest value and blue corresponds to the highest value
49
+ * @param level_min - The lowest value in the color map
50
+ * @param level_max - The highest value in the color map
51
+ * @param n_colors - The number of colors
52
+ * @returns a Colormap object
53
+ */
54
+ declare const redblue: (level_min: number, level_max: number, n_colors: number) => ColorMap;
55
+ /**
56
+ * Create a diverging blue/red colormap, where blue corresponds to the lowest value and red corresponds to the highest value
57
+ * @param level_min - The lowest value in the color map
58
+ * @param level_max - The highest value in the color map
59
+ * @param n_colors - The number of colors
60
+ * @returns a Colormap object
61
+ */
62
+ declare const bluered: (level_min: number, level_max: number, n_colors: number) => ColorMap;
63
+ /**
64
+ * Make a canvas image corresponding to a color map
65
+ * @param colormap - The color map to use
66
+ * @returns A canvas element containing each color of the color map
67
+ */
68
+ declare function makeTextureImage(colormap: ColorMap): HTMLCanvasElement;
69
+ export { ColorMap, bluered, redblue, pw_speed500mb, pw_speed850mb, pw_cape, pw_t2m, pw_td2m, makeTextureImage };
70
+ export type { Color };
@@ -0,0 +1,137 @@
1
+ import { hex2rgb, hsv2rgb, rgb2hex, rgb2hsv } from "./utils";
2
+ const spd500_colormap_data = { "levels": [20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140], "colors": [ "#e6f4ff", "#dbf0fe", "#d1ebfe", "#c6e7fd", "#bce3fd", "#b1dffc", "#a7dbfc", "#9cd6fb", "#92d2fb", "#87cefa", "#84c2f6", "#81b7f1", "#7eabed", "#7ba0e8", "#7994e4", "#7688df", "#737ddb", "#7071d6", "#6d66d2", "#6a5acd", "#7660cf", "#8366d0", "#8f6cd2", "#9c72d3", "#a878d5", "#b47ed6", "#c184d8", "#cd8ad9", "#da90db", "#e696dc", "#e390d9", "#e08ad6", "#dd84d3", "#da7ed0", "#d778cd", "#d472ca", "#d16cc7", "#ce66c4", "#cb60c1", "#c85abe", "#c453ba", "#c04cb6", "#bc45b2", "#b83eae", "#b437aa", "#b030a6", "#ac29a2", "#a8229e", "#a41b9a", "#a01496", "#a41080", "#a80e75", "#ac0e75", "#b00c6a", "#b4085f", "#b80649", "#bc043e", "#c00233", "#c80028", "#c80028", "#ca042a", "#cc082c", "#d00c2c", "#d21432", "#d41834", "#d41834", "#d61c36", "#da243a", "#dc283c", "#de2c3e", "#e03040", "#e23442", "#e43844", "#e63c46", "#e84048", "#ea444a", "#ec484c", "#ee4c4e", "#f05050", "#f16052", "#f27054", "#f38056", "#f49058", "#f5a05a", "#f6b05c", "#f7c05e", "#f8d060", "#f9e062", "#faf064", "#f7eb61", "#f4e65e", "#f1e15b", "#eedc58", "#ebd755", "#e8d252", "#e5cd4f", "#e2c84c", "#dfc349", "#dcbe46", "#d9b943", "#d6b440", "#d3af3d", "#d0aa3a", "#cda537", "#caa034", "#c79b31", "#c4962e", "#c1912b", "#be8c28", "#bb8725", "#b88222", "#b57d1f", "#b2781c", "#af7319", "#ac6e16", "#a96913", "#a66410", "#a35f0d", "#a05a0a", "#a05a0a" ] }
3
+ const spd850_colormap_data = { "levels": [20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80], "colors": [ "#f0f8ff", "#dbf0fe", "#c6e7fd", "#b1dffc", "#9cd6fb", "#87cefa", "#81b7f1", "#7ba0e8", "#7688df", "#7071d6", "#6a5acd", "#8366d0", "#9c72d3", "#b47ed6", "#cd8ad9", "#e696dc", "#e08ad6", "#da7ed0", "#d472ca", "#ce66c4", "#c85abe", "#c04cb6", "#b83eae", "#b030a6", "#a8229e", "#a01496", "#a81080", "#b00c6a", "#b8043e", "#c80028", "#c80028", "#d01030", "#d41834", "#d82038", "#dc283c", "#e03040", "#e43844", "#e84048", "#ec484c", "#f05050", "#f27054", "#f49058", "#f6b05c", "#f8d060", "#faf064", "#f4e65e", "#eedc58", "#e8d252", "#e2c84c", "#dcbe46", "#d6b440", "#d0aa3a", "#caa034", "#c4962e", "#be8c28", "#b88222", "#b2781c", "#ac6e16", "#a66410", "#a05a0a" ] }
4
+ const cape_colormap_data = { "levels": [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000, 2100, 2200, 2300, 2400, 2500, 2600, 2700, 2800, 2900, 3000, 3100, 3200, 3300, 3400, 3500, 3600, 3700, 3800, 3900, 4000, 4100, 4200, 4300, 4400, 4500, 4600, 4700, 4800, 4900, 5000, 5100, 5200, 5300, 5400, 5500, 5600, 5700, 5800, 5900, 6000, 6500, 7000, 7500, 8000, 8500, 9000, 9500, 10000], "colors": [ "#ffffff", "#f0f0f0", "#e1e1e1", "#d2d2d2", "#c3c3c3", "#a5a5a5", "#969696", "#878787", "#787878", "#696969", "#37536a", "#436075", "#506d80", "#5c7a8b", "#698796", "#7594a2", "#82a1ad", "#8eaeb8", "#9bbbc3", "#a7c8ce", "#e9dd96", "#e8d186", "#e7c575", "#e6b865", "#e5ac54", "#e5a044", "#e49433", "#e38723", "#e27b12", "#e16f02", "#dc4110", "#d33b17", "#ca351e", "#c12e25", "#b8282c", "#af2234", "#a61c3b", "#9d1542", "#940f49", "#8b0950", "#73088a", "#7e1894", "#8a289f", "#9538a9", "#a148b3", "#ac59be", "#b869c8", "#c379d2", "#cf89dd", "#da99e7", "#e9bec3", "#e3b0b7", "#dda3ac", "#d795a0", "#d18894", "#ca7a89", "#c46d7d", "#be5f71", "#b85266", "#b2445a", "#893d48", "#8f4752", "#96525b", "#9c5c65", "#a2676f", "#a97178", "#af7c82", "#b6868b" ] }
5
+ const t2m_colormap_data = { "levels": [-60, -59, -58, -57, -56, -55, -54, -53, -52, -51, -50, -49, -48, -47, -46, -45, -44, -43, -42, -41, -40, -39, -38, -37, -36, -35, -34, -33, -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120], "colors": [ "#235877", "#2a5f7c", "#316782", "#396e87", "#40768c", "#477d91", "#4e8497", "#558c9c", "#5d93a1", "#649ba6", "#6ba2ac", "#72a9b1", "#79b1b6", "#81b8bb", "#88c0c1", "#8fc7c6", "#96cecb", "#9dd6d0", "#a5ddd6", "#ace5db", "#b3ece0", "#b3ece0", "#b1e7df", "#b0e1dd", "#aedcdc", "#add7da", "#abd2d9", "#aaccd8", "#a8c7d6", "#a7c2d5", "#a5bcd4", "#a4b7d2", "#a2b2d1", "#9fa7ce", "#9ea2cd", "#9c9dcb", "#9c9dcb", "#9b97ca", "#9992c8", "#988dc7", "#9688c6", "#9582c4", "#9278c2", "#9073c0", "#8f6dbf", "#8d68bd", "#8c63bc", "#8a5dbb", "#8958b9", "#8753b8", "#864eb6", "#8448b5", "#8343b4", "#813eb2", "#8038b1", "#7e33b0", "#7d33ae", "#7b29ad", "#7a23ab", "#781eaa", "#a037af", "#a443b3", "#a74fb7", "#ab5cbb", "#af68bf", "#b374c3", "#b680c7", "#ba8dcc", "#be99d0", "#c1a5d4", "#c5b1d8", "#c9bddc", "#cdcae0", "#d0d6e4", "#d4e2e8", "#deecf2", "#d1e2ee", "#c5d9ea", "#b8cfe6", "#acc5e3", "#9fbbdf", "#92b2db", "#86a8d7", "#799ed3", "#6c94cf", "#608bcb", "#5381c7", "#4777c4", "#3a6dc0", "#2d64bc", "#215ab8", "#1450b4", "#0f4455", "#1c4e5a", "#2a585f", "#376363", "#456d68", "#52776d", "#5f8172", "#6d8c77", "#7a967c", "#88a080", "#95aa85", "#a3b58a", "#b0bf8f", "#bdc994", "#cbd399", "#d8de9d", "#e6e8a2", "#f3f2a7", "#f8eea2", "#f0e199", "#e8d591", "#e1c888", "#d9bc80", "#d1af77", "#c9a36f", "#c19666", "#ba8a5e", "#b27d55", "#aa714d", "#a26444", "#9b583c", "#8b3f2b", "#833222", "#7b261a", "#7b261a", "#741911", "#6c0d09", "#640000", "#5f0000", "#630507", "#670a0e", "#6c0f15", "#70141c", "#741824", "#781d2b", "#7d2232", "#812739", "#852c40", "#73372d", "#7a4036", "#80493f", "#875349", "#8e5c52", "#94655b", "#9b6e64", "#a88177", "#af8a80", "#b69389", "#bd9c92", "#c3a69c", "#caafa5", "#d1b8ae", "#d7c1b7", "#decac0", "#e5d4ca", "#ebddd3", "#f2e6dc", "#e8dfd6", "#e0d7cf", "#d8d0c8", "#d0c8c0", "#c8c0b9", "#c0b9b2", "#b7b1ab", "#afa9a4", "#a7a29c", "#9f9a95", "#97938e", "#8f8b87", "#878380", "#7f7c78", "#777471", "#6f6c6a", "#666563", "#5e5d5c", "#565554", "#4e4e4d", "#464646" ] }
6
+ const td2m_colormap_data = { "levels": [-40, -39, -38, -37, -36, -35, -34, -33, -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90], "colors": [ "#986d4d", "#966c4c", "#946b4c", "#926a4b", "#90694b", "#8e684a", "#8c664a", "#8a6549", "#886448", "#866348", "#846247", "#826147", "#806046", "#7e5f46", "#7c5e45", "#7a5d44", "#785b44", "#765a43", "#745943", "#725842", "#715742", "#6f5641", "#6d5540", "#6b5440", "#69533f", "#67523f", "#65503e", "#634f3d", "#614e3d", "#5f4d3c", "#5d4c3c", "#5b4b3b", "#594a3b", "#57493a", "#554839", "#534739", "#514538", "#4f4438", "#4d4337", "#4b4237", "#494136", "#4d4334", "#514738", "#564c3c", "#5a5041", "#5e5545", "#625949", "#675e4d", "#6b6251", "#6f6755", "#746b5a", "#78705e", "#7c7462", "#807966", "#857d6a", "#89826f", "#8d8673", "#928b77", "#968f7b", "#9a947f", "#9e9883", "#a39d88", "#a7a18c", "#aba690", "#afaa94", "#b8b39c", "#b8b39c", "#bcb8a1", "#c1bca5", "#c9c5ad", "#c9cab1", "#d2ceb6", "#d2ceb6", "#d6d3ba", "#dfdcc2", "#e3e0c6", "#e7e5ca", "#ebe9cf", "#f0eed3", "#f4f2d7", "#e6f5e6", "#d7f0d7", "#c8eac8", "#b9e5b9", "#aadfaa", "#9bda9b", "#8cd48c", "#7dcf7d", "#6ec96e", "#5fc45f", "#30ae30", "#2ca32c", "#279927", "#238e23", "#1e831e", "#1a791a", "#156e15", "#116311", "#0c590c", "#084e08", "#61a3af", "#5896a0", "#508992", "#477b83", "#3e6e74", "#366166", "#2d5457", "#244648", "#1c393a", "#132c2b", "#66669a", "#605e94", "#59568e", "#534e88", "#4d4682", "#463e7c", "#403676", "#3a2e70", "#33266a", "#2d1e64", "#724071", "#784573", "#7d4b75", "#835076", "#885678", "#8e5b7a", "#93617c", "#99667d", "#9e6c7f", "#a47181" ] }
7
+ function isColor(obj) {
8
+ return (typeof obj == 'object') && 'color' in obj && 'opacity' in obj;
9
+ }
10
+ /** A mapping from values to colors */
11
+ class ColorMap {
12
+ /**
13
+ * Create a color map
14
+ * @param levels - The list of levels. The number of levels should always be one more than the number of colors.
15
+ * @param colors - A list of colors
16
+ */
17
+ constructor(levels, colors) {
18
+ if (levels.length != colors.length + 1) {
19
+ throw `Mismatch between number of levels (${levels.length}) and number of colors (${colors.length}; expected ${levels.length - 1})`;
20
+ }
21
+ this.levels = levels;
22
+ this.colors = colors.map(c => isColor(c) ? c : { 'color': c, 'opacity': 1. });
23
+ }
24
+ /**
25
+ * @returns an array of hex color strings
26
+ */
27
+ getColors() {
28
+ return this.colors.map(s => s['color']);
29
+ }
30
+ /**
31
+ * @returns an array of opacities, one for each color in the color map
32
+ */
33
+ getOpacities() {
34
+ return this.colors.map(s => s['opacity']);
35
+ }
36
+ /**
37
+ * Make a new color map with different opacities. The opacities are set by func.
38
+ * @param func - A function which takes the two levels associated with a color (an upper and lower bound) and returns an opacity in the range from 0 to 1.
39
+ * @returns A new color map
40
+ */
41
+ withOpacity(func) {
42
+ const new_colors = this.colors.map((c, ic) => { return { 'color': c['color'], 'opacity': func(this.levels[ic], this.levels[ic + 1]) }; });
43
+ return new ColorMap(this.levels, new_colors);
44
+ }
45
+ /**
46
+ * Create a diverging color map using two input colors
47
+ * @param color1 - The color corresponding to the lowest value in the color map
48
+ * @param color2 - The color corresponding to the highest value in the color map
49
+ * @param level_min - The lowest value in the color map
50
+ * @param level_max - The highest value in the color map
51
+ * @param n_colors - The number of colors to use
52
+ * @returns a Colormap object
53
+ */
54
+ static diverging(color1, color2, level_min, level_max, n_colors) {
55
+ const stops = [];
56
+ const levels = [];
57
+ const level_step = (level_max - level_min) / (n_colors - 1);
58
+ const crossover = (level_max + level_min) / 2;
59
+ const crossover_hsv = [0, 0, 0.9];
60
+ const color1_hsv = rgb2hsv(hex2rgb(color1));
61
+ const color2_hsv = rgb2hsv(hex2rgb(color2));
62
+ for (let istop = 0; istop < n_colors; istop++) {
63
+ const level = level_min + istop * level_step;
64
+ let h, s, v;
65
+ let interp_fac;
66
+ if (level < crossover) {
67
+ interp_fac = (crossover - level) / (crossover - level_min);
68
+ [h, s, v] = [
69
+ color1_hsv[0],
70
+ crossover_hsv[1] + (color1_hsv[1] - crossover_hsv[1]) * interp_fac,
71
+ crossover_hsv[2] + (color1_hsv[2] - crossover_hsv[2]) * interp_fac
72
+ ];
73
+ }
74
+ else if (level >= crossover) {
75
+ interp_fac = (level - crossover) / (level_max - crossover);
76
+ [h, s, v] = [
77
+ color2_hsv[0],
78
+ crossover_hsv[1] + (color2_hsv[1] - crossover_hsv[1]) * interp_fac,
79
+ crossover_hsv[2] + (color2_hsv[2] - crossover_hsv[2]) * interp_fac
80
+ ];
81
+ }
82
+ const color = rgb2hex(hsv2rgb([h, s, v]));
83
+ stops.push({ 'color': color, 'opacity': Math.min(2 * interp_fac, 1) });
84
+ }
85
+ for (let ilev = 0; ilev <= n_colors; ilev++) {
86
+ const level_step = (level_max - level_min) / n_colors;
87
+ levels.push(level_min + ilev * level_step);
88
+ }
89
+ return new ColorMap(levels, stops);
90
+ }
91
+ }
92
+ // Some built-in colormaps
93
+ const pw_speed500mb = new ColorMap(spd500_colormap_data.levels, spd500_colormap_data.colors).withOpacity((levl, levu) => Math.min((levu - 20) / 10, 1.));
94
+ const pw_speed850mb = new ColorMap(spd850_colormap_data.levels, spd850_colormap_data.colors).withOpacity((levl, levu) => Math.min((levu - 20) / 10, 1.));
95
+ const pw_cape = new ColorMap(cape_colormap_data.levels, cape_colormap_data.colors).withOpacity((levl, levu) => Math.min(levu / 1000., 1.));
96
+ const pw_t2m = new ColorMap(t2m_colormap_data.levels, t2m_colormap_data.colors);
97
+ const pw_td2m = new ColorMap(td2m_colormap_data.levels, td2m_colormap_data.colors);
98
+ /**
99
+ * Create a diverging red/blue colormap, where red corresponds to the lowest value and blue corresponds to the highest value
100
+ * @param level_min - The lowest value in the color map
101
+ * @param level_max - The highest value in the color map
102
+ * @param n_colors - The number of colors
103
+ * @returns a Colormap object
104
+ */
105
+ const redblue = (level_min, level_max, n_colors) => {
106
+ return ColorMap.diverging('#ff0000', '#0000ff', level_min, level_max, n_colors);
107
+ };
108
+ /**
109
+ * Create a diverging blue/red colormap, where blue corresponds to the lowest value and red corresponds to the highest value
110
+ * @param level_min - The lowest value in the color map
111
+ * @param level_max - The highest value in the color map
112
+ * @param n_colors - The number of colors
113
+ * @returns a Colormap object
114
+ */
115
+ const bluered = (level_min, level_max, n_colors) => {
116
+ return ColorMap.diverging('#0000ff', '#ff0000', level_min, level_max, n_colors);
117
+ };
118
+ /**
119
+ * Make a canvas image corresponding to a color map
120
+ * @param colormap - The color map to use
121
+ * @returns A canvas element containing each color of the color map
122
+ */
123
+ function makeTextureImage(colormap) {
124
+ const cmap_image = document.createElement('canvas');
125
+ cmap_image.width = colormap.colors.length;
126
+ cmap_image.height = 1;
127
+ let ctx = cmap_image.getContext('2d');
128
+ colormap.colors.forEach((stop, istop) => {
129
+ if (ctx === null) {
130
+ throw "Could not get rendering context for colormap image canvas";
131
+ }
132
+ ctx.fillStyle = stop['color'] + Math.round(stop['opacity'] * 255).toString(16);
133
+ ctx.fillRect(istop, 0, 1, 1);
134
+ });
135
+ return cmap_image;
136
+ }
137
+ export { ColorMap, bluered, redblue, pw_speed500mb, pw_speed850mb, pw_cape, pw_t2m, pw_td2m, makeTextureImage };
@@ -0,0 +1,71 @@
1
+ import { WebGLAnyRenderingContext } from './AutumnTypes';
2
+ import { MapType } from './Map';
3
+ import { PlotComponent } from './PlotComponent';
4
+ import { RawScalarField } from './RawField';
5
+ import { WGLBuffer, WGLProgram, WGLTexture } from 'autumn-wgl';
6
+ interface ContourOptions {
7
+ /**
8
+ * The color of the contours as a hex color string
9
+ * @default '#000000'
10
+ */
11
+ color?: string;
12
+ /**
13
+ * The contour interval for drawing contours at regular intervals
14
+ * @default 1
15
+ */
16
+ interval?: number;
17
+ /**
18
+ * A list of arbitrary levels (up to 40) to contour. This overrides the `interval` option.
19
+ * @default Draw contours at regular intervals given by the `interval` option.
20
+ */
21
+ levels?: number[];
22
+ /**
23
+ * A function to thin the contours based on zoom level. The function should take a zoom level and return a number `n` that means to only show every
24
+ * `n`th contour.
25
+ * @default Don't thin the contours on any zoom level
26
+ */
27
+ thinner?: (zoom: number) => number;
28
+ }
29
+ interface ContourGLElems {
30
+ map: MapType;
31
+ program: WGLProgram;
32
+ vertices: WGLBuffer;
33
+ grid_cell_size: WGLBuffer;
34
+ fill_texture: WGLTexture;
35
+ texcoords: WGLBuffer;
36
+ }
37
+ /**
38
+ * A field of contoured data. The contours can optionally be thinned based on map zoom level.
39
+ * @example
40
+ * // Create a contoured height field, with black contours every 30 m (assuming the height field is in
41
+ * // meters) and only using every other contour when the map zoom level is less than 5.
42
+ * const contours = new Contour(height_field, {color: '#000000', interval: 30,
43
+ * thinner: zoom => zoom < 5 ? 2 : 1});
44
+ */
45
+ declare class Contour extends PlotComponent {
46
+ readonly field: RawScalarField;
47
+ readonly color: [number, number, number];
48
+ readonly interval: number;
49
+ readonly levels: number[];
50
+ readonly thinner: (zoom: number) => number;
51
+ /** @private */
52
+ gl_elems: ContourGLElems | null;
53
+ /**
54
+ * Create a contoured field
55
+ * @param field - The field to contour
56
+ * @param opts - Options for creating the contours
57
+ */
58
+ constructor(field: RawScalarField, opts: ContourOptions);
59
+ /**
60
+ * @internal
61
+ * Add the contours to a map
62
+ */
63
+ onAdd(map: MapType, gl: WebGLAnyRenderingContext): Promise<void>;
64
+ /**
65
+ * @internal
66
+ * Render the contours
67
+ */
68
+ render(gl: WebGLAnyRenderingContext, matrix: number[]): void;
69
+ }
70
+ export default Contour;
71
+ export type { ContourOptions };
package/lib/Contour.js ADDED
@@ -0,0 +1,172 @@
1
+ import { isWebGL2Ctx } from './AutumnTypes';
2
+ import { PlotComponent } from './PlotComponent';
3
+ import { hex2rgba } from './utils';
4
+ import { WGLProgram, WGLTexture } from 'autumn-wgl';
5
+ const contour_vertex_shader_src = `uniform mat4 u_matrix;
6
+
7
+ attribute vec2 a_pos;
8
+ attribute float a_grid_cell_size;
9
+ attribute vec2 a_tex_coord;
10
+
11
+ varying highp vec2 v_tex_coord;
12
+ varying highp float v_grid_cell_size;
13
+ varying highp float v_map_scale_fac;
14
+
15
+ void main() {
16
+ gl_Position = u_matrix * vec4(a_pos, 0.0, 1.0);
17
+ v_tex_coord = a_tex_coord;
18
+ v_grid_cell_size = a_grid_cell_size;
19
+
20
+
21
+ highp float lat = 2. * atan(exp(a_pos.y / 6371229.0)) - 3.1414592654 / 2.;
22
+ v_map_scale_fac = cos(lat);
23
+ }`
24
+ const contour_fragment_shader_src = `#extension GL_OES_standard_derivatives : enable
25
+ #define MAX_N_CONTOURS 40
26
+
27
+ varying highp vec2 v_tex_coord;
28
+ varying highp float v_grid_cell_size;
29
+ varying highp float v_map_scale_fac;
30
+
31
+ uniform sampler2D u_fill_sampler;
32
+ uniform highp float u_contour_interval;
33
+ uniform highp float u_contour_levels[MAX_N_CONTOURS];
34
+ uniform int u_num_contours;
35
+ uniform lowp float u_line_cutoff;
36
+ uniform lowp vec3 u_color;
37
+ uniform lowp vec2 u_step_size;
38
+ uniform lowp float u_zoom_fac;
39
+
40
+ void main() {
41
+ highp float field_val = texture2D(u_fill_sampler, v_tex_coord).r;
42
+
43
+
44
+ lowp vec2 ihat = vec2(u_step_size.x, 0.0);
45
+ lowp vec2 jhat = vec2(0.0, u_step_size.y);
46
+ highp float fv_xp1 = texture2D(u_fill_sampler, v_tex_coord + ihat).r;
47
+ highp float fv_xm1 = texture2D(u_fill_sampler, v_tex_coord - ihat).r;
48
+ highp float fv_yp1 = texture2D(u_fill_sampler, v_tex_coord + jhat).r;
49
+ highp float fv_ym1 = texture2D(u_fill_sampler, v_tex_coord - jhat).r;
50
+ highp float fwidth_field = sqrt(((fv_xp1 - fv_xm1) * (fv_xp1 - fv_xm1) + (fv_yp1 - fv_ym1) * (fv_yp1 - fv_ym1)) / (5e5 * v_grid_cell_size));
51
+
52
+
53
+ lowp float plot_val;
54
+
55
+ if (u_num_contours > 0) {
56
+ highp float min_contour_diff = u_contour_levels[1] - u_contour_levels[0];
57
+ bool assigned_contour = false;
58
+ highp float low_contour;
59
+ highp float high_contour;
60
+ highp float highest_contour;
61
+
62
+ for (int ncnt = 0; ncnt < MAX_N_CONTOURS; ncnt++) {
63
+ if (ncnt >= u_num_contours) { break; }
64
+
65
+ min_contour_diff = min(min_contour_diff, u_contour_levels[ncnt + 1] - u_contour_levels[ncnt]);
66
+
67
+ if (u_contour_levels[ncnt] < field_val && field_val < u_contour_levels[ncnt + 1]) {
68
+ assigned_contour = true;
69
+
70
+ low_contour = u_contour_levels[ncnt];
71
+ high_contour = u_contour_levels[ncnt + 1];
72
+ }
73
+
74
+ highest_contour = u_contour_levels[ncnt];
75
+ }
76
+
77
+ if (!assigned_contour) {
78
+ if (field_val < u_contour_levels[0]) {
79
+ plot_val = (u_contour_levels[0] - field_val) / min_contour_diff;
80
+ }
81
+ else if (field_val > highest_contour) {
82
+ plot_val = (field_val - highest_contour) / min_contour_diff;
83
+ }
84
+ }
85
+ else {
86
+ plot_val = min(field_val - low_contour, high_contour - field_val) / min_contour_diff;
87
+ }
88
+ }
89
+ else {
90
+ plot_val = fract(field_val / u_contour_interval);
91
+ if (plot_val > 0.5) plot_val = 1.0 - plot_val;
92
+ }
93
+
94
+ plot_val = plot_val / (max(0.001, fwidth_field / (u_zoom_fac * 0.125)));
95
+
96
+ if (plot_val > u_line_cutoff) discard;
97
+
98
+ gl_FragColor = vec4(u_color, 1. - (plot_val * plot_val / (u_line_cutoff * u_line_cutoff)));
99
+ }`
100
+ /**
101
+ * A field of contoured data. The contours can optionally be thinned based on map zoom level.
102
+ * &example
103
+ * // Create a contoured height field, with black contours every 30 m (assuming the height field is in
104
+ * // meters) and only using every other contour when the map zoom level is less than 5.
105
+ * const contours = new Contour(height_field, {color: '#000000', interval: 30,
106
+ * thinner: zoom => zoom < 5 ? 2 : 1});
107
+ */
108
+ class Contour extends PlotComponent {
109
+ /**
110
+ * Create a contoured field
111
+ * &param field - The field to contour
112
+ * &param opts - Options for creating the contours
113
+ */
114
+ constructor(field, opts) {
115
+ super();
116
+ this.field = field;
117
+ this.interval = opts.interval || 1;
118
+ this.levels = opts.levels || [];
119
+ const color = hex2rgba(opts.color || '#000000');
120
+ this.color = [color[0], color[1], color[2]];
121
+ this.thinner = opts.thinner || (() => 1);
122
+ this.gl_elems = null;
123
+ }
124
+ /**
125
+ * &internal
126
+ * Add the contours to a map
127
+ */
128
+ async onAdd(map, gl) {
129
+ // Basic procedure for these contours from https://www.shadertoy.com/view/lltBWM
130
+ gl.getExtension('OES_texture_float');
131
+ gl.getExtension('OES_texture_float_linear');
132
+ gl.getExtension('OES_standard_derivatives');
133
+ const program = new WGLProgram(gl, contour_vertex_shader_src, contour_fragment_shader_src);
134
+ const { vertices: verts_buf, texcoords: tex_coords_buf, cellsize: cellsize_buf } = await this.field.grid.getWGLBuffers(gl);
135
+ const vertices = verts_buf;
136
+ const texcoords = tex_coords_buf;
137
+ const grid_cell_size = cellsize_buf;
138
+ const format = isWebGL2Ctx(gl) ? gl.R32F : gl.LUMINANCE;
139
+ const fill_image = { 'format': format, 'type': gl.FLOAT,
140
+ 'width': this.field.grid.ni, 'height': this.field.grid.nj, 'image': this.field.data,
141
+ 'mag_filter': gl.LINEAR,
142
+ };
143
+ const fill_texture = new WGLTexture(gl, fill_image);
144
+ this.gl_elems = {
145
+ map: map, program: program, vertices: vertices, texcoords: texcoords, grid_cell_size: grid_cell_size, fill_texture: fill_texture
146
+ };
147
+ }
148
+ /**
149
+ * &internal
150
+ * Render the contours
151
+ */
152
+ render(gl, matrix) {
153
+ if (this.gl_elems === null)
154
+ return;
155
+ const gl_elems = this.gl_elems;
156
+ const zoom = gl_elems.map.getZoom();
157
+ const intv = this.thinner(zoom) * this.interval;
158
+ const cutoff = 0.5 / intv;
159
+ const step_size = [0.25 / this.field.grid.ni, 0.25 / this.field.grid.nj];
160
+ const zoom_fac = Math.pow(2, zoom);
161
+ let uniforms = { 'u_contour_interval': intv, 'u_line_cutoff': cutoff, 'u_color': this.color, 'u_step_size': step_size, 'u_zoom_fac': zoom_fac,
162
+ 'u_matrix': matrix, 'u_num_contours': 0, 'u_contour_levels': [0] };
163
+ if (this.levels.length > 0) {
164
+ uniforms = { ...uniforms, 'u_num_contours': this.levels.length, 'u_contour_levels': this.levels };
165
+ }
166
+ gl_elems.program.use({ 'a_pos': gl_elems.vertices, 'a_grid_cell_size': gl_elems.grid_cell_size, 'a_tex_coord': gl_elems.texcoords }, uniforms, { 'u_fill_sampler': gl_elems.fill_texture });
167
+ gl.enable(gl.BLEND);
168
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
169
+ gl_elems.program.draw();
170
+ }
171
+ }
172
+ export default Contour;