PageSourceSearch

https://www.kupfundusz.pl/assets/controllers/user/linechart_controller-7e32f400306a174c6b7c4d46e521f4ae.js

js kupfundusz.pl collected 2026-10-08 10:12:59 UTC 17,358 bytes, 240 lines download raw bytes

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.