├── empty-module.js ├── public ├── robots.txt ├── google1bd62c15ea25e3b9.html ├── favicon.ico ├── og-image.png ├── img │ ├── alder.png │ ├── alder.webp │ ├── eieform.png │ ├── eieform.webp │ ├── flytting.png │ ├── flytting.webp │ ├── levekaar.png │ ├── levekaar.webp │ ├── utdanning.png │ ├── boligpriser.png │ ├── boligpriser.webp │ ├── folkemengde.png │ ├── folkemengde.webp │ ├── innvandring.png │ ├── innvandring.webp │ ├── trangboddhet.png │ ├── utdanning.webp │ ├── bygningstyper.png │ ├── bygningstyper.webp │ ├── husholdninger.png │ ├── husholdninger.webp │ ├── trangboddhet.webp │ ├── kommunale-boliger.png │ └── kommunale-boliger.webp ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── mstile-150x150.png ├── Oslo-logo-morkeblaa-RGB.png ├── android-chrome-192x192.png ├── android-chrome-384x384.png ├── browserconfig.xml └── site.webmanifest ├── .env ├── src ├── util │ ├── graph-templates │ │ ├── graph-helpers │ │ │ ├── rowHelpers │ │ │ │ ├── _rowHelpers.js │ │ │ │ ├── createBars.js │ │ │ │ ├── getRowName.js │ │ │ │ ├── index.js │ │ │ │ ├── tooltipHandler.js │ │ │ │ ├── styleValueText.js │ │ │ │ ├── updateBars.js │ │ │ │ ├── styleRowName.js │ │ │ │ ├── styleRows.js │ │ │ │ └── initRows.js │ │ │ ├── closeButton │ │ │ │ ├── index.js │ │ │ │ ├── showOrHide.js │ │ │ │ └── init.js │ │ │ ├── boxplotHelpers │ │ │ │ ├── _legendConfig.js │ │ │ │ ├── positionLegend.js │ │ │ │ ├── rowMedianText.js │ │ │ │ ├── rowFill.js │ │ │ │ ├── rowGeography.js │ │ │ │ ├── rowMeanRect.js │ │ │ │ ├── rowMeanText.js │ │ │ │ ├── rowDivider.js │ │ │ │ ├── rowMedianRect.js │ │ │ │ ├── rowBox.js │ │ │ │ ├── index.js │ │ │ │ ├── createRowElements.js │ │ │ │ └── createBoxPlotLegend.js │ │ │ ├── drawVoronoi │ │ │ │ ├── enterVoronoiCell.js │ │ │ │ ├── styleTexts.js │ │ │ │ ├── styleCircles.js │ │ │ │ ├── generateVoronoiData.js │ │ │ │ ├── styleRects.js │ │ │ │ ├── index.js │ │ │ │ └── drawPaths.js │ │ │ ├── columnHelpers │ │ │ │ ├── positionColumns.js │ │ │ │ ├── initColumns.js │ │ │ │ ├── updateColSubheading.js │ │ │ │ ├── updateColHeading.js │ │ │ │ ├── index.js │ │ │ │ ├── updateColFill.js │ │ │ │ ├── createColumns.js │ │ │ │ ├── updateColArrow.js │ │ │ │ └── updateClickTrigger.js │ │ │ ├── resizeSvg.js │ │ │ ├── index.js │ │ │ ├── sortData.js │ │ │ └── generateTableData.js │ │ ├── colors.js │ │ └── locale.js │ ├── config.js │ ├── index.js │ ├── polyfills.js │ ├── downloadFile.js │ ├── debounce.js │ ├── tableToExcel.js │ ├── tooltip.js │ ├── positionLabels.js │ ├── downloadSvg.js │ ├── downloadPng.js │ └── tableToCsv.js ├── styles │ ├── fonts │ │ ├── OsloSans-Bold.woff │ │ ├── OsloSans-Light.woff │ │ ├── OsloSans-Medium.woff │ │ └── OsloSans-Regular.woff │ ├── _animations.scss │ ├── _variables.scss │ ├── _colors.scss │ ├── _fonts.scss │ ├── _typography.scss │ ├── _print.scss │ ├── _table.scss │ ├── _layout.scss │ └── main.scss ├── i18n.js ├── config │ ├── topics │ │ ├── dataSources.js │ │ ├── befolkningsutvikling.js │ │ ├── utdanning.js │ │ ├── fodteDodeFlytting.js │ │ ├── alder.js │ │ └── eierform.js │ ├── ageRanges.js │ ├── predefinedOptions.js │ ├── geoData │ │ ├── districts.js │ │ ├── sagene.js │ │ └── st_hanshaugen.js │ ├── allDistricts.js │ ├── topics.js │ └── districtNames.js ├── directives │ └── clickOutside.js ├── __tests__ │ ├── __snapshots__ │ │ └── app.spec.js.snap │ └── app.spec.js ├── components │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── navigationTopbar.spec.js.snap │ │ │ ├── VCategory.spec.js.snap │ │ │ └── graphInstance.spec.js.snap │ │ ├── VCategory.spec.js │ │ ├── graphInstance.spec.js │ │ ├── navigationDrawer.spec.js │ │ ├── navigationTopbar.spec.js │ │ └── graphCard.spec.js │ ├── OkIcon.vue │ ├── UxSignals.vue │ ├── TheFooter.vue │ ├── VCategory.vue │ ├── PktIconsSprite.vue │ ├── Modal.vue │ └── OsloLogo.vue ├── views │ ├── __tests__ │ │ ├── notFound.spec.js │ │ ├── district.spec.js │ │ ├── topic.spec.js │ │ └── __snapshots__ │ │ │ └── notFound.spec.js.snap │ ├── Topic.vue │ ├── District.vue │ └── NotFound.vue ├── main.js ├── assets │ └── d3 │ │ └── index.js ├── router.js └── store.js ├── .dockerignore ├── postcss.config.cjs ├── .eslintignore ├── .prettierrc ├── setup_file.js ├── .editorconfig ├── svgTransform.js ├── babel.config.cjs ├── .gitignore ├── vite.config.js ├── Dockerfile ├── server ├── package.json ├── auth │ ├── api.js │ ├── accessToken.js │ └── index.js ├── server.js └── routes.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md └── workflows │ ├── pull_request.yml │ ├── nodejs.yml │ └── codeql-analysis.yml ├── jest.config.cjs ├── punkt.config.json ├── LICENSE ├── README.md ├── .stylelintrc.cjs ├── .eslintrc.cjs ├── .circleci └── config.yml ├── package.json ├── index.html └── tests └── MockStore.js /empty-module.js: -------------------------------------------------------------------------------- 1 | module.exports = ''; 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_I18N_LOCALE=nb-no 2 | VITE_I18N_FALLBACK_LOCALE=nb-no 3 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/rowHelpers/_rowHelpers.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/google1bd62c15ea25e3b9.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google1bd62c15ea25e3b9.html -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/og-image.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | *.iml 4 | build 5 | npm-debug.log 6 | node_modules/ 7 | stats.json 8 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/img/alder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/alder.png -------------------------------------------------------------------------------- /public/img/alder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/alder.webp -------------------------------------------------------------------------------- /public/img/eieform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/eieform.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/img/eieform.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/eieform.webp -------------------------------------------------------------------------------- /public/img/flytting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/flytting.png -------------------------------------------------------------------------------- /public/img/flytting.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/flytting.webp -------------------------------------------------------------------------------- /public/img/levekaar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/levekaar.png -------------------------------------------------------------------------------- /public/img/levekaar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/levekaar.webp -------------------------------------------------------------------------------- /public/img/utdanning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/utdanning.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/boligpriser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/boligpriser.png -------------------------------------------------------------------------------- /public/img/boligpriser.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/boligpriser.webp -------------------------------------------------------------------------------- /public/img/folkemengde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/folkemengde.png -------------------------------------------------------------------------------- /public/img/folkemengde.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/folkemengde.webp -------------------------------------------------------------------------------- /public/img/innvandring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/innvandring.png -------------------------------------------------------------------------------- /public/img/innvandring.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/innvandring.webp -------------------------------------------------------------------------------- /public/img/trangboddhet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/trangboddhet.png -------------------------------------------------------------------------------- /public/img/utdanning.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/utdanning.webp -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/img/bygningstyper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/bygningstyper.png -------------------------------------------------------------------------------- /public/img/bygningstyper.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/bygningstyper.webp -------------------------------------------------------------------------------- /public/img/husholdninger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/husholdninger.png -------------------------------------------------------------------------------- /public/img/husholdninger.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/husholdninger.webp -------------------------------------------------------------------------------- /public/img/trangboddhet.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/trangboddhet.webp -------------------------------------------------------------------------------- /public/img/kommunale-boliger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/kommunale-boliger.png -------------------------------------------------------------------------------- /public/Oslo-logo-morkeblaa-RGB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/Oslo-logo-morkeblaa-RGB.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/android-chrome-384x384.png -------------------------------------------------------------------------------- /public/img/kommunale-boliger.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/public/img/kommunale-boliger.webp -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /docs/ 5 | /src/coverageReport 6 | /src/config/geodata 7 | /server/server.js 8 | -------------------------------------------------------------------------------- /src/styles/fonts/OsloSans-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/src/styles/fonts/OsloSans-Bold.woff -------------------------------------------------------------------------------- /src/styles/fonts/OsloSans-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/src/styles/fonts/OsloSans-Light.woff -------------------------------------------------------------------------------- /src/styles/fonts/OsloSans-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/src/styles/fonts/OsloSans-Medium.woff -------------------------------------------------------------------------------- /src/styles/fonts/OsloSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslokommune/bydelsfakta/HEAD/src/styles/fonts/OsloSans-Regular.woff -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/closeButton/index.js: -------------------------------------------------------------------------------- 1 | import init from './init'; 2 | import showOrHide from './showOrHide'; 3 | 4 | export { init, showOrHide }; 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "semi": true, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /src/util/config.js: -------------------------------------------------------------------------------- 1 | export const baseUrl = import.meta.env.PROD ? '' : ''; 2 | 3 | export const apiUrl = import.meta.env.PROD ? window.location.origin : 'http://localhost:5000'; 4 | -------------------------------------------------------------------------------- /setup_file.js: -------------------------------------------------------------------------------- 1 | function noOp() {} 2 | 3 | if (typeof window.URL.createObjectURL === 'undefined') { 4 | Object.defineProperty(window.URL, 'createObjectURL', { value: noOp }); 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /svgTransform.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | process() { 3 | return 'module.exports = {};'; 4 | }, 5 | getCacheKey() { 6 | // The output is always the same. 7 | return 'svgTransform'; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/styles/_animations.scss: -------------------------------------------------------------------------------- 1 | .fade-enter-active, 2 | .fade-leave-active { 3 | transition: opacity 0.5s; 4 | } 5 | 6 | .fade-enter-from, 7 | .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { 8 | opacity: 0; 9 | } 10 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/boxplotHelpers/_legendConfig.js: -------------------------------------------------------------------------------- 1 | const boxWidth = 180; 2 | const boxHeight = 20; 3 | const textLabels = ['25. prosentil', 'Median', '75. prosentil']; 4 | 5 | export { boxWidth, boxHeight, textLabels }; 6 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/rowHelpers/createBars.js: -------------------------------------------------------------------------------- 1 | export default function (selection) { 2 | return selection 3 | .selectAll('rect.bar') 4 | .data((d) => d.values) 5 | .join('rect') 6 | .attr('class', 'bar'); 7 | } 8 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/drawVoronoi/enterVoronoiCell.js: -------------------------------------------------------------------------------- 1 | export default function (enter) { 2 | const g = enter.append('g'); 3 | g.append('path'); 4 | g.append('rect'); 5 | g.append('circle'); 6 | g.append('text'); 7 | return g; 8 | } 9 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/columnHelpers/positionColumns.js: -------------------------------------------------------------------------------- 1 | export default function positionColumns(selection) { 2 | selection 3 | .transition() 4 | .duration(this.duration) 5 | .attr('transform', (d, i) => `translate(${this.x[i](0)},0)`); 6 | } 7 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/rowHelpers/getRowName.js: -------------------------------------------------------------------------------- 1 | export default function (d) { 2 | if (this.isMobileView && d.values.length) { 3 | return `${d.geography} (${this.format(d.values[0][this.method], this.method)})`; 4 | } 5 | return d.geography; 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $break-xs: 340px; 2 | $break-sm: 480px; 3 | $break-md: 768px; 4 | $break-lg: 1024px; 5 | $break-xl: 1200px; 6 | 7 | $sidebarWidth: 20rem; 8 | 9 | $font-small: 0.9rem; 10 | $font-body: 1rem; 11 | $font-medium: 1.15rem; 12 | $font-large: 1.45rem; 13 | $font-huge: 2.25rem; 14 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #b3f5ff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/resizeSvg.js: -------------------------------------------------------------------------------- 1 | export default function resizeSvg(selection) { 2 | selection 3 | .transition() 4 | .attr('height', this.padding.top + this.height + this.padding.bottom + this.sourceHeight) 5 | .attr('width', this.padding.left + this.width + this.padding.right); 6 | } 7 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | import allDistricts from '../config/allDistricts'; 2 | import { topics } from '../config/topics'; 3 | 4 | export const getDistrictName = (uri) => allDistricts.find((district) => district.uri === uri).value; 5 | 6 | export const getHumanReadableTopic = (id) => (topics[id] ? topics[id].text : ''); 7 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env'], 3 | plugins: [ 4 | function () { 5 | return { 6 | visitor: { 7 | MetaProperty(path) { 8 | path.replaceWithSourceString('process'); 9 | }, 10 | }, 11 | }; 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/closeButton/showOrHide.js: -------------------------------------------------------------------------------- 1 | // 2 | export default function showOrHide() { 3 | this.close 4 | .style('display', () => (this.selected > -1 ? 'block' : 'none')) 5 | .attr('transform', `translate(${this.width - 30}, -60)`) 6 | .attr('tabindex', this.selected === -1 ? false : 0); 7 | } 8 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/rowHelpers/index.js: -------------------------------------------------------------------------------- 1 | import initRows from './initRows'; 2 | import styleRows from './styleRows'; 3 | import createBars from './createBars'; 4 | import updateBars from './updateBars'; 5 | import handleTooltips from './tooltipHandler'; 6 | 7 | export { initRows, styleRows, createBars, updateBars, handleTooltips }; 8 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/columnHelpers/initColumns.js: -------------------------------------------------------------------------------- 1 | import createColumns from './createColumns'; 2 | 3 | export default function () { 4 | return this.canvas 5 | .select('g.columns') 6 | .selectAll('g.column') 7 | .data(this.filteredData.meta.series, (d) => d.heading + d.subheading) 8 | .join(createColumns); 9 | } 10 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | import nb from './locales/nb-no.json'; 3 | 4 | const i18n = createI18n({ 5 | locale: import.meta.env.VITE_I18N_LOCALE || 'nb-no', 6 | fallbackLocale: import.meta.env.VITE_I18N_FALLBACK_LOCALE || 'nb-no', 7 | messages: { 8 | 'nb-no': nb, 9 | }, 10 | }); 11 | 12 | export default i18n; 13 | -------------------------------------------------------------------------------- /src/config/topics/dataSources.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ssb: { 3 | name: 'Statistisk sentralbyrå', 4 | url: 'https://ssb.no/', 5 | }, 6 | oslo: { 7 | name: 'Oslo kommune', 8 | url: 'https://statistikkbanken.oslo.kommune.no/', 9 | }, 10 | boligmappa: { 11 | name: 'Boligmappa AS', 12 | url: 'https://www.hjemla.no', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/rowHelpers/tooltipHandler.js: -------------------------------------------------------------------------------- 1 | import { showTooltipOver, showTooltipMove, hideTooltip } from '@/util/tooltip'; 2 | 3 | export default function (selection) { 4 | selection 5 | .on('mouseover', (e, d) => showTooltipOver(this.format(d[this.method], this.method))) 6 | .on('mousemove', showTooltipMove) 7 | .on('mouseleave', hideTooltip); 8 | } 9 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/boxplotHelpers/positionLegend.js: -------------------------------------------------------------------------------- 1 | import { boxWidth } from './_legendConfig'; 2 | 3 | export default function (selection) { 4 | const offsetX = -this.padding.left + (this.padding.left + this.width + this.padding.right) - (boxWidth + 120); 5 | const offsetY = this.height + 30; 6 | 7 | selection.attr('transform', `translate(${[offsetX, offsetY]})`); 8 | } 9 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/drawVoronoi/styleTexts.js: -------------------------------------------------------------------------------- 1 | export default function (selection, dataAccessor) { 2 | selection 3 | .select('text') 4 | .attr('x', (d) => d.data.x + this.x.bandwidth() / 2 + 4) 5 | .attr('y', (d) => d.data.y - 8) 6 | .text(dataAccessor) 7 | .attr('opacity', 0) 8 | .style('pointer-events', 'none') 9 | .style('cursor', 'auto'); 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /docs 5 | /src/coverageReport 6 | /tests/e2e/videos/ 7 | /tests/e2e/screenshots/ 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw* 26 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/drawVoronoi/styleCircles.js: -------------------------------------------------------------------------------- 1 | export default function (selection) { 2 | selection 3 | .select('circle') 4 | .attr('r', 6) 5 | .attr('cx', (d) => d.data.x + this.x.bandwidth() / 2) 6 | .attr('cy', (d) => d.data.y) 7 | .attr('opacity', 0) 8 | .style('pointer-events', 'none') 9 | .attr('stroke', 'white') 10 | .attr('stroke-width', 2); 11 | } 12 | -------------------------------------------------------------------------------- /src/util/graph-templates/colors.js: -------------------------------------------------------------------------------- 1 | import { interpolateRdBu } from 'd3'; 2 | 3 | export const color = { 4 | yellow: '#F8C66B', 5 | light_yellow: '#F8F0DC', 6 | purple: '#292858', 7 | grey: '#cccccc', 8 | light_grey: '#F0F1F1', 9 | red: '#FF8174', 10 | blue: '#6EE9FF', 11 | positive: interpolateRdBu(0.9), 12 | negative: interpolateRdBu(0.1), 13 | }; 14 | 15 | export const interpolator = interpolateRdBu; 16 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import path from 'path'; 4 | 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | build: { 8 | outDir: 'docs', 9 | sourcemap: true, 10 | }, 11 | resolve: { 12 | alias: [ 13 | { 14 | find: '@', 15 | replacement: path.resolve(__dirname, 'src'), 16 | }, 17 | ], 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/directives/clickOutside.js: -------------------------------------------------------------------------------- 1 | export default { 2 | beforeMount: (el, binding) => { 3 | el.clickOutsideEvent = (event) => { 4 | if (!(el === event.target || el.contains(event.target))) { 5 | binding.value(); 6 | } 7 | }; 8 | document.body.addEventListener('click', el.clickOutsideEvent); 9 | }, 10 | unmounted: (el) => { 11 | document.body.removeEventListener('click', el.clickOutsideEvent); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/util/polyfills.js: -------------------------------------------------------------------------------- 1 | // IE11 polyfill for 'forEach' on nodelists 2 | if ('NodeList' in window && !NodeList.prototype.forEach) { 3 | NodeList.prototype.forEach = function (callback, thisArg) { 4 | thisArg = thisArg || window; 5 | for (let i = 0; i < this.length; i += 1) { 6 | callback.call(thisArg, this[i], i, this); 7 | } 8 | }; 9 | } 10 | 11 | if (typeof SVGElement.prototype.blur === 'undefined') { 12 | SVGElement.prototype.blur = function () {}; 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine@sha256:928b24aaadbd47c1a7722c563b471195ce54788bf8230ce807e1dd500aec0549 2 | 3 | WORKDIR /usr/src/app/ 4 | COPY ./docs ./docs 5 | COPY ./server ./server 6 | 7 | WORKDIR /usr/src/app/server 8 | RUN npm install --production 9 | 10 | WORKDIR /usr/src/app 11 | 12 | EXPOSE 5000 13 | 14 | RUN addgroup -S app 15 | RUN adduser -S -D -H -G app app 16 | RUN chown -R app:app /usr/src/app/ 17 | USER app 18 | 19 | CMD [ "node", "server/server.js", "--loglevel", "info" ] 20 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-384x384.png", 12 | "sizes": "384x384", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#B3F5FF", 17 | "background_color": "#B3F5FF", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/columnHelpers/updateColSubheading.js: -------------------------------------------------------------------------------- 1 | import util from '../../template-utils'; 2 | 3 | export default function (selection) { 4 | selection 5 | .select('text.colSubheading') 6 | .text((d, i) => util.truncate(d.subheading, this.x[i].range())) 7 | .style('display', () => (this.filteredData.meta.series.length > 1 || this.selected > -1 ? 'inherit' : 'none')) 8 | .attr('opacity', (d, i) => 9 | i === this.highlight || this.highlight === -1 || this.highlight === undefined ? 1 : 0.2 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/index.js: -------------------------------------------------------------------------------- 1 | import * as closeButton from './closeButton'; 2 | import * as rowHelpers from './rowHelpers'; 3 | import * as boxplotHelpers from './boxplotHelpers'; 4 | import * as columnHelpers from './columnHelpers'; 5 | import drawVoronoi from './drawVoronoi'; 6 | 7 | import resizeSvg from './resizeSvg'; 8 | import sortData from './sortData'; 9 | import generateTableData from './generateTableData'; 10 | 11 | export { closeButton, rowHelpers, resizeSvg, columnHelpers, sortData, generateTableData, boxplotHelpers, drawVoronoi }; 12 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/app.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App shallowmounts app 1`] = ` 4 | 25 | `; 26 | -------------------------------------------------------------------------------- /src/styles/_colors.scss: -------------------------------------------------------------------------------- 1 | $color-blue: #6ee9ff; 2 | $color-light-blue: #b3f5ff; 3 | $color-light-blue-2: #b2d2d8; 4 | $color-purple: #292858; 5 | $color-yellow: #f8c66b; 6 | $color-red: #ff8174; 7 | 8 | $color-bg: #f3f3f5; 9 | $color-border: #dae3e5; 10 | 11 | $color-grey-50: #f7f7f7; 12 | $color-grey-100: #e1e1e6; 13 | $color-grey-200: #c6c6cc; 14 | $color-grey-300: #adadb3; 15 | $color-grey-400: #939399; 16 | $color-grey-500: #797980; 17 | $color-grey-600: #5e5e66; 18 | $color-grey-700: #43434d; 19 | $color-grey-800: #242433; 20 | $color-grey-900: #0e0e1a; 21 | 22 | $color-link: #0075eb; 23 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "engines": { 11 | "node": "20" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "axios": "^1.6.0", 17 | "cheerio": "^1.0.0-rc.10", 18 | "compression": "^1.7.4", 19 | "cors": "^2.8.5", 20 | "date-fns": "^2.22.1", 21 | "express": "^4.18.2", 22 | "morgan": "^1.10.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/boxplotHelpers/rowMedianText.js: -------------------------------------------------------------------------------- 1 | export function initRowMedianText(selection) { 2 | selection 3 | .attr('class', 'median-value') 4 | .attr('text-anchor', 'start') 5 | .attr('font-size', 12) 6 | .attr('font-weight', 700) 7 | .attr('y', this.rowHeight / 2 + 5); 8 | } 9 | 10 | export function updateRowMedianText(selection) { 11 | selection 12 | .select('.median-value') 13 | .text((d) => `${d.median} år`) 14 | .transition() 15 | .duration(this.duration) 16 | .attr('x', (d) => this.gapX + this.width1 + this.x2(d.median) + 6); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/navigationTopbar.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TheNavigationTopbar renders correctly 1`] = ` 4 |
7 |

11 | Bydel Sagene 12 |

13 | 26 |
27 | `; 28 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/boxplotHelpers/rowFill.js: -------------------------------------------------------------------------------- 1 | import { color } from '@/util/graph-templates/colors'; 2 | 3 | export function initRowFill(selection) { 4 | selection 5 | .attr('class', 'rowFill') 6 | .attr('fill', color.purple) 7 | .attr('height', this.rowHeight) 8 | .attr('x', -this.padding.left); 9 | } 10 | 11 | export function updateRowFill(selection) { 12 | selection 13 | .select('.rowFill') 14 | .transition() 15 | .duration(this.duration) 16 | .attr('fill-opacity', (d) => (d.avgRow || d.totalRow ? 0.05 : 0)) 17 | .attr('width', this.width + this.padding.left + this.padding.right); 18 | } 19 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/columnHelpers/updateColHeading.js: -------------------------------------------------------------------------------- 1 | import util from '@/util/graph-templates/template-utils'; 2 | 3 | export default function (selection) { 4 | selection 5 | .select('text.colHeading') 6 | .style('display', () => (this.filteredData.meta.series.length > 1 || this.selected > -1 ? 'inherit' : 'none')) 7 | .text((d, i) => { 8 | const colWidth = this.x[i].range()[1] - this.x[i].range()[0]; 9 | return util.truncate(d.heading, colWidth); 10 | }) 11 | .attr('opacity', (d, i) => 12 | i === this.highlight || this.highlight === -1 || this.highlight === undefined ? 1 : 0.2 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/boxplotHelpers/rowGeography.js: -------------------------------------------------------------------------------- 1 | import { color } from '@/util/graph-templates/colors'; 2 | import util from '@/util/graph-templates/template-utils'; 3 | 4 | export function initRowGeography(selection) { 5 | selection 6 | .attr('class', 'geography') 7 | .attr('fill', color.purple) 8 | .attr('y', this.rowHeight / 2 + 6) 9 | .attr('x', -this.padding.left + 10); 10 | } 11 | 12 | export function updateRowGeography(selection) { 13 | selection 14 | .select('.geography') 15 | .attr('font-weight', (d) => (d.avgRow || d.totalRow ? 700 : 400)) 16 | .text((d) => util.truncate(d.geography, this.padding.left)); 17 | } 18 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/boxplotHelpers/rowMeanRect.js: -------------------------------------------------------------------------------- 1 | import { color } from '@/util/graph-templates/colors'; 2 | 3 | export function initMeanRect(selection) { 4 | selection 5 | .attr('class', 'mean-stroke') 6 | .attr('fill', color.purple) 7 | .attr('height', this.rowHeight) 8 | .attr('width', 3) 9 | .attr('shape-rendering', 'geometricPrecision') 10 | .attr('transform', `translate(-1.5, 0)`) 11 | .attr('y', 0); 12 | } 13 | 14 | export function updateRowMeanRect(selection) { 15 | selection 16 | .select('.mean-stroke') 17 | .transition() 18 | .duration(this.duration) 19 | .attr('x', (d) => this.x(d.mean)); 20 | } 21 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/boxplotHelpers/rowMeanText.js: -------------------------------------------------------------------------------- 1 | import { norwegianLocale } from '@/util/graph-templates/locale'; 2 | 3 | export function initMeanText(selection) { 4 | selection 5 | .attr('class', 'mean-value') 6 | .attr('text-anchor', 'start') 7 | .attr('font-size', 12) 8 | .attr('font-weight', 700) 9 | .attr('y', this.rowHeight / 2 + 5); 10 | } 11 | 12 | export function updateRowMeanText(selection) { 13 | selection 14 | .select('.mean-value') 15 | .text((d) => `${norwegianLocale.format(',.2f')(d.mean)} år`) 16 | .transition() 17 | .duration(this.duration) 18 | .attr('x', (d) => this.x(d.mean) + 6); 19 | } 20 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/boxplotHelpers/rowDivider.js: -------------------------------------------------------------------------------- 1 | import { color } from '@/util/graph-templates/colors'; 2 | 3 | export function initRowDivider(selection) { 4 | selection 5 | .attr('class', 'divider') 6 | .attr('fill', color.purple) 7 | .attr('x', -this.padding.left) 8 | .attr('height', 1) 9 | .attr('y', this.rowHeight); 10 | } 11 | 12 | export function updateRowDivider(selection) { 13 | selection 14 | .select('.divider') 15 | .transition() 16 | .duration(this.duration) 17 | .attr('fill-opacity', (d) => (d.avgRow || d.totalRow ? 0.5 : 0.2)) 18 | .attr('width', this.width + this.padding.left + this.padding.right); 19 | } 20 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/columnHelpers/index.js: -------------------------------------------------------------------------------- 1 | import createColumns from './createColumns'; 2 | import initColumns from './initColumns'; 3 | import positionColumns from './positionColumns'; 4 | import updateClickTrigger from './updateClickTrigger'; 5 | import updateColHeading from './updateColHeading'; 6 | import updateColSubheading from './updateColSubheading'; 7 | import updateColFill from './updateColFill'; 8 | import updateColArrow from './updateColArrow'; 9 | 10 | export { 11 | createColumns, 12 | initColumns, 13 | positionColumns, 14 | updateClickTrigger, 15 | updateColHeading, 16 | updateColSubheading, 17 | updateColFill, 18 | updateColArrow, 19 | }; 20 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/drawVoronoi/generateVoronoiData.js: -------------------------------------------------------------------------------- 1 | import { voronoi } from 'd3-voronoi'; 2 | 3 | export default function (data, shape) { 4 | // TODO: The `d3-voronoi` module is now obsolete, and d3@6 bundles the `d3-delaunay` module instead. 5 | // It can still be loaded as an independent module, but should be replaced by `d3-delaunay`. The 6 | // API, algorithms, and data structure have been changed. 7 | // See https://observablehq.com/@d3/d3v6-migration-guide#delaunay. 8 | return voronoi() 9 | .extent([[1, 1], shape]) 10 | .x((d) => d.x + this.x.bandwidth() / 2) 11 | .y((d) => d.y) 12 | .polygons(data) 13 | .filter(Boolean); 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-weight: 400; 3 | font-family: 'OsloSans'; 4 | font-style: normal; 5 | font-stretch: normal; 6 | src: url('./fonts/OsloSans-Regular.woff') format('woff'); 7 | font-display: auto; 8 | } 9 | 10 | @font-face { 11 | font-weight: 700; 12 | font-family: 'OsloSans'; 13 | font-style: normal; 14 | font-stretch: normal; 15 | src: url('./fonts/OsloSans-Bold.woff') format('woff'); 16 | font-display: auto; 17 | } 18 | 19 | @font-face { 20 | font-weight: 500; 21 | font-family: 'OsloSans'; 22 | font-style: normal; 23 | font-stretch: normal; 24 | src: url('./fonts/OsloSans-Medium.woff') format('woff'); 25 | font-display: auto; 26 | } 27 | -------------------------------------------------------------------------------- /src/util/downloadFile.js: -------------------------------------------------------------------------------- 1 | export default function (content, filename, filetype) { 2 | const url = URL.createObjectURL(content); 3 | const downloadLink = document.createElement('a'); 4 | if (window.navigator.msSaveOrOpenBlob) { 5 | downloadLink.href = '#'; 6 | downloadLink.download = ''; 7 | downloadLink.addEventListener('click', () => window.navigator.msSaveOrOpenBlob(content, `${filename}${filetype}`)); 8 | downloadLink.click(); 9 | } else { 10 | downloadLink.href = url; 11 | downloadLink.download = `${filename}${filetype}`; 12 | document.body.appendChild(downloadLink); 13 | downloadLink.click(); 14 | document.body.removeChild(downloadLink); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/drawVoronoi/styleRects.js: -------------------------------------------------------------------------------- 1 | import { color } from '@/util/graph-templates/colors'; 2 | import util from '@/util/graph-templates/template-utils'; 3 | 4 | export default function (selection, dataAccessor) { 5 | selection 6 | .select('rect') 7 | .attr('height', 25) 8 | .attr('data-value', dataAccessor) 9 | .attr('width', (d, i, j) => util.getTextWidth(j[i].dataset.value) + 12) 10 | .attr('x', (d) => d.data.x + this.x.bandwidth() / 2 - 2) 11 | .attr('y', (d) => d.data.y - 26) 12 | .attr('fill', color.yellow) 13 | .attr('stroke', 'white') 14 | .attr('rx', 12.5) 15 | .attr('opacity', 0) 16 | .style('pointer-events', 'none'); 17 | } 18 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/boxplotHelpers/rowMedianRect.js: -------------------------------------------------------------------------------- 1 | import { color } from '@/util/graph-templates/colors'; 2 | 3 | export function initRowMedianRect(selection) { 4 | selection 5 | .attr('class', 'median-stroke') 6 | .attr('fill', color.purple) 7 | .attr('height', this.rowHeight) 8 | .attr('width', 3) 9 | .attr('shape-rendering', 'geometricPrecision') 10 | .attr('transform', `translate(-1.5, 0)`) 11 | .attr('y', 0); 12 | } 13 | 14 | export function updateRowMedianRect(selection) { 15 | selection 16 | .select('.median-stroke') 17 | .transition() 18 | .duration(this.duration) 19 | .attr('x', (d) => this.gapX + this.width1 + this.x2(d.median)); 20 | } 21 | -------------------------------------------------------------------------------- /src/config/ageRanges.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { range: '', label: '-- Velg --', selected: true, disabled: true }, 3 | { range: '[0, 1]', label: '0–1 år' }, 4 | { range: '[2, 5]', label: '2–5 år' }, 5 | { range: '[6, 12]', label: '6–12 år' }, 6 | { range: '[13, 15]', label: '13–15 år' }, 7 | { range: '[16, 18]', label: '16–18 år' }, 8 | { range: '[19, 29]', label: '19–29 år' }, 9 | { range: '[30, 39]', label: '30–39 år' }, 10 | { range: '[40, 49]', label: '40–49 år' }, 11 | { range: '[50, 59]', label: '50–59 år' }, 12 | { range: '[60, 66]', label: '60–66 år' }, 13 | { range: '[67, 79]', label: '67–79 år' }, 14 | { range: '[80, 89]', label: '80–89 år' }, 15 | { range: '[90, 119]', label: '90+ år' }, 16 | ]; 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQUEST]" 5 | labels: enhancement 6 | assignees: aulonm 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/styles/_typography.scss: -------------------------------------------------------------------------------- 1 | @use './variables' as *; 2 | @use './colors' as *; 3 | 4 | h2.section-heading { 5 | width: 100%; 6 | margin-bottom: 0.5rem; 7 | padding: 0.5rem 1rem 0; 8 | color: $color-purple; 9 | font-weight: 500; 10 | font-size: $font-large; 11 | 12 | @media screen and (min-width: $break-md) { 13 | padding: 1.5rem 1rem 0.5rem; 14 | } 15 | } 16 | 17 | h3 { 18 | margin-bottom: 0.5em; 19 | font-weight: 500; 20 | } 21 | 22 | a { 23 | text-decoration: none; 24 | } 25 | 26 | svg a:not([href$='#']) { 27 | text-decoration: underline; 28 | } 29 | 30 | .tick text { 31 | color: $color-grey-600; 32 | font-weight: 500; 33 | font-size: $font-small; 34 | font-family: monospace; 35 | text-rendering: geometricPrecision; 36 | } 37 | -------------------------------------------------------------------------------- /src/util/debounce.js: -------------------------------------------------------------------------------- 1 | // Returns a function, that, as long as it continues to be invoked, will not 2 | // be triggered. The function will be called after it stops being called for 3 | // N milliseconds. If `immediate` is passed, trigger the function on the 4 | // leading edge, instead of the trailing. 5 | export default function (func, wait, immediate) { 6 | let timeout; 7 | return function () { 8 | const context = this; 9 | const args = arguments; 10 | const later = function () { 11 | timeout = null; 12 | if (!immediate) func.apply(context, args); 13 | }; 14 | const callNow = immediate && !timeout; 15 | clearTimeout(timeout); 16 | timeout = setTimeout(later, wait); 17 | if (callNow) func.apply(context, args); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | testEnvironmentOptions: { 4 | url: 'http://localhost/', 5 | customExportConditions: ['node', 'node-addons'], 6 | }, 7 | rootDir: './', 8 | moduleDirectories: ['node_modules', 'src'], 9 | moduleFileExtensions: ['js', 'vue'], 10 | transform: { 11 | '^.+\\.(ts|tsx|js|jsx)$': 'babel-jest', 12 | '^.+\\.vue$': '@vue/vue3-jest', 13 | }, 14 | transformIgnorePatterns: ['node_modules/(?!axios|d3|d3-array|internmap|delaunator|robust-predicates)'], 15 | moduleNameMapper: { 16 | '^@/(.*)$': '/src/$1', 17 | }, 18 | setupFiles: ['/setup_file.js'], 19 | globals: { 20 | 'vue-jest': { 21 | compilerOptions: { 22 | comments: false, 23 | }, 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/boxplotHelpers/rowBox.js: -------------------------------------------------------------------------------- 1 | import { color } from '@/util/graph-templates/colors'; 2 | 3 | export function initRowBox(selection) { 4 | selection 5 | .attr('class', 'box') 6 | .attr('fill', color.purple) 7 | .attr('stroke', color.purple) 8 | .attr('stroke-width', 1) 9 | .attr('fill-opacity', 0.2) 10 | .attr('rx', 2) 11 | .attr('shape-rendering', 'geometricPrecision') 12 | .attr('height', this.barHeight) 13 | .attr('y', (this.rowHeight - this.barHeight) / 2); 14 | } 15 | 16 | export function updateRowBox(selection) { 17 | selection 18 | .select('.box') 19 | .transition() 20 | .duration(this.duration) 21 | .attr('x', (d) => this.gapX + this.width1 + this.x2(d.low)) 22 | .attr('width', (d) => this.x2(d.high) - this.x2(d.low)); 23 | } 24 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/drawVoronoi/index.js: -------------------------------------------------------------------------------- 1 | import generateVoronoiData from './generateVoronoiData'; 2 | import enterVoronoiCell from './enterVoronoiCell'; 3 | import styleCircles from './styleCircles'; 4 | import styleRects from './styleRects'; 5 | import styleTexts from './styleTexts'; 6 | import drawPaths from './drawPaths'; 7 | 8 | export default function (container, flattenData, shape, dataAccessor) { 9 | const voronoiData = generateVoronoiData.call(this, flattenData, shape); 10 | 11 | // Draw DOM elements for each voronoi cell 12 | container 13 | .selectAll('g') 14 | .data(voronoiData) 15 | .join(enterVoronoiCell.bind(this)) 16 | .call(styleCircles.bind(this)) 17 | .call(styleRects.bind(this), dataAccessor) 18 | .call(styleTexts.bind(this), dataAccessor) 19 | .call(drawPaths.bind(this)); 20 | } 21 | -------------------------------------------------------------------------------- /src/util/tableToExcel.js: -------------------------------------------------------------------------------- 1 | import XLSX from 'xlsx/dist/xlsx.mini.min'; 2 | import downloadFile from './downloadFile'; 3 | 4 | export default (table) => { 5 | const td = table.querySelectorAll('td'); 6 | const caption = table.parentNode.querySelector('h3').innerHTML || 'title'; 7 | 8 | // Replace commas (Norwegian decimal delimiter) with periods in data cells 9 | td.forEach((d) => { 10 | d.setAttribute('data-pretty', d.innerHTML); 11 | d.innerHTML = d.innerHTML.replace(',', '.').replace(' ', ''); 12 | }); 13 | 14 | const wb = XLSX.utils.table_to_book(table); 15 | const wbout = XLSX.write(wb, { type: 'array', bookType: 'xlsx' }); 16 | downloadFile(new Blob([wbout]), caption, '.xlsx'); 17 | 18 | // Replace periods back to commas 19 | td.forEach((d) => { 20 | d.innerHTML = d.getAttribute('data-pretty'); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/rowHelpers/styleValueText.js: -------------------------------------------------------------------------------- 1 | import d3 from '@/assets/d3'; 2 | import { color } from '../../colors'; 3 | 4 | export default function (selection) { 5 | selection 6 | .classed('valueText', true) 7 | .attr('fill', color.purple) 8 | .attr('y', this.rowHeight / 2 + 4) 9 | .attr('x', (d, i) => this.x[i](0)) 10 | .text((d) => (d[this.method] ? this.format(d[this.method], this.method) : 'Ikke tilgjengelig')) 11 | .attr('opacity', (d, i, j) => { 12 | if (this.isMobileView && this.method === 'value') return 0; 13 | const parent = d3.select(j[i].parentNode); 14 | const avgOrTotal = JSON.parse(parent.attr('data-avgRow')) || JSON.parse(parent.attr('data-totalRow')); 15 | return avgOrTotal && this.method === 'value' && d.value > this.x[i].domain()[1] ? 1 : 0; 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/VCategory.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VCategory renders correctly 1`] = ` 4 | 10 | 11 | 16 | 21 | 27 | 28 | 32 | test 33 | 34 | 37 | test 38 | 39 | 40 | `; 41 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/drawVoronoi/drawPaths.js: -------------------------------------------------------------------------------- 1 | import d3 from '@/assets/d3'; 2 | 3 | export default function (selection) { 4 | const path = selection 5 | .select('path') 6 | .attr('d', (d) => `M${d.join('L')}Z`) 7 | .attr('fill-opacity', 0); 8 | 9 | // Handle mouse interactions 10 | path.on('mouseenter', ({ currentTarget }) => { 11 | const parent = d3.select(currentTarget.parentNode); 12 | parent.select('rect').attr('opacity', 1); 13 | parent.select('text').attr('opacity', 1); 14 | parent.select('circle').attr('opacity', 1); 15 | }); 16 | path.on('mouseleave', ({ currentTarget }) => { 17 | const parent = d3.select(currentTarget.parentNode); 18 | parent.select('rect').attr('opacity', 0); 19 | parent.select('text').attr('opacity', 0); 20 | parent.select('circle').attr('opacity', 0); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/util/tooltip.js: -------------------------------------------------------------------------------- 1 | import { select } from 'd3'; 2 | 3 | const body = select('body'); 4 | 5 | // Creates DOM elements for generic tooltip 6 | export const showTooltipOver = (str, delay = 0) => { 7 | const tooltip = body 8 | .append('div') 9 | .attr('class', 'tooltip') 10 | .attr('aria-hidden', true) 11 | .html(str) 12 | .style('top', '-30px') 13 | .style('left', function () { 14 | const div = select(this).node().getBoundingClientRect().width; 15 | return `${div / -2}px`; 16 | }); 17 | 18 | setTimeout(() => { 19 | tooltip.classed('showTooltip', true); 20 | }, delay); 21 | }; 22 | 23 | export const showTooltipMove = (e) => { 24 | if (!e) return; 25 | body.select('div.tooltip').style('transform', `translate(${e.pageX}px, ${e.pageY}px)`); 26 | }; 27 | 28 | export const hideTooltip = () => { 29 | body.select('div.tooltip').remove(); 30 | }; 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes #? 2 | 3 | Change/Fix/Added: 4 | 5 | ### Universal design checklist __(Submitter)__ 6 | - [ ] Semantic correct use of h1, h2, h3 etc? Start with h1,h2,h3 instead of h6,h7,h8. 7 | - [ ] All input, links, images, buttons and visible elements should have alt text. 8 | - [ ] input fields must have placeholder and label. 9 | 10 | ### Merge criteria for dev __(Submitter)__ 11 | - [ ] Go through *universal design* checklist 12 | - [ ] Does the new feature support mobile devices? (Responsive website) 13 | - [ ] Update Trello board 14 | - [ ] Merge into master (Merge button) 15 | - [ ] Delete branch 16 | 17 | ### Review criteria for merge __(Reviewer)__ 18 | - [ ] Have PR-author gone through *universal design* checklist? 19 | - [ ] Does it support mobile, tablet and desktop? 20 | - [ ] Code review 21 | - [ ] npm run test 22 | - [ ] Approve PR 23 | -------------------------------------------------------------------------------- /src/config/predefinedOptions.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { option: [], label: '- Velg byområde -', selected: true, disabled: true }, 3 | { option: ['04', '05', '06', '07', '08'], label: 'Vest' }, 4 | { option: ['01', '02', '03', '09', '10', '11', '12', '13', '14', '15'], label: 'Øst' }, 5 | { option: ['01', '02', '03', '04', '05'], label: 'Indre by' }, 6 | { option: ['01', '02', '03'], label: 'Indre by øst' }, 7 | { option: ['04', '05'], label: 'Indre by vest' }, 8 | { option: ['06', '07', '08', '09', '10', '11', '12', '13', '14', '15'], label: 'Ytre by' }, 9 | { option: ['06', '07', '08'], label: 'Ytre by vest' }, 10 | { option: ['09', '10', '11', '12'], label: 'Ytre by øst' }, 11 | { option: ['13', '14', '15'], label: 'Ytre by sør' }, 12 | { 13 | option: ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15'], 14 | label: 'Velg alle', 15 | }, 16 | ]; 17 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/columnHelpers/updateColFill.js: -------------------------------------------------------------------------------- 1 | export default function (selection) { 2 | selection 3 | .select('rect.colFill') 4 | .attr('y', -10) 5 | .transition() 6 | .duration(this.duration) 7 | .attr('height', this.height + 20) 8 | .duration(this.duration) 9 | .attr('width', (d, i) => { 10 | let val; 11 | const totalRow = this.filteredData.data.find((dj) => dj.totalRow); 12 | 13 | if (totalRow && totalRow.values && totalRow.values[i]) { 14 | val = totalRow.values[i][this.method]; 15 | } else { 16 | return 0; 17 | } 18 | 19 | if ((this.method === 'value' && val > this.x[i].domain()[1]) || this.isMobileView) { 20 | return 0; 21 | } 22 | if (this.filteredData.data.filter((dj) => dj.totalRow).length) { 23 | return this.x[0](val); 24 | } 25 | return 0; 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/boxplotHelpers/index.js: -------------------------------------------------------------------------------- 1 | import positionLegend from './positionLegend'; 2 | import createBoxPlotLegend from './createBoxPlotLegend'; 3 | import createRowElements from './createRowElements'; 4 | import { updateRowBox } from './rowBox'; 5 | import { updateRowGeography } from './rowGeography'; 6 | import { updateRowMedianText } from './rowMedianText'; 7 | import { updateRowMeanText } from './rowMeanText'; 8 | import { updateRowMeanRect } from './rowMeanRect'; 9 | import { updateRowMedianRect } from './rowMedianRect'; 10 | import { updateRowFill } from './rowFill'; 11 | import { updateRowDivider } from './rowDivider'; 12 | 13 | export { 14 | positionLegend, 15 | createBoxPlotLegend, 16 | createRowElements, 17 | updateRowBox, 18 | updateRowGeography, 19 | updateRowMedianText, 20 | updateRowMeanText, 21 | updateRowMeanRect, 22 | updateRowMedianRect, 23 | updateRowFill, 24 | updateRowDivider, 25 | }; 26 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/sortData.js: -------------------------------------------------------------------------------- 1 | export default function (data = [], options = {}) { 2 | if (typeof options.series === 'number') { 3 | const direction = options.direction ? options.direction : 'desc'; 4 | 5 | const idx = options.series; 6 | const mtd = this.method; 7 | 8 | data.data.sort((a, b) => { 9 | if (!a.values) { 10 | throw new Error('Invalid data structure'); 11 | } 12 | 13 | // Sort avgRow and totalRow 14 | if (b.totalRow && a.avgRow) return -1; 15 | if (a.totalRow && b.avgRow) return 1; 16 | if (a.totalRow || a.avgRow) return 1; 17 | if (b.totalRow || b.avgRow) return -1; 18 | if (a.avgRow || a.totalRow) return 1; 19 | 20 | // Sort the rest of the geographies 21 | if (direction === 'asc') { 22 | return a.values[idx][mtd] - b.values[idx][mtd]; 23 | } 24 | return b.values[idx][mtd] - a.values[idx][mtd]; 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/config/geoData/districts.js: -------------------------------------------------------------------------------- 1 | import alna from './alna'; 2 | import bjerke from './bjerke'; 3 | import frogner from './frogner'; 4 | import gamleoslo from './gamle_oslo'; 5 | import grorud from './grorud'; 6 | import grunerlokka from './grunerlokka'; 7 | import nordreaker from './nordre_aker'; 8 | import nordstrand from './nordstrand'; 9 | import oslo from './oslo'; 10 | import ostensjo from './ostensjo'; 11 | import sagene from './sagene'; 12 | import sondrenordstrand from './sondre_nordstrand'; 13 | import sthanshaugen from './st_hanshaugen'; 14 | import stovner from './stovner'; 15 | import ullern from './ullern'; 16 | import vestreaker from './vestre_aker'; 17 | 18 | export default { 19 | alna, 20 | bjerke, 21 | frogner, 22 | gamleoslo, 23 | grorud, 24 | grunerlokka, 25 | nordreaker, 26 | nordstrand, 27 | oslo, 28 | ostensjo, 29 | sagene, 30 | sondrenordstrand, 31 | sthanshaugen, 32 | stovner, 33 | ullern, 34 | vestreaker, 35 | }; 36 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/rowHelpers/updateBars.js: -------------------------------------------------------------------------------- 1 | export default function (selection) { 2 | selection 3 | .attr('height', (d, i, j) => { 4 | if (this.isMobileView) return 7; 5 | return j[0].parentNode.__data__.totalRow ? 2 : this.barHeight; 6 | }) 7 | .attr('y', (d, i, j) => { 8 | if (this.isMobileView) { 9 | return (this.rowHeight - this.barHeight) / 2 + 12; 10 | } 11 | return j[0].parentNode.__data__.totalRow ? this.rowHeight / 2 : (this.rowHeight - this.barHeight) / 2; 12 | }) 13 | .attr('opacity', (d, i) => 14 | i === this.highlight || this.highlight === -1 || this.highlight === undefined ? 1 : 0.2 15 | ) 16 | .transition() 17 | .duration(this.duration) 18 | .attr('width', (d, i) => { 19 | if (this.method === 'value' && d[this.method] > this.x[i].domain()[1]) return 0; 20 | return Math.max(this.x[0](d[this.method]), 0); 21 | }) 22 | .attr('x', (d, i) => this.x[i](0)); 23 | } 24 | -------------------------------------------------------------------------------- /server/auth/api.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const querystring = require('querystring'); 3 | 4 | const envs = { 5 | access_token_url: `${process.env.KEYCLOAK_SERVER_URL}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, 6 | }; 7 | 8 | const request = (params) => { 9 | return axios({ 10 | method: 'post', 11 | url: envs.access_token_url, 12 | data: querystring.stringify(params), 13 | headers: { 14 | 'Content-Type': 'application/x-www-form-urlencoded', 15 | }, 16 | }) 17 | .then((response) => { 18 | return Promise.resolve(response.data); 19 | }) 20 | .catch((error) => { 21 | if (error.errno === 'ETIMEDOUT') { 22 | return Promise.reject(new Error('Timeout')); 23 | } 24 | if (error.response === undefined) { 25 | return Promise.reject(new Error('Keycloak is most likely down')); 26 | } 27 | return Promise.reject(error.response); 28 | }); 29 | }; 30 | 31 | module.exports.request = request; 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: aulonm 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/views/__tests__/notFound.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import { createStore } from 'vuex'; 3 | import { createRouter, createWebHistory } from 'vue-router'; 4 | import i18n from '@/i18n'; 5 | import { routes } from '@/router'; 6 | import mockStore from '@/../tests/MockStore'; 7 | import NotFound from '../NotFound.vue'; 8 | 9 | describe('NotFound', () => { 10 | let wrapper = null; 11 | let router = null; 12 | let store = null; 13 | 14 | beforeEach(() => { 15 | store = createStore(mockStore); 16 | router = createRouter({ 17 | history: createWebHistory(), 18 | routes, 19 | }); 20 | 21 | wrapper = mount(NotFound, { 22 | global: { 23 | plugins: [router, store, i18n], 24 | stubs: { 25 | 'v-category': true, 26 | }, 27 | }, 28 | }); 29 | }); 30 | 31 | afterEach(() => { 32 | wrapper.unmount(); 33 | }); 34 | 35 | test('renders component correctly', () => { 36 | expect(wrapper.element).toMatchSnapshot(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /server/auth/accessToken.js: -------------------------------------------------------------------------------- 1 | const isDate = require('date-fns/isDate'); 2 | const addSeconds = require('date-fns/addSeconds'); 3 | const subSeconds = require('date-fns/subSeconds'); 4 | const isAfter = require('date-fns/isAfter'); 5 | 6 | const addExpirationInformation = (token) => { 7 | const parsedTokenProps = {}; 8 | 9 | if ('expires_in' in token) { 10 | if (!isDate(token.expires_in)) { 11 | parsedTokenProps.expires_at = addSeconds(new Date(), Number.parseInt(token.expires_in, 10)); 12 | } 13 | } 14 | 15 | if ('refresh_expires_in' in token) { 16 | if (!isDate(token.refresh_expires_in)) { 17 | parsedTokenProps.refresh_expires_at = addSeconds(new Date(), Number.parseInt(token.refresh_expires_in, 10)); 18 | } 19 | } 20 | 21 | return { ...token, ...parsedTokenProps }; 22 | }; 23 | 24 | const shouldRefreshToken = (expiresAt) => { 25 | return isAfter(new Date(), subSeconds(expiresAt, 10)); 26 | }; 27 | 28 | module.exports.shouldRefreshToken = shouldRefreshToken; 29 | module.exports.addExpirationInformation = addExpirationInformation; 30 | -------------------------------------------------------------------------------- /src/components/__tests__/VCategory.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, RouterLinkStub } from '@vue/test-utils'; 2 | import store from '@/store'; 3 | import i18n from '@/i18n'; 4 | import VCategory from '../VCategory.vue'; 5 | 6 | describe('VCategory', () => { 7 | let wrapper = null; 8 | 9 | beforeEach(() => { 10 | wrapper = mount(VCategory, { 11 | global: { 12 | plugins: [i18n, store], 13 | stubs: { RouterLink: RouterLinkStub }, 14 | }, 15 | props: { 16 | category: 'test', 17 | topic: 'test', 18 | bgImage: 'test', 19 | bgColor: 'black', 20 | txtColor: 'white', 21 | id: 'test', 22 | link: '/test', 23 | district: 'sagene', 24 | }, 25 | }); 26 | }); 27 | 28 | afterEach(() => { 29 | wrapper.unmount(); 30 | }); 31 | 32 | test('renders VCategory-component and finds main-container__item-class', () => { 33 | expect(wrapper.classes('main-container__item')).toBe(true); 34 | }); 35 | 36 | test('renders correctly', () => { 37 | expect(wrapper.element).toMatchSnapshot(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /punkt.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "svgsprite": { 3 | "files": [ 4 | "./node_modules/@oslokommune/punkt-assets/dist/icons/chevron-down.svg", 5 | "./node_modules/@oslokommune/punkt-assets/dist/icons/close.svg", 6 | "./node_modules/@oslokommune/punkt-assets/dist/icons/download.svg", 7 | "./node_modules/@oslokommune/punkt-assets/dist/icons/expand.svg", 8 | "./node_modules/@oslokommune/punkt-assets/dist/icons/graph.svg", 9 | "./node_modules/@oslokommune/punkt-assets/dist/icons/location-pin.svg", 10 | "./node_modules/@oslokommune/punkt-assets/dist/icons/menu.svg", 11 | "./node_modules/@oslokommune/punkt-assets/dist/icons/minimize.svg", 12 | "./node_modules/@oslokommune/punkt-assets/dist/icons/new-window-small.svg", 13 | "./node_modules/@oslokommune/punkt-assets/dist/icons/picture.svg", 14 | "./node_modules/@oslokommune/punkt-assets/dist/icons/question.svg", 15 | "./node_modules/@oslokommune/punkt-assets/dist/icons/table.svg" 16 | ], 17 | "output": { 18 | "fileType": "html", 19 | "filePath": "src/components/PktIconsSprite.vue" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/__tests__/graphInstance.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import Vue3Resize from 'vue3-resize'; 3 | import { topics } from '@/config/topics'; 4 | import store from '@/store'; 5 | import i18n from '@/i18n'; 6 | import GraphInstance from '../GraphInstance.vue'; 7 | 8 | describe('GraphInstance', () => { 9 | let wrapper = null; 10 | 11 | beforeEach(() => { 12 | GraphInstance.methods.draw = jest.fn(); 13 | 14 | wrapper = mount(GraphInstance, { 15 | global: { 16 | plugins: [store, i18n, Vue3Resize], 17 | stubs: { 18 | spinner: true, 19 | }, 20 | }, 21 | props: { 22 | settings: topics.alder.cards[0], 23 | showTable: false, 24 | mode: 'graph', 25 | }, 26 | }); 27 | }); 28 | 29 | afterEach(() => { 30 | wrapper.unmount(); 31 | }); 32 | 33 | test('renders graphInstance-component and finds graph__container-class', () => { 34 | expect(wrapper.classes('graph__shadow')).toBe(true); 35 | }); 36 | 37 | test('renders correctly', () => { 38 | expect(wrapper.element).toMatchSnapshot(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Oslo kommune 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/util/graph-templates/graph-helpers/columnHelpers/createColumns.js: -------------------------------------------------------------------------------- 1 | import { color } from '../../colors'; 2 | 3 | export default function (selection) { 4 | const g = selection.append('g').attr('class', 'column'); 5 | g.append('rect').call(styleColumnBackground); 6 | g.append('rect').call(styleClickTrigger); 7 | g.append('rect').call(styleArrow); 8 | g.append('text').call(styleColumnHeading); 9 | g.append('text').call(styleColumnSubheading); 10 | return g; 11 | } 12 | 13 | function styleColumnBackground(el) { 14 | el.attr('fill', color.light_grey).attr('class', 'colFill'); 15 | } 16 | 17 | function styleClickTrigger(el) { 18 | el.attr('fill', color.light_grey).attr('class', 'clickTrigger'); 19 | } 20 | 21 | function styleArrow(el) { 22 | el.attr('class', 'arrow').attr('width', 1).attr('height', 11); 23 | } 24 | 25 | function styleColumnHeading(el) { 26 | el.attr('class', 'colHeading').attr('transform', 'translate(0, -40)').style('pointer-events', 'none'); 27 | } 28 | 29 | function styleColumnSubheading(el) { 30 | el.attr('class', 'colSubheading').attr('transform', 'translate(0, -20)').style('pointer-events', 'none'); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/graphInstance.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`GraphInstance renders correctly 1`] = ` 4 |
9 | 29 |
32 |

35 | 38 | 39 | 40 |
41 | 42 |

43 |
47 |