1import{Controller}from '@hotwired/stimulus' 2import{useWindowResize}from 'stimulus-use' 3import{Datepicker}from 'vanillajs-datepicker' 4import{differenceInDays,differenceInMonths,format}from 'date-fns' 5import{line,curveLinear}from 'd3-shape' 6import{bisector,min,max}from 'd3-array' 7import{select,selectAll,create,pointer}from 'd3-selection' 8import{scaleLinear,scaleTime}from 'd3-scale' 9import{axisBottom,axisLeft}from 'd3-axis' 10import{formatLocale}from 'd3-format' 11import{timeFormatDefaultLocale,timeFormat,timeParse}from 'd3-time-format' 12import{brushX}from 'd3-brush' 13const curveStyle=curveLinear 14const margin={top:30,right:1,bottom:35,left:60} 15const units={value:{unit:' PLN',accur:2},ror:{unit:'%',accur:2}} 16const d3pl=formatLocale({decimal:',',thousands:' ',grouping:[3],currency:['zÅ',''],}) 17timeFormatDefaultLocale({dateTime:'%a %b %e %X %Y',date:'%Y-%m-%d',time:'%H:%M:%S',periods:['AM','PM'],days:['Niedziela','PoniedziaÅek','Wtorek','Åroda','Czwartek','PiÄ tek','Sobota'],shortDays:['Nie','Pon','Wto','Åro','Czw','PiÄ ','Sob'],months:['StyczeÅ','Luty','Marzec','KwiecieÅ','Maj','Czerwiec','Lipiec','SierpieÅ','WrzesieÅ','Październik','Listopad','GrudzieÅ',],shortMonths:['Sty','Lut','Mar','Kwi','Maj','Cze','Lip','Sie','Wrz','Paź','Lis','Gru'],}) 18Datepicker.locales.pl={days:['Niedziela','PoniedziaÅek','Wtorek','Åroda','Czwartek','PiÄ tek','Sobota'],daysShort:['Niedz.','Pon.','Wt.','År.','Czw.','PiÄ t.','Sob.'],daysMin:['Ndz.','Pn.','Wt.','År.','Czw.','Pt.','Sob.'],months:['StyczeÅ','Luty','Marzec','KwiecieÅ','Maj','Czerwiec','Lipiec','SierpieÅ','WrzesieÅ','Październik','Listopad','GrudzieÅ',],monthsShort:['Sty.','Lut.','Mar.','Kwi.','Maj','Cze.','Lip.','Sie.','Wrz.','Paź.','Lis.','Gru.'],today:'Dzisiaj',weekStart:1,clear:'WyczyÅÄ',format:'dd.mm.yyyy',} 19const predefinedColors=['#f1a3fe','#a5f5e8','#bcdfcb','#e2dbff','#f9ffa3','#e557ff','#4df0d2','#7fbef8','#bdaff7','#e2ef49','#ba41d1','#34cbb0','#369dfe','#9b81ff','#bcc835','#a231b6','#26a891','#2583dc','#856be9','#9faa20','#822493','#188d79','#1b66a9','#664cc9','#7e8810','#631b72','#0b6655','#1b4972','#422b9c','#5a6110',].reverse() 20let svg=null 21let _tmp=null 22let width=0 23let height=0 24let tooltipWidth=105 25let tooltipXWidth=80 26let tooltipHeightInitial=30 27let tooltipShift=5 28let firstDate=null 29let lastDate=null 30let datePickerStart=null 31let datePickerEnd=null 32let dataSet 33let data 34let mode 35let colors 36let labels 37let infos 38export default class extends Controller{static values={fund:String,url:String,mindate:String,maxdate:String,freedays:[],mode:String,} 39static targets=['plot','chart','btnval','btnror','datepicker_start','datepicker_end','score','score_rwd','benchmark','legend',] 40async connect(){this.benchmarkSelectorSetLabel() 41const options={format:'yyyy-mm-dd',language:'pl',daysOfWeekDisabled:[0,6],minDate:this.mindateValue,maxDate:this.maxdateValue,autohide:!0,orientation:'bottom auto',} 42svg=select(this.chartTarget) 43svg.attr('width',this.plotTarget.clientWidth) 44mode='ror' 45dataSet={} 46colors={} 47labels={} 48infos=[] 49firstDate=this.mindateValue 50lastDate=this.maxdateValue 51const response=await fetch(this.urlValue,{method:'GET',}) 52data=await response.json() 53colors[data.code]=data.hexColor 54labels[data.code]=data.name 55dataSet[data.code]=this.convert(data.data) 56infos=data.infos 57options.datesDisabled=data.freeDays.filter((item)=>{return item>=firstDate&&item<=lastDate}) 58datePickerStart=new Datepicker(this.datepicker_startTarget,options) 59datePickerEnd=new Datepicker(this.datepicker_endTarget,options) 60this.datepicker_startTarget.addEventListener('show',this.showDatepicker) 61this.datepicker_endTarget.addEventListener('show',this.showDatepicker) 62this.drawChart(svg,dataSet) 63useWindowResize(this)} 64resetLegendItems(){selectAll('.legend-of-chart .legend-item').remove()} 65addLegendItem(name,value,color){let legendItem=create('div').attr('class','legend-item') 66legendItem.append('span').attr('class','line').attr('style','background-color: '+color) 67legendItem.append('span').attr('class','legend-profile-value').html(value) 68legendItem.append('span').attr('class','legend-profile-name').html(name.startsWith('Årednia')?'Årednia':name) 69this.legendTarget.append(legendItem.node())} 70switchMode(e){this.btnrorTarget.classList.remove('active') 71this.btnvalTarget.classList.remove('active') 72e.currentTarget.classList.add('active') 73mode=e.currentTarget.dataset.mode 74if(mode==='value'){let checkedBoxes=this.benchmarkTarget.querySelectorAll('input:checked')
75checkedBoxes.forEach((item)=>{item.checked=!1}) 76this.benchmarkTarget.classList.add('disabled') 77this.benchmarkTarget.querySelector('.customSelectText').textContent='Wybierz' 78let fc=this.fundValue 79Object.entries(dataSet).forEach(function(item,position){const[code,data]=item 80if(code!==fc){delete dataSet[code]}})} 81if(mode==='ror'){this.benchmarkTarget.classList.remove('disabled')} 82this.drawChart(svg,dataSet)} 83changeDateRange(e){firstDate=this.datepicker_startTarget.value 84lastDate=this.datepicker_endTarget.value 85let dateRangeEqualsDatePickers=!1 86let listItems=this.scoreTarget.querySelectorAll('ul li') 87let listRwdItems=this.score_rwdTarget.querySelectorAll('ul li') 88let scoreTxt=this.scoreTarget.querySelector('.customSelectText') 89listItems.forEach((item)=>{let minDate=item.dataset.mindate 90let maxDate=item.dataset.maxdate 91if(minDate===firstDate&&maxDate===lastDate){dateRangeEqualsDatePickers=!0 92item.classList.add('active') 93scoreTxt.textContent=item.dataset.index}else{item.classList.remove('active')}}) 94listRwdItems.forEach((item)=>{let minDate=item.dataset.mindate 95let maxDate=item.dataset.maxdate 96if(minDate===firstDate&&maxDate===lastDate){item.classList.add('active')}else{item.classList.remove('active')}}) 97if(dateRangeEqualsDatePickers===!1){scoreTxt.textContent='WÅasny'} 98this.drawChart(svg,dataSet)} 99async changeBenchmark(e){let benchmark=e.currentTarget.dataset.index 100let name=e.currentTarget.dataset.name 101let checkboxes=this.benchmarkTarget.querySelectorAll('input:checked') 102if(checkboxes.length===0){this.benchmarkSelectorSetLabel()}else if(checkboxes.length===1){if(checkboxes[0].dataset.name.startsWith('Årednia')){this.benchmarkTarget.querySelector('.customSelectText').textContent='Årednia'}else{this.benchmarkTarget.querySelector('.customSelectText').textContent=checkboxes[0].dataset.name}}else{this.benchmarkTarget.querySelector('.customSelectText').textContent='Wybrane ('+checkboxes.length+')'} 103if(e.currentTarget.checked===!0){const response=await fetch(`/line-chart-bench-data/${benchmark}`,{method:'GET',}) 104const data=await response.json() 105colors[benchmark]=predefinedColors.pop() 106labels[benchmark]=name 107dataSet[benchmark]=this.convert(data.data)}else{predefinedColors.push(colors[benchmark]) 108delete colors[benchmark] 109delete labels[benchmark] 110delete dataSet[benchmark]} 111this.drawChart(svg,dataSet)} 112changePeriod(e){const minDate=e.currentTarget.dataset.mindate 113const maxDate=e.currentTarget.dataset.maxdate 114datePickerStart.setDate(minDate) 115datePickerEnd.setDate(maxDate) 116firstDate=minDate 117lastDate=maxDate 118this.drawChart(svg,dataSet)} 119drawChart=(svg,dataSet)=>{let idleTimeout 120let tooltipHeight=0 121let ymmin=1e10 122let ymmax=-1e10 123_tmp=this 124let modifiedDataSet={} 125_tmp.resetLegendItems() 126width=svg.attr('width')-margin.left-margin.right 127height=svg.attr('height')-margin.top-margin.bottom 128const xAxisTicks=differenceInDays(lastDate,firstDate)===1?2:4 129Object.entries(dataSet).forEach((item,position)=>{let[code,data]=item 130let stzw=null 131data=data.filter((d)=>{return(format(d.date,'yyyy-MM-dd')>=firstDate&&format(d.date,'yyyy-MM-dd')<=lastDate)}) 132if(mode==='ror'){modifiedDataSet[code]=[] 133for(let j=0;j<data.length;j++){if(stzw===null)stzw=data[j].value 134modifiedDataSet[code][j]={date:data[j].date,value:(data[j].value/stzw-1)*100}}}else{modifiedDataSet[code]=data} 135let[a,b]=_tmp.rMinMax(modifiedDataSet[code],function(d){return d.value}) 136ymmin=Math.min(ymmin,a) 137ymmax=Math.max(ymmax,b) 138tooltipHeight=tooltipHeightInitial*(position+1)-position*10 139let legendVal=modifiedDataSet[code][modifiedDataSet[code].length-1].value 140let fmtVal=d3pl.format('.2f')(legendVal)+' '+units[mode].unit 141_tmp.addLegendItem(labels[code],fmtVal,colors[code])}) 142svg.selectAll('*').remove() 143let chart=svg.append('g').attr('transform','translate('+margin.left+','+margin.top+')') 144let gridGroup=chart.append('g').attr('class','grid') 145let axisGroup=chart.append('g').attr('class','axis-group') 146let linesGroup=chart.append('g').attr('class','lines') 147let infoGroup=chart.append('g').attr('class','infos') 148let crosshairGroup=chart.append('g').attr('class','crosshair') 149let xScale=scaleTime().domain([this.parseTime(firstDate),this.parseTime(lastDate)]).range([0,width]) 150let yScale=scaleLinear().domain([ymmin,ymmax]).range([height,0]) 151let xAxism=axisGroup.append('g').attr('class','x-axis axis').attr('transform','translate(0,'+height+')') 152let yAxism=axisGroup.append('g').attr('class','y-axis axis') 153const getXAxisFormat=()=>{if(firstDate&&lastDate){const monthDiff=differenceInMonths(lastDate,firstDate) 154if(monthDiff>48){return'%Y'}else if(monthDiff<=48&&monthDiff>3){return'%b \'%g'}else{return'%Y-%m-%d'}}else{return'%Y'}} 155xAxism.call(axisBottom(xScale).tickFormat((d)=>timeFormat(getXAxisFormat())(d)).ticks(xAxisTicks)).call((d)=>{d.selectAll('.tick text').attr('text-anchor','middle').attr('font-size','12px').attr('dy',8) 156d.selectAll('.tick line').attr('stroke','#e0e0e0').attr('y2',0)}) 157if(differenceInDays(lastDate,firstDate)===1||differenceInDays(lastDate,firstDate)===2){svg.selectAll('.x-axis .tick:first-of-type text').attr('text-anchor','start') 158svg.selectAll('.x-axis .tick:last-of-type text').attr('text-anchor','end')}
159yAxism.call(axisLeft(yScale).tickSize(-width).tickFormat((d)=>d3pl.format('.'+units[mode].accur+'f')(d)+' '+units[mode].unit).ticks(6),).call((d)=>{d.selectAll('.tick text').attr('font-size','12px').attr('text-anchor','start').attr('dy',+8).attr('x',-margin.left).attr('dy','4') 160d.selectAll('.tick line').attr('opacity',0).attr('stroke','#e0e0e0')}) 161gridGroup.append('g').attr('class','x-grid-line grid-line').attr('pointer-events','none').attr('transform',`translate(0, ${height})`).call(axisBottom(xScale).tickSize(-(height??0)).ticks(4).tickFormat(()=>''),).call((d)=>{d.selectAll('.tick text').attr('text-anchor','middle').attr('dy',8) 162d.selectAll('.tick line').attr('stroke','#e0e0e0')}) 163gridGroup.append('g').attr('class','y-grid-line grid-line').attr('pointer-events','none').call(axisLeft(yScale).tickSize(-(width??0)).ticks(4).tickFormat(()=>''),) 164Object.entries(modifiedDataSet).forEach(function(item,position){const[code,data]=item 165linesGroup.append('path').data([data]).attr('class','line').attr('stroke-width','2').attr('fill','none').attr('stroke',colors[code]).attr('d',line().x(function(d){return xScale(d.date)}).y(function(d){return yScale(d.value)}).curve(curveStyle),)}) 166infos.forEach((item)=>{let d=_tmp.parseTime(format(item.date,'yyyy-MM-dd')) 167infoGroup.append('rect').attr('class','info-rect').attr('pointer-events','none').attr('width',Math.abs(xScale(d))>width?width:xScale(d)).attr('height',height).attr('y',0).attr('x',0) 168infoGroup.append('rect').attr('class','info-line').attr('pointer-events','none').attr('width',0.5).attr('height',height).attr('stroke-width',0.5).attr('stroke',item.hexColor).attr('y',0).attr('x',xScale(d)) 169infoGroup.append('circle').attr('cx',xScale(d)+1).attr('cy',-14).attr('r',8).attr('stroke-width',0).attr('fill','#3b4967') 170infoGroup.append('text').attr('text-anchor','middle').attr('fill','#ffffff').attr('x',xScale(d)+1).attr('y',-9).style('visibility','visible').attr('font-size','12px').html('i') 171infoGroup.append('circle').attr('cx',xScale(d)+1).attr('cy',-14).attr('r',8).attr('stroke-width',0).attr('fill','transparent').on('mousemove',function(event){select('.changeGroupTooltip').style('display','block').style('top',event.pageY+20+'px').style('left',event.pageX-130+'px').html('W dniu '+d.toLocaleDateString('pl-PL')+' fundusz zmieniÅ grupÄ zgodnie z metodologiÄ klasyfikacji funduszy. Poprzednia grupa: '+item.additionalValue,)}).on('mouseout',function(event){select('.changeGroupTooltip').style('display','none').html('')})}) 172const clip=svg.append('defs').append('svg:clipPath').attr('id','clip').append('svg:rect').attr('width',width).attr('height',height).attr('x',margin.left).attr('y',margin.top) 173const brushGroup=svg.append('g').style('pointer-events','visibleFill').attr('clip-path','url(#clip)') 174const crosshairLine=crosshairGroup.append('line').attr('class','crosshair-line').attr('pointer-events','none').attr('stroke-width',1.5).attr('stroke','#0F0F0F').attr('y1',0).attr('y2',height).attr('x1',-1500).attr('x2',-1500).style('visibility','visible') 175const tooltipX=crosshairGroup.append('g').attr('class','legend-x').attr('pointer-events','none').attr('font-size','14px').attr('style','transition: all 0s ease-in-out;').attr('transform','translate(-1500,'+height+')') 176tooltipX.append('rect').attr('class','tooltip').attr('width',tooltipXWidth).attr('height',tooltipHeightInitial).attr('stroke-width',0.5).attr('stroke','#C0C0C0').attr('fill','#FFFFFF').style('visibility','visible') 177const tooltipXTxt=tooltipX.append('text').attr('text-anchor','middle').attr('fill','#000000').attr('y',20).attr('x',40).style('visibility','visible') 178const tooltip=crosshairGroup.append('g').attr('class','tooltip').attr('pointer-events','none').attr('font-size','14px').attr('style','transition: all 0s ease-in-out;').attr('transform','translate(-1500,'+height+')') 179tooltip.append('rect').attr('width',tooltipWidth).attr('height',tooltipHeight).attr('stroke-width',0.5).attr('stroke','#C0C0C0').attr('fill','#FFFFFF') 180svg.on('mousemove',function(event){let[xCoord,yCoord]=pointer(event,this) 181if(xCoord<margin.left||xCoord>width+margin.left)return null 182if(yCoord<margin.top||yCoord>height+margin.top)return null 183xCoord=xCoord-margin.left 184yCoord=yCoord-margin.top 185const bisectDate=bisector((d)=>d.date).left 186const x0=xScale.invert(xCoord) 187tooltip.selectAll('text, circle').remove() 188Object.entries(modifiedDataSet).forEach(function(item,position){const[code,data]=item 189const i=bisectDate(data,x0,1) 190const d0=data[i-1] 191const d1=data[i] 192const d=x0-d0.date>d1.date-x0?d1:d0 193const dotX=12 194const textX=22 195tooltip.append('text').attr('text-anchor','start').attr('fill','#000000').attr('x',textX).attr('y',20*(position+1)).html(d3pl.format('.'+units[mode].accur+'f')(d.value)+''+units[mode].unit) 196tooltip.append('circle').attr('cx',dotX).attr('cy',20*(position+1)-4).attr('r',3.5).attr('fill',colors[code])}) 197crosshairLine.attr('x1',xCoord).attr('x2',xCoord)
198tooltipXTxt.html(timeFormat('%d.%m.%Y')(x0)) 199let xLeftShift=xCoord-tooltipXWidth/2 200let xRightShift=width-xCoord-tooltipXWidth/2 201if(xLeftShift<0&&xRightShift>0){tooltipX.attr('transform','translate('+(xCoord-tooltipXWidth/2-xLeftShift)+','+height+')',)}else if(xLeftShift>0&&xRightShift<0){tooltipX.attr('transform','translate('+(xCoord-tooltipXWidth/2+xRightShift)+','+height+')',)}else{tooltipX.attr('transform','translate('+(xCoord-tooltipXWidth/2)+','+height+')')} 202if(xCoord>=width-tooltipWidth-tooltipShift){xCoord=xCoord-tooltipWidth-tooltipShift}else{xCoord=xCoord+5} 203if(yCoord>=height-tooltipHeight-tooltipShift){yCoord=height-tooltipHeight-tooltipShift} 204tooltip.attr('transform','translate('+xCoord+','+yCoord+')')}).on('mouseout',function(event){crosshairLine.attr('x1',-1500).attr('x2',-1500) 205tooltipX.attr('transform','translate(-1500,'+height+')') 206tooltip.attr('transform','translate(-1500,'+height+')')}) 207const brush=brushX().extent([[0,0],[width,height],]).on('end',({selection})=>{if(!selection){if(!idleTimeout) 208return(idleTimeout=setTimeout(()=>{idleTimeout=null},350)) 209xScale.domain(_tmp.parseTime(firstDate),_tmp.parseTime(lastDate))}else{firstDate=timeFormat('%Y-%m-%d')(xScale.invert(selection[0])) 210lastDate=timeFormat('%Y-%m-%d')(xScale.invert(selection[1])) 211_tmp.datepicker_startTarget.value=firstDate 212_tmp.datepicker_endTarget.value=lastDate 213datePickerStart.setDate(firstDate) 214datePickerEnd.setDate(lastDate) 215brushGroup.select('.brush').call(brush.move,null)} 216xAxism.transition().duration(1000).call(axisBottom(xScale)) 217_tmp.drawChart(svg,dataSet)}).on('start',()=>{crosshairLine.attr('x1',-1500).attr('x2',-1500) 218tooltipX.attr('transform','translate(-1500,'+height+')') 219tooltip.attr('transform','translate(-1500,'+height+')')}) 220brushGroup.append('g').attr('class','brush').attr('transform','translate('+margin.left+','+margin.top+')').call(brush)} 221rMinMax=(data,f,dz=0.1)=>{let myMin=min(data,f) 222let myMax=max(data,f) 223let d=myMax-myMin 224myMin=myMin-dz*d 225myMax=myMax+dz*d 226if(myMin-myMax===0){myMax=myMax+1 227myMin=myMin-1} 228return[myMin,myMax]} 229parseTime=(d)=>{if(d.length===10){d=d+' 12:00'} 230return timeParse('%Y-%m-%d %H:%M')(d)} 231convert=(data)=>{let output=[] 232for(let i=0;i<data.length;i++){output[i]={date:this.parseTime(format(data[i].date,'yyyy-MM-dd')),value:data[i].value}} 233return output} 234windowResize({width,height,event}){this.benchmarkSelectorSetLabel() 235setTimeout(()=>{svg.attr('width',this.plotTarget.clientWidth) 236_tmp.drawChart(svg,dataSet)},200)} 237showDatepicker(e){let dropdown=e.currentTarget.parentNode.querySelector('div.datepicker-dropdown') 238dropdown.style.left=e.currentTarget.getBoundingClientRect().left-dropdown.offsetWidth/4+'px' 239dropdown.style.transitionDuration=`0ms`} 240benchmarkSelectorSetLabel(){this.benchmarkTarget.querySelector('.customSelectText').textContent=window.innerWidth<=1139?'Porównaj z...':'Wybierz'}}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.