From 50c77628da5e65c7bdd4d25712cc2f99ecf1e16a Mon Sep 17 00:00:00 2001 From: desafinadude Date: Tue, 3 Dec 2024 19:36:46 +0200 Subject: [PATCH] wip --- d3_script.js | 2499 +++++++++++++++++++++++++------------------------- 1 file changed, 1252 insertions(+), 1247 deletions(-) diff --git a/d3_script.js b/d3_script.js index f0e8384..54ede72 100644 --- a/d3_script.js +++ b/d3_script.js @@ -1,1409 +1,1414 @@ -DEBUG = true; +document.addEventListener('DOMContentLoaded', function () { + if(!window.scrollyInitialized) { + window.scrollyInitialized = true; -let chart_colors = { - gender_male: '#ff3d3b', - gender_female: '#ffd06c', + DEBUG = true; - prov_both: '#999999', - prov_inside: '#ffd06c', - prov_outside: '#ff3d3b', - donut1: '#999999', - donut2: '#ffd06c', - donut3: '#ff3d3b', - - safety_yes: '#ffd06c', - safety_no: '#ff3d3b', + let chart_colors = { + gender_male: '#ff3d3b', + gender_female: '#ffd06c', - women_safety_yes: '#FFD06C', - women_safety_no: '#FF3D3B', + prov_both: '#999999', + prov_inside: '#ffd06c', + prov_outside: '#ff3d3b', - unhygenic_seat: '#FFD06C', - unhygenic_bowl: '#FF3D3B', - unhygenic_wall: '#999999', + donut1: '#999999', + donut2: '#ffd06c', + donut3: '#ff3d3b', - return_visits_yes: '#ff3d3b', - return_visits_no: '#FFD06C', + + safety_yes: '#ffd06c', + safety_no: '#ff3d3b', - queues_over_20: '#ff3d3b', - queues_under_20: '#FFD06C' + women_safety_yes: '#FFD06C', + women_safety_no: '#FF3D3B', + unhygenic_seat: '#FFD06C', + unhygenic_bowl: '#FF3D3B', + unhygenic_wall: '#999999', + return_visits_yes: '#ff3d3b', + return_visits_no: '#FFD06C', + queues_over_20: '#ff3d3b', + queues_under_20: '#FFD06C' -} -let responsive_settings = { - gender_age_labels: 10, - dirty_toilets_labels: 10 -} + } + let responsive_settings = { + gender_age_labels: 10, + dirty_toilets_labels: 10 + } -document.addEventListener('DOMContentLoaded', function () { - // MEDIA QUERIES - const mediaQuery = window.matchMedia('(max-width: 768px)'); - function handleMediaQueryChange(e) { - if (e.matches) { - console.log('Screen is small (<=768px)'); + // MEDIA QUERIES - } else { - console.log('Screen is large (>768px)'); - responsive_settings.gender_age_labels = 16; - responsive_settings.dirty_toilets_labels = 16; - } - } + const mediaQuery = window.matchMedia('(max-width: 768px)'); - handleMediaQueryChange(mediaQuery); + function handleMediaQueryChange(e) { + if (e.matches) { + console.log('Screen is small (<=768px)'); - mediaQuery.addEventListener('change', handleMediaQueryChange); + } else { + console.log('Screen is large (>768px)'); + responsive_settings.gender_age_labels = 16; + responsive_settings.dirty_toilets_labels = 16; + } + } + handleMediaQueryChange(mediaQuery); + mediaQuery.addEventListener('change', handleMediaQueryChange); - gsap.registerPlugin(ScrollTrigger); - const background = document.querySelector('.scrolly-background'); - const offsetInPixels = window.innerHeight * 0.1; + gsap.registerPlugin(ScrollTrigger); - let currentBackgroundSrc = null; - let currentBackgroundColorClass = null; - let backgroundColorOverlay = document.querySelector('.scrolly-bg-color-overlay'); + const background = document.querySelector('.scrolly-background'); - function updateBackgroundImage(src) { + const offsetInPixels = window.innerHeight * 0.1; - if (!src || src === currentBackgroundSrc) { - return; - } + let currentBackgroundSrc = null; + let currentBackgroundColorClass = null; + let backgroundColorOverlay = document.querySelector('.scrolly-bg-color-overlay'); - const bgImage = document.querySelector('.scrolly-bg-image'); - - const overlayImage = document.createElement('img'); - overlayImage.src = src; - overlayImage.classList.add('scrolly-bg-image'); - overlayImage.style.position = 'absolute'; - overlayImage.style.top = '0'; - overlayImage.style.left = '0'; - overlayImage.style.width = '100%'; - overlayImage.style.height = '100%'; - overlayImage.style.objectFit = 'cover'; - overlayImage.style.zIndex = '-2'; - overlayImage.style.opacity = '0'; - - bgImage.parentElement.appendChild(overlayImage); - - gsap.to(overlayImage, { - opacity: 1, - duration: 0.5, - onComplete: () => { - bgImage.remove(); - - currentBackgroundSrc = src; - } - }); - } + function updateBackgroundImage(src) { - function updateBackgroundColorClass(newClass) { - if (newClass === currentBackgroundColorClass) { - return; - } + if (!src || src === currentBackgroundSrc) { + return; + } - if (currentBackgroundColorClass) { - backgroundColorOverlay.classList.remove(currentBackgroundColorClass); - } + const bgImage = document.querySelector('.scrolly-bg-image'); + + const overlayImage = document.createElement('img'); + overlayImage.src = src; + overlayImage.classList.add('scrolly-bg-image'); + overlayImage.style.position = 'absolute'; + overlayImage.style.top = '0'; + overlayImage.style.left = '0'; + overlayImage.style.width = '100%'; + overlayImage.style.height = '100%'; + overlayImage.style.objectFit = 'cover'; + overlayImage.style.zIndex = '-2'; + overlayImage.style.opacity = '0'; + + bgImage.parentElement.appendChild(overlayImage); + + gsap.to(overlayImage, { + opacity: 1, + duration: 0.5, + onComplete: () => { + bgImage.remove(); + + currentBackgroundSrc = src; + } + }); + } - if (newClass) { - backgroundColorOverlay.classList.add(newClass); - } + function updateBackgroundColorClass(newClass) { + if (newClass === currentBackgroundColorClass) { + return; + } - currentBackgroundColorClass = newClass; + if (currentBackgroundColorClass) { + backgroundColorOverlay.classList.remove(currentBackgroundColorClass); + } - } + if (newClass) { + backgroundColorOverlay.classList.add(newClass); + } - + currentBackgroundColorClass = newClass; - // THE SCROLLY + } - document.querySelectorAll('.scrolly-section').forEach((section, index) => { + - // DEBUG + // THE SCROLLY - const section_label = section.getAttribute('data-section-label'); + document.querySelectorAll('.scrolly-section').forEach((section, index) => { - if (DEBUG == true) { - let label = section.appendChild(document.createElement('div')); - label.classList.add('scrolly-section-label'); - label.textContent = index+1 + ': ' + section_label; - section.classList.add('debug'); - } + // DEBUG - const bg_src = section.getAttribute('data-background'); - const bg_color_class = section.getAttribute('data-section-color'); + const section_label = section.getAttribute('data-section-label'); - ScrollTrigger.create({ - trigger: section, - start: () => { - const backgroundRect = background.getBoundingClientRect(); - return `top ${backgroundRect.height + offsetInPixels}px`; - }, - end: () => { - const backgroundRect = background.getBoundingClientRect(); - return `top ${offsetInPixels + backgroundRect.height / 2}px`; - }, + if (DEBUG == true) { + let label = section.appendChild(document.createElement('div')); + label.classList.add('scrolly-section-label'); + label.textContent = index+1 + ': ' + section_label; + section.classList.add('debug'); + } - onEnter: () => { + const bg_src = section.getAttribute('data-background'); + const bg_color_class = section.getAttribute('data-section-color'); - // BACKGROUND TO BLACK + ScrollTrigger.create({ + trigger: section, + start: () => { + const backgroundRect = background.getBoundingClientRect(); + return `top ${backgroundRect.height + offsetInPixels}px`; + }, + end: () => { + const backgroundRect = background.getBoundingClientRect(); + return `top ${offsetInPixels + backgroundRect.height / 2}px`; + }, - // if (section_label == 'start') { - // document.body.classList.add('scrolly-showing'); - // } + onEnter: () => { - // if (section_label == 'conclusion') { - // document.body.classList.remove('scrolly-showing'); - // } + // BACKGROUND TO BLACK - if (bg_src) { - updateBackgroundImage(bg_src); - } + // if (section_label == 'start') { + // document.body.classList.add('scrolly-showing'); + // } - if (bg_color_class) { - updateBackgroundColorClass(bg_color_class); - } - }, - onLeaveBack: () => { - if (section_label == 'start' || section_label == 'conclusion') { - if (section_label == 'start') { - document.body.classList.remove('scrolly-showing'); - } - } + // if (section_label == 'conclusion') { + // document.body.classList.remove('scrolly-showing'); + // } - const prevSection = document.querySelectorAll('.scrolly-section')[index - 1]; - const prevBackground = prevSection ? prevSection.getAttribute('data-background') : null; - const prevColorClass = prevSection ? prevSection.getAttribute('data-section-color') : null; + if (bg_src) { + updateBackgroundImage(bg_src); + } - if (prevBackground) { - updateBackgroundImage(prevBackground); - } + if (bg_color_class) { + updateBackgroundColorClass(bg_color_class); + } + }, + onLeaveBack: () => { + if (section_label == 'start' || section_label == 'conclusion') { + if (section_label == 'start') { + document.body.classList.remove('scrolly-showing'); + } + } + + const prevSection = document.querySelectorAll('.scrolly-section')[index - 1]; + const prevBackground = prevSection ? prevSection.getAttribute('data-background') : null; + const prevColorClass = prevSection ? prevSection.getAttribute('data-section-color') : null; + + if (prevBackground) { + updateBackgroundImage(prevBackground); + } + + if (prevColorClass) { + updateBackgroundColorClass(prevColorClass); + } + } + }); + }); - if (prevColorClass) { - updateBackgroundColorClass(prevColorClass); - } - } - }); - }); + - + + + - - - + // END SCROLLY. UNSTICK - // END SCROLLY. UNSTICK + ScrollTrigger.create({ + trigger: '.scrolly-section[data-section-label="end"]', + start: () => { + const backgroundRect = background.getBoundingClientRect(); + return `top ${offsetInPixels}px`; + }, + onEnter: () => { - ScrollTrigger.create({ - trigger: '.scrolly-section[data-section-label="end"]', - start: () => { - const backgroundRect = background.getBoundingClientRect(); - return `top ${offsetInPixels}px`; - }, - onEnter: () => { + gsap.to('.scrolly-background', { + position: 'relative', + duration: 0.5, + ease: 'power1.inOut' + }); + }, + onLeaveBack: () => { - gsap.to('.scrolly-background', { - position: 'relative', - duration: 0.5, - ease: 'power1.inOut' + gsap.to('.scrolly-background', { + position: 'sticky', + duration: 0.5, + ease: 'power1.inOut' + }); + } }); - }, - onLeaveBack: () => { - gsap.to('.scrolly-background', { - position: 'sticky', - duration: 0.5, - ease: 'power1.inOut' - }); - } - }); + // SCROLLY CAPTIONS FADE INS - // SCROLLY CAPTIONS FADE INS + // gsap.utils.toArray('.scrolly-section .scrolly-caption-box').forEach((caption) => { + // gsap.to(caption, { + // opacity: 1, + // duration: 0.2, + // scrollTrigger: { + // trigger: caption, + // start: () => { + // const backgroundRect = background.getBoundingClientRect(); + // return `top ${backgroundRect.height + offsetInPixels}px`; + // }, + // end: () => { + // const backgroundRect = background.getBoundingClientRect(); + // return `top ${offsetInPixels + backgroundRect.height / 2}px`; + // }, + // scrub: true + // } + // }); + // }); - // gsap.utils.toArray('.scrolly-section .scrolly-caption-box').forEach((caption) => { - // gsap.to(caption, { - // opacity: 1, - // duration: 0.2, - // scrollTrigger: { - // trigger: caption, - // start: () => { - // const backgroundRect = background.getBoundingClientRect(); - // return `top ${backgroundRect.height + offsetInPixels}px`; - // }, - // end: () => { - // const backgroundRect = background.getBoundingClientRect(); - // return `top ${offsetInPixels + backgroundRect.height / 2}px`; - // }, - // scrub: true - // } - // }); - // }); + /// CUSTOMISATION STARTS HERE - /// CUSTOMISATION STARTS HERE + /// PINS - /// PINS + function pinSection(pinSection, startTrigger, endTrigger) { + ScrollTrigger.create({ + trigger: `.scrolly-section[data-section-label="${startTrigger}"]`, + start: () => { + return `top ${offsetInPixels}px`; + }, + endTrigger: `.scrolly-section[data-section-label="${endTrigger}"]`, + end: 'top center', + pin: `.scrolly-section[data-section-label="${pinSection}"]`, + pinSpacing: false, + scrub: true - function pinSection(pinSection, startTrigger, endTrigger) { - ScrollTrigger.create({ - trigger: `.scrolly-section[data-section-label="${startTrigger}"]`, - start: () => { - return `top ${offsetInPixels}px`; - }, - endTrigger: `.scrolly-section[data-section-label="${endTrigger}"]`, - end: 'top center', - pin: `.scrolly-section[data-section-label="${pinSection}"]`, - pinSpacing: false, - scrub: true + }); + } - }); - } + document.querySelectorAll('.scrolly-section[data-unpin-section]').forEach((section, index) => { - document.querySelectorAll('.scrolly-section[data-unpin-section]').forEach((section, index) => { + let section_label = section.getAttribute('data-section-label'); + let endTrigger = section.getAttribute('data-unpin-section'); - let section_label = section.getAttribute('data-section-label'); - let endTrigger = section.getAttribute('data-unpin-section'); + pinSection(section_label, section_label, endTrigger); - pinSection(section_label, section_label, endTrigger); + }); - }); + + + - - - - - ScrollTrigger.create({ - trigger: '.scrolly-section[data-section-label="conclusion"]', - start: () => { - const backgroundRect = background.getBoundingClientRect(); - return `top ${offsetInPixels}px`; - }, - pin: '.scrolly-section[data-section-label="conclusion"]', - pinSpacing: false, - scrub: true, - }); + ScrollTrigger.create({ + trigger: '.scrolly-section[data-section-label="conclusion"]', + start: () => { + const backgroundRect = background.getBoundingClientRect(); + return `top ${offsetInPixels}px`; + }, + pin: '.scrolly-section[data-section-label="conclusion"]', + pinSpacing: false, + scrub: true, + }); - /// ZOOMS AND PANS + /// ZOOMS AND PANS - function zoomAndPan(startTrigger, endTrigger, startZoom, startX, startY, endZoom, endX, endY) { + function zoomAndPan(startTrigger, endTrigger, startZoom, startX, startY, endZoom, endX, endY) { + + gsap.timeline({ + scrollTrigger: { + trigger: `.scrolly-section[data-section-label='${startTrigger}']`, + start: () => { + const backgroundRect = background.getBoundingClientRect(); + return `top ${backgroundRect.height + offsetInPixels}px`; + }, + endTrigger: `.scrolly-section[data-section-label='${endTrigger}']`, + end: 'top center', + scrub: true + } + }) + .from('.scrolly-bg-container', { + scale: startZoom, + x: startX, + y: startY, + duration: 0 + }) + .to('.scrolly-bg-container', { + scale: endZoom, + x: endX, + y: endY, + duration: 10 + }); - gsap.timeline({ - scrollTrigger: { - trigger: `.scrolly-section[data-section-label='${startTrigger}']`, - start: () => { - const backgroundRect = background.getBoundingClientRect(); - return `top ${backgroundRect.height + offsetInPixels}px`; - }, - endTrigger: `.scrolly-section[data-section-label='${endTrigger}']`, - end: 'top center', - scrub: true } - }) - .from('.scrolly-bg-container', { - scale: startZoom, - x: startX, - y: startY, - duration: 0 - }) - .to('.scrolly-bg-container', { - scale: endZoom, - x: endX, - y: endY, - duration: 10 - }); - } + // zoomAndPan('start', 'they-visited', 1, 0, 0, 1.1, 0, 0); + // zoomAndPan('they-visited', '313-users', 1, 0, 0, 1.4, -100, 0); + // zoomAndPan('gender-breakdown', 'outdoor-facilities', 1.4, 0, 0, 1, 0, 0); + // zoomAndPan('outdoor-facilities', 'disabled-access', 1.2, 0, 0, 1, 0, 0); + + + /// ANNOTATIONS + + + + function fadeOutChart(label, trigger) { + gsap.to( + `.${label} .scrolly-annotation-inner`, + { + opacity: 0, + ease: "none", + scrollTrigger: { + scrub: true, + trigger: `.scrolly-section[data-section-label='${trigger}']`, + start: () => { + const backgroundRect = background.getBoundingClientRect(); + return `top ${backgroundRect.height + offsetInPixels}px`; + }, + end: () => { + const backgroundRect = background.getBoundingClientRect(); + return `top ${offsetInPixels}px`; + } + } + }); + } + - // zoomAndPan('start', 'they-visited', 1, 0, 0, 1.1, 0, 0); - // zoomAndPan('they-visited', '313-users', 1, 0, 0, 1.4, -100, 0); - // zoomAndPan('gender-breakdown', 'outdoor-facilities', 1.4, 0, 0, 1, 0, 0); - // zoomAndPan('outdoor-facilities', 'disabled-access', 1.2, 0, 0, 1, 0, 0); + function annotationFadeIn(label, timeIn, offset, speed) { + let selector = `.scrolly-slide-annotation[data-annotation-label='${label}']`; - /// ANNOTATIONS + let timeInSlide = document.querySelector(`.scrolly-section[data-section-label='${timeIn}']`); + if(timeInSlide == null) { + console.log('Slide not found: ' + timeIn); + } + + let timeInOffset = timeInSlide.getBoundingClientRect().top; + const backgroundRect = background.getBoundingClientRect(); - function fadeOutChart(label, trigger) { - gsap.to( - `.${label} .scrolly-annotation-inner`, - { - opacity: 0, - ease: "none", - scrollTrigger: { - scrub: true, - trigger: `.scrolly-section[data-section-label='${trigger}']`, - start: () => { - const backgroundRect = background.getBoundingClientRect(); - return `top ${backgroundRect.height + offsetInPixels}px`; - }, - end: () => { - const backgroundRect = background.getBoundingClientRect(); - return `top ${offsetInPixels}px`; + gsap.fromTo( + selector, + { opacity: 0 }, + { + opacity: 1, + ease: "none", + scrollTrigger: { + scrub: true, + start: `${timeInOffset + offset} ${offsetInPixels + backgroundRect.height}`, + end: `${timeInOffset + offset + speed} ${offsetInPixels}`, + } } - } - }); - } - + ); - function annotationFadeIn(label, timeIn, offset, speed) { + } - let selector = `.scrolly-slide-annotation[data-annotation-label='${label}']`; + function annotationFadeOut(label, timeOut, offset, speed) { - let timeInSlide = document.querySelector(`.scrolly-section[data-section-label='${timeIn}']`); + let selector = `.scrolly-slide-annotation[data-annotation-label='${label}']`; - if(timeInSlide == null) { - console.log('Slide not found: ' + timeIn); - } - - let timeInOffset = timeInSlide.getBoundingClientRect().top; - - const backgroundRect = background.getBoundingClientRect(); - - gsap.fromTo( - selector, - { opacity: 0 }, - { - opacity: 1, - ease: "none", - scrollTrigger: { - scrub: true, - start: `${timeInOffset + offset} ${offsetInPixels + backgroundRect.height}`, - end: `${timeInOffset + offset + speed} ${offsetInPixels}`, + let timeOutSlide = document.querySelector(`.scrolly-section[data-section-label='${timeOut}']`); + + if(timeOutSlide == null) { + console.log('Slide not found: ' + timeOut); } + + let timeOutOffset = timeOutSlide.getBoundingClientRect().top; + + const backgroundRect = background.getBoundingClientRect(); + + // Fade out + gsap.fromTo( + selector, + { opacity: 1 }, + { + opacity: 0, + ease: "none", + scrollTrigger: { + scrub: true, + start: `${timeOutOffset + offset} ${offsetInPixels + backgroundRect.height}`, + end: `${timeOutOffset + offset + speed} ${offsetInPixels}`, + }, + immediateRender: false + } + ); + } - ); - } + document.querySelectorAll('.scrolly-slide-annotation[data-annotation-in]').forEach((annotation, index) => { - function annotationFadeOut(label, timeOut, offset, speed) { + let annotation_label = annotation.getAttribute('data-annotation-label'); + let annotation_time_in = annotation.getAttribute('data-annotation-in'); + let annotation_offset = annotation.getAttribute('data-annotation-offset') ? parseFloat(annotation.getAttribute('data-annotation-offset')) : 0; + let annotation_speed = annotation.getAttribute('data-annotation-speed') ? parseFloat(annotation.getAttribute('data-annotation-speed')) : 0; - let selector = `.scrolly-slide-annotation[data-annotation-label='${label}']`; + annotationFadeIn(annotation_label, annotation_time_in, annotation_offset, annotation_speed); - let timeOutSlide = document.querySelector(`.scrolly-section[data-section-label='${timeOut}']`); + }); - if(timeOutSlide == null) { - console.log('Slide not found: ' + timeOut); - } + document.querySelectorAll('.scrolly-slide-annotation[data-annotation-in]').forEach((annotation, index) => { - let timeOutOffset = timeOutSlide.getBoundingClientRect().top; - - const backgroundRect = background.getBoundingClientRect(); - - // Fade out - gsap.fromTo( - selector, - { opacity: 1 }, - { - opacity: 0, - ease: "none", - scrollTrigger: { - scrub: true, - start: `${timeOutOffset + offset} ${offsetInPixels + backgroundRect.height}`, - end: `${timeOutOffset + offset + speed} ${offsetInPixels}`, - }, - immediateRender: false - } - ); + let annotation_label = annotation.getAttribute('data-annotation-label'); + let annotation_time_out = annotation.getAttribute('data-annotation-out'); + let annotation_offset_out = annotation.getAttribute('data-annotation-offset-out') ? parseFloat(annotation.getAttribute('data-annotation-offset-out')) : 0; + let annotation_speed_out = annotation.getAttribute('data-annotation-speed-out') ? parseFloat(annotation.getAttribute('data-annotation-speed-out')) : 0; - } + annotationFadeOut(annotation_label, annotation_time_out, annotation_offset_out, annotation_speed_out); - document.querySelectorAll('.scrolly-slide-annotation[data-annotation-in]').forEach((annotation, index) => { + }); + - let annotation_label = annotation.getAttribute('data-annotation-label'); - let annotation_time_in = annotation.getAttribute('data-annotation-in'); - let annotation_offset = annotation.getAttribute('data-annotation-offset') ? parseFloat(annotation.getAttribute('data-annotation-offset')) : 0; - let annotation_speed = annotation.getAttribute('data-annotation-speed') ? parseFloat(annotation.getAttribute('data-annotation-speed')) : 0; - annotationFadeIn(annotation_label, annotation_time_in, annotation_offset, annotation_speed); - }); - document.querySelectorAll('.scrolly-slide-annotation[data-annotation-in]').forEach((annotation, index) => { - let annotation_label = annotation.getAttribute('data-annotation-label'); - let annotation_time_out = annotation.getAttribute('data-annotation-out'); - let annotation_offset_out = annotation.getAttribute('data-annotation-offset-out') ? parseFloat(annotation.getAttribute('data-annotation-offset-out')) : 0; - let annotation_speed_out = annotation.getAttribute('data-annotation-speed-out') ? parseFloat(annotation.getAttribute('data-annotation-speed-out')) : 0; - annotationFadeOut(annotation_label, annotation_time_out, annotation_offset_out, annotation_speed_out); + // CHARTS + + // CHART DATA + + const female_age_distribution = [21, 57, 36, 33].map((k) => -k); + const male_age_distribution = [27, 55, 45, 38]; - }); + const inside_outside_blank = [ + { + label: '1', + value: 0 + }, + { + label: '2', + value: 0 + }, + { + label: '3', + value: 0 + } + ] + + const fs_inside_outside = [ + { + label: '1', + value: 0 + }, { + label: '2', + value: 20, + },{ + label: '3', + value: 0 + } + ]; + + const lp_inside_outside = [ + { + label: '1', + value: 8 + }, { + label: '2', + value: 0, + },{ + label: '3', + value: 12 + } + ]; + + const kzn_inside_outside = [ + { + label: '1', + value: 9 + }, { + label: '2', + value: 7, + },{ + label: '3', + value: 4 + } + ]; + + const fs_disabled_access = [ + { + label: '1', + value: 0 + }, { + label: '2', + value: 4, + },{ + label: '3', + value: 16 + } + ]; + + const lp_disabled_access = [ + { + label: '1', + value: 0 + }, { + label: '2', + value: 16, + },{ + label: '3', + value: 6 + } + ]; + + const kzn_disabled_access = [ + { + label: '1', + value: 0 + }, { + label: '2', + value: 16, + },{ + label: '3', + value: 4 + } + ]; + + const fs_unusable_toilets = [ + { + label: '1', + value: 0 + }, { + label: '2', + value: 44, + },{ + label: '3', + value: 65 + } + ]; + + const lp_unusable_toilets = [ + { + label: '1', + value: 0 + }, { + label: '2', + value: 6, + },{ + label: '3', + value: 95 + } + ]; + + const kzn_unusable_toilets = [ + { + label: '', + value: 0 + }, { + label: 'Yes', + value: 27, + },{ + label: 'No', + value: 76 + } + ]; + + const fs_toilet_paper = [ + { + label: '', + value: 0 + }, { + label: 'Yes', + value: 3, + },{ + label: 'No', + value: 7 + } + ]; + + const lp_toilet_paper = [ + { + label: '', + value: 0 + }, { + label: 'Yes', + value: 0, + },{ + label: 'No', + value: 10 + } + ]; + + const kzn_toilet_paper = [ + { + label: '', + value: 0 + }, { + label: 'Yes', + value: 3, + },{ + label: 'No', + value: 7 + } + ]; + + const fs_no_sanitiser = [ + { + label: '', + value: 0 + }, { + label: 'Yes', + value: 6, + },{ + label: 'No', + value: 14 + } + ]; + + const lp_no_sanitiser = [ + { + label: '', + value: 0 + }, { + label: 'Yes', + value: 2, + },{ + label: 'No', + value: 18 + } + ]; + + const kzn_no_sanitiser = [ + { + label: '', + value: 0 + }, { + label: 'Yes', + value: 7, + },{ + label: 'No', + value: 13 + } + ]; + + const unhygenic_toilets_seat = [2, 2, 9]; + const unhygenic_toilets_bowl = [3, 4, 8]; + const unhygenic_toilets_wall = [0, 2, 3]; + const queues_21_50 = [7, 2]; + const queues_51_70 = [11, 3]; + const queues_71_100 = [11, 3]; + const queues_100 = [11, 12]; - // CHARTS + const safety_chart_female = [78, 69]; + const safety_chart_male = [67, 98]; - // CHART DATA + const women_safety_yes = [13, 46, 27, 36]; + const women_safety_no = [24, 29, 32, 26]; - const female_age_distribution = [21, 57, 36, 33].map((k) => -k); - const male_age_distribution = [27, 55, 45, 38]; + const return_visits_fs = [72, 37]; + const return_visits_lp = [64, 37]; + const return_visits_kzn = [13, 10]; - + let activeData = { + fs: fs_inside_outside, + lp: lp_inside_outside, + kzn: kzn_inside_outside + }; + - const inside_outside_blank = [ - { - label: '1', - value: 0 - }, - { - label: '2', - value: 0 - }, - { - label: '3', - value: 0 - } - ] - - const fs_inside_outside = [ - { - label: '1', - value: 0 - }, { - label: '2', - value: 20, - },{ - label: '3', - value: 0 - } - ]; - - const lp_inside_outside = [ - { - label: '1', - value: 8 - }, { - label: '2', - value: 0, - },{ - label: '3', - value: 12 - } - ]; - - const kzn_inside_outside = [ - { - label: '1', - value: 9 - }, { - label: '2', - value: 7, - },{ - label: '3', - value: 4 - } - ]; - - const fs_disabled_access = [ - { - label: '1', - value: 0 - }, { - label: '2', - value: 4, - },{ - label: '3', - value: 16 - } - ]; - - const lp_disabled_access = [ - { - label: '1', - value: 0 - }, { - label: '2', - value: 16, - },{ - label: '3', - value: 6 - } - ]; - - const kzn_disabled_access = [ - { - label: '1', - value: 0 - }, { - label: '2', - value: 16, - },{ - label: '3', - value: 4 - } - ]; - - const fs_unusable_toilets = [ - { - label: '1', - value: 0 - }, { - label: '2', - value: 44, - },{ - label: '3', - value: 65 - } - ]; - - const lp_unusable_toilets = [ - { - label: '1', - value: 0 - }, { - label: '2', - value: 6, - },{ - label: '3', - value: 95 - } - ]; - - const kzn_unusable_toilets = [ - { - label: '', - value: 0 - }, { - label: 'Yes', - value: 27, - },{ - label: 'No', - value: 76 - } - ]; - - const fs_toilet_paper = [ - { - label: '', - value: 0 - }, { - label: 'Yes', - value: 3, - },{ - label: 'No', - value: 7 - } - ]; - - const lp_toilet_paper = [ - { - label: '', - value: 0 - }, { - label: 'Yes', - value: 0, - },{ - label: 'No', - value: 10 - } - ]; - - const kzn_toilet_paper = [ - { - label: '', - value: 0 - }, { - label: 'Yes', - value: 3, - },{ - label: 'No', - value: 7 - } - ]; - - const fs_no_sanitiser = [ - { - label: '', - value: 0 - }, { - label: 'Yes', - value: 6, - },{ - label: 'No', - value: 14 - } - ]; - - const lp_no_sanitiser = [ - { - label: '', - value: 0 - }, { - label: 'Yes', - value: 2, - },{ - label: 'No', - value: 18 - } - ]; - - const kzn_no_sanitiser = [ - { - label: '', - value: 0 - }, { - label: 'Yes', - value: 7, - },{ - label: 'No', - value: 13 - } - ]; - - - - - const unhygenic_toilets_seat = [2, 2, 9]; - const unhygenic_toilets_bowl = [3, 4, 8]; - const unhygenic_toilets_wall = [0, 2, 3]; - - const queues_21_50 = [7, 2]; - const queues_51_70 = [11, 3]; - const queues_71_100 = [11, 3]; - const queues_100 = [11, 12]; - - const safety_chart_female = [78, 69]; - const safety_chart_male = [67, 98]; - - const women_safety_yes = [13, 46, 27, 36]; - const women_safety_no = [24, 29, 32, 26]; - - const return_visits_fs = [72, 37]; - const return_visits_lp = [64, 37]; - const return_visits_kzn = [13, 10]; - - let activeData = { - fs: fs_inside_outside, - lp: lp_inside_outside, - kzn: kzn_inside_outside - }; - - - // POPULATION CHART - - - function drawChart(id, data, title, colors = [chart_colors.donut1, chart_colors.donut2, chart_colors.donut3]) { - const container = d3.select(`#${id}`); - const rect = container.node().getBoundingClientRect(); - const width = rect.width; - const height = rect.height; - const margin = { top: 0, right: 0, bottom: 0, left: 0 }; - const radius = Math.min(width, height) / 2 - Math.max(margin.top, margin.right); - - // Set color scale - let color = d3.scaleOrdinal() - .domain(data.map(d => d.label)) - .range(colors); - - // Check if SVG exists - let svg = container.select('svg'); - if (svg.empty()) { - // Create SVG element - svg = container - .append('svg') - .attr('width', '100%') - .attr('height', '100%') - .attr('viewBox', `0 0 ${width} ${height}`) - .attr('preserveAspectRatio', 'xMinYMin meet') - .append('g') - .attr('transform', `translate(${width / 2}, ${height / 2})`); - - // Add title - svg.append('text') - .attr('class', 'chart-title') - .attr('text-anchor', 'middle') - .attr('fill', '#fff') - .attr('font-size', '0.9em') - .attr('dy', '0.5em') - .attr('font-weight', 'bold') - .text(title); - } else { - svg = svg.select('g'); - svg.attr('transform', `translate(${width / 2}, ${height / 2})`); - } - - // Generate the pie and arcs - const pie = d3.pie() - .value(d => d.value) - .sort(null); - - const arc = d3.arc() - .innerRadius(radius * 0.5) - .outerRadius(radius); - - // Bind data to arcs - const arcs = svg.selectAll('.arc') - .data(pie(data), d => d.data.label); - - // Exit - arcs.exit() - .remove(); - - // Enter - const arcsEnter = arcs.enter() - .append('g') - .attr('class', 'arc'); - - arcsEnter.append('path') - .attr('fill', d => color(d.data.label)) - .each(function (d) { this._current = d; }); - - // Update - arcsEnter.merge(arcs).select('path') - .transition() - .duration(750) - .attrTween('d', function (d) { - const interpolate = d3.interpolate(this._current, d); - this._current = interpolate(1); - return function (t) { - return arc(interpolate(t)); - }; - }); - - // Add and update points - arcsEnter.merge(arcs).each(function (d) { - const numPoints = 5; - - const pointsData = []; - for (let i = 1; i <= numPoints; i++) { - const t = i / (numPoints + 1); - const angle = d.startAngle + t * (d.endAngle - d.startAngle); - const innerRadius = arc.innerRadius()(d); - const outerRadius = arc.outerRadius()(d); - const middleRadius = (innerRadius + outerRadius) / 2; - const x = middleRadius * Math.cos(angle - Math.PI / 2); - const y = middleRadius * Math.sin(angle - Math.PI / 2); - pointsData.push({ x, y }); - } - - let pointsGroup = d3.select(this).select('.points-group'); - if (pointsGroup.empty()) { - pointsGroup = d3.select(this).append('g').attr('class', 'points-group'); + // POPULATION CHART + + + function drawChart(id, data, title, colors = [chart_colors.donut1, chart_colors.donut2, chart_colors.donut3]) { + const container = d3.select(`#${id}`); + const rect = container.node().getBoundingClientRect(); + const width = rect.width; + const height = rect.height; + const margin = { top: 0, right: 0, bottom: 0, left: 0 }; + const radius = Math.min(width, height) / 2 - Math.max(margin.top, margin.right); + + // Set color scale + let color = d3.scaleOrdinal() + .domain(data.map(d => d.label)) + .range(colors); + + // Check if SVG exists + let svg = container.select('svg'); + if (svg.empty()) { + // Create SVG element + svg = container + .append('svg') + .attr('width', '100%') + .attr('height', '100%') + .attr('viewBox', `0 0 ${width} ${height}`) + .attr('preserveAspectRatio', 'xMinYMin meet') + .append('g') + .attr('transform', `translate(${width / 2}, ${height / 2})`); + + // Add title + svg.append('text') + .attr('class', 'chart-title') + .attr('text-anchor', 'middle') + .attr('fill', '#fff') + .attr('font-size', '0.9em') + .attr('dy', '0.5em') + .attr('font-weight', 'bold') + .text(title); + } else { + svg = svg.select('g'); + svg.attr('transform', `translate(${width / 2}, ${height / 2})`); + } + + // Generate the pie and arcs + const pie = d3.pie() + .value(d => d.value) + .sort(null); + + const arc = d3.arc() + .innerRadius(radius * 0.5) + .outerRadius(radius); + + // Bind data to arcs + const arcs = svg.selectAll('.arc') + .data(pie(data), d => d.data.label); + + // Exit + arcs.exit() + .remove(); + + // Enter + const arcsEnter = arcs.enter() + .append('g') + .attr('class', 'arc'); + + arcsEnter.append('path') + .attr('fill', d => color(d.data.label)) + .each(function (d) { this._current = d; }); + + // Update + arcsEnter.merge(arcs).select('path') + .transition() + .duration(750) + .attrTween('d', function (d) { + const interpolate = d3.interpolate(this._current, d); + this._current = interpolate(1); + return function (t) { + return arc(interpolate(t)); + }; + }); + + // Add and update points + arcsEnter.merge(arcs).each(function (d) { + const numPoints = 5; + + const pointsData = []; + for (let i = 1; i <= numPoints; i++) { + const t = i / (numPoints + 1); + const angle = d.startAngle + t * (d.endAngle - d.startAngle); + const innerRadius = arc.innerRadius()(d); + const outerRadius = arc.outerRadius()(d); + const middleRadius = (innerRadius + outerRadius) / 2; + const x = middleRadius * Math.cos(angle - Math.PI / 2); + const y = middleRadius * Math.sin(angle - Math.PI / 2); + pointsData.push({ x, y }); + } + + let pointsGroup = d3.select(this).select('.points-group'); + if (pointsGroup.empty()) { + pointsGroup = d3.select(this).append('g').attr('class', 'points-group'); + } + + const circles = pointsGroup.selectAll('circle') + .data(pointsData); + + circles.exit().remove(); + + circles.enter() + .append('circle') + .attr('class', (d, i) => `point-${i}`) + .attr('r', 1) + .attr('fill', '#000') + .attr('cx', d => d.x) + .attr('cy', d => d.y); + + circles.transition() + .duration(750) + .attr('cx', d => d.x) + .attr('cy', d => d.y); + }); } - - const circles = pointsGroup.selectAll('circle') - .data(pointsData); - - circles.exit().remove(); - - circles.enter() - .append('circle') - .attr('class', (d, i) => `point-${i}`) - .attr('r', 1) - .attr('fill', '#000') - .attr('cx', d => d.x) - .attr('cy', d => d.y); - - circles.transition() - .duration(750) - .attr('cx', d => d.x) - .attr('cy', d => d.y); - }); - } - - - function updateChart(id, newData, radius, colors = [chart_colors.donut1, chart_colors.donut2, chart_colors.donut3]) { - const container = d3.select(`#${id}`); - const svg = container.select('svg').select('g'); - - // Set color scale - const color = d3.scaleOrdinal() - .domain(newData.map(d => d.label)) - .range(colors); - - // Generate the pie - const pie = d3.pie() - .value(d => d.value) - .sort(null); - - // Generate the arcs - const arc = d3.arc() - .innerRadius(radius * 0.5) - .outerRadius(radius); - - // Bind data - const arcs = svg.selectAll('.arc') - .data(pie(newData), d => d.data.label); - - // Exit - arcs.exit().remove(); - - // Enter - const arcsEnter = arcs.enter() - .append('g') - .attr('class', 'arc'); - - arcsEnter.append('path') - .attr('fill', d => color(d.data.label)) - .each(function (d) { - this._current = { startAngle: d.startAngle, endAngle: d.startAngle }; - }); - - // Update - arcsEnter.merge(arcs).select('path') - .transition() - .duration(750) - .attrTween('d', function (d) { - const interpolate = d3.interpolate(this._current, d); - this._current = interpolate(1); - return function (t) { - return arc(interpolate(t)); - }; - }); - - // Add and update points - arcsEnter.merge(arcs).each(function (d) { - const numPoints = 5; - - const pointsData = []; - for (let i = 1; i <= numPoints; i++) { - const t = i / (numPoints + 1); - const angle = d.startAngle + t * (d.endAngle - d.startAngle); - const innerRadius = arc.innerRadius()(d); - const outerRadius = arc.outerRadius()(d); - const middleRadius = (innerRadius + outerRadius) / 2; - const x = middleRadius * Math.cos(angle - Math.PI / 2); - const y = middleRadius * Math.sin(angle - Math.PI / 2); - pointsData.push({ x, y }); + + + function updateChart(id, newData, radius, colors = [chart_colors.donut1, chart_colors.donut2, chart_colors.donut3]) { + const container = d3.select(`#${id}`); + const svg = container.select('svg').select('g'); + + // Set color scale + const color = d3.scaleOrdinal() + .domain(newData.map(d => d.label)) + .range(colors); + + // Generate the pie + const pie = d3.pie() + .value(d => d.value) + .sort(null); + + // Generate the arcs + const arc = d3.arc() + .innerRadius(radius * 0.5) + .outerRadius(radius); + + // Bind data + const arcs = svg.selectAll('.arc') + .data(pie(newData), d => d.data.label); + + // Exit + arcs.exit().remove(); + + // Enter + const arcsEnter = arcs.enter() + .append('g') + .attr('class', 'arc'); + + arcsEnter.append('path') + .attr('fill', d => color(d.data.label)) + .each(function (d) { + this._current = { startAngle: d.startAngle, endAngle: d.startAngle }; + }); + + // Update + arcsEnter.merge(arcs).select('path') + .transition() + .duration(750) + .attrTween('d', function (d) { + const interpolate = d3.interpolate(this._current, d); + this._current = interpolate(1); + return function (t) { + return arc(interpolate(t)); + }; + }); + + // Add and update points + arcsEnter.merge(arcs).each(function (d) { + const numPoints = 5; + + const pointsData = []; + for (let i = 1; i <= numPoints; i++) { + const t = i / (numPoints + 1); + const angle = d.startAngle + t * (d.endAngle - d.startAngle); + const innerRadius = arc.innerRadius()(d); + const outerRadius = arc.outerRadius()(d); + const middleRadius = (innerRadius + outerRadius) / 2; + const x = middleRadius * Math.cos(angle - Math.PI / 2); + const y = middleRadius * Math.sin(angle - Math.PI / 2); + pointsData.push({ x, y }); + } + + let pointsGroup = d3.select(this).select('.points-group'); + if (pointsGroup.empty()) { + pointsGroup = d3.select(this).append('g').attr('class', 'points-group'); + } + + const circles = pointsGroup.selectAll('circle') + .data(pointsData); + + circles.exit().remove(); + + circles.enter() + .append('circle') + .attr('class', (d, i) => `point-${i}`) + .attr('r', 1) + .attr('fill', '#000') + .attr('cx', d => d.x) + .attr('cy', d => d.y); + + circles.transition() + .duration(750) + .attr('cx', d => d.x) + .attr('cy', d => d.y); + }); } - - let pointsGroup = d3.select(this).select('.points-group'); - if (pointsGroup.empty()) { - pointsGroup = d3.select(this).append('g').attr('class', 'points-group'); + + + + + + + function updateAllCharts() { + const rect = d3.select('#fs').node().getBoundingClientRect(); + const radius = Math.min(rect.width, rect.height) / 2; + + updateChart('fs', activeData.fs, radius); + updateChart('lp', activeData.lp, radius); + updateChart('kzn', activeData.kzn, radius); } - - const circles = pointsGroup.selectAll('circle') - .data(pointsData); - - circles.exit().remove(); - - circles.enter() - .append('circle') - .attr('class', (d, i) => `point-${i}`) - .attr('r', 1) - .attr('fill', '#000') - .attr('cx', d => d.x) - .attr('cy', d => d.y); - - circles.transition() - .duration(750) - .attr('cx', d => d.x) - .attr('cy', d => d.y); - }); - } - - - - - - - function updateAllCharts() { - const rect = d3.select('#fs').node().getBoundingClientRect(); - const radius = Math.min(rect.width, rect.height) / 2; - - updateChart('fs', activeData.fs, radius); - updateChart('lp', activeData.lp, radius); - updateChart('kzn', activeData.kzn, radius); - } - - // on pressing a keyboard key (for debugging) - document.addEventListener('keydown', function (event) { - if (event.key === 'ArrowRight') { - console.log('hey'); - const rect = d3.select('#fs').node().getBoundingClientRect(); - const radius = Math.min(rect.width, rect.height) / 2; - - console.log(fs_inside_outside); - - updateChart('fs', fs_inside_outside, radius); - updateChart('lp', lp_inside_outside, radius); - updateChart('kzn', kzn_inside_outside, radius); - } - }); - - + // on pressing a keyboard key (for debugging) + document.addEventListener('keydown', function (event) { + if (event.key === 'ArrowRight') { + console.log('hey'); + const rect = d3.select('#fs').node().getBoundingClientRect(); + const radius = Math.min(rect.width, rect.height) / 2; - + console.log(fs_inside_outside); - function add_label(chart, slice_id, point_id, hour, anchor, text, line_config = {}, text_config = {}) { + updateChart('fs', fs_inside_outside, radius); + updateChart('lp', lp_inside_outside, radius); + updateChart('kzn', kzn_inside_outside, radius); + } + }); + + + - let line_config_set = { - class_name: line_config.class_name || '', - length: line_config.length || 80, - color: line_config.color || 'black', - thickness: line_config.thickness || 1 - } - - let text_config_set = { - class_name: text_config.class_name || '', - color: text_config.color || 'black', - align: text_config.align || 'left' - } - // Get the point element - let point = d3.select(`#${chart} .${slice_id} .point-${point_id}`); - - // Get the bounding box of the point relative to the chart container - let chartNode = d3.select(`#${chart}`).node(); - let chartRect = chartNode.getBoundingClientRect(); - let pointRect = point.node().getBoundingClientRect(); - - // Calculate the position relative to the chart container - let relativeX = pointRect.left - chartRect.left; - let relativeY = pointRect.top - chartRect.top; - - // Existing line code - let angleDeg = (hour * 30) - 90; - let length = line_config_set.length; - let angleRad = angleDeg * (Math.PI / 180); - let dx = length * Math.cos(angleRad); - let dy = length * Math.sin(angleRad); - let endX = relativeX + dx; - let endY = relativeY + dy; - - let anchors = { - 'tl': '0%, 0%', - 'tc': '-50%, 0%', - 'tr': '-100%, 0%', - 'bl': '0%, -100%', - 'bc': '-50%, -100%', - 'br': '-100%, -100%' - }; - - // Append the line to the chart container - d3.select(`#${chart}`).append('div') - .attr('class', `scrolly-annotation-line ${line_config_set.class_name}`) - .style('top', `${relativeY}px`) - .style('left', `${relativeX}px`) - .style('width', `${line_config_set.length}px`) - .style('height', `${line_config_set.thickness}px`) - .style('background-color', line_config_set.color) - .style('position', 'absolute') - .style('transform-origin', 'top left') - .style('transform', `rotate(${angleDeg}deg)`); - - // Calculate text position with offset - let textOffset = 5; - let textX = endX + textOffset * Math.cos(angleRad); - let textY = endY + textOffset * Math.sin(angleRad); - - // Append the text to the chart container - d3.select(`#${chart}`).append('div') - .attr('class', `scrolly-annotation-text ${text_config_set.class_name}`) - .style('position', 'absolute') - .style('left', `${textX}px`) - .style('top', `${textY}px`) - .style('color', text_config_set.color) - .style('transform', `translate(${anchors[anchor]})`) - .style('text-align', text_config_set.align) - .html(text); - } - - - drawChart('fs', inside_outside_blank, 'FS'); - - drawChart('lp', inside_outside_blank, 'LP'); - - drawChart('kzn', inside_outside_blank, 'KZN'); - - - // add_label('kzn', 'category-a', 3, 4, 'tl', 'text here
how are you?', {color: '#fff'}, {align: 'left', color: '#fff'}); - - - // UNHYGENIC TOILETS - - - const toilet_data = [ - {label: 'FS', Seat: 0, "Toilet bowl": 0, Wall: 0}, - {label: 'KZN', Seat: 0, "Toilet bowl": 0, Wall: 0}, - {label: 'LP', Seat: 0, "Toilet bowl": 0, Wall: 0} - ]; - - const colors = { - Seat: '#ff5733', // Replace with scrolly_chart_colors.unhygenic_seat - "Toilet bowl": '#33cfff', // Replace with scrolly_chart_colors.unhygenic_bowl - Wall: '#bada55' // Replace with scrolly_chart_colors.unhygenic_wall - }; - - const margin = {top: 20, right: 20, bottom: 20, left: 100}; - const width = 800 - margin.left - margin.right; - const height = 400 - margin.top - margin.bottom; - - const svg = d3.select('#unhygenic-toilets-chart') - .append('svg') - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', `translate(${margin.left},${margin.top})`); - - const yScale = d3.scaleBand() - .domain(toilet_data.map(d => d.label)) - .range([0, height]) - .padding(0.1); - - const xScale = d3.scaleLinear() - .domain([0, d3.max(toilet_data, d => d.Seat + d["Toilet bowl"] + d.Wall)]) - .range([0, width]); - - const stack = d3.stack() - .keys(['Seat', 'Toilet bowl', 'Wall']) - .order(d3.stackOrderNone) - .offset(d3.stackOffsetNone); - - const stackedData = stack(toilet_data); - - const groups = svg.selectAll('.group') - .data(stackedData) - .enter() - .append('g') - .attr('fill', d => colors[d.key]); - - groups.selectAll('rect') - .data(d => d) - .enter() - .append('rect') - .attr('y', d => yScale(d.data.label)) - .attr('x', d => xScale(d[0])) - .attr('width', d => xScale(d[1]) - xScale(d[0])) - .attr('height', yScale.bandwidth()); - - svg.append('g') - .call(d3.axisLeft(yScale)) - .selectAll('text') - .style('font-size', '14px'); - - svg.append('g') - .attr('transform', `translate(0,${height})`) - .call(d3.axisBottom(xScale)) - .selectAll('text') - .style('display', 'none'); - - - - - - - - // DOUGHNUT CHARTS - - - - - // WOMEN SAFETY CHART - - - // RETURN VISIT - - - - - - - - - - // PROVINCE BREAKDOWN CHART ANIMATION - function fadeChart(chartId, opacity) { - d3.select(`${chartId}`) - .transition() - .duration(500) - .style('opacity', opacity); - } - - - // Inside/Outside Dataset - ScrollTrigger.create({ - trigger: ".scrolly-section[data-section-label='outdoor-facilities']", - start: "center center", - end: "bottom center", - scrub: false, - onEnter: () => { - fadeChart('.province-chart', 1); - activeData.fs = fs_inside_outside; - activeData.lp = lp_inside_outside; - activeData.kzn = kzn_inside_outside; - updateAllCharts(); - - }, - onLeaveBack: () => { - console.log('onLeaveBack triggered'); - fadeChart('.province-chart', 0); - activeData.fs = fs_inside_outside; - activeData.lp = lp_inside_outside; - activeData.kzn = kzn_inside_outside; - updateAllCharts(); - - } - }); + function add_label(chart, slice_id, point_id, hour, anchor, text, line_config = {}, text_config = {}) { - // Disabled Access Dataset - ScrollTrigger.create({ - trigger: ".scrolly-section[data-section-label='disabled-access']", - start: "center center", - end: "bottom center", - scrub: false, - onEnter: () => { - activeData.fs = fs_disabled_access; - activeData.lp = lp_disabled_access; - activeData.kzn = kzn_disabled_access; - updateAllCharts(); + let line_config_set = { + class_name: line_config.class_name || '', + length: line_config.length || 80, + color: line_config.color || 'black', + thickness: line_config.thickness || 1 + } + + let text_config_set = { + class_name: text_config.class_name || '', + color: text_config.color || 'black', + align: text_config.align || 'left' + } + + // Get the point element + let point = d3.select(`#${chart} .${slice_id} .point-${point_id}`); + + // Get the bounding box of the point relative to the chart container + let chartNode = d3.select(`#${chart}`).node(); + let chartRect = chartNode.getBoundingClientRect(); + let pointRect = point.node().getBoundingClientRect(); + + // Calculate the position relative to the chart container + let relativeX = pointRect.left - chartRect.left; + let relativeY = pointRect.top - chartRect.top; + + // Existing line code + let angleDeg = (hour * 30) - 90; + let length = line_config_set.length; + let angleRad = angleDeg * (Math.PI / 180); + let dx = length * Math.cos(angleRad); + let dy = length * Math.sin(angleRad); + let endX = relativeX + dx; + let endY = relativeY + dy; + + let anchors = { + 'tl': '0%, 0%', + 'tc': '-50%, 0%', + 'tr': '-100%, 0%', + 'bl': '0%, -100%', + 'bc': '-50%, -100%', + 'br': '-100%, -100%' + }; + + // Append the line to the chart container + d3.select(`#${chart}`).append('div') + .attr('class', `scrolly-annotation-line ${line_config_set.class_name}`) + .style('top', `${relativeY}px`) + .style('left', `${relativeX}px`) + .style('width', `${line_config_set.length}px`) + .style('height', `${line_config_set.thickness}px`) + .style('background-color', line_config_set.color) + .style('position', 'absolute') + .style('transform-origin', 'top left') + .style('transform', `rotate(${angleDeg}deg)`); + // Calculate text position with offset + let textOffset = 5; + let textX = endX + textOffset * Math.cos(angleRad); + let textY = endY + textOffset * Math.sin(angleRad); + + // Append the text to the chart container + d3.select(`#${chart}`).append('div') + .attr('class', `scrolly-annotation-text ${text_config_set.class_name}`) + .style('position', 'absolute') + .style('left', `${textX}px`) + .style('top', `${textY}px`) + .style('color', text_config_set.color) + .style('transform', `translate(${anchors[anchor]})`) + .style('text-align', text_config_set.align) + .html(text); + } + + + drawChart('fs', inside_outside_blank, 'FS'); + + drawChart('lp', inside_outside_blank, 'LP'); + + drawChart('kzn', inside_outside_blank, 'KZN'); + + + // add_label('kzn', 'category-a', 3, 4, 'tl', 'text here
how are you?', {color: '#fff'}, {align: 'left', color: '#fff'}); + + + // UNHYGENIC TOILETS - }, - onLeaveBack: () => { - console.log('onLeaveBack triggered'); - activeData.fs = fs_inside_outside; - activeData.lp = lp_inside_outside; - activeData.kzn = kzn_inside_outside; - updateAllCharts(); + const toilet_data = [ + {label: 'FS', Seat: 0, "Toilet bowl": 0, Wall: 0}, + {label: 'KZN', Seat: 0, "Toilet bowl": 0, Wall: 0}, + {label: 'LP', Seat: 0, "Toilet bowl": 0, Wall: 0} + ]; + + const colors = { + Seat: '#ff5733', // Replace with scrolly_chart_colors.unhygenic_seat + "Toilet bowl": '#33cfff', // Replace with scrolly_chart_colors.unhygenic_bowl + Wall: '#bada55' // Replace with scrolly_chart_colors.unhygenic_wall + }; + + const margin = {top: 20, right: 20, bottom: 20, left: 100}; + const width = 800 - margin.left - margin.right; + const height = 400 - margin.top - margin.bottom; + + const svg = d3.select('#unhygenic-toilets-chart') + .append('svg') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', `translate(${margin.left},${margin.top})`); - } - }); + const yScale = d3.scaleBand() + .domain(toilet_data.map(d => d.label)) + .range([0, height]) + .padding(0.1); + + const xScale = d3.scaleLinear() + .domain([0, d3.max(toilet_data, d => d.Seat + d["Toilet bowl"] + d.Wall)]) + .range([0, width]); + + const stack = d3.stack() + .keys(['Seat', 'Toilet bowl', 'Wall']) + .order(d3.stackOrderNone) + .offset(d3.stackOffsetNone); + + const stackedData = stack(toilet_data); + + const groups = svg.selectAll('.group') + .data(stackedData) + .enter() + .append('g') + .attr('fill', d => colors[d.key]); + + groups.selectAll('rect') + .data(d => d) + .enter() + .append('rect') + .attr('y', d => yScale(d.data.label)) + .attr('x', d => xScale(d[0])) + .attr('width', d => xScale(d[1]) - xScale(d[0])) + .attr('height', yScale.bandwidth()); + + svg.append('g') + .call(d3.axisLeft(yScale)) + .selectAll('text') + .style('font-size', '14px'); + + svg.append('g') + .attr('transform', `translate(0,${height})`) + .call(d3.axisBottom(xScale)) + .selectAll('text') + .style('display', 'none'); + - ScrollTrigger.create({ - trigger: ".scrolly-section[data-section-label='broken-toilets']", - start: "center center", - end: "bottom center", - scrub: false, - onEnter: () => { - activeData.fs = fs_unusable_toilets; - activeData.lp = lp_unusable_toilets; - activeData.kzn = kzn_unusable_toilets; - updateAllCharts(); - }, - onLeaveBack: () => { - console.log('onLeaveBack triggered'); - activeData.fs = fs_disabled_access; - activeData.lp = lp_disabled_access; - activeData.kzn = kzn_disabled_access; - updateAllCharts(); - } - }); - ScrollTrigger.create({ - trigger: ".scrolly-section[data-section-label='no-toilet-paper']", - start: "center center", - end: "bottom center", - scrub: false, - onEnter: () => { - activeData.fs = fs_toilet_paper; - activeData.lp = lp_toilet_paper; - activeData.kzn = kzn_toilet_paper; - updateAllCharts(); + // DOUGHNUT CHARTS + + + + // WOMEN SAFETY CHART - }, - onLeaveBack: () => { - console.log('onLeaveBack triggered'); - activeData.fs = fs_unusable_toilets; - activeData.lp = lp_unusable_toilets; - activeData.kzn = kzn_unusable_toilets; - updateAllCharts(); + // RETURN VISIT - } - }); + - ScrollTrigger.create({ - trigger: ".scrolly-section[data-section-label='no-sanitiser']", - start: "center center", - end: "bottom center", - scrub: false, - onEnter: () => { - activeData.fs = fs_no_sanitiser; - activeData.lp = lp_no_sanitiser; - activeData.kzn = kzn_no_sanitiser; - updateAllCharts(); - }, - onLeaveBack: () => { - console.log('onLeaveBack triggered'); - activeData.fs = fs_toilet_paper; - activeData.lp = lp_toilet_paper; - activeData.kzn = kzn_toilet_paper; - updateAllCharts(); + + - } - }); + // PROVINCE BREAKDOWN CHART ANIMATION + + function fadeChart(chartId, opacity) { + d3.select(`${chartId}`) + .transition() + .duration(500) + .style('opacity', opacity); + } + + + // Inside/Outside Dataset + ScrollTrigger.create({ + trigger: ".scrolly-section[data-section-label='outdoor-facilities']", + start: "center center", + end: "bottom center", + scrub: false, + onEnter: () => { + fadeChart('.province-chart', 1); + activeData.fs = fs_inside_outside; + activeData.lp = lp_inside_outside; + activeData.kzn = kzn_inside_outside; + updateAllCharts(); + + }, + onLeaveBack: () => { + console.log('onLeaveBack triggered'); + fadeChart('.province-chart', 0); + activeData.fs = fs_inside_outside; + activeData.lp = lp_inside_outside; + activeData.kzn = kzn_inside_outside; + updateAllCharts(); + + } + }); + // Disabled Access Dataset + ScrollTrigger.create({ + trigger: ".scrolly-section[data-section-label='disabled-access']", + start: "center center", + end: "bottom center", + scrub: false, + onEnter: () => { + activeData.fs = fs_disabled_access; + activeData.lp = lp_disabled_access; + activeData.kzn = kzn_disabled_access; + updateAllCharts(); + - fadeOutChart('province-chart', 'not-stocked2'); + }, + onLeaveBack: () => { + console.log('onLeaveBack triggered'); + activeData.fs = fs_inside_outside; + activeData.lp = lp_inside_outside; + activeData.kzn = kzn_inside_outside; + updateAllCharts(); + - + } + }); - // DIRTY TOILETS + ScrollTrigger.create({ + trigger: ".scrolly-section[data-section-label='broken-toilets']", + start: "center center", + end: "bottom center", + scrub: false, + onEnter: () => { + activeData.fs = fs_unusable_toilets; + activeData.lp = lp_unusable_toilets; + activeData.kzn = kzn_unusable_toilets; + updateAllCharts(); + - gsap - .timeline({ - scrollTrigger: { - trigger: ".scrolly-section[data-section-label='unhygenic-toilets']", - start: () => { - const backgroundRect = background.getBoundingClientRect(); - return `center ${backgroundRect.height + offsetInPixels}px`; }, - end: () => { - const backgroundRect = background.getBoundingClientRect(); - return `bottom ${backgroundRect.height + offsetInPixels}px`; + onLeaveBack: () => { + console.log('onLeaveBack triggered'); + activeData.fs = fs_disabled_access; + activeData.lp = lp_disabled_access; + activeData.kzn = kzn_disabled_access; + updateAllCharts(); + + + } + }); + + ScrollTrigger.create({ + trigger: ".scrolly-section[data-section-label='no-toilet-paper']", + start: "center center", + end: "bottom center", + scrub: false, + onEnter: () => { + activeData.fs = fs_toilet_paper; + activeData.lp = lp_toilet_paper; + activeData.kzn = kzn_toilet_paper; + updateAllCharts(); + + }, - scrub: true - } - }) - .fromTo( - ".unhygenic-chart", - { opacity: 0 }, - { opacity: 1, ease: "none" } - ); - - // FADE OUT PROVINCE CHART - - - // fadeOutChart('unhygenic-chart', 'unhygenic-toilets4'); - - // QUEUES CHART - - // gsap - // .timeline({ - // scrollTrigger: { - // trigger: ".scrolly-section[data-section-label='queues']", - // start: () => { - // const backgroundRect = background.getBoundingClientRect(); - // return `center ${backgroundRect.height + offsetInPixels}px`; - // }, - // end: () => { - // const backgroundRect = background.getBoundingClientRect(); - // return `bottom ${backgroundRect.height + offsetInPixels}px`; - // }, - // scrub: true - // } - // }) - // .fromTo( - // ".queues-charts", - // { opacity: 0 }, - // { opacity: 1, ease: "none" } - // ) - // .to(queues_chart_21_50.data.datasets[0].data, { endArray: queues_21_50, ease: "none", onUpdate: function () { queues_chart_21_50.update(); } }, 0) - // .to(queues_chart_51_70.data.datasets[0].data, { endArray: queues_51_70, ease: "none", onUpdate: function () { queues_chart_51_70.update(); } }, 0) - // .to(queues_chart_71_100.data.datasets[0].data, { endArray: queues_71_100, ease: "none", onUpdate: function () { queues_chart_71_100.update(); } }, 0) - // .to(queues_chart_100.data.datasets[0].data, { endArray: queues_100, ease: "none", onUpdate: function () { queues_chart_100.update(); } }, 0); - - // fadeOutChart('queues-charts', 'queues4'); - - // SAFETY - - // gsap - // .timeline({ - // scrollTrigger: { - // trigger: ".scrolly-section[data-section-label='unsafe']", - // start: () => { - // const backgroundRect = background.getBoundingClientRect(); - // return `center ${backgroundRect.height + offsetInPixels}px`; - // }, - // end: () => { - // const backgroundRect = background.getBoundingClientRect(); - // return `bottom ${backgroundRect.height + offsetInPixels}px`; - // }, - // scrub: true - // } - // }) - // .fromTo( - // ".safety-chart", - // { opacity: 0 }, - // { opacity: 1, ease: "none" } - // ) - // .to(safety_male.data.datasets[0].data, { endArray: safety_chart_male, ease: "none", onUpdate: function () { safety_male.update(); } }, 0) - // .to(safety_female.data.datasets[0].data, { endArray: safety_chart_female, ease: "none", onUpdate: function () { safety_female.update(); } }, 0); - - // fadeOutChart('safety-chart', 'women-unsafe'); - - // WOMEN SAFETY - - // gsap - // .timeline({ - // scrollTrigger: { - // trigger: ".scrolly-section[data-section-label='women-unsafe']", - // start: () => { - // const backgroundRect = background.getBoundingClientRect(); - // return `center ${backgroundRect.height + offsetInPixels}px`; - // }, - // end: () => { - // const backgroundRect = background.getBoundingClientRect(); - // return `bottom ${backgroundRect.height + offsetInPixels}px`; - // }, - // scrub: true - // } - // }) - // .fromTo( - // ".women-safety-chart", - // { opacity: 0 }, - // { opacity: 1, ease: "none" } - // ) - // .to(women_safety_chart.data.datasets[0].data, { endArray: women_safety_yes, ease: "none", onUpdate: function () { women_safety_chart.update(); } }, 0) - // .to(women_safety_chart.data.datasets[1].data, { endArray: women_safety_no, ease: "none", onUpdate: function () { women_safety_chart.update(); } }, 0); - - // fadeOutChart('women-safety-chart', 'return-visits'); - - - // RETURN VISITS - - // gsap - // .timeline({ - // scrollTrigger: { - // trigger: ".scrolly-section[data-section-label='return-visits']", - // start: () => { - // const backgroundRect = background.getBoundingClientRect(); - // return `center ${backgroundRect.height + offsetInPixels}px`; - // }, - // end: () => { - // const backgroundRect = background.getBoundingClientRect(); - // return `bottom ${backgroundRect.height + offsetInPixels}px`; - // }, - // scrub: true - // } - // }) - // .fromTo( - // ".return-visits-chart", - // { opacity: 0 }, - // { opacity: 1, ease: "none" } - // ) - // .to(return_visits_chart_fs.data.datasets[0].data, { endArray: return_visits_fs, ease: "none", onUpdate: function () { return_visits_chart_fs.update(); } }, 0) - // .to(return_visits_chart_lp.data.datasets[0].data, { endArray: return_visits_lp, ease: "none", onUpdate: function () { return_visits_chart_lp.update(); } }, 0) - // .to(return_visits_chart_kzn.data.datasets[0].data, { endArray: return_visits_kzn, ease: "none", onUpdate: function () { return_visits_chart_kzn.update(); } }, 0); - - // fadeOutChart('return-visits-chart', 'conclusion'); - -}) + onLeaveBack: () => { + console.log('onLeaveBack triggered'); + activeData.fs = fs_unusable_toilets; + activeData.lp = lp_unusable_toilets; + activeData.kzn = kzn_unusable_toilets; + updateAllCharts(); + + + } + }); + + ScrollTrigger.create({ + trigger: ".scrolly-section[data-section-label='no-sanitiser']", + start: "center center", + end: "bottom center", + scrub: false, + onEnter: () => { + activeData.fs = fs_no_sanitiser; + activeData.lp = lp_no_sanitiser; + activeData.kzn = kzn_no_sanitiser; + updateAllCharts(); + + + }, + onLeaveBack: () => { + console.log('onLeaveBack triggered'); + activeData.fs = fs_toilet_paper; + activeData.lp = lp_toilet_paper; + activeData.kzn = kzn_toilet_paper; + updateAllCharts(); + + + } + }); + + + fadeOutChart('province-chart', 'not-stocked2'); + + + + // DIRTY TOILETS + + gsap + .timeline({ + scrollTrigger: { + trigger: ".scrolly-section[data-section-label='unhygenic-toilets']", + start: () => { + const backgroundRect = background.getBoundingClientRect(); + return `center ${backgroundRect.height + offsetInPixels}px`; + }, + end: () => { + const backgroundRect = background.getBoundingClientRect(); + return `bottom ${backgroundRect.height + offsetInPixels}px`; + }, + scrub: true + } + }) + .fromTo( + ".unhygenic-chart", + { opacity: 0 }, + { opacity: 1, ease: "none" } + ); + + // FADE OUT PROVINCE CHART + + + // fadeOutChart('unhygenic-chart', 'unhygenic-toilets4'); + + // QUEUES CHART + + // gsap + // .timeline({ + // scrollTrigger: { + // trigger: ".scrolly-section[data-section-label='queues']", + // start: () => { + // const backgroundRect = background.getBoundingClientRect(); + // return `center ${backgroundRect.height + offsetInPixels}px`; + // }, + // end: () => { + // const backgroundRect = background.getBoundingClientRect(); + // return `bottom ${backgroundRect.height + offsetInPixels}px`; + // }, + // scrub: true + // } + // }) + // .fromTo( + // ".queues-charts", + // { opacity: 0 }, + // { opacity: 1, ease: "none" } + // ) + // .to(queues_chart_21_50.data.datasets[0].data, { endArray: queues_21_50, ease: "none", onUpdate: function () { queues_chart_21_50.update(); } }, 0) + // .to(queues_chart_51_70.data.datasets[0].data, { endArray: queues_51_70, ease: "none", onUpdate: function () { queues_chart_51_70.update(); } }, 0) + // .to(queues_chart_71_100.data.datasets[0].data, { endArray: queues_71_100, ease: "none", onUpdate: function () { queues_chart_71_100.update(); } }, 0) + // .to(queues_chart_100.data.datasets[0].data, { endArray: queues_100, ease: "none", onUpdate: function () { queues_chart_100.update(); } }, 0); + + // fadeOutChart('queues-charts', 'queues4'); + + // SAFETY + + // gsap + // .timeline({ + // scrollTrigger: { + // trigger: ".scrolly-section[data-section-label='unsafe']", + // start: () => { + // const backgroundRect = background.getBoundingClientRect(); + // return `center ${backgroundRect.height + offsetInPixels}px`; + // }, + // end: () => { + // const backgroundRect = background.getBoundingClientRect(); + // return `bottom ${backgroundRect.height + offsetInPixels}px`; + // }, + // scrub: true + // } + // }) + // .fromTo( + // ".safety-chart", + // { opacity: 0 }, + // { opacity: 1, ease: "none" } + // ) + // .to(safety_male.data.datasets[0].data, { endArray: safety_chart_male, ease: "none", onUpdate: function () { safety_male.update(); } }, 0) + // .to(safety_female.data.datasets[0].data, { endArray: safety_chart_female, ease: "none", onUpdate: function () { safety_female.update(); } }, 0); + + // fadeOutChart('safety-chart', 'women-unsafe'); + + // WOMEN SAFETY + + // gsap + // .timeline({ + // scrollTrigger: { + // trigger: ".scrolly-section[data-section-label='women-unsafe']", + // start: () => { + // const backgroundRect = background.getBoundingClientRect(); + // return `center ${backgroundRect.height + offsetInPixels}px`; + // }, + // end: () => { + // const backgroundRect = background.getBoundingClientRect(); + // return `bottom ${backgroundRect.height + offsetInPixels}px`; + // }, + // scrub: true + // } + // }) + // .fromTo( + // ".women-safety-chart", + // { opacity: 0 }, + // { opacity: 1, ease: "none" } + // ) + // .to(women_safety_chart.data.datasets[0].data, { endArray: women_safety_yes, ease: "none", onUpdate: function () { women_safety_chart.update(); } }, 0) + // .to(women_safety_chart.data.datasets[1].data, { endArray: women_safety_no, ease: "none", onUpdate: function () { women_safety_chart.update(); } }, 0); + + // fadeOutChart('women-safety-chart', 'return-visits'); + + + // RETURN VISITS + + // gsap + // .timeline({ + // scrollTrigger: { + // trigger: ".scrolly-section[data-section-label='return-visits']", + // start: () => { + // const backgroundRect = background.getBoundingClientRect(); + // return `center ${backgroundRect.height + offsetInPixels}px`; + // }, + // end: () => { + // const backgroundRect = background.getBoundingClientRect(); + // return `bottom ${backgroundRect.height + offsetInPixels}px`; + // }, + // scrub: true + // } + // }) + // .fromTo( + // ".return-visits-chart", + // { opacity: 0 }, + // { opacity: 1, ease: "none" } + // ) + // .to(return_visits_chart_fs.data.datasets[0].data, { endArray: return_visits_fs, ease: "none", onUpdate: function () { return_visits_chart_fs.update(); } }, 0) + // .to(return_visits_chart_lp.data.datasets[0].data, { endArray: return_visits_lp, ease: "none", onUpdate: function () { return_visits_chart_lp.update(); } }, 0) + // .to(return_visits_chart_kzn.data.datasets[0].data, { endArray: return_visits_kzn, ease: "none", onUpdate: function () { return_visits_chart_kzn.update(); } }, 0); + + // fadeOutChart('return-visits-chart', 'conclusion'); + + } + });