@icij/murmur-next 4.0.0

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.
Files changed (296) hide show
  1. package/.github/workflows/deploy-github-pages.yaml +50 -0
  2. package/.storybook/app.scss +14 -0
  3. package/.storybook/doc_variables.scss +20 -0
  4. package/.storybook/main.ts +35 -0
  5. package/.storybook/preview-head.html +2 -0
  6. package/.storybook/preview.ts +32 -0
  7. package/README.md +71 -0
  8. package/deploy.js +15 -0
  9. package/docs/components/ApiTable.vue +171 -0
  10. package/docs/components/App.vue +146 -0
  11. package/docs/components/CollapsibleBlock.vue +122 -0
  12. package/docs/components/DocsHeader.vue +68 -0
  13. package/docs/components/DocsMenu.vue +201 -0
  14. package/docs/components/DocsMenuSection.vue +109 -0
  15. package/docs/components/EditLink.vue +49 -0
  16. package/docs/components/OutboundLink.vue +13 -0
  17. package/docs/components/PalettePresenter.vue +96 -0
  18. package/docs/components/RepositoryLink.vue +28 -0
  19. package/docs/components/SampleCard.vue +119 -0
  20. package/docs/main.js +42 -0
  21. package/docs/pages/components/accordion/doc.md +96 -0
  22. package/docs/pages/components/active-text-truncate/doc.md +44 -0
  23. package/docs/pages/components/advanced-link-form/doc.md +105 -0
  24. package/docs/pages/components/brand/doc.md +30 -0
  25. package/docs/pages/components/brand-expansion/doc.md +70 -0
  26. package/docs/pages/components/confirm-button/doc.md +91 -0
  27. package/docs/pages/components/content-placeholder/doc.md +16 -0
  28. package/docs/pages/components/custom-pagination/doc.md +61 -0
  29. package/docs/pages/components/digits-input/doc.md +28 -0
  30. package/docs/pages/components/donate-form/doc.md +20 -0
  31. package/docs/pages/components/embed-form/doc.md +22 -0
  32. package/docs/pages/components/embeddable-footer/doc.md +60 -0
  33. package/docs/pages/components/follow-us-popover/doc.md +5 -0
  34. package/docs/pages/components/generic-footer/doc.md +21 -0
  35. package/docs/pages/components/generic-header/doc.md +24 -0
  36. package/docs/pages/components/haptic-copy/doc.md +27 -0
  37. package/docs/pages/components/imddb-header/doc.md +23 -0
  38. package/docs/pages/components/ordinal-legend/doc.md +44 -0
  39. package/docs/pages/components/range-picker/doc.md +86 -0
  40. package/docs/pages/components/responsive-iframe/doc.md +13 -0
  41. package/docs/pages/components/scale-legend/doc.md +65 -0
  42. package/docs/pages/components/secret-input/doc.md +12 -0
  43. package/docs/pages/components/selectable-dropdown/doc.md +156 -0
  44. package/docs/pages/components/sharing-options/doc.md +13 -0
  45. package/docs/pages/components/sharing-options-link/doc.md +36 -0
  46. package/docs/pages/components/sign-up-form/doc.md +13 -0
  47. package/docs/pages/components/slide-up-down/doc.md +28 -0
  48. package/docs/pages/components/textured-deck/doc.md +78 -0
  49. package/docs/pages/components/tiny-pagination/doc.md +92 -0
  50. package/docs/pages/datavisualisation/bars/doc.md +110 -0
  51. package/docs/pages/datavisualisation/columns/doc.md +165 -0
  52. package/docs/pages/datavisualisation/lines/doc.md +139 -0
  53. package/docs/pages/datavisualisation/stacked-bar/doc.md +160 -0
  54. package/docs/pages/datavisualisation/stacked-column/doc.md +191 -0
  55. package/docs/pages/getting-started/about-icij/doc.md +13 -0
  56. package/docs/pages/getting-started/custom-bootstrap/doc.md +36 -0
  57. package/docs/pages/getting-started/installation-guide/doc.md +59 -0
  58. package/docs/pages/getting-started/internationalization/doc.md +74 -0
  59. package/docs/pages/maps/choropleth-map/doc.md +420 -0
  60. package/docs/pages/maps/choropleth-map-annotation/doc.md +373 -0
  61. package/docs/pages/maps/symbol-map/doc.md +203 -0
  62. package/docs/pages/structure/breakpoints/doc.md +3 -0
  63. package/docs/pages/structure/grid/doc.md +3 -0
  64. package/docs/pages/utilities/assets/doc.md +138 -0
  65. package/docs/pages/utilities/config/doc.md +52 -0
  66. package/docs/pages/utilities/iframes/doc.md +3 -0
  67. package/docs/pages/visual/colors/doc.md +31 -0
  68. package/docs/pages/visual/iconography/doc.md +56 -0
  69. package/docs/pages/visual/states/doc.md +77 -0
  70. package/docs/pages/visual/themes/doc.md +3 -0
  71. package/docs/pages/visual/typography/doc.md +71 -0
  72. package/docs/routes.js +25 -0
  73. package/docs/store/index.js +21 -0
  74. package/docs/styles/app.scss +36 -0
  75. package/docs/styles/variables.scss +20 -0
  76. package/lib/assets/images/icij-full-white.svg +6 -0
  77. package/lib/assets/images/icij-full.svg +6 -0
  78. package/lib/assets/images/icij.png +0 -0
  79. package/lib/assets/images/icij.svg +46 -0
  80. package/lib/assets/images/icij@2x.png +0 -0
  81. package/lib/assets/images/murmur-dark.png +0 -0
  82. package/lib/assets/images/murmur-dark.svg +79 -0
  83. package/lib/assets/images/murmur-white.png +0 -0
  84. package/lib/assets/images/murmur-white.svg +68 -0
  85. package/lib/components/AccordionStep.vue +128 -0
  86. package/lib/components/AccordionWrapper.vue +138 -0
  87. package/lib/components/ActiveTextTruncate.vue +258 -0
  88. package/lib/components/AdvancedLinkForm.vue +273 -0
  89. package/lib/components/Brand.vue +150 -0
  90. package/lib/components/BrandExpansion.vue +237 -0
  91. package/lib/components/ConfirmButton.vue +204 -0
  92. package/lib/components/ContentPlaceholder.vue +100 -0
  93. package/lib/components/CustomPagination.vue +225 -0
  94. package/lib/components/DigitsInput.vue +180 -0
  95. package/lib/components/DonateForm.vue +367 -0
  96. package/lib/components/EmbedForm.vue +173 -0
  97. package/lib/components/EmbeddableFooter.vue +201 -0
  98. package/lib/components/Fa.js +3 -0
  99. package/lib/components/FollowUsPopover.vue +117 -0
  100. package/lib/components/GenericFooter.vue +218 -0
  101. package/lib/components/GenericHeader.vue +259 -0
  102. package/lib/components/HapticCopy.vue +256 -0
  103. package/lib/components/ImddbHeader.vue +336 -0
  104. package/lib/components/OrdinalLegend.vue +164 -0
  105. package/lib/components/RangePicker.vue +430 -0
  106. package/lib/components/ResponsiveIframe.vue +48 -0
  107. package/lib/components/ScaleLegend.vue +230 -0
  108. package/lib/components/SecretInput.vue +132 -0
  109. package/lib/components/SelectableDropdown.vue +368 -0
  110. package/lib/components/SharingOptions.vue +230 -0
  111. package/lib/components/SharingOptionsLink.vue +259 -0
  112. package/lib/components/SignUpForm.vue +181 -0
  113. package/lib/components/SlideUpDown.vue +131 -0
  114. package/lib/components/TexturedDeck.vue +101 -0
  115. package/lib/components/TinyPagination.vue +268 -0
  116. package/lib/components/index.js +31 -0
  117. package/lib/composables/chart.ts +182 -0
  118. package/lib/composables/resizeObserver.ts +37 -0
  119. package/lib/composables/sendEmail.ts +50 -0
  120. package/lib/config.default.ts +33 -0
  121. package/lib/config.ts +70 -0
  122. package/lib/d3-geo-projection.d.ts +1 -0
  123. package/lib/datavisualisations/BarChart.vue +275 -0
  124. package/lib/datavisualisations/ColumnChart.vue +527 -0
  125. package/lib/datavisualisations/LineChart.vue +274 -0
  126. package/lib/datavisualisations/StackedBarChart.vue +614 -0
  127. package/lib/datavisualisations/StackedColumnChart.vue +640 -0
  128. package/lib/datavisualisations/index.js +5 -0
  129. package/lib/enums.ts +25 -0
  130. package/lib/i18n.ts +16 -0
  131. package/lib/keys.ts +2 -0
  132. package/lib/locales/en.json +140 -0
  133. package/lib/locales/fr.json +117 -0
  134. package/lib/locales/locales/en.json +140 -0
  135. package/lib/locales/locales/fr.json +117 -0
  136. package/lib/main.ts +87 -0
  137. package/lib/maps/ChoroplethMap.vue +825 -0
  138. package/lib/maps/ChoroplethMapAnnotation.vue +336 -0
  139. package/lib/maps/SymbolMap.vue +628 -0
  140. package/lib/maps/index.js +3 -0
  141. package/lib/querystring-es3.d.ts +1 -0
  142. package/lib/shims-bootstrap-vue.d.ts +5 -0
  143. package/lib/shims-tsx.d.ts +11 -0
  144. package/lib/shims-vue.d.ts +14 -0
  145. package/lib/styles/functions.scss +20 -0
  146. package/lib/styles/lib.scss +19 -0
  147. package/lib/styles/mixins.scss +37 -0
  148. package/lib/styles/utilities.scss +18 -0
  149. package/lib/styles/variables.scss +94 -0
  150. package/lib/styles/variables_dark.scss +1 -0
  151. package/lib/types.ts +46 -0
  152. package/lib/utils/animation.ts +24 -0
  153. package/lib/utils/assets.ts +46 -0
  154. package/lib/utils/clipboard.ts +41 -0
  155. package/lib/utils/iframe-resizer.ts +49 -0
  156. package/lib/utils/placeholder.ts +66 -0
  157. package/lib/utils/placeholderTypes.ts +21 -0
  158. package/lib/utils/strings.ts +8 -0
  159. package/loaders/highlight-loader.js +13 -0
  160. package/loaders/markdown-loader.js +91 -0
  161. package/loaders/metadata-loader.js +18 -0
  162. package/loaders/sass-extract-loader.js +14 -0
  163. package/loaders/vue-docgen-loader.js +14 -0
  164. package/package.json +96 -0
  165. package/plugins/MdPluginTypes.ts +10 -0
  166. package/plugins/docs.ts +50 -0
  167. package/plugins/front-matter.ts +36 -0
  168. package/plugins/highlight.ts +27 -0
  169. package/plugins/markdown-it/api-table.ts +25 -0
  170. package/plugins/markdown-it/sample-card.ts +31 -0
  171. package/plugins/plugin-delete.ts +47 -0
  172. package/plugins/plugin-docgen.ts +23 -0
  173. package/plugins/sass-vars.ts +25 -0
  174. package/plugins/vue-docgen.ts +29 -0
  175. package/public/android-chrome-192x192.png +0 -0
  176. package/public/android-chrome-512x512.png +0 -0
  177. package/public/apple-touch-icon.png +0 -0
  178. package/public/assets/img/arrow-bottom.svg +3 -0
  179. package/public/assets/img/texture-brick-black.jpg +0 -0
  180. package/public/assets/img/texture-brick.jpg +0 -0
  181. package/public/assets/img/texture-carbon-black.jpg +0 -0
  182. package/public/assets/img/texture-carbon.jpg +0 -0
  183. package/public/assets/img/texture-crack-black.jpg +0 -0
  184. package/public/assets/img/texture-crack.jpg +0 -0
  185. package/public/assets/img/texture-rock-black.jpg +0 -0
  186. package/public/assets/img/texture-rock.jpg +0 -0
  187. package/public/assets/img/texture-sand-black.jpg +0 -0
  188. package/public/assets/img/texture-sand.jpg +0 -0
  189. package/public/assets/img/texture-silk-black.jpg +0 -0
  190. package/public/assets/img/texture-silk.jpg +0 -0
  191. package/public/assets/topojson/france-departments.json +1 -0
  192. package/public/assets/topojson/paris-arrondissements.json +1 -0
  193. package/public/assets/topojson/world-countries-sans-antarctica.json +1 -0
  194. package/public/favicon-16x16.png +0 -0
  195. package/public/favicon-32x32.png +0 -0
  196. package/public/favicon.ico +0 -0
  197. package/public/site.webmanifest +1 -0
  198. package/stories/assets/code-brackets.svg +1 -0
  199. package/stories/assets/colors.svg +1 -0
  200. package/stories/assets/comments.svg +1 -0
  201. package/stories/assets/direction.svg +1 -0
  202. package/stories/assets/flow.svg +1 -0
  203. package/stories/assets/plugin.svg +1 -0
  204. package/stories/assets/repo.svg +1 -0
  205. package/stories/assets/stackalt.svg +1 -0
  206. package/stories/getting-started/about-icij.mdx +14 -0
  207. package/stories/getting-started/custom-bootstrap.mdx +23 -0
  208. package/stories/getting-started/installation-guide.mdx +62 -0
  209. package/stories/getting-started/internationalization.mdx +63 -0
  210. package/stories/murmur/components/AccordionStep.stories.ts +33 -0
  211. package/stories/murmur/components/AccordionWrapper.stories.ts +69 -0
  212. package/stories/murmur/components/ActiveTextTruncate.stories.ts +32 -0
  213. package/stories/murmur/components/AdvancedLinkForm.stories.ts +77 -0
  214. package/stories/murmur/components/Brand.stories.ts +30 -0
  215. package/stories/murmur/components/BrandExpansion.stories.ts +41 -0
  216. package/stories/murmur/components/ConfirmButton.stories.ts +40 -0
  217. package/stories/murmur/components/ContentPlaceholder.stories.ts +41 -0
  218. package/stories/murmur/components/CustomPagination.stories.ts +42 -0
  219. package/stories/murmur/components/DigitsInput.stories.ts +29 -0
  220. package/stories/murmur/components/DonateForm.stories.ts +29 -0
  221. package/stories/murmur/components/EmbedForm.stories.ts +35 -0
  222. package/stories/murmur/components/EmbeddableFooter.stories.ts +59 -0
  223. package/stories/murmur/components/FollowUsPopover.stories.ts +24 -0
  224. package/stories/murmur/components/GenericFooter.stories.ts +27 -0
  225. package/stories/murmur/components/GenericHeader.stories.ts +27 -0
  226. package/stories/murmur/components/HapticCopy.stories.ts +40 -0
  227. package/stories/murmur/components/ImddbHeader.stories.ts +27 -0
  228. package/stories/murmur/components/OrdinalLegend.stories.ts +49 -0
  229. package/stories/murmur/components/RangePicker.stories.ts +98 -0
  230. package/stories/murmur/components/ResponsiveIframe.stories.ts +24 -0
  231. package/stories/murmur/components/ScaleLegend.stories.ts +65 -0
  232. package/stories/murmur/components/SecretInput.stories.ts +60 -0
  233. package/stories/murmur/components/SelectableDropdown.stories.ts +143 -0
  234. package/stories/murmur/components/SharingOptions.stories.ts +32 -0
  235. package/stories/murmur/components/SharingOptionsLink.stories.ts +53 -0
  236. package/stories/murmur/components/SignUpForm.stories.ts +51 -0
  237. package/stories/murmur/components/SlideUpDown.stories.ts +32 -0
  238. package/stories/murmur/components/TexturedDeck.stories.ts +83 -0
  239. package/stories/murmur/components/TinyPagination.stories.ts +65 -0
  240. package/stories/murmur/datavisualisations/BarChart.stories.ts +54 -0
  241. package/stories/murmur/datavisualisations/ColumnChart.stories.ts +88 -0
  242. package/stories/murmur/datavisualisations/LineChart.stories.ts +139 -0
  243. package/stories/murmur/datavisualisations/StackedBarChart.stories.ts +199 -0
  244. package/stories/murmur/datavisualisations/StackedColumnChart.stories.ts +136 -0
  245. package/stories/murmur/decorators.ts +108 -0
  246. package/stories/murmur/maps/ChoroplethMap.stories.ts +440 -0
  247. package/stories/murmur/maps/ChoroplethMapAnnotation.stories.ts +26 -0
  248. package/stories/murmur/maps/SymbolMap.stories.ts +24 -0
  249. package/stories/murmur/utils.ts +7 -0
  250. package/tests/unit/components/AccordionStep.spec.ts +157 -0
  251. package/tests/unit/components/AccordionWrapper.spec.ts +57 -0
  252. package/tests/unit/components/ActiveTextTruncate.spec.js +30 -0
  253. package/tests/unit/components/AdvancedLinkForm.spec.js +124 -0
  254. package/tests/unit/components/Brand.spec.js +50 -0
  255. package/tests/unit/components/ContentPlaceholder.spec.js +29 -0
  256. package/tests/unit/components/CustomPagination.spec.js +72 -0
  257. package/tests/unit/components/DigitsInput.spec.ts +157 -0
  258. package/tests/unit/components/DonateForm.spec.js +149 -0
  259. package/tests/unit/components/EmbedForm.spec.js +108 -0
  260. package/tests/unit/components/EmbeddableFooter.spec.js +11 -0
  261. package/tests/unit/components/Fa.spec.js +18 -0
  262. package/tests/unit/components/FollowUsPopover.spec.js +29 -0
  263. package/tests/unit/components/GenericFooter.spec.js +29 -0
  264. package/tests/unit/components/GenericHeader.spec.js +104 -0
  265. package/tests/unit/components/HapticCopy.spec.js +123 -0
  266. package/tests/unit/components/ImddbHeader.spec.js +96 -0
  267. package/tests/unit/components/OrdinalLegend.spec.js +120 -0
  268. package/tests/unit/components/RangePicker.spec.ts +87 -0
  269. package/tests/unit/components/ResponsiveIframe.spec.js +20 -0
  270. package/tests/unit/components/ScaleLegend.spec.js +139 -0
  271. package/tests/unit/components/SecretInput.spec.js +81 -0
  272. package/tests/unit/components/SelectableDropdown.spec.js +160 -0
  273. package/tests/unit/components/SharingOptions.spec.js +125 -0
  274. package/tests/unit/components/SharingOptionsLink.spec.js +184 -0
  275. package/tests/unit/components/SignUpForm.spec.js +145 -0
  276. package/tests/unit/components/SlideUpDown.spec.js +59 -0
  277. package/tests/unit/components/TinyPagination.spec.js +46 -0
  278. package/tests/unit/config.spec.js +136 -0
  279. package/tests/unit/datavisualisations/BarChart.spec.js +63 -0
  280. package/tests/unit/datavisualisations/ColumnChart.spec.js +344 -0
  281. package/tests/unit/datavisualisations/LineChart.spec.js +155 -0
  282. package/tests/unit/datavisualisations/StackedBarChart.spec.js +294 -0
  283. package/tests/unit/datavisualisations/StackedColumnChart.spec.js +443 -0
  284. package/tests/unit/i18n.spec.ts +19 -0
  285. package/tests/unit/main.spec.js +82 -0
  286. package/tests/unit/maps/ChoroplethMap.spec.js +214 -0
  287. package/tests/unit/maps/ChoroplethMapAnnotation.spec.ts +186 -0
  288. package/tests/unit/maps/SymbolMap.spec.js +92 -0
  289. package/tests/unit/require.spec.js +22 -0
  290. package/tests/unit/setup.js +13 -0
  291. package/tests/unit/utils/assets.spec.js +61 -0
  292. package/tests/unit/utils/clipboard.spec.js +18 -0
  293. package/tests/unit/utils/iframe-resizer.spec.js +71 -0
  294. package/tsconfig.json +35 -0
  295. package/vite.config.ts +79 -0
  296. package/vitest.config.ts +19 -0
