@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.
- package/.github/workflows/deploy-github-pages.yaml +50 -0
- package/.storybook/app.scss +14 -0
- package/.storybook/doc_variables.scss +20 -0
- package/.storybook/main.ts +35 -0
- package/.storybook/preview-head.html +2 -0
- package/.storybook/preview.ts +32 -0
- package/README.md +71 -0
- package/deploy.js +15 -0
- package/docs/components/ApiTable.vue +171 -0
- package/docs/components/App.vue +146 -0
- package/docs/components/CollapsibleBlock.vue +122 -0
- package/docs/components/DocsHeader.vue +68 -0
- package/docs/components/DocsMenu.vue +201 -0
- package/docs/components/DocsMenuSection.vue +109 -0
- package/docs/components/EditLink.vue +49 -0
- package/docs/components/OutboundLink.vue +13 -0
- package/docs/components/PalettePresenter.vue +96 -0
- package/docs/components/RepositoryLink.vue +28 -0
- package/docs/components/SampleCard.vue +119 -0
- package/docs/main.js +42 -0
- package/docs/pages/components/accordion/doc.md +96 -0
- package/docs/pages/components/active-text-truncate/doc.md +44 -0
- package/docs/pages/components/advanced-link-form/doc.md +105 -0
- package/docs/pages/components/brand/doc.md +30 -0
- package/docs/pages/components/brand-expansion/doc.md +70 -0
- package/docs/pages/components/confirm-button/doc.md +91 -0
- package/docs/pages/components/content-placeholder/doc.md +16 -0
- package/docs/pages/components/custom-pagination/doc.md +61 -0
- package/docs/pages/components/digits-input/doc.md +28 -0
- package/docs/pages/components/donate-form/doc.md +20 -0
- package/docs/pages/components/embed-form/doc.md +22 -0
- package/docs/pages/components/embeddable-footer/doc.md +60 -0
- package/docs/pages/components/follow-us-popover/doc.md +5 -0
- package/docs/pages/components/generic-footer/doc.md +21 -0
- package/docs/pages/components/generic-header/doc.md +24 -0
- package/docs/pages/components/haptic-copy/doc.md +27 -0
- package/docs/pages/components/imddb-header/doc.md +23 -0
- package/docs/pages/components/ordinal-legend/doc.md +44 -0
- package/docs/pages/components/range-picker/doc.md +86 -0
- package/docs/pages/components/responsive-iframe/doc.md +13 -0
- package/docs/pages/components/scale-legend/doc.md +65 -0
- package/docs/pages/components/secret-input/doc.md +12 -0
- package/docs/pages/components/selectable-dropdown/doc.md +156 -0
- package/docs/pages/components/sharing-options/doc.md +13 -0
- package/docs/pages/components/sharing-options-link/doc.md +36 -0
- package/docs/pages/components/sign-up-form/doc.md +13 -0
- package/docs/pages/components/slide-up-down/doc.md +28 -0
- package/docs/pages/components/textured-deck/doc.md +78 -0
- package/docs/pages/components/tiny-pagination/doc.md +92 -0
- package/docs/pages/datavisualisation/bars/doc.md +110 -0
- package/docs/pages/datavisualisation/columns/doc.md +165 -0
- package/docs/pages/datavisualisation/lines/doc.md +139 -0
- package/docs/pages/datavisualisation/stacked-bar/doc.md +160 -0
- package/docs/pages/datavisualisation/stacked-column/doc.md +191 -0
- package/docs/pages/getting-started/about-icij/doc.md +13 -0
- package/docs/pages/getting-started/custom-bootstrap/doc.md +36 -0
- package/docs/pages/getting-started/installation-guide/doc.md +59 -0
- package/docs/pages/getting-started/internationalization/doc.md +74 -0
- package/docs/pages/maps/choropleth-map/doc.md +420 -0
- package/docs/pages/maps/choropleth-map-annotation/doc.md +373 -0
- package/docs/pages/maps/symbol-map/doc.md +203 -0
- package/docs/pages/structure/breakpoints/doc.md +3 -0
- package/docs/pages/structure/grid/doc.md +3 -0
- package/docs/pages/utilities/assets/doc.md +138 -0
- package/docs/pages/utilities/config/doc.md +52 -0
- package/docs/pages/utilities/iframes/doc.md +3 -0
- package/docs/pages/visual/colors/doc.md +31 -0
- package/docs/pages/visual/iconography/doc.md +56 -0
- package/docs/pages/visual/states/doc.md +77 -0
- package/docs/pages/visual/themes/doc.md +3 -0
- package/docs/pages/visual/typography/doc.md +71 -0
- package/docs/routes.js +25 -0
- package/docs/store/index.js +21 -0
- package/docs/styles/app.scss +36 -0
- package/docs/styles/variables.scss +20 -0
- package/lib/assets/images/icij-full-white.svg +6 -0
- package/lib/assets/images/icij-full.svg +6 -0
- package/lib/assets/images/icij.png +0 -0
- package/lib/assets/images/icij.svg +46 -0
- package/lib/assets/images/icij@2x.png +0 -0
- package/lib/assets/images/murmur-dark.png +0 -0
- package/lib/assets/images/murmur-dark.svg +79 -0
- package/lib/assets/images/murmur-white.png +0 -0
- package/lib/assets/images/murmur-white.svg +68 -0
- package/lib/components/AccordionStep.vue +128 -0
- package/lib/components/AccordionWrapper.vue +138 -0
- package/lib/components/ActiveTextTruncate.vue +258 -0
- package/lib/components/AdvancedLinkForm.vue +273 -0
- package/lib/components/Brand.vue +150 -0
- package/lib/components/BrandExpansion.vue +237 -0
- package/lib/components/ConfirmButton.vue +204 -0
- package/lib/components/ContentPlaceholder.vue +100 -0
- package/lib/components/CustomPagination.vue +225 -0
- package/lib/components/DigitsInput.vue +180 -0
- package/lib/components/DonateForm.vue +367 -0
- package/lib/components/EmbedForm.vue +173 -0
- package/lib/components/EmbeddableFooter.vue +201 -0
- package/lib/components/Fa.js +3 -0
- package/lib/components/FollowUsPopover.vue +117 -0
- package/lib/components/GenericFooter.vue +218 -0
- package/lib/components/GenericHeader.vue +259 -0
- package/lib/components/HapticCopy.vue +256 -0
- package/lib/components/ImddbHeader.vue +336 -0
- package/lib/components/OrdinalLegend.vue +164 -0
- package/lib/components/RangePicker.vue +430 -0
- package/lib/components/ResponsiveIframe.vue +48 -0
- package/lib/components/ScaleLegend.vue +230 -0
- package/lib/components/SecretInput.vue +132 -0
- package/lib/components/SelectableDropdown.vue +368 -0
- package/lib/components/SharingOptions.vue +230 -0
- package/lib/components/SharingOptionsLink.vue +259 -0
- package/lib/components/SignUpForm.vue +181 -0
- package/lib/components/SlideUpDown.vue +131 -0
- package/lib/components/TexturedDeck.vue +101 -0
- package/lib/components/TinyPagination.vue +268 -0
- package/lib/components/index.js +31 -0
- package/lib/composables/chart.ts +182 -0
- package/lib/composables/resizeObserver.ts +37 -0
- package/lib/composables/sendEmail.ts +50 -0
- package/lib/config.default.ts +33 -0
- package/lib/config.ts +70 -0
- package/lib/d3-geo-projection.d.ts +1 -0
- package/lib/datavisualisations/BarChart.vue +275 -0
- package/lib/datavisualisations/ColumnChart.vue +527 -0
- package/lib/datavisualisations/LineChart.vue +274 -0
- package/lib/datavisualisations/StackedBarChart.vue +614 -0
- package/lib/datavisualisations/StackedColumnChart.vue +640 -0
- package/lib/datavisualisations/index.js +5 -0
- package/lib/enums.ts +25 -0
- package/lib/i18n.ts +16 -0
- package/lib/keys.ts +2 -0
- package/lib/locales/en.json +140 -0
- package/lib/locales/fr.json +117 -0
- package/lib/locales/locales/en.json +140 -0
- package/lib/locales/locales/fr.json +117 -0
- package/lib/main.ts +87 -0
- package/lib/maps/ChoroplethMap.vue +825 -0
- package/lib/maps/ChoroplethMapAnnotation.vue +336 -0
- package/lib/maps/SymbolMap.vue +628 -0
- package/lib/maps/index.js +3 -0
- package/lib/querystring-es3.d.ts +1 -0
- package/lib/shims-bootstrap-vue.d.ts +5 -0
- package/lib/shims-tsx.d.ts +11 -0
- package/lib/shims-vue.d.ts +14 -0
- package/lib/styles/functions.scss +20 -0
- package/lib/styles/lib.scss +19 -0
- package/lib/styles/mixins.scss +37 -0
- package/lib/styles/utilities.scss +18 -0
- package/lib/styles/variables.scss +94 -0
- package/lib/styles/variables_dark.scss +1 -0
- package/lib/types.ts +46 -0
- package/lib/utils/animation.ts +24 -0
- package/lib/utils/assets.ts +46 -0
- package/lib/utils/clipboard.ts +41 -0
- package/lib/utils/iframe-resizer.ts +49 -0
- package/lib/utils/placeholder.ts +66 -0
- package/lib/utils/placeholderTypes.ts +21 -0
- package/lib/utils/strings.ts +8 -0
- package/loaders/highlight-loader.js +13 -0
- package/loaders/markdown-loader.js +91 -0
- package/loaders/metadata-loader.js +18 -0
- package/loaders/sass-extract-loader.js +14 -0
- package/loaders/vue-docgen-loader.js +14 -0
- package/package.json +96 -0
- package/plugins/MdPluginTypes.ts +10 -0
- package/plugins/docs.ts +50 -0
- package/plugins/front-matter.ts +36 -0
- package/plugins/highlight.ts +27 -0
- package/plugins/markdown-it/api-table.ts +25 -0
- package/plugins/markdown-it/sample-card.ts +31 -0
- package/plugins/plugin-delete.ts +47 -0
- package/plugins/plugin-docgen.ts +23 -0
- package/plugins/sass-vars.ts +25 -0
- package/plugins/vue-docgen.ts +29 -0
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/img/arrow-bottom.svg +3 -0
- package/public/assets/img/texture-brick-black.jpg +0 -0
- package/public/assets/img/texture-brick.jpg +0 -0
- package/public/assets/img/texture-carbon-black.jpg +0 -0
- package/public/assets/img/texture-carbon.jpg +0 -0
- package/public/assets/img/texture-crack-black.jpg +0 -0
- package/public/assets/img/texture-crack.jpg +0 -0
- package/public/assets/img/texture-rock-black.jpg +0 -0
- package/public/assets/img/texture-rock.jpg +0 -0
- package/public/assets/img/texture-sand-black.jpg +0 -0
- package/public/assets/img/texture-sand.jpg +0 -0
- package/public/assets/img/texture-silk-black.jpg +0 -0
- package/public/assets/img/texture-silk.jpg +0 -0
- package/public/assets/topojson/france-departments.json +1 -0
- package/public/assets/topojson/paris-arrondissements.json +1 -0
- package/public/assets/topojson/world-countries-sans-antarctica.json +1 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/site.webmanifest +1 -0
- package/stories/assets/code-brackets.svg +1 -0
- package/stories/assets/colors.svg +1 -0
- package/stories/assets/comments.svg +1 -0
- package/stories/assets/direction.svg +1 -0
- package/stories/assets/flow.svg +1 -0
- package/stories/assets/plugin.svg +1 -0
- package/stories/assets/repo.svg +1 -0
- package/stories/assets/stackalt.svg +1 -0
- package/stories/getting-started/about-icij.mdx +14 -0
- package/stories/getting-started/custom-bootstrap.mdx +23 -0
- package/stories/getting-started/installation-guide.mdx +62 -0
- package/stories/getting-started/internationalization.mdx +63 -0
- package/stories/murmur/components/AccordionStep.stories.ts +33 -0
- package/stories/murmur/components/AccordionWrapper.stories.ts +69 -0
- package/stories/murmur/components/ActiveTextTruncate.stories.ts +32 -0
- package/stories/murmur/components/AdvancedLinkForm.stories.ts +77 -0
- package/stories/murmur/components/Brand.stories.ts +30 -0
- package/stories/murmur/components/BrandExpansion.stories.ts +41 -0
- package/stories/murmur/components/ConfirmButton.stories.ts +40 -0
- package/stories/murmur/components/ContentPlaceholder.stories.ts +41 -0
- package/stories/murmur/components/CustomPagination.stories.ts +42 -0
- package/stories/murmur/components/DigitsInput.stories.ts +29 -0
- package/stories/murmur/components/DonateForm.stories.ts +29 -0
- package/stories/murmur/components/EmbedForm.stories.ts +35 -0
- package/stories/murmur/components/EmbeddableFooter.stories.ts +59 -0
- package/stories/murmur/components/FollowUsPopover.stories.ts +24 -0
- package/stories/murmur/components/GenericFooter.stories.ts +27 -0
- package/stories/murmur/components/GenericHeader.stories.ts +27 -0
- package/stories/murmur/components/HapticCopy.stories.ts +40 -0
- package/stories/murmur/components/ImddbHeader.stories.ts +27 -0
- package/stories/murmur/components/OrdinalLegend.stories.ts +49 -0
- package/stories/murmur/components/RangePicker.stories.ts +98 -0
- package/stories/murmur/components/ResponsiveIframe.stories.ts +24 -0
- package/stories/murmur/components/ScaleLegend.stories.ts +65 -0
- package/stories/murmur/components/SecretInput.stories.ts +60 -0
- package/stories/murmur/components/SelectableDropdown.stories.ts +143 -0
- package/stories/murmur/components/SharingOptions.stories.ts +32 -0
- package/stories/murmur/components/SharingOptionsLink.stories.ts +53 -0
- package/stories/murmur/components/SignUpForm.stories.ts +51 -0
- package/stories/murmur/components/SlideUpDown.stories.ts +32 -0
- package/stories/murmur/components/TexturedDeck.stories.ts +83 -0
- package/stories/murmur/components/TinyPagination.stories.ts +65 -0
- package/stories/murmur/datavisualisations/BarChart.stories.ts +54 -0
- package/stories/murmur/datavisualisations/ColumnChart.stories.ts +88 -0
- package/stories/murmur/datavisualisations/LineChart.stories.ts +139 -0
- package/stories/murmur/datavisualisations/StackedBarChart.stories.ts +199 -0
- package/stories/murmur/datavisualisations/StackedColumnChart.stories.ts +136 -0
- package/stories/murmur/decorators.ts +108 -0
- package/stories/murmur/maps/ChoroplethMap.stories.ts +440 -0
- package/stories/murmur/maps/ChoroplethMapAnnotation.stories.ts +26 -0
- package/stories/murmur/maps/SymbolMap.stories.ts +24 -0
- package/stories/murmur/utils.ts +7 -0
- package/tests/unit/components/AccordionStep.spec.ts +157 -0
- package/tests/unit/components/AccordionWrapper.spec.ts +57 -0
- package/tests/unit/components/ActiveTextTruncate.spec.js +30 -0
- package/tests/unit/components/AdvancedLinkForm.spec.js +124 -0
- package/tests/unit/components/Brand.spec.js +50 -0
- package/tests/unit/components/ContentPlaceholder.spec.js +29 -0
- package/tests/unit/components/CustomPagination.spec.js +72 -0
- package/tests/unit/components/DigitsInput.spec.ts +157 -0
- package/tests/unit/components/DonateForm.spec.js +149 -0
- package/tests/unit/components/EmbedForm.spec.js +108 -0
- package/tests/unit/components/EmbeddableFooter.spec.js +11 -0
- package/tests/unit/components/Fa.spec.js +18 -0
- package/tests/unit/components/FollowUsPopover.spec.js +29 -0
- package/tests/unit/components/GenericFooter.spec.js +29 -0
- package/tests/unit/components/GenericHeader.spec.js +104 -0
- package/tests/unit/components/HapticCopy.spec.js +123 -0
- package/tests/unit/components/ImddbHeader.spec.js +96 -0
- package/tests/unit/components/OrdinalLegend.spec.js +120 -0
- package/tests/unit/components/RangePicker.spec.ts +87 -0
- package/tests/unit/components/ResponsiveIframe.spec.js +20 -0
- package/tests/unit/components/ScaleLegend.spec.js +139 -0
- package/tests/unit/components/SecretInput.spec.js +81 -0
- package/tests/unit/components/SelectableDropdown.spec.js +160 -0
- package/tests/unit/components/SharingOptions.spec.js +125 -0
- package/tests/unit/components/SharingOptionsLink.spec.js +184 -0
- package/tests/unit/components/SignUpForm.spec.js +145 -0
- package/tests/unit/components/SlideUpDown.spec.js +59 -0
- package/tests/unit/components/TinyPagination.spec.js +46 -0
- package/tests/unit/config.spec.js +136 -0
- package/tests/unit/datavisualisations/BarChart.spec.js +63 -0
- package/tests/unit/datavisualisations/ColumnChart.spec.js +344 -0
- package/tests/unit/datavisualisations/LineChart.spec.js +155 -0
- package/tests/unit/datavisualisations/StackedBarChart.spec.js +294 -0
- package/tests/unit/datavisualisations/StackedColumnChart.spec.js +443 -0
- package/tests/unit/i18n.spec.ts +19 -0
- package/tests/unit/main.spec.js +82 -0
- package/tests/unit/maps/ChoroplethMap.spec.js +214 -0
- package/tests/unit/maps/ChoroplethMapAnnotation.spec.ts +186 -0
- package/tests/unit/maps/SymbolMap.spec.js +92 -0
- package/tests/unit/require.spec.js +22 -0
- package/tests/unit/setup.js +13 -0
- package/tests/unit/utils/assets.spec.js +61 -0
- package/tests/unit/utils/clipboard.spec.js +18 -0
- package/tests/unit/utils/iframe-resizer.spec.js +71 -0
- package/tsconfig.json +35 -0
- package/vite.config.ts +79 -0
- 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>
|