@@ -0,0 +1,825 @@
1
+ <script lang="ts">
2
+ import {clamp, debounce, get, kebabCase, keys, max, min, pickBy, values} from 'lodash'
3
+
4
+ import * as d3 from 'd3'
5
+ import {geoRobinson} from 'd3-geo-projection'
6
+ import type {GeoProjection} from 'd3-geo'
7
+ import {geoGraticule} from 'd3-geo'
8
+ import {feature} from 'topojson'
9
+ import {GeometryCollection} from "topojson-specification";
10
+
11
+ import {
12
+ ComponentPublicInstance,
13
+ computed,
14
+ defineComponent,
15
+ PropType,
16
+ provide,
17
+ ref,
18
+ watch
19
+ } from 'vue'
20
+
21
+ import {ParentKey} from "@/keys";
22
+ import {MapTransform, ParentMap} from "@/types";
23
+ import config from '../config'
24
+ import {chartEmits, chartProps, getChartProps, useChart} from '@/composables/chart'
25
+ import ScaleLegend from '@/components/ScaleLegend.vue'
26
+
27
+ export default defineComponent({
28
+ name: 'ChoroplethMap',
29
+ components: {
30
+ ScaleLegend
31
+ },
32
+ props: {
33
+ /**
34
+ * Covers the empty values with a hatched pattern.
35
+ */
36
+ hatchEmpty: {
37
+ type: Boolean
38
+ },
39
+ /**
40
+ * Hide the legend of the map.
41
+ */
42
+ hideLegend: {
43
+ type: Boolean
44
+ },
45
+ /**
46
+ * Change the scale function used to get calculate a feature color.
47
+ */
48
+ featureColorScale: {
49
+ type: Function,
50
+ default: null
51
+ },
52
+ /**
53
+ * Change the color of the outline.
54
+ */
55
+ outlineColor: {
56
+ type: String,
57
+ default: 'currentColor'
58
+ },
59
+ /**
60
+ * Change the color of the graticule.
61
+ */
62
+ graticuleColor: {
63
+ type: String,
64
+ default: 'currentColor'
65
+ },
66
+ /**
67
+ * Maximum value to use in the color scale.
68
+ */
69
+ max: {
70
+ type: Number as PropType<number | null>,
71
+ default: null
72
+ },
73
+ /**
74
+ * Minimum value to use in the color scale.
75
+ */
76
+ min: {
77
+ type: Number as PropType<number | null>,
78
+ default: null
79
+ },
80
+ /**
81
+ * If true the map should be clickable (and zoom on a given feature).
82
+ */
83
+ clickable: {
84
+ type: Boolean
85
+ },
86
+ /**
87
+ * Field in the topojson containing all the feature objects.
88
+ */
89
+ topojsonObjects: {
90
+ type: String,
91
+ default: 'countries1'
92
+ },
93
+ /**
94
+ * Field in the topojson objects containing the id of a feature. This field supports dot notation for nested values.
95
+ */
96
+ topojsonObjectsPath: {
97
+ type: [String, Array] as PropType<string | string[]>,
98
+ default: 'id'
99
+ },
100
+ /**
101
+ * URL of the topojson.
102
+ */
103
+ topojsonUrl: {
104
+ type: String,
105
+ default: () => {
106
+ return config.get('map.topojson.world-countries-sans-antarctica')
107
+ }
108
+ },
109
+ /**
110
+ * Duration of the transitions.
111
+ */
112
+ transitionDuration: {
113
+ type: Number,
114
+ default: 750
115
+ },
116
+ /**
117
+ * If true the user will be able to navigate in the map with drag and mouse wheel.
118
+ */
119
+ zoomable: {
120
+ type: Boolean
121
+ },
122
+ /**
123
+ * Set to true if your projection is spherical.
124
+ */
125
+ spherical: {
126
+ type: Boolean
127
+ },
128
+ /**
129
+ * Minium zoom value.
130
+ */
131
+ zoomMin: {
132
+ type: Number,
133
+ default: 1
134
+ },
135
+ /**
136
+ * Maximum zoom value.
137
+ */
138
+ zoomMax: {
139
+ type: Number,
140
+ default: 8
141
+ },
142
+ /**
143
+ * Initial zoom value.
144
+ */
145
+ zoom: {
146
+ type: Number,
147
+ default: null
148
+ },
149
+ /**
150
+ * Initial center of the map.
151
+ */
152
+ center: {
153
+ type: Array as PropType<number[]>,
154
+ default: null
155
+ },
156
+ /**
157
+ * Projection object from d3 to draw the features.
158
+ * @see https://d3js.org/d3-geo/projection
159
+ */
160
+ projection: {
161
+ type: Function,
162
+ default: geoRobinson
163
+ },
164
+ /**
165
+ * If true the map will display an sphere outline arround the world.
166
+ */
167
+ outline: {
168
+ type: Boolean
169
+ },
170
+ /**
171
+ * If true the map will display a graticule grid (representing parallels and meridians).
172
+ */
173
+ graticule: {
174
+ type: Boolean
175
+ },
176
+ /**
177
+ * Maximum height used by the map.
178
+ */
179
+ height: {
180
+ type: String,
181
+ default: '300px'
182
+ },
183
+ /**
184
+ * Neutral color of the map's features.
185
+ */
186
+ color: {
187
+ type: String,
188
+ default: '#fff'
189
+ },
190
+ /**
191
+ * Neutral color of the map s features in social mode.
192
+ */
193
+ socialColor: {
194
+ type: String,
195
+ default: '#000'
196
+ },
197
+ ...chartProps()
198
+ },
199
+ emits: ["click", 'reset', 'zoomed', ...chartEmits],
200
+ setup(props, {emit}) {
201
+
202
+ const resizable = ref<ComponentPublicInstance<HTMLElement> | null>(null)
203
+ const topojson = ref<any>(null)
204
+ const topojsonPromise = ref<any | null>(null)
205
+ const mapRect = ref<DOMRect>(new DOMRect(0, 0, 0, 0))
206
+ const featureCursor = ref<{ [cursor: string]: string } | null>(null)
207
+ const featureZoom = ref<string | null>(null)
208
+ const isLoaded = ref<boolean>(false)
209
+ const mapTransform = ref<MapTransform>({k: 1, x: 0, y: 0, rotateX: 0, rotateY: 0})
210
+ const debouncedDraw = debounce(function () {
211
+ draw()
212
+ }, 10)
213
+
214
+ const {loadedData} = useChart(resizable, getChartProps(props), {emit}, isLoaded, debouncedDraw, afterLoaded)
215
+
216
+ async function afterLoaded() {
217
+ return new Promise<void>(async (resolve) => {
218
+ await loadTopojson()
219
+ draw()
220
+ resolve()
221
+ })
222
+ }
223
+
224
+ const sphericalCenter = computed((): [number, number] => {
225
+ const [lng = 0, lat = 0] = props.center ?? [0, 0]
226
+ return [-lng, -lat]
227
+ })
228
+
229
+ const planarCenter = computed((): [number, number] => {
230
+ const [lng = 0, lat = 0] = props.center ?? [0, 0]
231
+ return [lng, lat]
232
+ })
233
+ const featureColorScaleEnd = computed(() => {
234
+ const defaultColor = '#852308';
235
+ const node = map.value?.node();
236
+ if (isLoaded.value && node) {
237
+ const computedStyle = window.getComputedStyle(node)
238
+ return computedStyle.getPropertyValue('--primary') || defaultColor
239
+ }
240
+ return defaultColor
241
+ })
242
+ const featureColorScaleStart = computed(() => {
243
+ // `socialMode` is always different from null but accessing it will make
244
+ // this computed property reactive.
245
+ const defaultColor = '#fff';
246
+ const node = map.value?.node();
247
+ if (isLoaded.value && props.socialMode !== null && node) {
248
+ const computedStyle = window.getComputedStyle(node)
249
+ return computedStyle.getPropertyValue('color') || defaultColor
250
+ }
251
+ return defaultColor
252
+ })
253
+ const featureColor = computed(() => {
254
+ return (d: number) => {
255
+ const id = get(d, props.topojsonObjectsPath)
256
+ const hasIdProp = loadedData.value && id in loadedData.value;
257
+ return hasIdProp ? featureColorScaleFunction.value(loadedData.value[id]) : undefined
258
+ }
259
+ })
260
+ const featureColorScaleFunction = computed(() => {
261
+ if (props.featureColorScale !== null) {
262
+ return props.featureColorScale
263
+ }
264
+ return defaultFeatureColorScale.value
265
+ })
266
+
267
+ const graticuleLines = computed(() => {
268
+ return geoGraticule().step([20, 20])()
269
+ })
270
+
271
+ const defaultFeatureColorScale = computed(() => {
272
+ return d3
273
+ .scaleSequential()
274
+ .domain([Math.max(1, minValue.value), maxValue.value])
275
+ .range([featureColorScaleStart.value, featureColorScaleEnd.value])
276
+ })
277
+ const initialFeaturePath = computed(() => {
278
+ return featurePath.value.projection(initialMapProjection.value)
279
+ })
280
+ const initialGraticulePath = computed(() => {
281
+ return initialFeaturePath.value(graticuleLines.value)
282
+ })
283
+
284
+ const initialMapProjection = computed(() => {
285
+ if (props.spherical) {
286
+ return mapProjection.value.rotate(sphericalCenter.value)
287
+ .fitHeight(mapHeight.value, geojson.value)
288
+ .translate([mapWidth.value / 2, mapHeight.value / 2])
289
+ }
290
+ return mapProjection.value.center(planarCenter.value)
291
+ })
292
+ const featurePath = computed(() => {
293
+ return d3.geoPath().projection(mapProjection.value)
294
+ })
295
+ const hasCursor = computed(() => {
296
+ return !!featureCursor.value
297
+ })
298
+ const hasZoom = computed(() => {
299
+ return !!featureZoom.value
300
+ })
301
+
302
+
303
+ const geojson = computed(() => {
304
+ const object = get(topojson.value, ['objects', props.topojsonObjects], null)
305
+ return feature(topojson.value, object as GeometryCollection)
306
+ })
307
+
308
+ const mapClass = computed(() => {
309
+ return {
310
+ 'choropleth-map--has-cursor': hasCursor.value,
311
+ 'choropleth-map--has-zoom': hasZoom.value,
312
+ 'choropleth-map--hatch-empty': props.hatchEmpty
313
+ }
314
+ })
315
+ const mapProjection = computed(() => {
316
+ if (!props.projection) {
317
+ throw new Error("props.projection is " + props.projection)
318
+ }
319
+ return props.projection().fitSize([mapWidth.value, mapHeight.value], geojson.value) as GeoProjection
320
+ })
321
+ const rotatingMapProjection = computed(() => {
322
+ const {rotateX = null, rotateY = null} = mapTransform.value
323
+ let proj
324
+ let text
325
+ if (rotateX !== null && rotateY !== null) {
326
+ text="rotate"
327
+ proj= mapProjection.value.rotate([rotateX, rotateY]) ?? null
328
+ }else {
329
+ text="normal"
330
+ proj= mapProjection.value
331
+
332
+ }
333
+ return proj
334
+
335
+ })
336
+
337
+ const mapCenter = computed(() => {
338
+ return mapProjection.value.center()
339
+ })
340
+ const mapZoom = computed(() => {
341
+ return d3
342
+ .zoom()
343
+ .scaleExtent([props.zoomMin, props.zoomMax])
344
+ .translateExtent([
345
+ [0, 0],
346
+ [mapWidth.value, mapHeight.value]
347
+ ])
348
+ .on('zoom', mapZoomed)
349
+ })
350
+
351
+ const mapSphericalZoom = computed(() => {
352
+ return d3.zoom(map.value).scaleExtent([props.zoomMin, props.zoomMax]).on('zoom', mapSphericalZoomed)
353
+ })
354
+ const mapRotate = computed(() => {
355
+ return d3.drag(map.value).on('drag', mapRotated)
356
+ })
357
+ const mapHeight = computed(() => {
358
+ return mapRect.value.height
359
+ })
360
+
361
+ const mapWidth = computed(() => {
362
+ return mapRect.value.width
363
+ })
364
+
365
+ const mapStyle = computed(() => {
366
+ const {k = 0, x = 0, y = 0, rotateX = 0, rotateY = 0} = mapTransform.value
367
+ return {
368
+ '--map-height': props.height,
369
+ '--map-color': props.color,
370
+ '--map-social-color': props.socialColor,
371
+ '--map-scale': k,
372
+ '--map-translate-x': x,
373
+ '--map-translate-y': y,
374
+ '--map-rotate-x': rotateX,
375
+ '--map-rotate-y': rotateY
376
+ }
377
+ })
378
+
379
+ const map = computed((): d3.Selection<SVGElement, unknown, null, undefined> | null => {
380
+ const selection = d3.select(resizable.value).select<SVGElement>('svg')
381
+ if (!selection) {
382
+ throw new Error("Empty SVG selection")
383
+ }
384
+ return selection
385
+ })
386
+ const maxValue = computed(() => {
387
+ if (props.max !== null) {
388
+ return props.max
389
+ }
390
+ return max<number>(values(loadedData.value)) || 0
391
+ })
392
+ const minValue = computed((): number => {
393
+ if (props.min !== null) {
394
+ return props.min
395
+ }
396
+ return min(values(loadedData.value)) || 0
397
+ })
398
+ const transformOrigin = computed(() => {
399
+ return props.spherical ? '50% 50%' : '0 0'
400
+ })
401
+
402
+ function setMapNodeSize({width, height}) {
403
+ const node = map.value?.node();
404
+ if (node) {
405
+ node["width"] = width
406
+ node["height"] = height
407
+ }
408
+ }
409
+
410
+ const cursorValue = computed(() => {
411
+ return featureCursor.value?.data ?? null
412
+ })
413
+ const isReady = computed(() => {
414
+ return loadedData.value && isLoaded.value && topojson.value
415
+ })
416
+
417
+ function prepare() {
418
+ if (!map.value) {
419
+ throw new Error("Map is null")
420
+ }
421
+ // Set the map sizes
422
+ mapRect.value = map.value.node()?.getBoundingClientRect() as DOMRect
423
+ // Remove any existing country
424
+ map.value.selectAll('.choropleth-map__main__outline > *').remove()
425
+ map.value.selectAll('.choropleth-map__main__graticule > *').remove()
426
+ map.value.selectAll('.choropleth-map__main__features > *').remove()
427
+ // Return the map to allow chaining
428
+ return map.value
429
+ }
430
+
431
+ function prepareZoom() {
432
+ if (props.zoomable) {
433
+ map.value?.call(mapZoom.value)
434
+ }
435
+
436
+ // User can zoom on the map
437
+ if (props.zoomable && props.spherical) {
438
+ map.value?.call(mapRotate.value).call(mapSphericalZoom.value)
439
+ } else if (props.zoomable) {
440
+ map.value?.call(mapZoom.value)
441
+ }
442
+ // An initial zoom value is given
443
+ if (props.zoom || props.spherical) {
444
+ applyZoom(props.zoom ?? props.zoomMin, 0)
445
+ }
446
+ }
447
+
448
+ function draw() {
449
+ prepare()
450
+ drawOutline()
451
+ drawGraticule()
452
+ drawFeatures()
453
+ prepareZoom()
454
+ }
455
+
456
+ function drawOutline() {
457
+ map.value?.select('.choropleth-map__main__outline')
458
+ .append('path')
459
+ .attr('d', initialFeaturePath.value({type: 'Sphere'}))
460
+ .attr('stroke', props.outlineColor)
461
+ }
462
+
463
+ function drawGraticule() {
464
+ map.value?.select('.choropleth-map__main__graticule')
465
+ .append('path')
466
+ .attr('d', initialGraticulePath.value)
467
+ .attr('stroke', props.graticuleColor)
468
+ }
469
+
470
+ function drawFeatures() {
471
+ const features = map.value?.select('.choropleth-map__main__features')
472
+ .selectAll('.choropleth-map__main__features__item')
473
+ .data(geojson.value.features)
474
+ .enter()
475
+ .append('path')
476
+ if (!features) {
477
+ throw new Error("features is undefined")
478
+ }
479
+ features
480
+ .attr('class', featureClass)
481
+ .attr('d', initialFeaturePath.value)
482
+ .on('mouseover', featureMouseOver)
483
+ .on('mouseleave', featureMouseLeave)
484
+ .on('click', mapClicked)
485
+ .style('color', featureColor.value)
486
+ }
487
+
488
+ function update() {
489
+ // Bind geojson features to path
490
+ if (!map.value) {
491
+ return
492
+ }
493
+ map.value.selectAll('.choropleth-map__main__features__item')
494
+ .data(geojson.value.features)
495
+ .attr('class', featureClass)
496
+ .style('color', featureColor.value)
497
+ }
498
+
499
+ function featureClass(d: string) {
500
+ return keys(pickBy(featureClassObject(d), (value) => value)).join(' ')
501
+ }
502
+
503
+ function featureClassObject(d: string) {
504
+ const pathClass = 'choropleth-map__main__features__item'
505
+ const id = get(d, props.topojsonObjectsPath)
506
+ return {
507
+ [pathClass]: true,
508
+ [`${pathClass}--identifier-${kebabCase(id)}`]: true,
509
+ [`${pathClass}--empty`]: loadedData.value && !(id in loadedData.value),
510
+ [`${pathClass}--zoomed`]: featureZoom.value === id,
511
+ [`${pathClass}--cursored`]: featureCursor.value === id
512
+ }
513
+ }
514
+
515
+ function featureMouseLeave() {
516
+ featureCursor.value = null
517
+ }
518
+
519
+ function featureMouseOver(_: any, d: number) {
520
+ const id = get(d, props.topojsonObjectsPath)
521
+ const cursorId = loadedData.value && id in loadedData.value ? id : null
522
+ updateFeatureCursor(cursorId)
523
+ }
524
+
525
+ function updateFeatureCursor(id: any | null) {
526
+ featureCursor.value = id
527
+ }
528
+
529
+ async function loadTopojson() {
530
+ if (!topojsonPromise.value) {
531
+ if (!props.topojsonUrl?.length) {
532
+ throw new Error("Empty topojsonUrl")
533
+ }
534
+ topojsonPromise.value = d3.json(props.topojsonUrl)
535
+ topojson.value = await topojsonPromise.value
536
+ }
537
+ return topojsonPromise.value
538
+ }
539
+
540
+
541
+ async function mapClicked(event: MouseEvent, d: number) {
542
+ /**
543
+ * A click on a feature
544
+ * @event click
545
+ * @param Clicked feature
546
+ */
547
+ emit('click', d)
548
+ // Don't zoom on the map feature
549
+ if (!props.clickable) {
550
+ return
551
+ }
552
+ if (featureZoom.value === get(d, props.topojsonObjectsPath)) {
553
+ return resetZoom(event, d)
554
+ }
555
+ //TODO CD: it was a promise, should it be one?
556
+ setFeatureZoom(d, d3.pointer(event, map.value?.node()))
557
+ /**
558
+ * A zoom on a feature ended
559
+ * @event zoomed
560
+ * @param Zoomed feature
561
+ */
562
+ emit('zoomed', d)
563
+ }
564
+
565
+ function mapSphericalZoomed({transform: {k}}: { transform: MapTransform }) {
566
+ const transform = `scale(${k})`
567
+ mapTransform.value = {...mapTransform.value, k}
568
+ applyTransformToTrackedElements(transform)
569
+ }
570
+
571
+ function mapZoomed({transform}: { transform: MapTransform }) {
572
+ mapTransform.value = transform
573
+ applyTransformToTrackedElements(transform)
574
+ }
575
+
576
+ function mapRotated(event: Event) {
577
+ const {yaw, pitch} = calculateRotation(event)
578
+ applyRotation(yaw, pitch)
579
+ }
580
+
581
+ function calculateRotation(event: Event) {
582
+ const sensitivity = 75
583
+ const k = sensitivity / mapProjection.value.scale()
584
+ const [rotateX, rotateY] = mapProjection.value.rotate()
585
+ const yaw = rotateX + event.dx * k
586
+ const pitch = rotateY - event.dy * k
587
+ return {yaw, pitch}
588
+ }
589
+
590
+ function applyTransformToTrackedElements(transform) {
591
+ map.value?.selectAll('.choropleth-map__main__tracked').attr('transform', transform)
592
+ }
593
+
594
+ function applyRotation(rotateX: number, rotateY: number) {
595
+ mapTransform.value = {...mapTransform.value, rotateX, rotateY}
596
+ const featuresPaths = initialFeaturePath.value.projection(rotatingMapProjection.value)
597
+ const graticulePaths = featuresPaths(graticuleLines.value)
598
+ map.value?.selectAll('g.choropleth-map__main__features path').attr('d', featuresPaths)
599
+ map.value?.selectAll('g.choropleth-map__main__graticule path').attr('d', graticulePaths)
600
+ }
601
+
602
+ function applyZoomIdentity(zoomIdentity, pointer: number[] | null = null, transitionDuration = props.transitionDuration) {
603
+ return map.value?.transition()
604
+ .duration(transitionDuration)
605
+ .call(mapZoom.value.transform, zoomIdentity, pointer)
606
+ .end()
607
+ }
608
+
609
+ function reapplyZoom() {
610
+ mapTransform.value = {k: 1, x: 0, y: 0, rotateX: 0, rotateY: 0}
611
+ applyZoomIdentity(d3.zoomIdentity)
612
+ featureZoom.value = null
613
+ emitResetEvent()
614
+ }
615
+
616
+ function resetZoom(_event: MouseEvent, _d: number) {
617
+ map.value?.style('--map-scale', 1)
618
+ .transition()
619
+ .duration(props.transitionDuration)
620
+ .call(mapZoom.value?.transform, d3.zoomIdentity)
621
+ featureZoom.value = null
622
+ emitResetEvent()
623
+ }
624
+
625
+ function emitResetEvent() {
626
+ /**
627
+ * The zoom on the map was reset to its initial <slot ate></slot>
628
+ * @event reset
629
+ */
630
+ emit('reset')
631
+
632
+ }
633
+
634
+ function setFeaturesClasses() {
635
+ map.value?.selectAll('.choropleth-map__main__features__item').attr('class', featureClass)
636
+ }
637
+
638
+ function setFeatureZoom(d: any, pointer = [0, 0]) {
639
+
640
+ featureZoom.value = get(d, props.topojsonObjectsPath)
641
+ const [[x0, y0], [x1, y1]] = featurePath.value.bounds(d)
642
+ const scale = Math.min(8, 0.9 / Math.max((x1 - x0) / mapWidth.value, (y1 - y0) / mapHeight.value))
643
+ const zoomIdentity = d3.zoomIdentity
644
+ .translate(mapWidth.value / 2, mapHeight.value / 2)
645
+ .scale(scale)
646
+ .translate(-(x0 + x1) / 2, -(y0 + y1) / 2)
647
+ return map.value?.style('--map-scale', scale)
648
+ .transition()
649
+ .duration(props.transitionDuration)
650
+ .call(mapZoom.value?.transform, zoomIdentity, pointer)
651
+ .end()
652
+ }
653
+
654
+ function calculateFeatureZoomIdentity(d: any) {
655
+ const [[x0, y0], [x1, y1]] = featurePath.value.bounds(d)
656
+ const scale = Math.min(8, 0.9 / Math.max((x1 - x0) / mapWidth.value, (y1 - y0) / mapHeight.value))
657
+ const translateX = -(x0 + x1) / 2
658
+ const translateY = -(y0 + y1) / 2
659
+ return d3.zoomIdentity
660
+ .translate(mapWidth.value / 2, mapHeight.value / 2)
661
+ .scale(scale)
662
+ .translate(translateX, translateY)
663
+ }
664
+
665
+ function applyFeatureZoom(d: any, pointer = [0, 0]) {
666
+ const zoomIdentity = calculateFeatureZoomIdentity(d)
667
+ featureZoom.value = get(d, props.topojsonObjectsPath)
668
+ mapTransform.value = {k: zoomIdentity.k, x: zoomIdentity.x, y: zoomIdentity.y, rotateX: 0, rotateY: 0}
669
+ return applyZoomIdentity(zoomIdentity, pointer)
670
+ }
671
+
672
+ function applyZoom(zoom: number, transitionDuration = props.transitionDuration) {
673
+ const zoomScale = clamp(zoom, props.zoomMin, props.zoomMax)
674
+ if (props.spherical) {
675
+ return setSphericalZoom(zoomScale, transitionDuration)
676
+ } else {
677
+ return setPlanarZoom(zoomScale, transitionDuration)
678
+ }
679
+ }
680
+
681
+ function setSphericalZoom(zoomScale: number, transitionDuration: number) {
682
+ const zoomIdentity = d3.zoomIdentity.scale(zoomScale)
683
+ mapTransform.value = {...mapTransform.value, k: zoomScale}
684
+ return applyZoomIdentity(zoomIdentity, null, transitionDuration)
685
+ }
686
+
687
+ function setPlanarZoom(zoomScale: number, transitionDuration: number) {
688
+
689
+ const [x, y] = mapProjection.value(mapCenter.value)
690
+ const [translateX, translateY] = [mapWidth.value / 2 - zoomScale * x, mapHeight.value / 2 - zoomScale * y]
691
+ const zoomIdentity = d3.zoomIdentity.translate(translateX, translateY).scale(zoomScale)
692
+ mapTransform.value = {k: zoomScale, x: translateX, y: translateY, rotateX: 0, rotateY: 0}
693
+ return applyZoomIdentity(zoomIdentity, null, transitionDuration)
694
+ }
695
+
696
+ watch(() => props.socialMode, () => {
697
+ draw()
698
+ })
699
+ watch(() => props.data, () => {
700
+ update()
701
+ })
702
+ watch(() => featureZoom.value, () => {
703
+ setFeaturesClasses()
704
+ })
705
+ watch(() => featureCursor.value, () => {
706
+ setFeaturesClasses()
707
+ })
708
+
709
+ provide<ParentMap>(ParentKey, {
710
+ mapRect, mapTransform, rotatingMapProjection
711
+ })
712
+
713
+ return {
714
+ cursorValue,
715
+ debouncedDraw,
716
+ draw,
717
+ featureColorScaleEnd,
718
+ featureColorScaleFunction,
719
+ featureColorScaleStart,
720
+ featureCursor,
721
+ isLoaded,
722
+ isReady,
723
+ loadTopojson,
724
+ mapClass,
725
+ mapRect,
726
+ mapStyle,
727
+ mapTransform,
728
+ maxValue,
729
+ minValue,
730
+ resizable,
731
+ rotatingMapProjection,
732
+ setMapNodeSize,
733
+ topojsonPromise,
734
+ transformOrigin,
735
+ updateFeatureCursor
736
+ }
737
+ }
738
+ })
739
+ </script>
740
+ <template>
741
+ <div ref="resizable" :class="mapClass" :style="mapStyle" class="choropleth-map" @click="draw">
742
+ <svg :viewbox="`0 0 ${mapRect.width} ${mapRect.height}`" class="choropleth-map__main">
743
+ <pattern id="diagonalHatch" height="1" patternTransform="rotate(45 0 0)" patternUnits="userSpaceOnUse" width="1">
744
+ <rect :fill="featureColorScaleEnd" height="1" width="1"/>
745
+ <line :style="{ stroke: featureColorScaleStart, strokeWidth: 1 }" x1="0" x2="0" y1="0" y2="1"/>
746
+ </pattern>
747
+ <g :transform-origin="transformOrigin" class="choropleth-map__main__tracked">
748
+ <g v-if="graticule" class="choropleth-map__main__graticule"></g>
749
+ <g class="choropleth-map__main__features"></g>
750
+ <g v-if="outline" class="choropleth-map__main__outline"></g>
751
+ <slot v-if="isReady"/>
752
+ </g>
753
+ </svg>
754
+ <scale-legend
755
+ v-if="!hideLegend && isReady"
756
+ :color-scale="featureColorScaleFunction"
757
+ :color-scale-end="featureColorScaleEnd"
758
+ :color-scale-start="featureColorScaleStart"
759
+ :cursor-value="cursorValue"
760
+ :max="maxValue"
761
+ :min="minValue"
762
+ class="choropleth-map__legend"
763
+ >
764
+ <template #cursor="{ value }">
765
+ <slot name="legend-cursor" v-bind="{ value, identifier: featureCursor }"/>
766
+ </template>
767
+ </scale-legend>
768
+ </div>
769
+ </template>
770
+
771
+ <style lang="scss" scoped>
772
+ @import '../styles/lib';
773
+
774
+ .choropleth-map {
775
+ --map-scale: 1;
776
+ --map-color: #fff;
777
+ --map-social-color: #000;
778
+
779
+ position: relative;
780
+
781
+ &__main {
782
+ min-height: var(--map-height, 300px);
783
+ height: 100%;
784
+ width: 100%;
785
+ color: var(--map-color);
786
+
787
+ .chart--social-mode & {
788
+ color: var(--map-social-color);
789
+ }
790
+
791
+ &:deep(.choropleth-map__main__outline),
792
+ &:deep(.choropleth-map__main__graticule) {
793
+ fill: transparent;
794
+ pointer-events: none;
795
+ stroke-width: calc(1px / var(--map-scale, 1));
796
+ }
797
+
798
+ &:deep(.choropleth-map__main__features__item) {
799
+ stroke: currentColor;
800
+ stroke-width: calc(1px / var(--map-scale, 1));
801
+ fill: currentColor;
802
+ transition: opacity 750ms, filter 750ms, fill 750ms;
803
+
804
+ .choropleth-map__main__features__item--empty {
805
+ opacity: 0.8;
806
+
807
+ .choropleth-map--hatch-empty & {
808
+ opacity: 0.3;
809
+ fill: url('#diagonalHatch');
810
+ }
811
+ }
812
+
813
+ .choropleth-map--has-zoom &:not(.choropleth-map__main__features__item--zoomed) {
814
+ filter: grayscale(90%);
815
+ }
816
+ }
817
+ }
818
+
819
+ &__legend {
820
+ position: absolute;
821
+ left: 0;
822
+ bottom: 0;
823
+ }
824
+ }
825
+ </style>