12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017110181101911020110211102211023110241102511026110271102811029110301103111032110331103411035110361103711038110391104011041110421104311044110451104611047110481104911050110511105211053110541105511056110571105811059110601106111062110631106411065110661106711068110691107011071110721107311074110751107611077110781107911080110811108211083110841108511086110871108811089110901109111092110931109411095110961109711098110991110011101111021110311104111051110611107111081110911110111111111211113111141111511116111171111811119111201112111122111231112411125111261112711128111291113011131111321113311134111351113611137111381113911140111411114211143111441114511146111471114811149111501115111152111531115411155111561115711158111591116011161111621116311164111651116611167111681116911170111711117211173111741117511176111771117811179111801118111182111831118411185111861118711188111891119011191111921119311194111951119611197111981119911200112011120211203112041120511206112071120811209112101121111212112131121411215112161121711218112191122011221112221122311224112251122611227112281122911230112311123211233112341123511236112371123811239112401124111242112431124411245112461124711248112491125011251112521125311254112551125611257112581125911260112611126211263112641126511266112671126811269112701127111272112731127411275112761127711278112791128011281112821128311284112851128611287112881128911290112911129211293112941129511296112971129811299113001130111302113031130411305113061130711308113091131011311113121131311314113151131611317113181131911320113211132211323113241132511326113271132811329113301133111332113331133411335113361133711338113391134011341113421134311344113451134611347113481134911350113511135211353113541135511356113571135811359113601136111362113631136411365113661136711368113691137011371113721137311374113751137611377113781137911380113811138211383113841138511386113871138811389113901139111392113931139411395113961139711398113991140011401114021140311404114051140611407114081140911410114111141211413114141141511416114171141811419114201142111422114231142411425114261142711428114291143011431114321143311434114351143611437114381143911440114411144211443114441144511446114471144811449114501145111452114531145411455114561145711458114591146011461114621146311464114651146611467114681146911470114711147211473114741147511476114771147811479114801148111482114831148411485114861148711488114891149011491114921149311494114951149611497114981149911500115011150211503115041150511506115071150811509115101151111512115131151411515115161151711518115191152011521115221152311524115251152611527115281152911530115311153211533115341153511536115371153811539115401154111542115431154411545115461154711548115491155011551115521155311554115551155611557115581155911560115611156211563115641156511566115671156811569115701157111572115731157411575115761157711578115791158011581115821158311584115851158611587115881158911590115911159211593115941159511596115971159811599116001160111602116031160411605116061160711608116091161011611116121161311614116151161611617116181161911620116211162211623116241162511626116271162811629116301163111632116331163411635116361163711638116391164011641116421164311644116451164611647116481164911650116511165211653116541165511656116571165811659116601166111662116631166411665116661166711668116691167011671116721167311674116751167611677116781167911680116811168211683116841168511686116871168811689116901169111692116931169411695116961169711698116991170011701117021170311704117051170611707117081170911710117111171211713117141171511716117171171811719117201172111722117231172411725117261172711728117291173011731117321173311734117351173611737117381173911740117411174211743117441174511746117471174811749117501175111752117531175411755117561175711758117591176011761117621176311764117651176611767117681176911770117711177211773117741177511776117771177811779117801178111782117831178411785117861178711788117891179011791117921179311794117951179611797117981179911800118011180211803118041180511806118071180811809118101181111812118131181411815118161181711818118191182011821118221182311824118251182611827118281182911830118311183211833118341183511836118371183811839118401184111842118431184411845118461184711848118491185011851118521185311854118551185611857118581185911860118611186211863118641186511866118671186811869118701187111872118731187411875118761187711878118791188011881118821188311884118851188611887118881188911890118911189211893118941189511896118971189811899119001190111902119031190411905119061190711908119091191011911119121191311914119151191611917119181191911920119211192211923119241192511926119271192811929119301193111932119331193411935119361193711938119391194011941119421194311944119451194611947119481194911950119511195211953119541195511956119571195811959119601196111962119631196411965119661196711968119691197011971119721197311974119751197611977119781197911980119811198211983119841198511986119871198811989119901199111992119931199411995119961199711998119991200012001120021200312004120051200612007120081200912010120111201212013120141201512016120171201812019120201202112022120231202412025120261202712028120291203012031120321203312034120351203612037120381203912040120411204212043120441204512046120471204812049120501205112052120531205412055120561205712058120591206012061120621206312064120651206612067120681206912070120711207212073120741207512076120771207812079120801208112082120831208412085120861208712088120891209012091120921209312094120951209612097120981209912100121011210212103121041210512106121071210812109121101211112112121131211412115121161211712118121191212012121121221212312124121251212612127121281212912130121311213212133121341213512136121371213812139121401214112142121431214412145121461214712148121491215012151121521215312154121551215612157121581215912160121611216212163121641216512166121671216812169121701217112172121731217412175121761217712178121791218012181121821218312184121851218612187121881218912190121911219212193121941219512196121971219812199122001220112202122031220412205122061220712208122091221012211122121221312214122151221612217122181221912220122211222212223122241222512226122271222812229122301223112232122331223412235122361223712238122391224012241122421224312244122451224612247122481224912250122511225212253122541225512256122571225812259122601226112262122631226412265122661226712268122691227012271122721227312274122751227612277122781227912280122811228212283122841228512286122871228812289122901229112292122931229412295122961229712298122991230012301123021230312304123051230612307123081230912310123111231212313123141231512316123171231812319123201232112322123231232412325123261232712328123291233012331123321233312334123351233612337123381233912340123411234212343123441234512346123471234812349123501235112352123531235412355123561235712358123591236012361123621236312364123651236612367123681236912370123711237212373123741237512376123771237812379123801238112382123831238412385123861238712388123891239012391123921239312394123951239612397123981239912400124011240212403124041240512406124071240812409124101241112412124131241412415124161241712418124191242012421124221242312424124251242612427124281242912430124311243212433124341243512436124371243812439124401244112442124431244412445124461244712448124491245012451124521245312454124551245612457124581245912460124611246212463124641246512466124671246812469124701247112472124731247412475124761247712478124791248012481124821248312484124851248612487124881248912490124911249212493124941249512496124971249812499125001250112502125031250412505125061250712508125091251012511125121251312514125151251612517125181251912520125211252212523125241252512526125271252812529125301253112532125331253412535125361253712538125391254012541125421254312544125451254612547125481254912550125511255212553125541255512556125571255812559125601256112562125631256412565125661256712568125691257012571125721257312574125751257612577125781257912580125811258212583125841258512586125871258812589125901259112592125931259412595125961259712598125991260012601126021260312604126051260612607126081260912610126111261212613126141261512616126171261812619126201262112622126231262412625126261262712628126291263012631126321263312634126351263612637126381263912640126411264212643126441264512646126471264812649126501265112652126531265412655126561265712658126591266012661126621266312664126651266612667126681266912670126711267212673126741267512676126771267812679126801268112682126831268412685126861268712688126891269012691126921269312694126951269612697126981269912700127011270212703127041270512706127071270812709127101271112712127131271412715127161271712718127191272012721127221272312724127251272612727127281272912730127311273212733127341273512736127371273812739127401274112742127431274412745127461274712748127491275012751127521275312754127551275612757127581275912760127611276212763127641276512766127671276812769127701277112772127731277412775127761277712778127791278012781127821278312784127851278612787127881278912790127911279212793127941279512796127971279812799128001280112802128031280412805128061280712808128091281012811128121281312814128151281612817128181281912820128211282212823128241282512826128271282812829128301283112832128331283412835128361283712838128391284012841128421284312844128451284612847128481284912850128511285212853128541285512856128571285812859128601286112862128631286412865128661286712868128691287012871128721287312874128751287612877128781287912880128811288212883128841288512886128871288812889128901289112892128931289412895128961289712898128991290012901129021290312904129051290612907129081290912910129111291212913129141291512916129171291812919129201292112922129231292412925129261292712928129291293012931129321293312934129351293612937129381293912940129411294212943129441294512946129471294812949129501295112952129531295412955129561295712958129591296012961129621296312964129651296612967129681296912970129711297212973129741297512976129771297812979129801298112982129831298412985129861298712988129891299012991129921299312994129951299612997129981299913000130011300213003130041300513006130071300813009130101301113012130131301413015130161301713018130191302013021130221302313024130251302613027130281302913030130311303213033130341303513036130371303813039130401304113042130431304413045130461304713048130491305013051130521305313054130551305613057130581305913060130611306213063130641306513066130671306813069130701307113072130731307413075130761307713078130791308013081130821308313084130851308613087130881308913090130911309213093130941309513096130971309813099131001310113102131031310413105131061310713108131091311013111131121311313114131151311613117131181311913120131211312213123131241312513126131271312813129131301313113132131331313413135131361313713138131391314013141131421314313144131451314613147131481314913150131511315213153131541315513156131571315813159131601316113162131631316413165131661316713168131691317013171131721317313174131751317613177131781317913180131811318213183131841318513186131871318813189131901319113192131931319413195131961319713198131991320013201132021320313204132051320613207132081320913210132111321213213132141321513216132171321813219132201322113222132231322413225132261322713228132291323013231132321323313234132351323613237132381323913240132411324213243132441324513246132471324813249132501325113252132531325413255132561325713258132591326013261132621326313264132651326613267132681326913270132711327213273132741327513276132771327813279132801328113282132831328413285132861328713288132891329013291132921329313294132951329613297132981329913300133011330213303133041330513306133071330813309133101331113312133131331413315133161331713318133191332013321133221332313324133251332613327133281332913330133311333213333133341333513336133371333813339133401334113342133431334413345133461334713348133491335013351133521335313354133551335613357133581335913360133611336213363133641336513366133671336813369133701337113372133731337413375133761337713378133791338013381133821338313384133851338613387133881338913390133911339213393133941339513396133971339813399134001340113402134031340413405134061340713408134091341013411134121341313414134151341613417134181341913420134211342213423134241342513426134271342813429134301343113432134331343413435134361343713438134391344013441134421344313444134451344613447134481344913450134511345213453134541345513456134571345813459134601346113462134631346413465134661346713468134691347013471134721347313474134751347613477134781347913480134811348213483134841348513486134871348813489134901349113492134931349413495134961349713498134991350013501135021350313504135051350613507135081350913510135111351213513135141351513516135171351813519135201352113522135231352413525135261352713528135291353013531135321353313534135351353613537135381353913540135411354213543135441354513546135471354813549135501355113552135531355413555135561355713558135591356013561135621356313564135651356613567135681356913570135711357213573135741357513576135771357813579135801358113582135831358413585135861358713588135891359013591135921359313594135951359613597135981359913600136011360213603136041360513606136071360813609136101361113612136131361413615136161361713618136191362013621136221362313624136251362613627136281362913630136311363213633136341363513636136371363813639136401364113642136431364413645136461364713648136491365013651136521365313654136551365613657136581365913660136611366213663136641366513666136671366813669136701367113672136731367413675136761367713678136791368013681136821368313684136851368613687136881368913690136911369213693136941369513696136971369813699137001370113702137031370413705137061370713708137091371013711137121371313714137151371613717137181371913720137211372213723137241372513726137271372813729137301373113732137331373413735137361373713738137391374013741137421374313744137451374613747137481374913750137511375213753137541375513756137571375813759137601376113762137631376413765137661376713768137691377013771137721377313774137751377613777137781377913780137811378213783137841378513786137871378813789137901379113792137931379413795137961379713798137991380013801138021380313804138051380613807138081380913810138111381213813138141381513816138171381813819138201382113822138231382413825138261382713828138291383013831138321383313834138351383613837138381383913840138411384213843138441384513846138471384813849138501385113852138531385413855138561385713858138591386013861138621386313864138651386613867138681386913870138711387213873138741387513876138771387813879138801388113882138831388413885138861388713888138891389013891138921389313894138951389613897138981389913900139011390213903139041390513906139071390813909139101391113912139131391413915139161391713918139191392013921139221392313924139251392613927139281392913930139311393213933139341393513936139371393813939139401394113942139431394413945139461394713948139491395013951139521395313954139551395613957139581395913960139611396213963139641396513966139671396813969139701397113972139731397413975139761397713978139791398013981139821398313984139851398613987139881398913990139911399213993139941399513996139971399813999140001400114002140031400414005140061400714008140091401014011140121401314014140151401614017140181401914020140211402214023140241402514026140271402814029140301403114032140331403414035140361403714038140391404014041140421404314044140451404614047140481404914050140511405214053140541405514056140571405814059140601406114062140631406414065140661406714068140691407014071140721407314074140751407614077140781407914080140811408214083140841408514086140871408814089140901409114092140931409414095140961409714098140991410014101141021410314104141051410614107141081410914110141111411214113141141411514116141171411814119141201412114122141231412414125141261412714128141291413014131141321413314134141351413614137141381413914140141411414214143141441414514146141471414814149141501415114152141531415414155141561415714158141591416014161141621416314164141651416614167141681416914170141711417214173141741417514176141771417814179141801418114182141831418414185141861418714188141891419014191141921419314194141951419614197141981419914200142011420214203142041420514206142071420814209142101421114212142131421414215142161421714218142191422014221142221422314224142251422614227142281422914230142311423214233142341423514236142371423814239142401424114242142431424414245142461424714248142491425014251142521425314254142551425614257142581425914260142611426214263142641426514266142671426814269142701427114272142731427414275142761427714278142791428014281142821428314284142851428614287142881428914290142911429214293142941429514296142971429814299143001430114302143031430414305143061430714308143091431014311143121431314314143151431614317143181431914320143211432214323143241432514326143271432814329143301433114332143331433414335143361433714338143391434014341143421434314344143451434614347143481434914350143511435214353143541435514356143571435814359143601436114362143631436414365143661436714368143691437014371143721437314374143751437614377143781437914380143811438214383143841438514386143871438814389143901439114392143931439414395143961439714398143991440014401144021440314404144051440614407144081440914410144111441214413144141441514416144171441814419144201442114422144231442414425144261442714428144291443014431144321443314434144351443614437144381443914440144411444214443144441444514446144471444814449144501445114452144531445414455144561445714458144591446014461144621446314464144651446614467144681446914470144711447214473144741447514476144771447814479144801448114482144831448414485144861448714488144891449014491144921449314494144951449614497144981449914500145011450214503145041450514506145071450814509145101451114512145131451414515145161451714518145191452014521145221452314524145251452614527145281452914530145311453214533145341453514536145371453814539145401454114542145431454414545145461454714548145491455014551145521455314554145551455614557145581455914560145611456214563145641456514566145671456814569145701457114572145731457414575145761457714578145791458014581145821458314584145851458614587145881458914590145911459214593145941459514596145971459814599146001460114602146031460414605146061460714608146091461014611146121461314614146151461614617146181461914620146211462214623146241462514626146271462814629146301463114632146331463414635146361463714638146391464014641146421464314644146451464614647146481464914650146511465214653146541465514656146571465814659146601466114662146631466414665146661466714668146691467014671146721467314674146751467614677146781467914680146811468214683146841468514686146871468814689146901469114692146931469414695146961469714698146991470014701147021470314704147051470614707147081470914710147111471214713147141471514716147171471814719147201472114722147231472414725147261472714728147291473014731147321473314734147351473614737147381473914740147411474214743147441474514746147471474814749147501475114752147531475414755147561475714758147591476014761147621476314764147651476614767147681476914770147711477214773147741477514776147771477814779147801478114782147831478414785147861478714788147891479014791147921479314794147951479614797147981479914800148011480214803148041480514806148071480814809148101481114812148131481414815148161481714818148191482014821148221482314824148251482614827148281482914830148311483214833148341483514836148371483814839148401484114842148431484414845148461484714848148491485014851148521485314854148551485614857148581485914860148611486214863148641486514866148671486814869148701487114872148731487414875148761487714878148791488014881148821488314884148851488614887148881488914890148911489214893148941489514896148971489814899149001490114902149031490414905149061490714908149091491014911149121491314914149151491614917149181491914920149211492214923149241492514926149271492814929149301493114932149331493414935149361493714938149391494014941149421494314944149451494614947149481494914950149511495214953149541495514956149571495814959149601496114962149631496414965149661496714968149691497014971149721497314974149751497614977149781497914980149811498214983149841498514986149871498814989149901499114992149931499414995149961499714998149991500015001150021500315004150051500615007150081500915010150111501215013150141501515016150171501815019150201502115022150231502415025150261502715028150291503015031150321503315034150351503615037150381503915040150411504215043150441504515046150471504815049150501505115052150531505415055150561505715058150591506015061150621506315064150651506615067150681506915070150711507215073150741507515076150771507815079150801508115082150831508415085150861508715088150891509015091150921509315094150951509615097150981509915100151011510215103151041510515106151071510815109151101511115112151131511415115151161511715118151191512015121151221512315124151251512615127151281512915130151311513215133151341513515136151371513815139151401514115142151431514415145151461514715148151491515015151151521515315154151551515615157151581515915160151611516215163151641516515166151671516815169151701517115172151731517415175151761517715178151791518015181151821518315184151851518615187151881518915190151911519215193151941519515196151971519815199152001520115202152031520415205152061520715208152091521015211152121521315214152151521615217152181521915220152211522215223152241522515226152271522815229152301523115232152331523415235152361523715238152391524015241152421524315244152451524615247152481524915250152511525215253152541525515256152571525815259152601526115262152631526415265152661526715268152691527015271152721527315274152751527615277152781527915280152811528215283152841528515286152871528815289152901529115292152931529415295152961529715298152991530015301153021530315304153051530615307153081530915310153111531215313153141531515316153171531815319153201532115322153231532415325153261532715328153291533015331153321533315334153351533615337153381533915340153411534215343153441534515346153471534815349153501535115352153531535415355153561535715358153591536015361153621536315364153651536615367153681536915370153711537215373153741537515376153771537815379153801538115382153831538415385153861538715388153891539015391153921539315394153951539615397153981539915400154011540215403154041540515406154071540815409154101541115412154131541415415154161541715418154191542015421154221542315424154251542615427154281542915430154311543215433154341543515436154371543815439154401544115442154431544415445154461544715448154491545015451154521545315454154551545615457154581545915460154611546215463154641546515466154671546815469154701547115472154731547415475154761547715478154791548015481154821548315484154851548615487154881548915490154911549215493154941549515496154971549815499155001550115502155031550415505155061550715508155091551015511155121551315514155151551615517155181551915520155211552215523155241552515526155271552815529155301553115532155331553415535155361553715538155391554015541155421554315544155451554615547155481554915550155511555215553155541555515556155571555815559155601556115562155631556415565155661556715568155691557015571155721557315574155751557615577155781557915580155811558215583155841558515586155871558815589155901559115592155931559415595155961559715598155991560015601156021560315604156051560615607156081560915610156111561215613156141561515616156171561815619156201562115622156231562415625156261562715628156291563015631156321563315634156351563615637156381563915640156411564215643156441564515646156471564815649156501565115652156531565415655156561565715658156591566015661156621566315664156651566615667156681566915670156711567215673156741567515676156771567815679156801568115682156831568415685156861568715688156891569015691156921569315694156951569615697156981569915700157011570215703157041570515706157071570815709157101571115712157131571415715157161571715718157191572015721157221572315724157251572615727157281572915730157311573215733157341573515736157371573815739157401574115742157431574415745157461574715748157491575015751157521575315754157551575615757157581575915760157611576215763157641576515766157671576815769157701577115772157731577415775157761577715778157791578015781157821578315784157851578615787157881578915790157911579215793157941579515796157971579815799158001580115802158031580415805158061580715808158091581015811158121581315814158151581615817158181581915820158211582215823158241582515826158271582815829158301583115832158331583415835158361583715838158391584015841158421584315844158451584615847158481584915850158511585215853158541585515856158571585815859158601586115862158631586415865158661586715868158691587015871158721587315874158751587615877158781587915880158811588215883158841588515886158871588815889158901589115892158931589415895158961589715898158991590015901159021590315904159051590615907159081590915910159111591215913159141591515916159171591815919159201592115922159231592415925159261592715928159291593015931159321593315934159351593615937159381593915940159411594215943159441594515946159471594815949159501595115952159531595415955159561595715958159591596015961159621596315964159651596615967159681596915970159711597215973159741597515976159771597815979159801598115982159831598415985159861598715988159891599015991159921599315994159951599615997159981599916000160011600216003160041600516006160071600816009160101601116012160131601416015160161601716018160191602016021160221602316024160251602616027160281602916030160311603216033160341603516036160371603816039160401604116042160431604416045160461604716048160491605016051160521605316054160551605616057160581605916060160611606216063160641606516066160671606816069160701607116072160731607416075160761607716078160791608016081160821608316084160851608616087160881608916090160911609216093160941609516096160971609816099161001610116102161031610416105161061610716108161091611016111161121611316114161151611616117161181611916120161211612216123161241612516126161271612816129161301613116132161331613416135161361613716138161391614016141161421614316144161451614616147161481614916150161511615216153161541615516156161571615816159161601616116162161631616416165161661616716168161691617016171161721617316174161751617616177161781617916180161811618216183161841618516186161871618816189161901619116192161931619416195161961619716198161991620016201162021620316204162051620616207162081620916210162111621216213162141621516216162171621816219162201622116222162231622416225162261622716228162291623016231162321623316234162351623616237162381623916240162411624216243162441624516246162471624816249162501625116252162531625416255162561625716258162591626016261162621626316264162651626616267162681626916270162711627216273162741627516276162771627816279162801628116282162831628416285162861628716288162891629016291162921629316294162951629616297162981629916300163011630216303163041630516306163071630816309163101631116312163131631416315163161631716318163191632016321163221632316324163251632616327163281632916330163311633216333163341633516336163371633816339163401634116342163431634416345163461634716348163491635016351163521635316354163551635616357163581635916360163611636216363163641636516366163671636816369163701637116372163731637416375163761637716378163791638016381163821638316384163851638616387163881638916390163911639216393163941639516396163971639816399164001640116402164031640416405164061640716408164091641016411164121641316414164151641616417164181641916420164211642216423164241642516426164271642816429164301643116432164331643416435164361643716438164391644016441164421644316444164451644616447164481644916450164511645216453164541645516456164571645816459164601646116462164631646416465164661646716468164691647016471164721647316474164751647616477164781647916480164811648216483164841648516486164871648816489164901649116492164931649416495164961649716498164991650016501165021650316504165051650616507165081650916510165111651216513165141651516516165171651816519165201652116522165231652416525165261652716528165291653016531165321653316534165351653616537165381653916540165411654216543165441654516546165471654816549165501655116552165531655416555165561655716558165591656016561165621656316564165651656616567165681656916570165711657216573165741657516576165771657816579165801658116582165831658416585165861658716588165891659016591165921659316594165951659616597165981659916600166011660216603166041660516606166071660816609166101661116612166131661416615166161661716618166191662016621166221662316624166251662616627166281662916630166311663216633166341663516636166371663816639166401664116642166431664416645166461664716648166491665016651166521665316654166551665616657166581665916660166611666216663166641666516666166671666816669166701667116672166731667416675166761667716678166791668016681166821668316684166851668616687166881668916690166911669216693166941669516696166971669816699167001670116702167031670416705167061670716708167091671016711167121671316714167151671616717167181671916720167211672216723167241672516726167271672816729167301673116732167331673416735167361673716738167391674016741167421674316744167451674616747167481674916750167511675216753167541675516756167571675816759167601676116762167631676416765167661676716768167691677016771167721677316774167751677616777167781677916780167811678216783167841678516786167871678816789167901679116792167931679416795167961679716798167991680016801168021680316804168051680616807168081680916810168111681216813168141681516816168171681816819168201682116822168231682416825168261682716828168291683016831168321683316834168351683616837168381683916840168411684216843168441684516846168471684816849168501685116852168531685416855168561685716858168591686016861168621686316864168651686616867168681686916870168711687216873168741687516876168771687816879168801688116882168831688416885168861688716888168891689016891168921689316894168951689616897168981689916900169011690216903169041690516906169071690816909169101691116912169131691416915169161691716918169191692016921169221692316924169251692616927169281692916930169311693216933169341693516936169371693816939169401694116942169431694416945169461694716948169491695016951169521695316954169551695616957169581695916960169611696216963169641696516966169671696816969169701697116972169731697416975169761697716978169791698016981169821698316984169851698616987169881698916990169911699216993169941699516996169971699816999170001700117002170031700417005170061700717008170091701017011170121701317014170151701617017170181701917020170211702217023170241702517026170271702817029170301703117032170331703417035170361703717038170391704017041170421704317044170451704617047170481704917050170511705217053170541705517056170571705817059170601706117062170631706417065170661706717068170691707017071170721707317074170751707617077170781707917080170811708217083170841708517086170871708817089170901709117092170931709417095170961709717098170991710017101171021710317104171051710617107171081710917110171111711217113171141711517116171171711817119171201712117122171231712417125171261712717128171291713017131171321713317134171351713617137171381713917140171411714217143171441714517146171471714817149171501715117152171531715417155171561715717158171591716017161171621716317164171651716617167171681716917170171711717217173171741717517176171771717817179171801718117182171831718417185171861718717188171891719017191171921719317194171951719617197171981719917200172011720217203172041720517206172071720817209172101721117212172131721417215172161721717218172191722017221172221722317224172251722617227172281722917230172311723217233172341723517236172371723817239172401724117242172431724417245172461724717248172491725017251172521725317254172551725617257172581725917260172611726217263172641726517266172671726817269172701727117272172731727417275172761727717278172791728017281172821728317284172851728617287172881728917290172911729217293172941729517296172971729817299173001730117302173031730417305173061730717308173091731017311173121731317314173151731617317173181731917320173211732217323173241732517326173271732817329173301733117332173331733417335173361733717338173391734017341173421734317344173451734617347173481734917350173511735217353173541735517356173571735817359173601736117362173631736417365173661736717368173691737017371173721737317374173751737617377173781737917380173811738217383173841738517386173871738817389173901739117392173931739417395173961739717398173991740017401174021740317404174051740617407174081740917410174111741217413174141741517416174171741817419174201742117422174231742417425174261742717428174291743017431174321743317434174351743617437174381743917440174411744217443174441744517446174471744817449174501745117452174531745417455174561745717458174591746017461174621746317464174651746617467174681746917470174711747217473174741747517476174771747817479174801748117482174831748417485174861748717488174891749017491174921749317494174951749617497174981749917500175011750217503175041750517506175071750817509175101751117512175131751417515175161751717518175191752017521175221752317524175251752617527175281752917530175311753217533175341753517536175371753817539175401754117542175431754417545175461754717548175491755017551175521755317554175551755617557175581755917560175611756217563175641756517566175671756817569175701757117572175731757417575175761757717578175791758017581175821758317584175851758617587175881758917590175911759217593175941759517596175971759817599176001760117602176031760417605176061760717608176091761017611176121761317614176151761617617176181761917620176211762217623176241762517626176271762817629176301763117632176331763417635176361763717638176391764017641176421764317644176451764617647176481764917650176511765217653176541765517656176571765817659176601766117662176631766417665176661766717668176691767017671176721767317674176751767617677176781767917680176811768217683176841768517686176871768817689176901769117692176931769417695176961769717698176991770017701177021770317704177051770617707177081770917710177111771217713177141771517716177171771817719177201772117722177231772417725177261772717728177291773017731177321773317734177351773617737177381773917740177411774217743177441774517746177471774817749177501775117752177531775417755177561775717758177591776017761177621776317764177651776617767177681776917770177711777217773177741777517776177771777817779177801778117782177831778417785177861778717788177891779017791177921779317794177951779617797177981779917800178011780217803178041780517806178071780817809178101781117812178131781417815178161781717818178191782017821178221782317824178251782617827178281782917830178311783217833178341783517836178371783817839178401784117842178431784417845178461784717848178491785017851178521785317854178551785617857178581785917860178611786217863178641786517866178671786817869178701787117872178731787417875178761787717878178791788017881178821788317884178851788617887178881788917890178911789217893178941789517896178971789817899179001790117902179031790417905179061790717908179091791017911179121791317914179151791617917179181791917920179211792217923179241792517926179271792817929179301793117932179331793417935179361793717938179391794017941179421794317944179451794617947179481794917950179511795217953179541795517956179571795817959179601796117962179631796417965179661796717968179691797017971179721797317974179751797617977179781797917980179811798217983179841798517986179871798817989179901799117992179931799417995179961799717998179991800018001180021800318004180051800618007180081800918010180111801218013180141801518016180171801818019180201802118022180231802418025180261802718028180291803018031180321803318034180351803618037180381803918040180411804218043180441804518046180471804818049180501805118052180531805418055180561805718058180591806018061180621806318064180651806618067180681806918070180711807218073180741807518076180771807818079180801808118082180831808418085180861808718088180891809018091180921809318094180951809618097180981809918100181011810218103181041810518106181071810818109181101811118112181131811418115181161811718118181191812018121181221812318124181251812618127181281812918130181311813218133181341813518136181371813818139181401814118142181431814418145181461814718148181491815018151181521815318154181551815618157181581815918160181611816218163181641816518166181671816818169181701817118172181731817418175181761817718178181791818018181181821818318184181851818618187181881818918190181911819218193181941819518196181971819818199182001820118202182031820418205182061820718208182091821018211182121821318214182151821618217182181821918220182211822218223182241822518226182271822818229182301823118232182331823418235182361823718238182391824018241182421824318244182451824618247182481824918250182511825218253182541825518256182571825818259182601826118262182631826418265182661826718268182691827018271182721827318274182751827618277182781827918280182811828218283182841828518286182871828818289182901829118292182931829418295182961829718298182991830018301183021830318304183051830618307183081830918310183111831218313183141831518316183171831818319183201832118322183231832418325183261832718328183291833018331183321833318334183351833618337183381833918340183411834218343183441834518346183471834818349183501835118352183531835418355183561835718358183591836018361183621836318364183651836618367183681836918370183711837218373183741837518376183771837818379183801838118382183831838418385183861838718388183891839018391183921839318394183951839618397183981839918400184011840218403184041840518406184071840818409184101841118412184131841418415184161841718418184191842018421184221842318424184251842618427184281842918430184311843218433184341843518436184371843818439184401844118442184431844418445184461844718448184491845018451184521845318454184551845618457184581845918460184611846218463184641846518466184671846818469184701847118472184731847418475184761847718478184791848018481184821848318484184851848618487184881848918490184911849218493184941849518496184971849818499185001850118502185031850418505185061850718508185091851018511185121851318514185151851618517185181851918520185211852218523185241852518526185271852818529185301853118532185331853418535185361853718538185391854018541185421854318544185451854618547185481854918550185511855218553185541855518556185571855818559185601856118562185631856418565185661856718568185691857018571185721857318574185751857618577185781857918580185811858218583185841858518586185871858818589185901859118592185931859418595185961859718598185991860018601186021860318604186051860618607186081860918610186111861218613186141861518616186171861818619186201862118622186231862418625186261862718628186291863018631186321863318634186351863618637186381863918640186411864218643186441864518646186471864818649186501865118652186531865418655186561865718658186591866018661186621866318664186651866618667186681866918670186711867218673186741867518676186771867818679186801868118682186831868418685186861868718688186891869018691186921869318694186951869618697186981869918700187011870218703187041870518706187071870818709187101871118712187131871418715187161871718718187191872018721187221872318724187251872618727187281872918730187311873218733187341873518736187371873818739187401874118742187431874418745187461874718748187491875018751187521875318754187551875618757187581875918760187611876218763187641876518766187671876818769187701877118772187731877418775187761877718778187791878018781187821878318784187851878618787187881878918790187911879218793187941879518796187971879818799188001880118802188031880418805188061880718808188091881018811188121881318814188151881618817188181881918820188211882218823188241882518826188271882818829188301883118832188331883418835188361883718838188391884018841188421884318844188451884618847188481884918850188511885218853188541885518856188571885818859188601886118862188631886418865188661886718868188691887018871188721887318874188751887618877188781887918880188811888218883188841888518886188871888818889188901889118892188931889418895188961889718898188991890018901189021890318904189051890618907189081890918910189111891218913189141891518916189171891818919189201892118922189231892418925189261892718928189291893018931189321893318934189351893618937189381893918940189411894218943189441894518946189471894818949189501895118952189531895418955189561895718958189591896018961189621896318964189651896618967189681896918970189711897218973189741897518976189771897818979189801898118982189831898418985189861898718988189891899018991189921899318994189951899618997189981899919000190011900219003190041900519006190071900819009190101901119012190131901419015190161901719018190191902019021190221902319024190251902619027190281902919030190311903219033190341903519036190371903819039190401904119042190431904419045190461904719048190491905019051190521905319054190551905619057190581905919060190611906219063190641906519066190671906819069190701907119072190731907419075190761907719078190791908019081190821908319084190851908619087190881908919090190911909219093190941909519096190971909819099191001910119102191031910419105191061910719108191091911019111191121911319114191151911619117191181911919120191211912219123191241912519126191271912819129191301913119132191331913419135191361913719138191391914019141191421914319144191451914619147191481914919150191511915219153191541915519156191571915819159191601916119162191631916419165191661916719168191691917019171191721917319174191751917619177191781917919180191811918219183191841918519186191871918819189191901919119192191931919419195191961919719198191991920019201192021920319204192051920619207192081920919210192111921219213192141921519216192171921819219192201922119222192231922419225192261922719228192291923019231192321923319234192351923619237192381923919240192411924219243192441924519246192471924819249192501925119252192531925419255192561925719258192591926019261192621926319264192651926619267192681926919270192711927219273192741927519276192771927819279192801928119282192831928419285192861928719288192891929019291192921929319294192951929619297192981929919300193011930219303193041930519306193071930819309193101931119312193131931419315193161931719318193191932019321193221932319324193251932619327193281932919330193311933219333193341933519336193371933819339193401934119342193431934419345193461934719348193491935019351193521935319354193551935619357193581935919360193611936219363193641936519366193671936819369193701937119372193731937419375193761937719378193791938019381193821938319384193851938619387193881938919390193911939219393193941939519396193971939819399194001940119402194031940419405194061940719408194091941019411194121941319414194151941619417194181941919420194211942219423194241942519426194271942819429194301943119432194331943419435194361943719438194391944019441194421944319444194451944619447194481944919450194511945219453194541945519456194571945819459194601946119462194631946419465194661946719468194691947019471194721947319474194751947619477194781947919480194811948219483194841948519486194871948819489194901949119492194931949419495194961949719498194991950019501195021950319504195051950619507195081950919510195111951219513195141951519516195171951819519195201952119522195231952419525195261952719528195291953019531195321953319534195351953619537195381953919540195411954219543195441954519546195471954819549195501955119552195531955419555195561955719558195591956019561195621956319564195651956619567195681956919570195711957219573195741957519576195771957819579195801958119582195831958419585195861958719588195891959019591195921959319594195951959619597195981959919600196011960219603196041960519606196071960819609196101961119612196131961419615196161961719618196191962019621196221962319624196251962619627196281962919630196311963219633196341963519636196371963819639196401964119642196431964419645196461964719648196491965019651196521965319654196551965619657196581965919660196611966219663196641966519666196671966819669196701967119672196731967419675196761967719678196791968019681196821968319684196851968619687196881968919690196911969219693196941969519696196971969819699197001970119702197031970419705197061970719708197091971019711197121971319714197151971619717197181971919720197211972219723197241972519726197271972819729197301973119732197331973419735197361973719738197391974019741197421974319744197451974619747197481974919750197511975219753197541975519756197571975819759197601976119762197631976419765197661976719768197691977019771197721977319774197751977619777197781977919780197811978219783197841978519786197871978819789197901979119792197931979419795197961979719798197991980019801198021980319804198051980619807198081980919810198111981219813198141981519816198171981819819198201982119822198231982419825198261982719828198291983019831198321983319834198351983619837198381983919840198411984219843198441984519846198471984819849198501985119852198531985419855198561985719858198591986019861198621986319864198651986619867198681986919870198711987219873198741987519876198771987819879198801988119882198831988419885198861988719888198891989019891198921989319894198951989619897198981989919900199011990219903199041990519906199071990819909199101991119912199131991419915199161991719918199191992019921199221992319924199251992619927199281992919930199311993219933199341993519936199371993819939199401994119942199431994419945199461994719948199491995019951199521995319954199551995619957199581995919960199611996219963199641996519966199671996819969199701997119972199731997419975199761997719978199791998019981199821998319984199851998619987199881998919990199911999219993199941999519996199971999819999200002000120002200032000420005200062000720008200092001020011200122001320014200152001620017200182001920020200212002220023200242002520026200272002820029200302003120032200332003420035200362003720038200392004020041200422004320044200452004620047200482004920050200512005220053200542005520056200572005820059200602006120062200632006420065200662006720068200692007020071200722007320074200752007620077200782007920080200812008220083200842008520086200872008820089200902009120092200932009420095200962009720098200992010020101201022010320104201052010620107201082010920110201112011220113201142011520116201172011820119201202012120122201232012420125201262012720128201292013020131201322013320134201352013620137201382013920140201412014220143201442014520146201472014820149201502015120152201532015420155201562015720158201592016020161201622016320164201652016620167201682016920170201712017220173201742017520176201772017820179201802018120182201832018420185201862018720188201892019020191201922019320194201952019620197201982019920200202012020220203202042020520206202072020820209202102021120212202132021420215202162021720218202192022020221202222022320224202252022620227202282022920230202312023220233202342023520236202372023820239202402024120242202432024420245202462024720248202492025020251202522025320254202552025620257202582025920260202612026220263202642026520266202672026820269202702027120272202732027420275202762027720278202792028020281202822028320284202852028620287202882028920290202912029220293202942029520296202972029820299203002030120302203032030420305203062030720308203092031020311203122031320314203152031620317203182031920320203212032220323203242032520326203272032820329203302033120332203332033420335203362033720338203392034020341203422034320344203452034620347203482034920350203512035220353203542035520356203572035820359203602036120362203632036420365203662036720368203692037020371203722037320374203752037620377203782037920380203812038220383203842038520386203872038820389203902039120392203932039420395203962039720398203992040020401204022040320404204052040620407204082040920410204112041220413204142041520416204172041820419204202042120422204232042420425204262042720428204292043020431204322043320434204352043620437204382043920440204412044220443204442044520446204472044820449204502045120452204532045420455204562045720458204592046020461204622046320464204652046620467204682046920470204712047220473204742047520476204772047820479204802048120482204832048420485204862048720488204892049020491204922049320494204952049620497204982049920500205012050220503205042050520506205072050820509205102051120512205132051420515205162051720518205192052020521205222052320524205252052620527205282052920530205312053220533205342053520536205372053820539205402054120542205432054420545205462054720548205492055020551205522055320554205552055620557205582055920560205612056220563205642056520566205672056820569205702057120572205732057420575205762057720578205792058020581205822058320584205852058620587205882058920590205912059220593205942059520596205972059820599206002060120602206032060420605206062060720608206092061020611206122061320614206152061620617206182061920620206212062220623206242062520626206272062820629206302063120632206332063420635206362063720638206392064020641206422064320644206452064620647206482064920650206512065220653206542065520656206572065820659206602066120662206632066420665206662066720668206692067020671206722067320674206752067620677206782067920680206812068220683206842068520686206872068820689206902069120692206932069420695206962069720698206992070020701207022070320704207052070620707207082070920710207112071220713207142071520716207172071820719207202072120722207232072420725207262072720728207292073020731207322073320734207352073620737207382073920740207412074220743207442074520746207472074820749207502075120752207532075420755207562075720758207592076020761207622076320764207652076620767207682076920770207712077220773207742077520776207772077820779207802078120782207832078420785207862078720788207892079020791207922079320794207952079620797207982079920800208012080220803208042080520806208072080820809208102081120812208132081420815208162081720818208192082020821208222082320824208252082620827208282082920830208312083220833208342083520836208372083820839208402084120842208432084420845208462084720848208492085020851208522085320854208552085620857208582085920860208612086220863208642086520866208672086820869208702087120872208732087420875208762087720878208792088020881208822088320884208852088620887208882088920890208912089220893208942089520896208972089820899209002090120902209032090420905209062090720908209092091020911209122091320914209152091620917209182091920920209212092220923209242092520926209272092820929209302093120932209332093420935209362093720938209392094020941209422094320944209452094620947209482094920950209512095220953209542095520956209572095820959209602096120962209632096420965209662096720968209692097020971209722097320974209752097620977209782097920980209812098220983209842098520986209872098820989209902099120992209932099420995209962099720998209992100021001210022100321004210052100621007210082100921010210112101221013210142101521016210172101821019210202102121022210232102421025210262102721028210292103021031210322103321034210352103621037210382103921040210412104221043210442104521046210472104821049210502105121052210532105421055210562105721058210592106021061210622106321064210652106621067210682106921070210712107221073210742107521076210772107821079210802108121082210832108421085210862108721088210892109021091210922109321094210952109621097210982109921100211012110221103211042110521106211072110821109211102111121112211132111421115211162111721118211192112021121211222112321124211252112621127211282112921130211312113221133211342113521136211372113821139211402114121142211432114421145211462114721148211492115021151211522115321154211552115621157211582115921160211612116221163211642116521166211672116821169211702117121172211732117421175211762117721178211792118021181211822118321184211852118621187211882118921190211912119221193211942119521196211972119821199212002120121202212032120421205212062120721208212092121021211212122121321214212152121621217212182121921220212212122221223212242122521226212272122821229212302123121232212332123421235212362123721238212392124021241212422124321244212452124621247212482124921250212512125221253212542125521256212572125821259212602126121262212632126421265212662126721268212692127021271212722127321274212752127621277212782127921280212812128221283212842128521286212872128821289212902129121292212932129421295212962129721298212992130021301213022130321304213052130621307213082130921310213112131221313213142131521316213172131821319213202132121322213232132421325213262132721328213292133021331213322133321334213352133621337213382133921340213412134221343213442134521346213472134821349213502135121352213532135421355213562135721358213592136021361213622136321364213652136621367213682136921370213712137221373213742137521376213772137821379213802138121382213832138421385213862138721388213892139021391213922139321394213952139621397213982139921400214012140221403214042140521406214072140821409214102141121412214132141421415214162141721418214192142021421214222142321424214252142621427214282142921430214312143221433214342143521436214372143821439214402144121442214432144421445214462144721448214492145021451214522145321454214552145621457214582145921460214612146221463214642146521466214672146821469214702147121472214732147421475214762147721478214792148021481214822148321484214852148621487214882148921490214912149221493214942149521496214972149821499215002150121502215032150421505215062150721508215092151021511215122151321514215152151621517215182151921520215212152221523215242152521526215272152821529215302153121532215332153421535215362153721538215392154021541215422154321544215452154621547215482154921550215512155221553215542155521556215572155821559215602156121562215632156421565215662156721568215692157021571215722157321574215752157621577215782157921580215812158221583215842158521586215872158821589215902159121592215932159421595215962159721598215992160021601216022160321604216052160621607216082160921610216112161221613216142161521616216172161821619216202162121622216232162421625216262162721628216292163021631216322163321634216352163621637216382163921640216412164221643216442164521646216472164821649216502165121652216532165421655216562165721658216592166021661216622166321664216652166621667216682166921670216712167221673216742167521676216772167821679216802168121682216832168421685216862168721688216892169021691216922169321694216952169621697216982169921700217012170221703217042170521706217072170821709217102171121712217132171421715217162171721718217192172021721217222172321724217252172621727217282172921730217312173221733217342173521736217372173821739217402174121742217432174421745217462174721748217492175021751217522175321754217552175621757217582175921760217612176221763217642176521766217672176821769217702177121772217732177421775217762177721778217792178021781217822178321784217852178621787217882178921790217912179221793217942179521796217972179821799218002180121802218032180421805218062180721808218092181021811218122181321814218152181621817218182181921820218212182221823218242182521826218272182821829218302183121832218332183421835218362183721838218392184021841218422184321844218452184621847218482184921850218512185221853218542185521856218572185821859218602186121862218632186421865218662186721868218692187021871218722187321874218752187621877218782187921880218812188221883218842188521886218872188821889218902189121892218932189421895218962189721898218992190021901219022190321904219052190621907219082190921910219112191221913219142191521916219172191821919219202192121922219232192421925219262192721928219292193021931219322193321934219352193621937219382193921940219412194221943219442194521946219472194821949219502195121952219532195421955219562195721958219592196021961219622196321964219652196621967219682196921970219712197221973219742197521976219772197821979219802198121982219832198421985219862198721988219892199021991219922199321994219952199621997219982199922000220012200222003220042200522006220072200822009220102201122012220132201422015220162201722018220192202022021220222202322024220252202622027220282202922030220312203222033220342203522036220372203822039220402204122042220432204422045220462204722048220492205022051220522205322054220552205622057220582205922060220612206222063220642206522066220672206822069220702207122072220732207422075220762207722078220792208022081220822208322084220852208622087220882208922090220912209222093220942209522096220972209822099221002210122102221032210422105221062210722108221092211022111221122211322114221152211622117221182211922120221212212222123221242212522126221272212822129221302213122132221332213422135221362213722138221392214022141221422214322144221452214622147221482214922150221512215222153221542215522156221572215822159221602216122162221632216422165221662216722168221692217022171221722217322174221752217622177221782217922180221812218222183221842218522186221872218822189221902219122192221932219422195221962219722198221992220022201222022220322204222052220622207222082220922210222112221222213222142221522216222172221822219222202222122222222232222422225222262222722228222292223022231222322223322234222352223622237222382223922240222412224222243222442224522246222472224822249222502225122252222532225422255222562225722258222592226022261222622226322264222652226622267222682226922270222712227222273222742227522276222772227822279222802228122282222832228422285222862228722288222892229022291222922229322294222952229622297222982229922300223012230222303223042230522306223072230822309223102231122312223132231422315223162231722318223192232022321223222232322324223252232622327223282232922330223312233222333223342233522336223372233822339223402234122342223432234422345223462234722348223492235022351223522235322354223552235622357223582235922360223612236222363223642236522366223672236822369223702237122372223732237422375223762237722378223792238022381223822238322384223852238622387223882238922390223912239222393223942239522396223972239822399224002240122402224032240422405224062240722408224092241022411224122241322414224152241622417224182241922420224212242222423224242242522426224272242822429224302243122432224332243422435224362243722438224392244022441224422244322444224452244622447224482244922450224512245222453224542245522456224572245822459224602246122462224632246422465224662246722468224692247022471224722247322474224752247622477224782247922480224812248222483224842248522486224872248822489224902249122492224932249422495224962249722498224992250022501225022250322504225052250622507225082250922510225112251222513225142251522516225172251822519225202252122522225232252422525225262252722528225292253022531225322253322534225352253622537225382253922540225412254222543225442254522546225472254822549225502255122552225532255422555225562255722558225592256022561225622256322564225652256622567225682256922570225712257222573225742257522576225772257822579225802258122582225832258422585225862258722588225892259022591225922259322594225952259622597225982259922600226012260222603226042260522606226072260822609226102261122612226132261422615226162261722618226192262022621226222262322624226252262622627226282262922630226312263222633226342263522636226372263822639226402264122642226432264422645226462264722648226492265022651226522265322654226552265622657226582265922660226612266222663226642266522666226672266822669226702267122672226732267422675226762267722678226792268022681226822268322684226852268622687226882268922690226912269222693226942269522696226972269822699227002270122702227032270422705227062270722708227092271022711227122271322714227152271622717227182271922720227212272222723227242272522726227272272822729227302273122732227332273422735227362273722738227392274022741227422274322744227452274622747227482274922750227512275222753227542275522756227572275822759227602276122762227632276422765227662276722768227692277022771227722277322774227752277622777227782277922780227812278222783227842278522786227872278822789227902279122792227932279422795227962279722798227992280022801228022280322804228052280622807228082280922810228112281222813228142281522816228172281822819228202282122822228232282422825228262282722828228292283022831228322283322834228352283622837228382283922840228412284222843228442284522846228472284822849228502285122852228532285422855228562285722858228592286022861228622286322864228652286622867228682286922870228712287222873228742287522876228772287822879228802288122882228832288422885228862288722888228892289022891228922289322894228952289622897228982289922900229012290222903229042290522906229072290822909229102291122912229132291422915229162291722918229192292022921229222292322924229252292622927229282292922930229312293222933229342293522936229372293822939229402294122942229432294422945229462294722948229492295022951229522295322954229552295622957229582295922960229612296222963229642296522966229672296822969229702297122972229732297422975229762297722978229792298022981229822298322984229852298622987229882298922990229912299222993229942299522996229972299822999230002300123002230032300423005230062300723008230092301023011230122301323014230152301623017230182301923020230212302223023230242302523026230272302823029230302303123032230332303423035230362303723038230392304023041230422304323044230452304623047230482304923050230512305223053230542305523056230572305823059230602306123062230632306423065230662306723068230692307023071230722307323074230752307623077230782307923080230812308223083230842308523086230872308823089230902309123092230932309423095230962309723098230992310023101231022310323104231052310623107231082310923110231112311223113231142311523116231172311823119231202312123122231232312423125231262312723128231292313023131231322313323134231352313623137231382313923140231412314223143231442314523146231472314823149231502315123152231532315423155231562315723158231592316023161231622316323164231652316623167231682316923170231712317223173231742317523176231772317823179231802318123182231832318423185231862318723188231892319023191231922319323194231952319623197231982319923200232012320223203232042320523206232072320823209232102321123212232132321423215232162321723218232192322023221232222322323224232252322623227232282322923230232312323223233232342323523236232372323823239232402324123242232432324423245232462324723248232492325023251232522325323254232552325623257232582325923260232612326223263232642326523266232672326823269232702327123272232732327423275232762327723278232792328023281232822328323284232852328623287232882328923290232912329223293232942329523296232972329823299233002330123302233032330423305233062330723308233092331023311233122331323314233152331623317233182331923320233212332223323233242332523326233272332823329233302333123332233332333423335233362333723338233392334023341233422334323344233452334623347233482334923350233512335223353233542335523356233572335823359233602336123362233632336423365233662336723368233692337023371233722337323374233752337623377233782337923380233812338223383233842338523386233872338823389233902339123392233932339423395233962339723398233992340023401234022340323404234052340623407234082340923410234112341223413234142341523416234172341823419234202342123422234232342423425234262342723428234292343023431234322343323434234352343623437234382343923440234412344223443234442344523446234472344823449234502345123452234532345423455234562345723458234592346023461234622346323464234652346623467234682346923470234712347223473234742347523476234772347823479234802348123482234832348423485234862348723488234892349023491234922349323494234952349623497234982349923500235012350223503235042350523506235072350823509235102351123512235132351423515235162351723518235192352023521235222352323524235252352623527235282352923530235312353223533235342353523536235372353823539235402354123542235432354423545235462354723548235492355023551235522355323554235552355623557235582355923560235612356223563235642356523566235672356823569235702357123572235732357423575235762357723578235792358023581235822358323584235852358623587235882358923590235912359223593235942359523596235972359823599236002360123602236032360423605236062360723608236092361023611236122361323614236152361623617236182361923620236212362223623236242362523626236272362823629236302363123632236332363423635236362363723638236392364023641236422364323644236452364623647236482364923650236512365223653236542365523656236572365823659236602366123662236632366423665236662366723668236692367023671236722367323674236752367623677236782367923680236812368223683236842368523686236872368823689236902369123692236932369423695236962369723698236992370023701237022370323704237052370623707237082370923710237112371223713237142371523716237172371823719237202372123722237232372423725237262372723728237292373023731237322373323734237352373623737237382373923740237412374223743237442374523746237472374823749237502375123752237532375423755237562375723758237592376023761237622376323764237652376623767237682376923770237712377223773237742377523776237772377823779237802378123782237832378423785237862378723788237892379023791237922379323794237952379623797237982379923800238012380223803238042380523806238072380823809238102381123812238132381423815238162381723818238192382023821238222382323824238252382623827238282382923830238312383223833238342383523836238372383823839238402384123842238432384423845238462384723848238492385023851238522385323854238552385623857238582385923860238612386223863238642386523866238672386823869238702387123872238732387423875238762387723878238792388023881238822388323884238852388623887238882388923890238912389223893238942389523896238972389823899239002390123902239032390423905239062390723908239092391023911239122391323914239152391623917239182391923920239212392223923239242392523926239272392823929239302393123932239332393423935239362393723938239392394023941239422394323944239452394623947239482394923950239512395223953239542395523956239572395823959239602396123962239632396423965239662396723968239692397023971239722397323974239752397623977239782397923980239812398223983239842398523986239872398823989239902399123992239932399423995239962399723998239992400024001240022400324004240052400624007240082400924010240112401224013240142401524016240172401824019240202402124022240232402424025240262402724028240292403024031240322403324034240352403624037240382403924040240412404224043240442404524046240472404824049240502405124052240532405424055240562405724058240592406024061240622406324064240652406624067240682406924070240712407224073240742407524076240772407824079240802408124082240832408424085240862408724088240892409024091240922409324094240952409624097240982409924100241012410224103241042410524106241072410824109241102411124112241132411424115241162411724118241192412024121241222412324124241252412624127241282412924130241312413224133241342413524136241372413824139241402414124142241432414424145241462414724148241492415024151241522415324154241552415624157241582415924160241612416224163241642416524166241672416824169241702417124172241732417424175241762417724178241792418024181241822418324184241852418624187241882418924190241912419224193241942419524196241972419824199242002420124202242032420424205242062420724208242092421024211242122421324214242152421624217242182421924220242212422224223242242422524226242272422824229242302423124232242332423424235242362423724238242392424024241242422424324244242452424624247242482424924250242512425224253242542425524256242572425824259242602426124262242632426424265242662426724268242692427024271242722427324274242752427624277242782427924280242812428224283242842428524286242872428824289242902429124292242932429424295242962429724298242992430024301243022430324304243052430624307243082430924310243112431224313243142431524316243172431824319243202432124322243232432424325243262432724328243292433024331243322433324334243352433624337243382433924340243412434224343243442434524346243472434824349243502435124352243532435424355243562435724358243592436024361243622436324364243652436624367243682436924370243712437224373243742437524376243772437824379243802438124382243832438424385243862438724388243892439024391243922439324394243952439624397243982439924400244012440224403244042440524406244072440824409244102441124412244132441424415244162441724418244192442024421244222442324424244252442624427244282442924430244312443224433244342443524436244372443824439244402444124442244432444424445244462444724448244492445024451244522445324454244552445624457244582445924460244612446224463244642446524466244672446824469244702447124472244732447424475244762447724478244792448024481244822448324484244852448624487244882448924490244912449224493244942449524496244972449824499245002450124502245032450424505245062450724508245092451024511245122451324514245152451624517245182451924520245212452224523245242452524526245272452824529245302453124532245332453424535245362453724538245392454024541245422454324544245452454624547245482454924550245512455224553245542455524556245572455824559245602456124562245632456424565245662456724568245692457024571245722457324574245752457624577245782457924580245812458224583245842458524586245872458824589245902459124592245932459424595245962459724598245992460024601246022460324604246052460624607246082460924610246112461224613246142461524616246172461824619246202462124622246232462424625246262462724628246292463024631246322463324634246352463624637246382463924640246412464224643246442464524646246472464824649246502465124652246532465424655246562465724658246592466024661246622466324664246652466624667246682466924670246712467224673246742467524676246772467824679246802468124682246832468424685246862468724688246892469024691246922469324694246952469624697246982469924700247012470224703247042470524706247072470824709247102471124712247132471424715247162471724718247192472024721247222472324724247252472624727247282472924730247312473224733247342473524736247372473824739247402474124742247432474424745247462474724748247492475024751247522475324754247552475624757247582475924760247612476224763247642476524766247672476824769247702477124772247732477424775247762477724778247792478024781247822478324784247852478624787247882478924790247912479224793247942479524796247972479824799248002480124802248032480424805248062480724808248092481024811248122481324814248152481624817248182481924820248212482224823248242482524826248272482824829248302483124832248332483424835248362483724838248392484024841248422484324844248452484624847248482484924850248512485224853248542485524856248572485824859248602486124862248632486424865248662486724868248692487024871248722487324874248752487624877248782487924880248812488224883248842488524886248872488824889248902489124892248932489424895248962489724898248992490024901249022490324904249052490624907249082490924910249112491224913249142491524916249172491824919249202492124922249232492424925249262492724928249292493024931249322493324934249352493624937249382493924940249412494224943249442494524946249472494824949249502495124952249532495424955249562495724958249592496024961249622496324964249652496624967249682496924970249712497224973249742497524976249772497824979249802498124982249832498424985249862498724988249892499024991249922499324994249952499624997249982499925000250012500225003250042500525006250072500825009250102501125012250132501425015250162501725018250192502025021250222502325024250252502625027250282502925030250312503225033250342503525036250372503825039250402504125042250432504425045250462504725048250492505025051250522505325054250552505625057250582505925060250612506225063250642506525066250672506825069250702507125072250732507425075250762507725078250792508025081250822508325084250852508625087250882508925090250912509225093250942509525096250972509825099251002510125102251032510425105251062510725108251092511025111251122511325114251152511625117251182511925120251212512225123251242512525126251272512825129251302513125132251332513425135251362513725138251392514025141251422514325144251452514625147251482514925150251512515225153251542515525156251572515825159251602516125162251632516425165251662516725168251692517025171251722517325174251752517625177251782517925180251812518225183251842518525186251872518825189251902519125192251932519425195251962519725198251992520025201252022520325204252052520625207252082520925210252112521225213252142521525216252172521825219252202522125222252232522425225252262522725228252292523025231252322523325234252352523625237252382523925240252412524225243252442524525246252472524825249252502525125252252532525425255252562525725258252592526025261252622526325264252652526625267252682526925270252712527225273252742527525276252772527825279252802528125282252832528425285252862528725288252892529025291252922529325294252952529625297252982529925300253012530225303253042530525306253072530825309253102531125312253132531425315253162531725318253192532025321253222532325324253252532625327253282532925330253312533225333253342533525336253372533825339253402534125342253432534425345253462534725348253492535025351253522535325354253552535625357253582535925360253612536225363253642536525366253672536825369253702537125372253732537425375253762537725378253792538025381253822538325384253852538625387253882538925390253912539225393253942539525396253972539825399254002540125402254032540425405254062540725408254092541025411254122541325414254152541625417254182541925420254212542225423254242542525426254272542825429254302543125432254332543425435254362543725438254392544025441254422544325444254452544625447254482544925450254512545225453254542545525456254572545825459254602546125462254632546425465254662546725468254692547025471254722547325474254752547625477254782547925480254812548225483254842548525486254872548825489254902549125492254932549425495254962549725498254992550025501255022550325504255052550625507255082550925510255112551225513255142551525516255172551825519255202552125522255232552425525255262552725528255292553025531255322553325534255352553625537255382553925540255412554225543255442554525546255472554825549255502555125552255532555425555255562555725558255592556025561255622556325564255652556625567255682556925570255712557225573255742557525576255772557825579255802558125582255832558425585255862558725588255892559025591255922559325594255952559625597255982559925600256012560225603256042560525606256072560825609256102561125612256132561425615256162561725618256192562025621256222562325624256252562625627256282562925630256312563225633256342563525636256372563825639256402564125642256432564425645256462564725648256492565025651256522565325654256552565625657256582565925660256612566225663256642566525666256672566825669256702567125672256732567425675256762567725678256792568025681256822568325684256852568625687256882568925690256912569225693256942569525696256972569825699257002570125702257032570425705257062570725708257092571025711257122571325714257152571625717257182571925720257212572225723257242572525726257272572825729257302573125732257332573425735257362573725738257392574025741257422574325744257452574625747257482574925750257512575225753257542575525756257572575825759257602576125762257632576425765257662576725768257692577025771257722577325774257752577625777257782577925780257812578225783257842578525786257872578825789257902579125792257932579425795257962579725798257992580025801258022580325804258052580625807258082580925810258112581225813258142581525816258172581825819258202582125822258232582425825258262582725828258292583025831258322583325834258352583625837258382583925840258412584225843258442584525846258472584825849258502585125852258532585425855258562585725858258592586025861258622586325864258652586625867258682586925870258712587225873258742587525876258772587825879258802588125882258832588425885258862588725888258892589025891258922589325894258952589625897258982589925900259012590225903259042590525906259072590825909259102591125912259132591425915259162591725918259192592025921259222592325924259252592625927259282592925930259312593225933259342593525936259372593825939259402594125942259432594425945259462594725948259492595025951259522595325954259552595625957259582595925960259612596225963259642596525966259672596825969259702597125972259732597425975259762597725978259792598025981259822598325984259852598625987259882598925990259912599225993259942599525996259972599825999260002600126002260032600426005260062600726008260092601026011260122601326014260152601626017260182601926020260212602226023260242602526026260272602826029260302603126032260332603426035260362603726038260392604026041260422604326044260452604626047260482604926050260512605226053260542605526056260572605826059260602606126062260632606426065260662606726068260692607026071260722607326074260752607626077260782607926080260812608226083260842608526086260872608826089260902609126092260932609426095260962609726098260992610026101261022610326104261052610626107261082610926110261112611226113261142611526116261172611826119261202612126122261232612426125261262612726128261292613026131261322613326134261352613626137261382613926140261412614226143261442614526146261472614826149261502615126152261532615426155261562615726158261592616026161261622616326164261652616626167261682616926170261712617226173261742617526176261772617826179261802618126182261832618426185261862618726188261892619026191261922619326194261952619626197261982619926200262012620226203262042620526206262072620826209262102621126212262132621426215262162621726218262192622026221262222622326224262252622626227262282622926230262312623226233262342623526236262372623826239262402624126242262432624426245262462624726248262492625026251262522625326254262552625626257262582625926260262612626226263262642626526266262672626826269262702627126272262732627426275262762627726278262792628026281262822628326284262852628626287262882628926290262912629226293262942629526296262972629826299263002630126302263032630426305263062630726308263092631026311263122631326314263152631626317263182631926320263212632226323263242632526326263272632826329263302633126332263332633426335263362633726338263392634026341263422634326344263452634626347263482634926350263512635226353263542635526356263572635826359263602636126362263632636426365263662636726368263692637026371263722637326374263752637626377263782637926380263812638226383263842638526386263872638826389263902639126392263932639426395263962639726398263992640026401264022640326404264052640626407264082640926410264112641226413264142641526416264172641826419264202642126422264232642426425264262642726428264292643026431264322643326434264352643626437264382643926440264412644226443264442644526446264472644826449264502645126452264532645426455264562645726458264592646026461264622646326464264652646626467264682646926470264712647226473264742647526476264772647826479264802648126482264832648426485264862648726488264892649026491264922649326494264952649626497264982649926500265012650226503265042650526506265072650826509265102651126512265132651426515265162651726518265192652026521265222652326524265252652626527265282652926530265312653226533265342653526536265372653826539265402654126542265432654426545265462654726548265492655026551265522655326554265552655626557265582655926560265612656226563265642656526566265672656826569265702657126572265732657426575265762657726578265792658026581265822658326584265852658626587265882658926590265912659226593265942659526596265972659826599266002660126602266032660426605266062660726608266092661026611266122661326614266152661626617266182661926620266212662226623266242662526626266272662826629266302663126632266332663426635266362663726638266392664026641266422664326644266452664626647266482664926650266512665226653266542665526656266572665826659266602666126662266632666426665266662666726668266692667026671266722667326674266752667626677266782667926680266812668226683266842668526686266872668826689266902669126692266932669426695266962669726698266992670026701267022670326704267052670626707267082670926710267112671226713267142671526716267172671826719267202672126722267232672426725267262672726728267292673026731267322673326734267352673626737267382673926740267412674226743267442674526746267472674826749267502675126752267532675426755267562675726758267592676026761267622676326764267652676626767267682676926770267712677226773267742677526776267772677826779267802678126782267832678426785267862678726788267892679026791267922679326794267952679626797267982679926800268012680226803268042680526806268072680826809268102681126812268132681426815268162681726818268192682026821268222682326824268252682626827268282682926830268312683226833268342683526836268372683826839268402684126842268432684426845268462684726848268492685026851268522685326854268552685626857268582685926860268612686226863268642686526866268672686826869268702687126872268732687426875268762687726878268792688026881268822688326884268852688626887268882688926890268912689226893268942689526896268972689826899269002690126902269032690426905269062690726908269092691026911269122691326914269152691626917269182691926920269212692226923269242692526926269272692826929269302693126932269332693426935269362693726938269392694026941269422694326944269452694626947269482694926950269512695226953269542695526956269572695826959269602696126962269632696426965269662696726968269692697026971269722697326974269752697626977269782697926980269812698226983269842698526986269872698826989269902699126992269932699426995269962699726998269992700027001270022700327004270052700627007270082700927010270112701227013270142701527016270172701827019270202702127022270232702427025270262702727028270292703027031270322703327034270352703627037270382703927040270412704227043270442704527046270472704827049270502705127052270532705427055270562705727058270592706027061270622706327064270652706627067270682706927070270712707227073270742707527076270772707827079270802708127082270832708427085270862708727088270892709027091270922709327094270952709627097270982709927100271012710227103271042710527106271072710827109271102711127112271132711427115271162711727118271192712027121271222712327124271252712627127271282712927130271312713227133271342713527136271372713827139271402714127142271432714427145271462714727148271492715027151271522715327154271552715627157271582715927160271612716227163271642716527166271672716827169271702717127172271732717427175271762717727178271792718027181271822718327184271852718627187271882718927190271912719227193271942719527196271972719827199272002720127202272032720427205272062720727208272092721027211272122721327214272152721627217272182721927220272212722227223272242722527226272272722827229272302723127232272332723427235272362723727238272392724027241272422724327244272452724627247272482724927250272512725227253272542725527256272572725827259272602726127262272632726427265272662726727268272692727027271272722727327274272752727627277272782727927280272812728227283272842728527286272872728827289272902729127292272932729427295272962729727298272992730027301273022730327304273052730627307273082730927310273112731227313273142731527316273172731827319273202732127322273232732427325273262732727328273292733027331273322733327334273352733627337273382733927340273412734227343273442734527346273472734827349273502735127352273532735427355273562735727358273592736027361273622736327364273652736627367273682736927370273712737227373273742737527376273772737827379273802738127382273832738427385273862738727388273892739027391273922739327394273952739627397273982739927400274012740227403274042740527406274072740827409274102741127412274132741427415274162741727418274192742027421274222742327424274252742627427274282742927430274312743227433274342743527436274372743827439274402744127442274432744427445274462744727448274492745027451274522745327454274552745627457274582745927460274612746227463274642746527466274672746827469274702747127472274732747427475274762747727478274792748027481274822748327484274852748627487274882748927490274912749227493274942749527496274972749827499275002750127502275032750427505275062750727508275092751027511275122751327514275152751627517275182751927520275212752227523275242752527526275272752827529275302753127532275332753427535275362753727538275392754027541275422754327544275452754627547275482754927550275512755227553275542755527556275572755827559275602756127562275632756427565275662756727568275692757027571275722757327574275752757627577275782757927580275812758227583275842758527586275872758827589275902759127592275932759427595275962759727598275992760027601276022760327604276052760627607276082760927610276112761227613276142761527616276172761827619276202762127622276232762427625276262762727628276292763027631276322763327634276352763627637276382763927640276412764227643276442764527646276472764827649276502765127652276532765427655276562765727658276592766027661276622766327664276652766627667276682766927670276712767227673276742767527676276772767827679276802768127682276832768427685276862768727688276892769027691276922769327694276952769627697276982769927700277012770227703277042770527706277072770827709277102771127712277132771427715277162771727718277192772027721277222772327724277252772627727277282772927730277312773227733277342773527736277372773827739277402774127742277432774427745277462774727748277492775027751277522775327754277552775627757277582775927760277612776227763277642776527766277672776827769277702777127772277732777427775277762777727778277792778027781277822778327784277852778627787277882778927790277912779227793277942779527796277972779827799278002780127802278032780427805278062780727808278092781027811278122781327814278152781627817278182781927820278212782227823278242782527826278272782827829278302783127832278332783427835278362783727838278392784027841278422784327844278452784627847278482784927850278512785227853278542785527856278572785827859278602786127862278632786427865278662786727868278692787027871278722787327874278752787627877278782787927880278812788227883278842788527886278872788827889278902789127892278932789427895278962789727898278992790027901279022790327904279052790627907279082790927910279112791227913279142791527916279172791827919279202792127922279232792427925279262792727928279292793027931279322793327934279352793627937279382793927940279412794227943279442794527946279472794827949279502795127952279532795427955279562795727958279592796027961279622796327964279652796627967279682796927970279712797227973279742797527976279772797827979279802798127982279832798427985279862798727988279892799027991279922799327994279952799627997279982799928000280012800228003280042800528006280072800828009280102801128012280132801428015280162801728018280192802028021280222802328024280252802628027280282802928030280312803228033280342803528036280372803828039280402804128042280432804428045280462804728048280492805028051280522805328054280552805628057280582805928060280612806228063280642806528066280672806828069280702807128072280732807428075280762807728078280792808028081280822808328084280852808628087280882808928090280912809228093280942809528096280972809828099281002810128102281032810428105281062810728108281092811028111281122811328114281152811628117281182811928120281212812228123281242812528126281272812828129281302813128132281332813428135281362813728138281392814028141281422814328144281452814628147281482814928150281512815228153281542815528156281572815828159281602816128162281632816428165281662816728168281692817028171281722817328174281752817628177281782817928180281812818228183281842818528186281872818828189281902819128192281932819428195281962819728198281992820028201282022820328204282052820628207282082820928210282112821228213282142821528216282172821828219282202822128222282232822428225282262822728228282292823028231282322823328234282352823628237282382823928240282412824228243282442824528246282472824828249282502825128252282532825428255282562825728258282592826028261282622826328264282652826628267282682826928270282712827228273282742827528276282772827828279282802828128282282832828428285282862828728288282892829028291282922829328294282952829628297282982829928300283012830228303283042830528306283072830828309283102831128312283132831428315283162831728318283192832028321283222832328324283252832628327283282832928330283312833228333283342833528336283372833828339283402834128342283432834428345283462834728348283492835028351283522835328354283552835628357283582835928360283612836228363283642836528366283672836828369283702837128372283732837428375283762837728378283792838028381283822838328384283852838628387283882838928390283912839228393283942839528396283972839828399284002840128402284032840428405284062840728408284092841028411284122841328414284152841628417284182841928420284212842228423284242842528426284272842828429284302843128432284332843428435284362843728438284392844028441284422844328444284452844628447284482844928450284512845228453284542845528456284572845828459284602846128462284632846428465284662846728468284692847028471284722847328474284752847628477284782847928480284812848228483284842848528486284872848828489284902849128492284932849428495284962849728498284992850028501285022850328504285052850628507285082850928510285112851228513285142851528516285172851828519285202852128522285232852428525285262852728528285292853028531285322853328534285352853628537285382853928540285412854228543285442854528546285472854828549285502855128552285532855428555285562855728558285592856028561285622856328564285652856628567285682856928570285712857228573285742857528576285772857828579285802858128582285832858428585285862858728588285892859028591285922859328594285952859628597285982859928600286012860228603286042860528606286072860828609286102861128612286132861428615286162861728618286192862028621286222862328624286252862628627286282862928630286312863228633286342863528636286372863828639286402864128642286432864428645286462864728648286492865028651286522865328654286552865628657286582865928660286612866228663286642866528666286672866828669286702867128672286732867428675286762867728678286792868028681286822868328684286852868628687286882868928690286912869228693286942869528696286972869828699287002870128702287032870428705287062870728708287092871028711287122871328714287152871628717287182871928720287212872228723287242872528726287272872828729287302873128732287332873428735287362873728738287392874028741287422874328744287452874628747287482874928750287512875228753287542875528756287572875828759287602876128762287632876428765287662876728768287692877028771287722877328774287752877628777287782877928780287812878228783287842878528786287872878828789287902879128792287932879428795287962879728798287992880028801288022880328804288052880628807288082880928810288112881228813288142881528816288172881828819288202882128822288232882428825288262882728828288292883028831288322883328834288352883628837288382883928840288412884228843288442884528846288472884828849288502885128852288532885428855288562885728858288592886028861288622886328864288652886628867288682886928870288712887228873288742887528876288772887828879288802888128882288832888428885288862888728888288892889028891288922889328894288952889628897288982889928900289012890228903289042890528906289072890828909289102891128912289132891428915289162891728918289192892028921289222892328924289252892628927289282892928930289312893228933289342893528936289372893828939289402894128942289432894428945289462894728948289492895028951289522895328954289552895628957289582895928960289612896228963289642896528966289672896828969289702897128972289732897428975289762897728978289792898028981289822898328984289852898628987289882898928990289912899228993289942899528996289972899828999290002900129002290032900429005290062900729008290092901029011290122901329014290152901629017290182901929020290212902229023290242902529026290272902829029290302903129032290332903429035290362903729038290392904029041290422904329044290452904629047290482904929050290512905229053290542905529056290572905829059290602906129062290632906429065290662906729068290692907029071290722907329074290752907629077290782907929080290812908229083290842908529086290872908829089290902909129092290932909429095290962909729098290992910029101291022910329104291052910629107291082910929110291112911229113291142911529116291172911829119291202912129122291232912429125291262912729128291292913029131291322913329134291352913629137291382913929140291412914229143291442914529146291472914829149291502915129152291532915429155291562915729158291592916029161291622916329164291652916629167291682916929170291712917229173291742917529176291772917829179291802918129182291832918429185291862918729188291892919029191291922919329194291952919629197291982919929200292012920229203292042920529206292072920829209292102921129212292132921429215292162921729218292192922029221292222922329224292252922629227292282922929230292312923229233292342923529236292372923829239292402924129242292432924429245292462924729248292492925029251292522925329254292552925629257292582925929260292612926229263292642926529266292672926829269292702927129272292732927429275292762927729278292792928029281292822928329284292852928629287292882928929290292912929229293292942929529296292972929829299293002930129302293032930429305293062930729308293092931029311293122931329314293152931629317293182931929320293212932229323293242932529326293272932829329293302933129332293332933429335293362933729338293392934029341293422934329344293452934629347293482934929350293512935229353293542935529356293572935829359293602936129362293632936429365293662936729368293692937029371293722937329374293752937629377293782937929380293812938229383293842938529386293872938829389293902939129392293932939429395293962939729398293992940029401294022940329404294052940629407294082940929410294112941229413294142941529416294172941829419294202942129422294232942429425294262942729428294292943029431294322943329434294352943629437294382943929440294412944229443294442944529446294472944829449294502945129452294532945429455294562945729458294592946029461294622946329464294652946629467294682946929470294712947229473294742947529476294772947829479294802948129482294832948429485294862948729488294892949029491294922949329494294952949629497294982949929500295012950229503295042950529506295072950829509295102951129512295132951429515295162951729518295192952029521295222952329524295252952629527295282952929530295312953229533295342953529536295372953829539295402954129542295432954429545295462954729548295492955029551295522955329554295552955629557295582955929560295612956229563295642956529566295672956829569295702957129572295732957429575295762957729578295792958029581295822958329584295852958629587295882958929590295912959229593295942959529596295972959829599296002960129602296032960429605296062960729608296092961029611296122961329614296152961629617296182961929620296212962229623296242962529626296272962829629296302963129632296332963429635296362963729638296392964029641296422964329644296452964629647296482964929650296512965229653296542965529656296572965829659296602966129662296632966429665296662966729668296692967029671296722967329674296752967629677296782967929680296812968229683296842968529686296872968829689296902969129692296932969429695296962969729698296992970029701297022970329704297052970629707297082970929710297112971229713297142971529716297172971829719297202972129722297232972429725297262972729728297292973029731297322973329734297352973629737297382973929740297412974229743297442974529746297472974829749297502975129752297532975429755297562975729758297592976029761297622976329764297652976629767297682976929770297712977229773297742977529776297772977829779297802978129782297832978429785297862978729788297892979029791297922979329794297952979629797297982979929800298012980229803298042980529806298072980829809298102981129812298132981429815298162981729818298192982029821298222982329824298252982629827298282982929830298312983229833298342983529836298372983829839298402984129842298432984429845298462984729848298492985029851298522985329854298552985629857298582985929860298612986229863298642986529866298672986829869298702987129872298732987429875298762987729878298792988029881298822988329884298852988629887298882988929890298912989229893298942989529896298972989829899299002990129902299032990429905299062990729908299092991029911299122991329914299152991629917299182991929920299212992229923299242992529926299272992829929299302993129932299332993429935299362993729938299392994029941299422994329944299452994629947299482994929950299512995229953299542995529956299572995829959299602996129962299632996429965299662996729968299692997029971299722997329974299752997629977299782997929980299812998229983299842998529986299872998829989299902999129992299932999429995299962999729998299993000030001300023000330004300053000630007300083000930010300113001230013300143001530016300173001830019300203002130022300233002430025300263002730028300293003030031300323003330034300353003630037300383003930040300413004230043300443004530046300473004830049300503005130052300533005430055300563005730058300593006030061300623006330064300653006630067300683006930070300713007230073300743007530076300773007830079300803008130082300833008430085300863008730088300893009030091300923009330094300953009630097300983009930100301013010230103301043010530106301073010830109301103011130112301133011430115301163011730118301193012030121301223012330124301253012630127301283012930130301313013230133301343013530136301373013830139301403014130142301433014430145301463014730148301493015030151301523015330154301553015630157301583015930160301613016230163301643016530166301673016830169301703017130172301733017430175301763017730178301793018030181301823018330184301853018630187301883018930190301913019230193301943019530196301973019830199302003020130202302033020430205302063020730208302093021030211302123021330214302153021630217302183021930220302213022230223302243022530226302273022830229302303023130232302333023430235302363023730238302393024030241302423024330244302453024630247302483024930250302513025230253302543025530256302573025830259302603026130262302633026430265302663026730268302693027030271302723027330274302753027630277302783027930280302813028230283302843028530286302873028830289302903029130292302933029430295302963029730298302993030030301303023030330304303053030630307303083030930310303113031230313303143031530316303173031830319303203032130322303233032430325303263032730328303293033030331303323033330334303353033630337303383033930340303413034230343303443034530346303473034830349303503035130352303533035430355303563035730358303593036030361303623036330364303653036630367303683036930370303713037230373303743037530376303773037830379303803038130382303833038430385303863038730388303893039030391303923039330394303953039630397303983039930400304013040230403304043040530406304073040830409304103041130412304133041430415304163041730418304193042030421304223042330424304253042630427304283042930430304313043230433304343043530436304373043830439304403044130442304433044430445304463044730448304493045030451304523045330454304553045630457304583045930460304613046230463304643046530466304673046830469304703047130472304733047430475304763047730478304793048030481304823048330484304853048630487304883048930490304913049230493304943049530496304973049830499305003050130502305033050430505305063050730508305093051030511305123051330514305153051630517305183051930520305213052230523305243052530526305273052830529305303053130532305333053430535305363053730538305393054030541305423054330544305453054630547305483054930550305513055230553305543055530556305573055830559305603056130562305633056430565305663056730568305693057030571305723057330574305753057630577305783057930580305813058230583305843058530586305873058830589305903059130592305933059430595305963059730598305993060030601306023060330604306053060630607306083060930610306113061230613306143061530616306173061830619306203062130622306233062430625306263062730628306293063030631306323063330634306353063630637306383063930640306413064230643306443064530646306473064830649306503065130652306533065430655306563065730658306593066030661306623066330664306653066630667306683066930670306713067230673306743067530676306773067830679306803068130682306833068430685306863068730688306893069030691306923069330694306953069630697306983069930700307013070230703307043070530706307073070830709307103071130712307133071430715307163071730718307193072030721307223072330724307253072630727307283072930730307313073230733307343073530736307373073830739307403074130742307433074430745307463074730748307493075030751307523075330754307553075630757307583075930760307613076230763307643076530766307673076830769307703077130772307733077430775307763077730778307793078030781307823078330784307853078630787307883078930790307913079230793307943079530796307973079830799308003080130802308033080430805308063080730808308093081030811308123081330814308153081630817308183081930820308213082230823308243082530826308273082830829308303083130832308333083430835308363083730838308393084030841308423084330844308453084630847308483084930850308513085230853308543085530856308573085830859308603086130862308633086430865308663086730868308693087030871308723087330874308753087630877308783087930880308813088230883308843088530886308873088830889308903089130892308933089430895308963089730898308993090030901309023090330904309053090630907309083090930910309113091230913309143091530916309173091830919309203092130922309233092430925309263092730928309293093030931309323093330934309353093630937309383093930940309413094230943309443094530946309473094830949309503095130952309533095430955309563095730958309593096030961309623096330964309653096630967309683096930970309713097230973309743097530976309773097830979309803098130982309833098430985309863098730988309893099030991309923099330994309953099630997309983099931000310013100231003310043100531006310073100831009310103101131012310133101431015310163101731018310193102031021310223102331024310253102631027310283102931030310313103231033310343103531036310373103831039310403104131042310433104431045310463104731048310493105031051310523105331054310553105631057310583105931060310613106231063310643106531066310673106831069310703107131072310733107431075310763107731078310793108031081310823108331084310853108631087310883108931090310913109231093310943109531096310973109831099311003110131102311033110431105311063110731108311093111031111311123111331114311153111631117311183111931120311213112231123311243112531126311273112831129311303113131132311333113431135311363113731138311393114031141311423114331144311453114631147311483114931150311513115231153311543115531156311573115831159311603116131162311633116431165311663116731168311693117031171311723117331174311753117631177311783117931180311813118231183311843118531186311873118831189311903119131192311933119431195311963119731198311993120031201312023120331204312053120631207312083120931210312113121231213312143121531216312173121831219312203122131222312233122431225312263122731228312293123031231312323123331234312353123631237312383123931240312413124231243312443124531246312473124831249312503125131252312533125431255312563125731258312593126031261312623126331264312653126631267312683126931270312713127231273312743127531276312773127831279312803128131282312833128431285312863128731288312893129031291312923129331294312953129631297312983129931300313013130231303313043130531306313073130831309313103131131312313133131431315313163131731318313193132031321313223132331324313253132631327313283132931330313313133231333313343133531336313373133831339313403134131342313433134431345313463134731348313493135031351313523135331354313553135631357313583135931360313613136231363313643136531366313673136831369313703137131372313733137431375313763137731378313793138031381313823138331384313853138631387313883138931390313913139231393313943139531396313973139831399314003140131402314033140431405314063140731408314093141031411314123141331414314153141631417314183141931420314213142231423314243142531426314273142831429314303143131432314333143431435314363143731438314393144031441314423144331444314453144631447314483144931450314513145231453314543145531456314573145831459314603146131462314633146431465314663146731468314693147031471314723147331474314753147631477314783147931480314813148231483314843148531486314873148831489314903149131492314933149431495314963149731498314993150031501315023150331504315053150631507315083150931510315113151231513315143151531516315173151831519315203152131522315233152431525315263152731528315293153031531315323153331534315353153631537315383153931540315413154231543315443154531546315473154831549315503155131552315533155431555315563155731558315593156031561315623156331564315653156631567315683156931570315713157231573315743157531576315773157831579315803158131582315833158431585315863158731588315893159031591315923159331594315953159631597315983159931600316013160231603316043160531606316073160831609316103161131612316133161431615316163161731618316193162031621316223162331624316253162631627316283162931630316313163231633316343163531636316373163831639316403164131642316433164431645316463164731648316493165031651316523165331654316553165631657316583165931660316613166231663316643166531666316673166831669316703167131672316733167431675316763167731678316793168031681316823168331684316853168631687316883168931690316913169231693316943169531696316973169831699317003170131702317033170431705317063170731708317093171031711317123171331714317153171631717317183171931720317213172231723317243172531726317273172831729317303173131732317333173431735317363173731738317393174031741317423174331744317453174631747317483174931750317513175231753317543175531756317573175831759317603176131762317633176431765317663176731768317693177031771317723177331774317753177631777317783177931780317813178231783317843178531786317873178831789317903179131792317933179431795317963179731798317993180031801318023180331804318053180631807318083180931810318113181231813318143181531816318173181831819318203182131822318233182431825318263182731828318293183031831318323183331834318353183631837318383183931840318413184231843318443184531846318473184831849318503185131852318533185431855318563185731858318593186031861318623186331864318653186631867318683186931870318713187231873318743187531876318773187831879318803188131882318833188431885318863188731888318893189031891318923189331894318953189631897318983189931900319013190231903319043190531906319073190831909319103191131912319133191431915319163191731918319193192031921319223192331924319253192631927319283192931930319313193231933319343193531936319373193831939319403194131942319433194431945319463194731948319493195031951319523195331954319553195631957319583195931960319613196231963319643196531966319673196831969319703197131972319733197431975319763197731978319793198031981319823198331984319853198631987319883198931990319913199231993319943199531996319973199831999320003200132002320033200432005320063200732008 |
- /**
- * @license Highstock JS v5.0.12 (2017-05-24)
- *
- * (c) 2009-2016 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- 'use strict';
- (function(root, factory) {
- if (typeof module === 'object' && module.exports) {
- module.exports = root.document ?
- factory(root) :
- factory;
- } else {
- root.Highcharts = factory(root);
- }
- }(typeof window !== 'undefined' ? window : this, function(win) {
- var Highcharts = (function() {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- /* global window */
- var win = window,
- doc = win.document;
- var SVG_NS = 'http://www.w3.org/2000/svg',
- userAgent = (win.navigator && win.navigator.userAgent) || '',
- svg = doc && doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect,
- isMS = /(edge|msie|trident)/i.test(userAgent) && !window.opera,
- vml = !svg,
- isFirefox = /Firefox/.test(userAgent),
- hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4; // issue #38
- var Highcharts = win.Highcharts ? win.Highcharts.error(16, true) : {
- product: 'Highstock',
- version: '5.0.12',
- deg2rad: Math.PI * 2 / 360,
- doc: doc,
- hasBidiBug: hasBidiBug,
- hasTouch: doc && doc.documentElement.ontouchstart !== undefined,
- isMS: isMS,
- isWebKit: /AppleWebKit/.test(userAgent),
- isFirefox: isFirefox,
- isTouchDevice: /(Mobile|Android|Windows Phone)/.test(userAgent),
- SVG_NS: SVG_NS,
- chartCount: 0,
- seriesTypes: {},
- symbolSizes: {},
- svg: svg,
- vml: vml,
- win: win,
- marginNames: ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'],
- noop: function() {
- return undefined;
- },
- /**
- * An array containing the current chart objects in the page. A chart's
- * position in the array is preserved throughout the page's lifetime. When
- * a chart is destroyed, the array item becomes `undefined`.
- * @type {Array.<Highcharts.Chart>}
- * @memberOf Highcharts
- */
- charts: []
- };
- return Highcharts;
- }());
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- /* eslint max-len: ["warn", 80, 4] */
- /**
- * The Highcharts object is the placeholder for all other members, and various
- * utility functions. The most important member of the namespace would be the
- * chart constructor.
- *
- * @example
- * var chart = Highcharts.chart('container', { ... });
- *
- * @namespace Highcharts
- */
- var timers = [];
- var charts = H.charts,
- doc = H.doc,
- win = H.win;
- /**
- * Provide error messages for debugging, with links to online explanation. This
- * function can be overridden to provide custom error handling.
- *
- * @function #error
- * @memberOf Highcharts
- * @param {Number|String} code - The error code. See [errors.xml]{@link
- * https://github.com/highcharts/highcharts/blob/master/errors/errors.xml}
- * for available codes. If it is a string, the error message is printed
- * directly in the console.
- * @param {Boolean} [stop=false] - Whether to throw an error or just log a
- * warning in the console.
- *
- * @sample highcharts/chart/highcharts-error/ Custom error handler
- */
- H.error = function(code, stop) {
- var msg = H.isNumber(code) ?
- 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code :
- code;
- if (stop) {
- throw new Error(msg);
- }
- // else ...
- if (win.console) {
- console.log(msg); // eslint-disable-line no-console
- }
- };
- /**
- * An animator object used internally. One instance applies to one property
- * (attribute or style prop) on one element. Animation is always initiated
- * through {@link SVGElement#animate}.
- *
- * @constructor Fx
- * @memberOf Highcharts
- * @param {HTMLDOMElement|SVGElement} elem - The element to animate.
- * @param {AnimationOptions} options - Animation options.
- * @param {string} prop - The single attribute or CSS property to animate.
- * @private
- *
- * @example
- * var rect = renderer.rect(0, 0, 10, 10).add();
- * rect.animate({ width: 100 });
- */
- H.Fx = function(elem, options, prop) {
- this.options = options;
- this.elem = elem;
- this.prop = prop;
- };
- H.Fx.prototype = {
- /**
- * Set the current step of a path definition on SVGElement.
- *
- * @function #dSetter
- * @memberOf Highcharts.Fx
- */
- dSetter: function() {
- var start = this.paths[0],
- end = this.paths[1],
- ret = [],
- now = this.now,
- i = start.length,
- startVal;
- // Land on the final path without adjustment points appended in the ends
- if (now === 1) {
- ret = this.toD;
- } else if (i === end.length && now < 1) {
- while (i--) {
- startVal = parseFloat(start[i]);
- ret[i] =
- isNaN(startVal) ? // a letter instruction like M or L
- start[i] :
- now * (parseFloat(end[i] - startVal)) + startVal;
- }
- // If animation is finished or length not matching, land on right value
- } else {
- ret = end;
- }
- this.elem.attr('d', ret, null, true);
- },
- /**
- * Update the element with the current animation step.
- *
- * @function #update
- * @memberOf Highcharts.Fx
- */
- update: function() {
- var elem = this.elem,
- prop = this.prop, // if destroyed, it is null
- now = this.now,
- step = this.options.step;
- // Animation setter defined from outside
- if (this[prop + 'Setter']) {
- this[prop + 'Setter']();
- // Other animations on SVGElement
- } else if (elem.attr) {
- if (elem.element) {
- elem.attr(prop, now, null, true);
- }
- // HTML styles, raw HTML content like container size
- } else {
- elem.style[prop] = now + this.unit;
- }
- if (step) {
- step.call(elem, now, this);
- }
- },
- /**
- * Run an animation.
- *
- * @function #run
- * @memberOf Highcharts.Fx
- * @param {Number} from - The current value, value to start from.
- * @param {Number} to - The end value, value to land on.
- * @param {String} [unit] - The property unit, for example `px`.
- * @returns {void}
- */
- run: function(from, to, unit) {
- var self = this,
- timer = function(gotoEnd) {
- return timer.stopped ? false : self.step(gotoEnd);
- },
- i;
- this.startTime = +new Date();
- this.start = from;
- this.end = to;
- this.unit = unit;
- this.now = this.start;
- this.pos = 0;
- timer.elem = this.elem;
- timer.prop = this.prop;
- if (timer() && timers.push(timer) === 1) {
- timer.timerId = setInterval(function() {
- for (i = 0; i < timers.length; i++) {
- if (!timers[i]()) {
- timers.splice(i--, 1);
- }
- }
- if (!timers.length) {
- clearInterval(timer.timerId);
- }
- }, 13);
- }
- },
- /**
- * Run a single step in the animation.
- *
- * @function #step
- * @memberOf Highcharts.Fx
- * @param {Boolean} [gotoEnd] - Whether to go to the endpoint of the
- * animation after abort.
- * @returns {Boolean} Returns `true` if animation continues.
- */
- step: function(gotoEnd) {
- var t = +new Date(),
- ret,
- done,
- options = this.options,
- elem = this.elem,
- complete = options.complete,
- duration = options.duration,
- curAnim = options.curAnim;
- if (elem.attr && !elem.element) { // #2616, element is destroyed
- ret = false;
- } else if (gotoEnd || t >= duration + this.startTime) {
- this.now = this.end;
- this.pos = 1;
- this.update();
- curAnim[this.prop] = true;
- done = true;
- H.objectEach(curAnim, function(val) {
- if (val !== true) {
- done = false;
- }
- });
- if (done && complete) {
- complete.call(elem);
- }
- ret = false;
- } else {
- this.pos = options.easing((t - this.startTime) / duration);
- this.now = this.start + ((this.end - this.start) * this.pos);
- this.update();
- ret = true;
- }
- return ret;
- },
- /**
- * Prepare start and end values so that the path can be animated one to one.
- *
- * @function #initPath
- * @memberOf Highcharts.Fx
- * @param {SVGElement} elem - The SVGElement item.
- * @param {String} fromD - Starting path definition.
- * @param {Array} toD - Ending path definition.
- * @returns {Array} An array containing start and end paths in array form
- * so that they can be animated in parallel.
- */
- initPath: function(elem, fromD, toD) {
- fromD = fromD || '';
- var shift,
- startX = elem.startX,
- endX = elem.endX,
- bezier = fromD.indexOf('C') > -1,
- numParams = bezier ? 7 : 3,
- fullLength,
- slice,
- i,
- start = fromD.split(' '),
- end = toD.slice(), // copy
- isArea = elem.isArea,
- positionFactor = isArea ? 2 : 1,
- reverse;
- /**
- * In splines make moveTo and lineTo points have six parameters like
- * bezier curves, to allow animation one-to-one.
- */
- function sixify(arr) {
- var isOperator,
- nextIsOperator;
- i = arr.length;
- while (i--) {
- // Fill in dummy coordinates only if the next operator comes
- // three places behind (#5788)
- isOperator = arr[i] === 'M' || arr[i] === 'L';
- nextIsOperator = /[a-zA-Z]/.test(arr[i + 3]);
- if (isOperator && nextIsOperator) {
- arr.splice(
- i + 1, 0,
- arr[i + 1], arr[i + 2],
- arr[i + 1], arr[i + 2]
- );
- }
- }
- }
- /**
- * Insert an array at the given position of another array
- */
- function insertSlice(arr, subArr, index) {
- [].splice.apply(
- arr, [index, 0].concat(subArr)
- );
- }
- /**
- * If shifting points, prepend a dummy point to the end path.
- */
- function prepend(arr, other) {
- while (arr.length < fullLength) {
- // Move to, line to or curve to?
- arr[0] = other[fullLength - arr.length];
- // Prepend a copy of the first point
- insertSlice(arr, arr.slice(0, numParams), 0);
- // For areas, the bottom path goes back again to the left, so we
- // need to append a copy of the last point.
- if (isArea) {
- insertSlice(
- arr,
- arr.slice(arr.length - numParams), arr.length
- );
- i--;
- }
- }
- arr[0] = 'M';
- }
- /**
- * Copy and append last point until the length matches the end length
- */
- function append(arr, other) {
- var i = (fullLength - arr.length) / numParams;
- while (i > 0 && i--) {
- // Pull out the slice that is going to be appended or inserted.
- // In a line graph, the positionFactor is 1, and the last point
- // is sliced out. In an area graph, the positionFactor is 2,
- // causing the middle two points to be sliced out, since an area
- // path starts at left, follows the upper path then turns and
- // follows the bottom back.
- slice = arr.slice().splice(
- (arr.length / positionFactor) - numParams,
- numParams * positionFactor
- );
- // Move to, line to or curve to?
- slice[0] = other[fullLength - numParams - (i * numParams)];
- // Disable first control point
- if (bezier) {
- slice[numParams - 6] = slice[numParams - 2];
- slice[numParams - 5] = slice[numParams - 1];
- }
- // Now insert the slice, either in the middle (for areas) or at
- // the end (for lines)
- insertSlice(arr, slice, arr.length / positionFactor);
- if (isArea) {
- i--;
- }
- }
- }
- if (bezier) {
- sixify(start);
- sixify(end);
- }
- // For sideways animation, find out how much we need to shift to get the
- // start path Xs to match the end path Xs.
- if (startX && endX) {
- for (i = 0; i < startX.length; i++) {
- // Moving left, new points coming in on right
- if (startX[i] === endX[0]) {
- shift = i;
- break;
- // Moving right
- } else if (startX[0] ===
- endX[endX.length - startX.length + i]) {
- shift = i;
- reverse = true;
- break;
- }
- }
- if (shift === undefined) {
- start = [];
- }
- }
- if (start.length && H.isNumber(shift)) {
- // The common target length for the start and end array, where both
- // arrays are padded in opposite ends
- fullLength = end.length + shift * positionFactor * numParams;
- if (!reverse) {
- prepend(end, start);
- append(start, end);
- } else {
- prepend(start, end);
- append(end, start);
- }
- }
- return [start, end];
- }
- }; // End of Fx prototype
- /**
- * Handle animation of the color attributes directly.
- */
- H.Fx.prototype.fillSetter =
- H.Fx.prototype.strokeSetter = function() {
- this.elem.attr(
- this.prop,
- H.color(this.start).tweenTo(H.color(this.end), this.pos),
- null,
- true
- );
- };
- /**
- * Utility function to extend an object with the members of another.
- *
- * @function #extend
- * @memberOf Highcharts
- * @param {Object} a - The object to be extended.
- * @param {Object} b - The object to add to the first one.
- * @returns {Object} Object a, the original object.
- */
- H.extend = function(a, b) {
- var n;
- if (!a) {
- a = {};
- }
- for (n in b) {
- a[n] = b[n];
- }
- return a;
- };
- /**
- * Utility function to deep merge two or more objects and return a third object.
- * If the first argument is true, the contents of the second object is copied
- * into the first object. The merge function can also be used with a single
- * object argument to create a deep copy of an object.
- *
- * @function #merge
- * @memberOf Highcharts
- * @param {Boolean} [extend] - Whether to extend the left-side object (a) or
- return a whole new object.
- * @param {Object} a - The first object to extend. When only this is given, the
- function returns a deep copy.
- * @param {...Object} [n] - An object to merge into the previous one.
- * @returns {Object} - The merged object. If the first argument is true, the
- * return is the same as the second argument.
- */
- H.merge = function() {
- var i,
- args = arguments,
- len,
- ret = {},
- doCopy = function(copy, original) {
- // An object is replacing a primitive
- if (typeof copy !== 'object') {
- copy = {};
- }
- H.objectEach(original, function(value, key) {
- // Copy the contents of objects, but not arrays or DOM nodes
- if (
- H.isObject(value, true) &&
- !H.isClass(value) &&
- !H.isDOMElement(value)
- ) {
- copy[key] = doCopy(copy[key] || {}, value);
- // Primitives and arrays are copied over directly
- } else {
- copy[key] = original[key];
- }
- });
- return copy;
- };
- // If first argument is true, copy into the existing object. Used in
- // setOptions.
- if (args[0] === true) {
- ret = args[1];
- args = Array.prototype.slice.call(args, 2);
- }
- // For each argument, extend the return
- len = args.length;
- for (i = 0; i < len; i++) {
- ret = doCopy(ret, args[i]);
- }
- return ret;
- };
- /**
- * Shortcut for parseInt
- * @ignore
- * @param {Object} s
- * @param {Number} mag Magnitude
- */
- H.pInt = function(s, mag) {
- return parseInt(s, mag || 10);
- };
- /**
- * Utility function to check for string type.
- *
- * @function #isString
- * @memberOf Highcharts
- * @param {Object} s - The item to check.
- * @returns {Boolean} - True if the argument is a string.
- */
- H.isString = function(s) {
- return typeof s === 'string';
- };
- /**
- * Utility function to check if an item is an array.
- *
- * @function #isArray
- * @memberOf Highcharts
- * @param {Object} obj - The item to check.
- * @returns {Boolean} - True if the argument is an array.
- */
- H.isArray = function(obj) {
- var str = Object.prototype.toString.call(obj);
- return str === '[object Array]' || str === '[object Array Iterator]';
- };
- /**
- * Utility function to check if an item is of type object.
- *
- * @function #isObject
- * @memberOf Highcharts
- * @param {Object} obj - The item to check.
- * @param {Boolean} [strict=false] - Also checks that the object is not an
- * array.
- * @returns {Boolean} - True if the argument is an object.
- */
- H.isObject = function(obj, strict) {
- return !!obj && typeof obj === 'object' && (!strict || !H.isArray(obj));
- };
- /**
- * Utility function to check if an Object is a HTML Element.
- *
- * @function #isDOMElement
- * @memberOf Highcharts
- * @param {Object} obj - The item to check.
- * @returns {Boolean} - True if the argument is a HTML Element.
- */
- H.isDOMElement = function(obj) {
- return H.isObject(obj) && typeof obj.nodeType === 'number';
- };
- /**
- * Utility function to check if an Object is an class.
- *
- * @function #isClass
- * @memberOf Highcharts
- * @param {Object} obj - The item to check.
- * @returns {Boolean} - True if the argument is an class.
- */
- H.isClass = function(obj) {
- var c = obj && obj.constructor;
- return !!(
- H.isObject(obj, true) &&
- !H.isDOMElement(obj) &&
- (c && c.name && c.name !== 'Object')
- );
- };
- /**
- * Utility function to check if an item is of type number.
- *
- * @function #isNumber
- * @memberOf Highcharts
- * @param {Object} n - The item to check.
- * @returns {Boolean} - True if the item is a number and is not NaN.
- */
- H.isNumber = function(n) {
- return typeof n === 'number' && !isNaN(n);
- };
- /**
- * Remove the last occurence of an item from an array.
- *
- * @function #erase
- * @memberOf Highcharts
- * @param {Array} arr - The array.
- * @param {*} item - The item to remove.
- */
- H.erase = function(arr, item) {
- var i = arr.length;
- while (i--) {
- if (arr[i] === item) {
- arr.splice(i, 1);
- break;
- }
- }
- };
- /**
- * Check if an object is null or undefined.
- *
- * @function #defined
- * @memberOf Highcharts
- * @param {Object} obj - The object to check.
- * @returns {Boolean} - False if the object is null or undefined, otherwise
- * true.
- */
- H.defined = function(obj) {
- return obj !== undefined && obj !== null;
- };
- /**
- * Set or get an attribute or an object of attributes. To use as a setter, pass
- * a key and a value, or let the second argument be a collection of keys and
- * values. To use as a getter, pass only a string as the second argument.
- *
- * @function #attr
- * @memberOf Highcharts
- * @param {Object} elem - The DOM element to receive the attribute(s).
- * @param {String|Object} [prop] - The property or an object of key-value pairs.
- * @param {String} [value] - The value if a single property is set.
- * @returns {*} When used as a getter, return the value.
- */
- H.attr = function(elem, prop, value) {
- var ret;
- // if the prop is a string
- if (H.isString(prop)) {
- // set the value
- if (H.defined(value)) {
- elem.setAttribute(prop, value);
- // get the value
- } else if (elem && elem.getAttribute) {
- ret = elem.getAttribute(prop);
- }
- // else if prop is defined, it is a hash of key/value pairs
- } else if (H.defined(prop) && H.isObject(prop)) {
- H.objectEach(prop, function(val, key) {
- elem.setAttribute(key, val);
- });
- }
- return ret;
- };
- /**
- * Check if an element is an array, and if not, make it into an array.
- *
- * @function #splat
- * @memberOf Highcharts
- * @param obj {*} - The object to splat.
- * @returns {Array} The produced or original array.
- */
- H.splat = function(obj) {
- return H.isArray(obj) ? obj : [obj];
- };
- /**
- * Set a timeout if the delay is given, otherwise perform the function
- * synchronously.
- *
- * @function #syncTimeout
- * @memberOf Highcharts
- * @param {Function} fn - The function callback.
- * @param {Number} delay - Delay in milliseconds.
- * @param {Object} [context] - The context.
- * @returns {Number} An identifier for the timeout that can later be cleared
- * with clearTimeout.
- */
- H.syncTimeout = function(fn, delay, context) {
- if (delay) {
- return setTimeout(fn, delay, context);
- }
- fn.call(0, context);
- };
- /**
- * Return the first value that is not null or undefined.
- *
- * @function #pick
- * @memberOf Highcharts
- * @param {...*} items - Variable number of arguments to inspect.
- * @returns {*} The value of the first argument that is not null or undefined.
- */
- H.pick = function() {
- var args = arguments,
- i,
- arg,
- length = args.length;
- for (i = 0; i < length; i++) {
- arg = args[i];
- if (arg !== undefined && arg !== null) {
- return arg;
- }
- }
- };
- /**
- * @typedef {Object} CSSObject - A style object with camel case property names.
- * The properties can be whatever styles are supported on the given SVG or HTML
- * element.
- * @example
- * {
- * fontFamily: 'monospace',
- * fontSize: '1.2em'
- * }
- */
- /**
- * Set CSS on a given element.
- *
- * @function #css
- * @memberOf Highcharts
- * @param {HTMLDOMElement} el - A HTML DOM element.
- * @param {CSSObject} styles - Style object with camel case property names.
- * @returns {void}
- */
- H.css = function(el, styles) {
- if (H.isMS && !H.svg) { // #2686
- if (styles && styles.opacity !== undefined) {
- styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')';
- }
- }
- H.extend(el.style, styles);
- };
- /**
- * A HTML DOM element.
- * @typedef {Object} HTMLDOMElement
- */
- /**
- * Utility function to create an HTML element with attributes and styles.
- *
- * @function #createElement
- * @memberOf Highcharts
- * @param {String} tag - The HTML tag.
- * @param {Object} [attribs] - Attributes as an object of key-value pairs.
- * @param {CSSObject} [styles] - Styles as an object of key-value pairs.
- * @param {Object} [parent] - The parent HTML object.
- * @param {Boolean} [nopad=false] - If true, remove all padding, border and
- * margin.
- * @returns {HTMLDOMElement} The created DOM element.
- */
- H.createElement = function(tag, attribs, styles, parent, nopad) {
- var el = doc.createElement(tag),
- css = H.css;
- if (attribs) {
- H.extend(el, attribs);
- }
- if (nopad) {
- css(el, {
- padding: 0,
- border: 'none',
- margin: 0
- });
- }
- if (styles) {
- css(el, styles);
- }
- if (parent) {
- parent.appendChild(el);
- }
- return el;
- };
- /**
- * Extend a prototyped class by new members.
- *
- * @function #extendClass
- * @memberOf Highcharts
- * @param {Object} parent - The parent prototype to inherit.
- * @param {Object} members - A collection of prototype members to add or
- * override compared to the parent prototype.
- * @returns {Object} A new prototype.
- */
- H.extendClass = function(parent, members) {
- var object = function() {};
- object.prototype = new parent(); // eslint-disable-line new-cap
- H.extend(object.prototype, members);
- return object;
- };
- /**
- * Left-pad a string to a given length by adding a character repetetively.
- *
- * @function #pad
- * @memberOf Highcharts
- * @param {Number} number - The input string or number.
- * @param {Number} length - The desired string length.
- * @param {String} [padder=0] - The character to pad with.
- * @returns {String} The padded string.
- */
- H.pad = function(number, length, padder) {
- return new Array((length || 2) + 1 -
- String(number).length).join(padder || 0) + number;
- };
- /**
- * @typedef {Number|String} RelativeSize - If a number is given, it defines the
- * pixel length. If a percentage string is given, like for example `'50%'`,
- * the setting defines a length relative to a base size, for example the size
- * of a container.
- */
- /**
- * Return a length based on either the integer value, or a percentage of a base.
- *
- * @function #relativeLength
- * @memberOf Highcharts
- * @param {RelativeSize} value - A percentage string or a number.
- * @param {Number} base - The full length that represents 100%.
- * @returns {Number} The computed length.
- */
- H.relativeLength = function(value, base) {
- return (/%$/).test(value) ?
- base * parseFloat(value) / 100 :
- parseFloat(value);
- };
- /**
- * Wrap a method with extended functionality, preserving the original function.
- *
- * @function #wrap
- * @memberOf Highcharts
- * @param {Object} obj - The context object that the method belongs to. In real
- * cases, this is often a prototype.
- * @param {String} method - The name of the method to extend.
- * @param {Function} func - A wrapper function callback. This function is called
- * with the same arguments as the original function, except that the
- * original function is unshifted and passed as the first argument.
- * @returns {void}
- */
- H.wrap = function(obj, method, func) {
- var proceed = obj[method];
- obj[method] = function() {
- var args = Array.prototype.slice.call(arguments),
- outerArgs = arguments,
- ctx = this,
- ret;
- ctx.proceed = function() {
- proceed.apply(ctx, arguments.length ? arguments : outerArgs);
- };
- args.unshift(proceed);
- ret = func.apply(this, args);
- ctx.proceed = null;
- return ret;
- };
- };
- /**
- * Get the time zone offset based on the current timezone information as set in
- * the global options.
- *
- * @function #getTZOffset
- * @memberOf Highcharts
- * @param {Number} timestamp - The JavaScript timestamp to inspect.
- * @return {Number} - The timezone offset in minutes compared to UTC.
- */
- H.getTZOffset = function(timestamp) {
- var d = H.Date;
- return ((d.hcGetTimezoneOffset && d.hcGetTimezoneOffset(timestamp)) ||
- d.hcTimezoneOffset || 0) * 60000;
- };
- /**
- * Formats a JavaScript date timestamp (milliseconds since Jan 1st 1970) into a
- * human readable date string. The format is a subset of the formats for PHP's
- * [strftime]{@link
- * http://www.php.net/manual/en/function.strftime.php} function. Additional
- * formats can be given in the {@link Highcharts.dateFormats} hook.
- *
- * @function #dateFormat
- * @memberOf Highcharts
- * @param {String} format - The desired format where various time
- * representations are prefixed with %.
- * @param {Number} timestamp - The JavaScript timestamp.
- * @param {Boolean} [capitalize=false] - Upper case first letter in the return.
- * @returns {String} The formatted date.
- */
- H.dateFormat = function(format, timestamp, capitalize) {
- if (!H.defined(timestamp) || isNaN(timestamp)) {
- return H.defaultOptions.lang.invalidDate || '';
- }
- format = H.pick(format, '%Y-%m-%d %H:%M:%S');
- var D = H.Date,
- date = new D(timestamp - H.getTZOffset(timestamp)),
- // get the basic time values
- hours = date[D.hcGetHours](),
- day = date[D.hcGetDay](),
- dayOfMonth = date[D.hcGetDate](),
- month = date[D.hcGetMonth](),
- fullYear = date[D.hcGetFullYear](),
- lang = H.defaultOptions.lang,
- langWeekdays = lang.weekdays,
- shortWeekdays = lang.shortWeekdays,
- pad = H.pad,
- // List all format keys. Custom formats can be added from the outside.
- replacements = H.extend({
- //-- Day
- // Short weekday, like 'Mon'
- 'a': shortWeekdays ?
- shortWeekdays[day] : langWeekdays[day].substr(0, 3),
- // Long weekday, like 'Monday'
- 'A': langWeekdays[day],
- // Two digit day of the month, 01 to 31
- 'd': pad(dayOfMonth),
- // Day of the month, 1 through 31
- 'e': pad(dayOfMonth, 2, ' '),
- 'w': day,
- // Week (none implemented)
- //'W': weekNumber(),
- //-- Month
- // Short month, like 'Jan'
- 'b': lang.shortMonths[month],
- // Long month, like 'January'
- 'B': lang.months[month],
- // Two digit month number, 01 through 12
- 'm': pad(month + 1),
- //-- Year
- // Two digits year, like 09 for 2009
- 'y': fullYear.toString().substr(2, 2),
- // Four digits year, like 2009
- 'Y': fullYear,
- //-- Time
- // Two digits hours in 24h format, 00 through 23
- 'H': pad(hours),
- // Hours in 24h format, 0 through 23
- 'k': hours,
- // Two digits hours in 12h format, 00 through 11
- 'I': pad((hours % 12) || 12),
- // Hours in 12h format, 1 through 12
- 'l': (hours % 12) || 12,
- // Two digits minutes, 00 through 59
- 'M': pad(date[D.hcGetMinutes]()),
- // Upper case AM or PM
- 'p': hours < 12 ? 'AM' : 'PM',
- // Lower case AM or PM
- 'P': hours < 12 ? 'am' : 'pm',
- // Two digits seconds, 00 through 59
- 'S': pad(date.getSeconds()),
- // Milliseconds (naming from Ruby)
- 'L': pad(Math.round(timestamp % 1000), 3)
- },
- /**
- * A hook for defining additional date format specifiers. New
- * specifiers are defined as key-value pairs by using the specifier
- * as key, and a function which takes the timestamp as value. This
- * function returns the formatted portion of the date.
- *
- * @type {Object}
- * @name dateFormats
- * @memberOf Highcharts
- * @sample highcharts/global/dateformats/ Adding support for week
- * number
- */
- H.dateFormats
- );
- // Do the replaces
- H.objectEach(replacements, function(val, key) {
- // Regex would do it in one line, but this is faster
- while (format.indexOf('%' + key) !== -1) {
- format = format.replace(
- '%' + key,
- typeof val === 'function' ? val(timestamp) : val
- );
- }
- });
- // Optionally capitalize the string and return
- return capitalize ?
- format.substr(0, 1).toUpperCase() + format.substr(1) :
- format;
- };
- /**
- * Format a single variable. Similar to sprintf, without the % prefix.
- *
- * @example
- * formatSingle('.2f', 5); // => '5.00'.
- *
- * @function #formatSingle
- * @memberOf Highcharts
- * @param {String} format The format string.
- * @param {*} val The value.
- * @returns {String} The formatted representation of the value.
- */
- H.formatSingle = function(format, val) {
- var floatRegex = /f$/,
- decRegex = /\.([0-9])/,
- lang = H.defaultOptions.lang,
- decimals;
- if (floatRegex.test(format)) { // float
- decimals = format.match(decRegex);
- decimals = decimals ? decimals[1] : -1;
- if (val !== null) {
- val = H.numberFormat(
- val,
- decimals,
- lang.decimalPoint,
- format.indexOf(',') > -1 ? lang.thousandsSep : ''
- );
- }
- } else {
- val = H.dateFormat(format, val);
- }
- return val;
- };
- /**
- * Format a string according to a subset of the rules of Python's String.format
- * method.
- *
- * @function #format
- * @memberOf Highcharts
- * @param {String} str The string to format.
- * @param {Object} ctx The context, a collection of key-value pairs where each
- * key is replaced by its value.
- * @returns {String} The formatted string.
- *
- * @example
- * var s = Highcharts.format(
- * 'The {color} fox was {len:.2f} feet long',
- * { color: 'red', len: Math.PI }
- * );
- * // => The red fox was 3.14 feet long
- */
- H.format = function(str, ctx) {
- var splitter = '{',
- isInside = false,
- segment,
- valueAndFormat,
- path,
- i,
- len,
- ret = [],
- val,
- index;
- while (str) {
- index = str.indexOf(splitter);
- if (index === -1) {
- break;
- }
- segment = str.slice(0, index);
- if (isInside) { // we're on the closing bracket looking back
- valueAndFormat = segment.split(':');
- path = valueAndFormat.shift().split('.'); // get first and leave
- len = path.length;
- val = ctx;
- // Assign deeper paths
- for (i = 0; i < len; i++) {
- val = val[path[i]];
- }
- // Format the replacement
- if (valueAndFormat.length) {
- val = H.formatSingle(valueAndFormat.join(':'), val);
- }
- // Push the result and advance the cursor
- ret.push(val);
- } else {
- ret.push(segment);
- }
- str = str.slice(index + 1); // the rest
- isInside = !isInside; // toggle
- splitter = isInside ? '}' : '{'; // now look for next matching bracket
- }
- ret.push(str);
- return ret.join('');
- };
- /**
- * Get the magnitude of a number.
- *
- * @function #getMagnitude
- * @memberOf Highcharts
- * @param {Number} number The number.
- * @returns {Number} The magnitude, where 1-9 are magnitude 1, 10-99 magnitude 2
- * etc.
- */
- H.getMagnitude = function(num) {
- return Math.pow(10, Math.floor(Math.log(num) / Math.LN10));
- };
- /**
- * Take an interval and normalize it to multiples of round numbers.
- *
- * @todo Move this function to the Axis prototype. It is here only for
- * historical reasons.
- * @function #normalizeTickInterval
- * @memberOf Highcharts
- * @param {Number} interval - The raw, un-rounded interval.
- * @param {Array} [multiples] - Allowed multiples.
- * @param {Number} [magnitude] - The magnitude of the number.
- * @param {Boolean} [allowDecimals] - Whether to allow decimals.
- * @param {Boolean} [hasTickAmount] - If it has tickAmount, avoid landing
- * on tick intervals lower than original.
- * @returns {Number} The normalized interval.
- */
- H.normalizeTickInterval = function(interval, multiples, magnitude,
- allowDecimals, hasTickAmount) {
- var normalized,
- i,
- retInterval = interval;
- // round to a tenfold of 1, 2, 2.5 or 5
- magnitude = H.pick(magnitude, 1);
- normalized = interval / magnitude;
- // multiples for a linear scale
- if (!multiples) {
- multiples = hasTickAmount ?
- // Finer grained ticks when the tick amount is hard set, including
- // when alignTicks is true on multiple axes (#4580).
- [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10] :
- // Else, let ticks fall on rounder numbers
- [1, 2, 2.5, 5, 10];
- // the allowDecimals option
- if (allowDecimals === false) {
- if (magnitude === 1) {
- multiples = H.grep(multiples, function(num) {
- return num % 1 === 0;
- });
- } else if (magnitude <= 0.1) {
- multiples = [1 / magnitude];
- }
- }
- }
- // normalize the interval to the nearest multiple
- for (i = 0; i < multiples.length; i++) {
- retInterval = multiples[i];
- // only allow tick amounts smaller than natural
- if ((hasTickAmount && retInterval * magnitude >= interval) ||
- (!hasTickAmount && (normalized <= (multiples[i] +
- (multiples[i + 1] || multiples[i])) / 2))) {
- break;
- }
- }
- // Multiply back to the correct magnitude. Correct floats to appropriate
- // precision (#6085).
- retInterval = H.correctFloat(
- retInterval * magnitude, -Math.round(Math.log(0.001) / Math.LN10)
- );
- return retInterval;
- };
- /**
- * Sort an object array and keep the order of equal items. The ECMAScript
- * standard does not specify the behaviour when items are equal.
- *
- * @function #stableSort
- * @memberOf Highcharts
- * @param {Array} arr - The array to sort.
- * @param {Function} sortFunction - The function to sort it with, like with
- * regular Array.prototype.sort.
- * @returns {void}
- */
- H.stableSort = function(arr, sortFunction) {
- var length = arr.length,
- sortValue,
- i;
- // Add index to each item
- for (i = 0; i < length; i++) {
- arr[i].safeI = i; // stable sort index
- }
- arr.sort(function(a, b) {
- sortValue = sortFunction(a, b);
- return sortValue === 0 ? a.safeI - b.safeI : sortValue;
- });
- // Remove index from items
- for (i = 0; i < length; i++) {
- delete arr[i].safeI; // stable sort index
- }
- };
- /**
- * Non-recursive method to find the lowest member of an array. `Math.min` raises
- * a maximum call stack size exceeded error in Chrome when trying to apply more
- * than 150.000 points. This method is slightly slower, but safe.
- *
- * @function #arrayMin
- * @memberOf Highcharts
- * @param {Array} data An array of numbers.
- * @returns {Number} The lowest number.
- */
- H.arrayMin = function(data) {
- var i = data.length,
- min = data[0];
- while (i--) {
- if (data[i] < min) {
- min = data[i];
- }
- }
- return min;
- };
- /**
- * Non-recursive method to find the lowest member of an array. `Math.max` raises
- * a maximum call stack size exceeded error in Chrome when trying to apply more
- * than 150.000 points. This method is slightly slower, but safe.
- *
- * @function #arrayMax
- * @memberOf Highcharts
- * @param {Array} data - An array of numbers.
- * @returns {Number} The highest number.
- */
- H.arrayMax = function(data) {
- var i = data.length,
- max = data[0];
- while (i--) {
- if (data[i] > max) {
- max = data[i];
- }
- }
- return max;
- };
- /**
- * Utility method that destroys any SVGElement instances that are properties on
- * the given object. It loops all properties and invokes destroy if there is a
- * destroy method. The property is then delete.
- *
- * @function #destroyObjectProperties
- * @memberOf Highcharts
- * @param {Object} obj - The object to destroy properties on.
- * @param {Object} [except] - Exception, do not destroy this property, only
- * delete it.
- * @returns {void}
- */
- H.destroyObjectProperties = function(obj, except) {
- H.objectEach(obj, function(val, n) {
- // If the object is non-null and destroy is defined
- if (val && val !== except && val.destroy) {
- // Invoke the destroy
- val.destroy();
- }
- // Delete the property from the object.
- delete obj[n];
- });
- };
- /**
- * Discard a HTML element by moving it to the bin and delete.
- *
- * @function #discardElement
- * @memberOf Highcharts
- * @param {HTMLDOMElement} element - The HTML node to discard.
- * @returns {void}
- */
- H.discardElement = function(element) {
- var garbageBin = H.garbageBin;
- // create a garbage bin element, not part of the DOM
- if (!garbageBin) {
- garbageBin = H.createElement('div');
- }
- // move the node and empty bin
- if (element) {
- garbageBin.appendChild(element);
- }
- garbageBin.innerHTML = '';
- };
- /**
- * Fix JS round off float errors.
- *
- * @function #correctFloat
- * @memberOf Highcharts
- * @param {Number} num - A float number to fix.
- * @param {Number} [prec=14] - The precision.
- * @returns {Number} The corrected float number.
- */
- H.correctFloat = function(num, prec) {
- return parseFloat(
- num.toPrecision(prec || 14)
- );
- };
- /**
- * Set the global animation to either a given value, or fall back to the given
- * chart's animation option.
- *
- * @function #setAnimation
- * @memberOf Highcharts
- * @param {Boolean|Animation} animation - The animation object.
- * @param {Object} chart - The chart instance.
- * @returns {void}
- * @todo This function always relates to a chart, and sets a property on the
- * renderer, so it should be moved to the SVGRenderer.
- */
- H.setAnimation = function(animation, chart) {
- chart.renderer.globalAnimation = H.pick(
- animation,
- chart.options.chart.animation,
- true
- );
- };
- /**
- * Get the animation in object form, where a disabled animation is always
- * returned as `{ duration: 0 }`.
- *
- * @function #animObject
- * @memberOf Highcharts
- * @param {Boolean|AnimationOptions} animation - An animation setting. Can be an
- * object with duration, complete and easing properties, or a boolean to
- * enable or disable.
- * @returns {AnimationOptions} An object with at least a duration property.
- */
- H.animObject = function(animation) {
- return H.isObject(animation) ?
- H.merge(animation) : {
- duration: animation ? 500 : 0
- };
- };
- /**
- * The time unit lookup
- */
- H.timeUnits = {
- millisecond: 1,
- second: 1000,
- minute: 60000,
- hour: 3600000,
- day: 24 * 3600000,
- week: 7 * 24 * 3600000,
- month: 28 * 24 * 3600000,
- year: 364 * 24 * 3600000
- };
- /**
- * Format a number and return a string based on input settings.
- *
- * @function #numberFormat
- * @memberOf Highcharts
- * @param {Number} number - The input number to format.
- * @param {Number} decimals - The amount of decimals. A value of -1 preserves
- * the amount in the input number.
- * @param {String} [decimalPoint] - The decimal point, defaults to the one given
- * in the lang options, or a dot.
- * @param {String} [thousandsSep] - The thousands separator, defaults to the one
- * given in the lang options, or a space character.
- * @returns {String} The formatted number.
- *
- * @sample members/highcharts-numberformat/ Custom number format
- */
- H.numberFormat = function(number, decimals, decimalPoint, thousandsSep) {
- number = +number || 0;
- decimals = +decimals;
- var lang = H.defaultOptions.lang,
- origDec = (number.toString().split('.')[1] || '').length,
- strinteger,
- thousands,
- ret,
- roundedNumber;
- if (decimals === -1) {
- // Preserve decimals. Not huge numbers (#3793).
- decimals = Math.min(origDec, 20);
- } else if (!H.isNumber(decimals)) {
- decimals = 2;
- }
- // Add another decimal to avoid rounding errors of float numbers. (#4573)
- // Then use toFixed to handle rounding.
- roundedNumber = (
- Math.abs(number) + Math.pow(10, -Math.max(decimals, origDec) - 1)
- ).toFixed(decimals);
- // A string containing the positive integer component of the number
- strinteger = String(H.pInt(roundedNumber));
- // Leftover after grouping into thousands. Can be 0, 1 or 3.
- thousands = strinteger.length > 3 ? strinteger.length % 3 : 0;
- // Language
- decimalPoint = H.pick(decimalPoint, lang.decimalPoint);
- thousandsSep = H.pick(thousandsSep, lang.thousandsSep);
- // Start building the return
- ret = number < 0 ? '-' : '';
- // Add the leftover after grouping into thousands. For example, in the
- // number 42 000 000, this line adds 42.
- ret += thousands ? strinteger.substr(0, thousands) + thousandsSep : '';
- // Add the remaining thousands groups, joined by the thousands separator
- ret += strinteger
- .substr(thousands)
- .replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep);
- // Add the decimal point and the decimal component
- if (decimals) {
- // Get the decimal component
- ret += decimalPoint + roundedNumber.slice(-decimals);
- }
- return ret;
- };
- /**
- * Easing definition
- * @ignore
- * @param {Number} pos Current position, ranging from 0 to 1.
- */
- Math.easeInOutSine = function(pos) {
- return -0.5 * (Math.cos(Math.PI * pos) - 1);
- };
- /**
- * Get the computed CSS value for given element and property, only for numerical
- * properties. For width and height, the dimension of the inner box (excluding
- * padding) is returned. Used for fitting the chart within the container.
- *
- * @function #getStyle
- * @memberOf Highcharts
- * @param {HTMLDOMElement} el - A HTML element.
- * @param {String} prop - The property name.
- * @param {Boolean} [toInt=true] - Parse to integer.
- * @returns {Number} - The numeric value.
- */
- H.getStyle = function(el, prop, toInt) {
- var style;
- // For width and height, return the actual inner pixel size (#4913)
- if (prop === 'width') {
- return Math.min(el.offsetWidth, el.scrollWidth) -
- H.getStyle(el, 'padding-left') -
- H.getStyle(el, 'padding-right');
- } else if (prop === 'height') {
- return Math.min(el.offsetHeight, el.scrollHeight) -
- H.getStyle(el, 'padding-top') -
- H.getStyle(el, 'padding-bottom');
- }
- // Otherwise, get the computed style
- style = win.getComputedStyle(el, undefined);
- if (style) {
- style = style.getPropertyValue(prop);
- if (H.pick(toInt, true)) {
- style = H.pInt(style);
- }
- }
- return style;
- };
- /**
- * Search for an item in an array.
- *
- * @function #inArray
- * @memberOf Highcharts
- * @param {*} item - The item to search for.
- * @param {arr} arr - The array or node collection to search in.
- * @returns {Number} - The index within the array, or -1 if not found.
- */
- H.inArray = function(item, arr) {
- return arr.indexOf ? arr.indexOf(item) : [].indexOf.call(arr, item);
- };
- /**
- * Filter an array by a callback.
- *
- * @function #grep
- * @memberOf Highcharts
- * @param {Array} arr - The array to filter.
- * @param {Function} callback - The callback function. The function receives the
- * item as the first argument. Return `true` if the item is to be
- * preserved.
- * @returns {Array} - A new, filtered array.
- */
- H.grep = function(arr, callback) {
- return [].filter.call(arr, callback);
- };
- /**
- * Return the value of the first element in the array that satisfies the
- * provided testing function.
- *
- * @function #find
- * @memberOf Highcharts
- * @param {Array} arr - The array to test.
- * @param {Function} callback - The callback function. The function receives the
- * item as the first argument. Return `true` if this item satisfies the
- * condition.
- * @returns {Mixed} - The value of the element.
- */
- H.find = function(arr, callback) {
- return [].find.call(arr, callback);
- };
- /**
- * Map an array by a callback.
- *
- * @function #map
- * @memberOf Highcharts
- * @param {Array} arr - The array to map.
- * @param {Function} fn - The callback function. Return the new value for the
- * new array.
- * @returns {Array} - A new array item with modified items.
- */
- H.map = function(arr, fn) {
- var results = [],
- i = 0,
- len = arr.length;
- for (; i < len; i++) {
- results[i] = fn.call(arr[i], arr[i], i, arr);
- }
- return results;
- };
- /**
- * Get the element's offset position, corrected for `overflow: auto`.
- *
- * @function #offset
- * @memberOf Highcharts
- * @param {HTMLDOMElement} el - The HTML element.
- * @returns {Object} An object containing `left` and `top` properties for the
- * position in the page.
- */
- H.offset = function(el) {
- var docElem = doc.documentElement,
- box = el.getBoundingClientRect();
- return {
- top: box.top + (win.pageYOffset || docElem.scrollTop) -
- (docElem.clientTop || 0),
- left: box.left + (win.pageXOffset || docElem.scrollLeft) -
- (docElem.clientLeft || 0)
- };
- };
- /**
- * Stop running animation.
- *
- * @todo A possible extension to this would be to stop a single property, when
- * we want to continue animating others. Then assign the prop to the timer
- * in the Fx.run method, and check for the prop here. This would be an
- * improvement in all cases where we stop the animation from .attr. Instead of
- * stopping everything, we can just stop the actual attributes we're setting.
- *
- * @function #stop
- * @memberOf Highcharts
- * @param {SVGElement} el - The SVGElement to stop animation on.
- * @param {string} [prop] - The property to stop animating. If given, the stop
- * method will stop a single property from animating, while others continue.
- * @returns {void}
- */
- H.stop = function(el, prop) {
- var i = timers.length;
- // Remove timers related to this element (#4519)
- while (i--) {
- if (timers[i].elem === el && (!prop || prop === timers[i].prop)) {
- timers[i].stopped = true; // #4667
- }
- }
- };
- /**
- * Iterate over an array.
- *
- * @function #each
- * @memberOf Highcharts
- * @param {Array} arr - The array to iterate over.
- * @param {Function} fn - The iterator callback. It passes three arguments:
- * * item - The array item.
- * * index - The item's index in the array.
- * * arr - The array that each is being applied to.
- * @param {Object} [ctx] The context.
- */
- H.each = function(arr, fn, ctx) { // modern browsers
- return Array.prototype.forEach.call(arr, fn, ctx);
- };
- /**
- * Iterate over object key pairs in an object.
- *
- * @function #objectEach
- * @memberOf Highcharts
- * @param {Object} obj - The object to iterate over.
- * @param {Function} fn - The iterator callback. It passes three arguments:
- * * value - The property value.
- * * key - The property key.
- * * obj - The object that objectEach is being applied to.
- * @param {Object} ctx The context
- */
- H.objectEach = function(obj, fn, ctx) {
- for (var key in obj) {
- if (obj.hasOwnProperty(key)) {
- fn.call(ctx, obj[key], key, obj);
- }
- }
- };
- /**
- * Add an event listener.
- *
- * @function #addEvent
- * @memberOf Highcharts
- * @param {Object} el - The element or object to add a listener to. It can be a
- * {@link HTMLDOMElement}, an {@link SVGElement} or any other object.
- * @param {String} type - The event type.
- * @param {Function} fn - The function callback to execute when the event is
- * fired.
- * @returns {Function} A callback function to remove the added event.
- */
- H.addEvent = function(el, type, fn) {
- var events = el.hcEvents = el.hcEvents || {};
- function wrappedFn(e) {
- e.target = e.srcElement || win; // #2820
- fn.call(el, e);
- }
- // Handle DOM events in modern browsers
- if (el.addEventListener) {
- el.addEventListener(type, fn, false);
- // Handle old IE implementation
- } else if (el.attachEvent) {
- if (!el.hcEventsIE) {
- el.hcEventsIE = {};
- }
- // Link wrapped fn with original fn, so we can get this in removeEvent
- el.hcEventsIE[fn.toString()] = wrappedFn;
- el.attachEvent('on' + type, wrappedFn);
- }
- if (!events[type]) {
- events[type] = [];
- }
- events[type].push(fn);
- // Return a function that can be called to remove this event.
- return function() {
- H.removeEvent(el, type, fn);
- };
- };
- /**
- * Remove an event that was added with {@link Highcharts#addEvent}.
- *
- * @function #removeEvent
- * @memberOf Highcharts
- * @param {Object} el - The element to remove events on.
- * @param {String} [type] - The type of events to remove. If undefined, all
- * events are removed from the element.
- * @param {Function} [fn] - The specific callback to remove. If undefined, all
- * events that match the element and optionally the type are removed.
- * @returns {void}
- */
- H.removeEvent = function(el, type, fn) {
- var events,
- hcEvents = el.hcEvents,
- index;
- function removeOneEvent(type, fn) {
- if (el.removeEventListener) {
- el.removeEventListener(type, fn, false);
- } else if (el.attachEvent) {
- fn = el.hcEventsIE[fn.toString()];
- el.detachEvent('on' + type, fn);
- }
- }
- function removeAllEvents() {
- var types,
- len;
- if (!el.nodeName) {
- return; // break on non-DOM events
- }
- if (type) {
- types = {};
- types[type] = true;
- } else {
- types = hcEvents;
- }
- H.objectEach(types, function(val, n) {
- if (hcEvents[n]) {
- len = hcEvents[n].length;
- while (len--) {
- removeOneEvent(n, hcEvents[n][len]);
- }
- }
- });
- }
- if (hcEvents) {
- if (type) {
- events = hcEvents[type] || [];
- if (fn) {
- index = H.inArray(fn, events);
- if (index > -1) {
- events.splice(index, 1);
- hcEvents[type] = events;
- }
- removeOneEvent(type, fn);
- } else {
- removeAllEvents();
- hcEvents[type] = [];
- }
- } else {
- removeAllEvents();
- el.hcEvents = {};
- }
- }
- };
- /**
- * Fire an event that was registered with {@link Highcharts#addEvent}.
- *
- * @function #fireEvent
- * @memberOf Highcharts
- * @param {Object} el - The object to fire the event on. It can be a
- * {@link HTMLDOMElement}, an {@link SVGElement} or any other object.
- * @param {String} type - The type of event.
- * @param {Object} [eventArguments] - Custom event arguments that are passed on
- * as an argument to the event handler.
- * @param {Function} [defaultFunction] - The default function to execute if the
- * other listeners haven't returned false.
- * @returns {void}
- */
- H.fireEvent = function(el, type, eventArguments, defaultFunction) {
- var e,
- hcEvents = el.hcEvents,
- events,
- len,
- i,
- fn;
- eventArguments = eventArguments || {};
- if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) {
- e = doc.createEvent('Events');
- e.initEvent(type, true, true);
- //e.target = el;
- H.extend(e, eventArguments);
- if (el.dispatchEvent) {
- el.dispatchEvent(e);
- } else {
- el.fireEvent(type, e);
- }
- } else if (hcEvents) {
- events = hcEvents[type] || [];
- len = events.length;
- if (!eventArguments.target) { // We're running a custom event
- H.extend(eventArguments, {
- // Attach a simple preventDefault function to skip default
- // handler if called. The built-in defaultPrevented property is
- // not overwritable (#5112)
- preventDefault: function() {
- eventArguments.defaultPrevented = true;
- },
- // Setting target to native events fails with clicking the
- // zoom-out button in Chrome.
- target: el,
- // If the type is not set, we're running a custom event (#2297).
- // If it is set, we're running a browser event, and setting it
- // will cause en error in IE8 (#2465).
- type: type
- });
- }
- for (i = 0; i < len; i++) {
- fn = events[i];
- // If the event handler return false, prevent the default handler
- // from executing
- if (fn && fn.call(el, eventArguments) === false) {
- eventArguments.preventDefault();
- }
- }
- }
- // Run the default if not prevented
- if (defaultFunction && !eventArguments.defaultPrevented) {
- defaultFunction(eventArguments);
- }
- };
- /**
- * An animation configuration. Animation configurations can also be defined as
- * booleans, where `false` turns off animation and `true` defaults to a duration
- * of 500ms.
- * @typedef {Object} AnimationOptions
- * @property {Number} duration - The animation duration in milliseconds.
- * @property {String} [easing] - The name of an easing function as defined on
- * the `Math` object.
- * @property {Function} [complete] - A callback function to exectute when the
- * animation finishes.
- * @property {Function} [step] - A callback function to execute on each step of
- * each attribute or CSS property that's being animated. The first argument
- * contains information about the animation and progress.
- */
- /**
- * The global animate method, which uses Fx to create individual animators.
- *
- * @function #animate
- * @memberOf Highcharts
- * @param {HTMLDOMElement|SVGElement} el - The element to animate.
- * @param {Object} params - An object containing key-value pairs of the
- * properties to animate. Supports numeric as pixel-based CSS properties
- * for HTML objects and attributes for SVGElements.
- * @param {AnimationOptions} [opt] - Animation options.
- */
- H.animate = function(el, params, opt) {
- var start,
- unit = '',
- end,
- fx,
- args;
- if (!H.isObject(opt)) { // Number or undefined/null
- args = arguments;
- opt = {
- duration: args[2],
- easing: args[3],
- complete: args[4]
- };
- }
- if (!H.isNumber(opt.duration)) {
- opt.duration = 400;
- }
- opt.easing = typeof opt.easing === 'function' ?
- opt.easing :
- (Math[opt.easing] || Math.easeInOutSine);
- opt.curAnim = H.merge(params);
- H.objectEach(params, function(val, prop) {
- // Stop current running animation of this property
- H.stop(el, prop);
- fx = new H.Fx(el, opt, prop);
- end = null;
- if (prop === 'd') {
- fx.paths = fx.initPath(
- el,
- el.d,
- params.d
- );
- fx.toD = params.d;
- start = 0;
- end = 1;
- } else if (el.attr) {
- start = el.attr(prop);
- } else {
- start = parseFloat(H.getStyle(el, prop)) || 0;
- if (prop !== 'opacity') {
- unit = 'px';
- }
- }
- if (!end) {
- end = val;
- }
- if (end && end.match && end.match('px')) {
- end = end.replace(/px/g, ''); // #4351
- }
- fx.run(start, end, unit);
- });
- };
- /**
- * Factory to create new series prototypes.
- *
- * @function #seriesType
- * @memberOf Highcharts
- *
- * @param {String} type - The series type name.
- * @param {String} parent - The parent series type name. Use `line` to inherit
- * from the basic {@link Series} object.
- * @param {Object} options - The additional default options that is merged with
- * the parent's options.
- * @param {Object} props - The properties (functions and primitives) to set on
- * the new prototype.
- * @param {Object} [pointProps] - Members for a series-specific extension of the
- * {@link Point} prototype if needed.
- * @returns {*} - The newly created prototype as extended from {@link Series}
- * or its derivatives.
- */
- // docs: add to API + extending Highcharts
- H.seriesType = function(type, parent, options, props, pointProps) {
- var defaultOptions = H.getOptions(),
- seriesTypes = H.seriesTypes;
- if (seriesTypes[type]) {
- return H.error(27); // Series type already defined
- }
- // Merge the options
- defaultOptions.plotOptions[type] = H.merge(
- defaultOptions.plotOptions[parent],
- options
- );
- // Create the class
- seriesTypes[type] = H.extendClass(seriesTypes[parent] ||
- function() {}, props);
- seriesTypes[type].prototype.type = type;
- // Create the point class if needed
- if (pointProps) {
- seriesTypes[type].prototype.pointClass =
- H.extendClass(H.Point, pointProps);
- }
- return seriesTypes[type];
- };
- /**
- * Get a unique key for using in internal element id's and pointers. The key
- * is composed of a random hash specific to this Highcharts instance, and a
- * counter.
- * @function #uniqueKey
- * @memberOf Highcharts
- * @return {string} The key.
- * @example
- * var id = H.uniqueKey(); // => 'highcharts-x45f6hp-0'
- */
- H.uniqueKey = (function() {
- var uniqueKeyHash = Math.random().toString(36).substring(2, 9),
- idCounter = 0;
- return function() {
- return 'highcharts-' + uniqueKeyHash + '-' + idCounter++;
- };
- }());
- /**
- * Register Highcharts as a plugin in jQuery
- */
- if (win.jQuery) {
- win.jQuery.fn.highcharts = function() {
- var args = [].slice.call(arguments);
- if (this[0]) { // this[0] is the renderTo div
- // Create the chart
- if (args[0]) {
- new H[ // eslint-disable-line no-new
- // Constructor defaults to Chart
- H.isString(args[0]) ? args.shift() : 'Chart'
- ](this[0], args[0], args[1]);
- return this;
- }
- // When called without parameters or with the return argument,
- // return an existing chart
- return charts[H.attr(this[0], 'data-highcharts-chart')];
- }
- };
- }
- /**
- * Compatibility section to add support for legacy IE. This can be removed if
- * old IE support is not needed.
- */
- if (doc && !doc.defaultView) {
- H.getStyle = function(el, prop) {
- var val,
- alias = {
- width: 'clientWidth',
- height: 'clientHeight'
- }[prop];
- if (el.style[prop]) {
- return H.pInt(el.style[prop]);
- }
- if (prop === 'opacity') {
- prop = 'filter';
- }
- // Getting the rendered width and height
- if (alias) {
- el.style.zoom = 1;
- return Math.max(el[alias] - 2 * H.getStyle(el, 'padding'), 0);
- }
- val = el.currentStyle[prop.replace(/\-(\w)/g, function(a, b) {
- return b.toUpperCase();
- })];
- if (prop === 'filter') {
- val = val.replace(
- /alpha\(opacity=([0-9]+)\)/,
- function(a, b) {
- return b / 100;
- }
- );
- }
- return val === '' ? 1 : H.pInt(val);
- };
- }
- if (!Array.prototype.forEach) {
- H.each = function(arr, fn, ctx) { // legacy
- var i = 0,
- len = arr.length;
- for (; i < len; i++) {
- if (fn.call(ctx, arr[i], i, arr) === false) {
- return i;
- }
- }
- };
- }
- if (!Array.prototype.indexOf) {
- H.inArray = function(item, arr) {
- var len,
- i = 0;
- if (arr) {
- len = arr.length;
- for (; i < len; i++) {
- if (arr[i] === item) {
- return i;
- }
- }
- }
- return -1;
- };
- }
- if (!Array.prototype.filter) {
- H.grep = function(elements, fn) {
- var ret = [],
- i = 0,
- length = elements.length;
- for (; i < length; i++) {
- if (fn(elements[i], i)) {
- ret.push(elements[i]);
- }
- }
- return ret;
- };
- }
- if (!Array.prototype.find) {
- H.find = function(arr, fn) {
- var i,
- length = arr.length;
- for (i = 0; i < length; i++) {
- if (fn(arr[i], i)) {
- return arr[i];
- }
- }
- };
- }
- //--- End compatibility section ---
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var each = H.each,
- isNumber = H.isNumber,
- map = H.map,
- merge = H.merge,
- pInt = H.pInt;
- /**
- * @typedef {string} ColorString
- * A valid color to be parsed and handled by Highcharts. Highcharts internally
- * supports hex colors like `#ffffff`, rgb colors like `rgb(255,255,255)` and
- * rgba colors like `rgba(255,255,255,1)`. Other colors may be supported by the
- * browsers and displayed correctly, but Highcharts is not able to process them
- * and apply concepts like opacity and brightening.
- */
- /**
- * Handle color operations. The object methods are chainable.
- * @param {String} input The input color in either rbga or hex format
- */
- H.Color = function(input) {
- // Backwards compatibility, allow instanciation without new
- if (!(this instanceof H.Color)) {
- return new H.Color(input);
- }
- // Initialize
- this.init(input);
- };
- H.Color.prototype = {
- // Collection of parsers. This can be extended from the outside by pushing parsers
- // to Highcharts.Color.prototype.parsers.
- parsers: [{
- // RGBA color
- regex: /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/,
- parse: function(result) {
- return [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
- }
- }, {
- // RGB color
- regex: /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/,
- parse: function(result) {
- return [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1];
- }
- }],
- // Collection of named colors. Can be extended from the outside by adding
- // colors to Highcharts.Color.prototype.names.
- names: {
- none: 'rgba(255,255,255,0)',
- white: '#ffffff',
- black: '#000000'
- },
- /**
- * Parse the input color to rgba array
- * @param {String} input
- */
- init: function(input) {
- var result,
- rgba,
- i,
- parser,
- len;
- this.input = input = this.names[
- input && input.toLowerCase ?
- input.toLowerCase() :
- ''
- ] || input;
- // Gradients
- if (input && input.stops) {
- this.stops = map(input.stops, function(stop) {
- return new H.Color(stop[1]);
- });
- // Solid colors
- } else {
- // Check if it's possible to do bitmasking instead of regex
- if (input && input[0] === '#') {
- len = input.length;
- input = parseInt(input.substr(1), 16);
- // Handle long-form, e.g. #AABBCC
- if (len === 7) {
- rgba = [
- (input & 0xFF0000) >> 16,
- (input & 0xFF00) >> 8,
- (input & 0xFF),
- 1
- ];
- // Handle short-form, e.g. #ABC
- // In short form, the value is assumed to be the same
- // for both nibbles for each component. e.g. #ABC = #AABBCC
- } else if (len === 4) {
- rgba = [
- ((input & 0xF00) >> 4) | (input & 0xF00) >> 8,
- ((input & 0xF0) >> 4) | (input & 0xF0),
- ((input & 0xF) << 4) | (input & 0xF),
- 1
- ];
- }
- }
- // Otherwise, check regex parsers
- if (!rgba) {
- i = this.parsers.length;
- while (i-- && !rgba) {
- parser = this.parsers[i];
- result = parser.regex.exec(input);
- if (result) {
- rgba = parser.parse(result);
- }
- }
- }
- }
- this.rgba = rgba || [];
- },
- /**
- * Return the color a specified format
- * @param {String} format
- */
- get: function(format) {
- var input = this.input,
- rgba = this.rgba,
- ret;
- if (this.stops) {
- ret = merge(input);
- ret.stops = [].concat(ret.stops);
- each(this.stops, function(stop, i) {
- ret.stops[i] = [ret.stops[i][0], stop.get(format)];
- });
- // it's NaN if gradient colors on a column chart
- } else if (rgba && isNumber(rgba[0])) {
- if (format === 'rgb' || (!format && rgba[3] === 1)) {
- ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')';
- } else if (format === 'a') {
- ret = rgba[3];
- } else {
- ret = 'rgba(' + rgba.join(',') + ')';
- }
- } else {
- ret = input;
- }
- return ret;
- },
- /**
- * Brighten the color
- * @param {Number} alpha
- */
- brighten: function(alpha) {
- var i,
- rgba = this.rgba;
- if (this.stops) {
- each(this.stops, function(stop) {
- stop.brighten(alpha);
- });
- } else if (isNumber(alpha) && alpha !== 0) {
- for (i = 0; i < 3; i++) {
- rgba[i] += pInt(alpha * 255);
- if (rgba[i] < 0) {
- rgba[i] = 0;
- }
- if (rgba[i] > 255) {
- rgba[i] = 255;
- }
- }
- }
- return this;
- },
- /**
- * Set the color's opacity to a given alpha value
- * @param {Number} alpha
- */
- setOpacity: function(alpha) {
- this.rgba[3] = alpha;
- return this;
- },
- /*
- * Return an intermediate color between two colors.
- *
- * @param {Highcharts.Color} to
- * The color object to tween to.
- * @param {Number} pos
- * The intermediate position, where 0 is the from color (current
- * color item), and 1 is the `to` color.
- *
- * @return {String}
- * The intermediate color in rgba notation.
- */
- tweenTo: function(to, pos) {
- // Check for has alpha, because rgba colors perform worse due to lack of
- // support in WebKit.
- var from = this,
- hasAlpha,
- ret;
- // Unsupported color, return to-color (#3920)
- if (!to.rgba.length) {
- ret = to.input || 'none';
- // Interpolate
- } else {
- from = from.rgba;
- to = to.rgba;
- hasAlpha = (to[3] !== 1 || from[3] !== 1);
- ret = (hasAlpha ? 'rgba(' : 'rgb(') +
- Math.round(to[0] + (from[0] - to[0]) * (1 - pos)) + ',' +
- Math.round(to[1] + (from[1] - to[1]) * (1 - pos)) + ',' +
- Math.round(to[2] + (from[2] - to[2]) * (1 - pos)) +
- (hasAlpha ?
- (',' + (to[3] + (from[3] - to[3]) * (1 - pos))) :
- '') + ')';
- }
- return ret;
- }
- };
- H.color = function(input) {
- return new H.Color(input);
- };
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var SVGElement,
- SVGRenderer,
- addEvent = H.addEvent,
- animate = H.animate,
- attr = H.attr,
- charts = H.charts,
- color = H.color,
- css = H.css,
- createElement = H.createElement,
- defined = H.defined,
- deg2rad = H.deg2rad,
- destroyObjectProperties = H.destroyObjectProperties,
- doc = H.doc,
- each = H.each,
- extend = H.extend,
- erase = H.erase,
- grep = H.grep,
- hasTouch = H.hasTouch,
- inArray = H.inArray,
- isArray = H.isArray,
- isFirefox = H.isFirefox,
- isMS = H.isMS,
- isObject = H.isObject,
- isString = H.isString,
- isWebKit = H.isWebKit,
- merge = H.merge,
- noop = H.noop,
- objectEach = H.objectEach,
- pick = H.pick,
- pInt = H.pInt,
- removeEvent = H.removeEvent,
- splat = H.splat,
- stop = H.stop,
- svg = H.svg,
- SVG_NS = H.SVG_NS,
- symbolSizes = H.symbolSizes,
- win = H.win;
- /**
- * @typedef {Object} SVGDOMElement - An SVG DOM element.
- */
- /**
- * The SVGElement prototype is a JavaScript wrapper for SVG elements used in the
- * rendering layer of Highcharts. Combined with the {@link
- * Highcharts.SVGRenderer} object, these prototypes allow freeform annotation
- * in the charts or even in HTML pages without instanciating a chart. The
- * SVGElement can also wrap HTML labels, when `text` or `label` elements are
- * created with the `useHTML` parameter.
- *
- * The SVGElement instances are created through factory functions on the
- * {@link Highcharts.SVGRenderer} object, like
- * [rect]{@link Highcharts.SVGRenderer#rect}, [path]{@link
- * Highcharts.SVGRenderer#path}, [text]{@link Highcharts.SVGRenderer#text},
- * [label]{@link Highcharts.SVGRenderer#label}, [g]{@link
- * Highcharts.SVGRenderer#g} and more.
- *
- * @class Highcharts.SVGElement
- */
- SVGElement = H.SVGElement = function() {
- return this;
- };
- extend(SVGElement.prototype, /** @lends Highcharts.SVGElement.prototype */ {
- // Default base for animation
- opacity: 1,
- SVG_NS: SVG_NS,
- /**
- * For labels, these CSS properties are applied to the `text` node directly.
- * @type {Array.<string>}
- */
- textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily',
- 'fontStyle', 'color', 'lineHeight', 'width', 'textAlign',
- 'textDecoration', 'textOverflow', 'textOutline'
- ],
- /**
- * Initialize the SVG renderer. This function only exists to make the
- * initiation process overridable. It should not be called directly.
- *
- * @param {HighchartsSVGRenderer} renderer
- * The SVGRenderer instance to initialize to.
- * @param {String} nodeName
- * The SVG node name.
- * @returns {void}
- */
- init: function(renderer, nodeName) {
- /**
- * The DOM node. Each SVGRenderer instance wraps a main DOM node, but
- * may also represent more nodes.
- * @type {SVGDOMNode|HTMLDOMNode}
- */
- this.element = nodeName === 'span' ?
- createElement(nodeName) :
- doc.createElementNS(this.SVG_NS, nodeName);
- /**
- * The renderer that the SVGElement belongs to.
- * @type {Highcharts.SVGRenderer}
- */
- this.renderer = renderer;
- },
- /**
- * Animate to given attributes or CSS properties.
- *
- * @param {SVGAttributes} params SVG attributes or CSS to animate.
- * @param {AnimationOptions} [options] Animation options.
- * @param {Function} [complete] Function to perform at the end of animation.
- *
- * @sample highcharts/members/element-on/
- * Setting some attributes by animation
- *
- * @returns {Highcharts.SVGElement} Returns the SVGElement for chaining.
- */
- animate: function(params, options, complete) {
- var animOptions = H.animObject(
- pick(options, this.renderer.globalAnimation, true)
- );
- if (animOptions.duration !== 0) {
- if (complete) { // allows using a callback with the global animation without overwriting it
- animOptions.complete = complete;
- }
- animate(this, params, animOptions);
- } else {
- this.attr(params, null, complete);
- if (animOptions.step) {
- animOptions.step.call(this);
- }
- }
- return this;
- },
- /**
- * @typedef {Object} GradientOptions
- * @property {Object} linearGradient Holds an object that defines the start
- * position and the end position relative to the shape.
- * @property {Number} linearGradient.x1 Start horizontal position of the
- * gradient. Ranges 0-1.
- * @property {Number} linearGradient.x2 End horizontal position of the
- * gradient. Ranges 0-1.
- * @property {Number} linearGradient.y1 Start vertical position of the
- * gradient. Ranges 0-1.
- * @property {Number} linearGradient.y2 End vertical position of the
- * gradient. Ranges 0-1.
- * @property {Object} radialGradient Holds an object that defines the center
- * position and the radius.
- * @property {Number} radialGradient.cx Center horizontal position relative
- * to the shape. Ranges 0-1.
- * @property {Number} radialGradient.cy Center vertical position relative
- * to the shape. Ranges 0-1.
- * @property {Number} radialGradient.r Radius relative to the shape. Ranges
- * 0-1.
- * @property {Array.<Array>} stops The first item in each tuple is the
- * position in the gradient, where 0 is the start of the gradient and 1
- * is the end of the gradient. Multiple stops can be applied. The second
- * item is the color for each stop. This color can also be given in the
- * rgba format.
- *
- * @example
- * // Linear gradient used as a color option
- * color: {
- * linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 },
- * stops: [
- * [0, '#003399'], // start
- * [0.5, '#ffffff'], // middle
- * [1, '#3366AA'] // end
- * ]
- * }
- * }
- */
- /**
- * Build and apply an SVG gradient out of a common JavaScript configuration
- * object. This function is called from the attribute setters.
- *
- * @private
- * @param {GradientOptions} color The gradient options structure.
- * @param {string} prop The property to apply, can either be `fill` or
- * `stroke`.
- * @param {SVGDOMElement} elem SVG DOM element to apply the gradient on.
- */
- colorGradient: function(color, prop, elem) {
- var renderer = this.renderer,
- colorObject,
- gradName,
- gradAttr,
- radAttr,
- gradients,
- gradientObject,
- stops,
- stopColor,
- stopOpacity,
- radialReference,
- id,
- key = [],
- value;
- // Apply linear or radial gradients
- if (color.radialGradient) {
- gradName = 'radialGradient';
- } else if (color.linearGradient) {
- gradName = 'linearGradient';
- }
- if (gradName) {
- gradAttr = color[gradName];
- gradients = renderer.gradients;
- stops = color.stops;
- radialReference = elem.radialReference;
- // Keep < 2.2 kompatibility
- if (isArray(gradAttr)) {
- color[gradName] = gradAttr = {
- x1: gradAttr[0],
- y1: gradAttr[1],
- x2: gradAttr[2],
- y2: gradAttr[3],
- gradientUnits: 'userSpaceOnUse'
- };
- }
- // Correct the radial gradient for the radial reference system
- if (
- gradName === 'radialGradient' &&
- radialReference &&
- !defined(gradAttr.gradientUnits)
- ) {
- radAttr = gradAttr; // Save the radial attributes for updating
- gradAttr = merge(
- gradAttr,
- renderer.getRadialAttr(radialReference, radAttr), {
- gradientUnits: 'userSpaceOnUse'
- }
- );
- }
- // Build the unique key to detect whether we need to create a new element (#1282)
- objectEach(gradAttr, function(val, n) {
- if (n !== 'id') {
- key.push(n, val);
- }
- });
- objectEach(stops, function(val) {
- key.push(val);
- });
- key = key.join(',');
- // Check if a gradient object with the same config object is created within this renderer
- if (gradients[key]) {
- id = gradients[key].attr('id');
- } else {
- // Set the id and create the element
- gradAttr.id = id = H.uniqueKey();
- gradients[key] = gradientObject = renderer.createElement(gradName)
- .attr(gradAttr)
- .add(renderer.defs);
- gradientObject.radAttr = radAttr;
- // The gradient needs to keep a list of stops to be able to destroy them
- gradientObject.stops = [];
- each(stops, function(stop) {
- var stopObject;
- if (stop[1].indexOf('rgba') === 0) {
- colorObject = H.color(stop[1]);
- stopColor = colorObject.get('rgb');
- stopOpacity = colorObject.get('a');
- } else {
- stopColor = stop[1];
- stopOpacity = 1;
- }
- stopObject = renderer.createElement('stop').attr({
- offset: stop[0],
- 'stop-color': stopColor,
- 'stop-opacity': stopOpacity
- }).add(gradientObject);
- // Add the stop element to the gradient
- gradientObject.stops.push(stopObject);
- });
- }
- // Set the reference to the gradient object
- value = 'url(' + renderer.url + '#' + id + ')';
- elem.setAttribute(prop, value);
- elem.gradient = key;
- // Allow the color to be concatenated into tooltips formatters etc. (#2995)
- color.toString = function() {
- return value;
- };
- }
- },
- /**
- * Apply a text outline through a custom CSS property, by copying the text
- * element and apply stroke to the copy. Used internally. Contrast checks
- * at http://jsfiddle.net/highcharts/43soe9m1/2/ .
- *
- * @private
- * @param {String} textOutline A custom CSS `text-outline` setting, defined
- * by `width color`.
- * @example
- * // Specific color
- * text.css({
- * textOutline: '1px black'
- * });
- * // Automatic contrast
- * text.css({
- * color: '#000000', // black text
- * textOutline: '1px contrast' // => white outline
- * });
- */
- applyTextOutline: function(textOutline) {
- var elem = this.element,
- tspans,
- tspan,
- hasContrast = textOutline.indexOf('contrast') !== -1,
- styles = {},
- color,
- strokeWidth,
- firstRealChild,
- i;
- // When the text shadow is set to contrast, use dark stroke for light
- // text and vice versa.
- if (hasContrast) {
- styles.textOutline = textOutline = textOutline.replace(
- /contrast/g,
- this.renderer.getContrast(elem.style.fill)
- );
- }
- // Extract the stroke width and color
- textOutline = textOutline.split(' ');
- color = textOutline[textOutline.length - 1];
- strokeWidth = textOutline[0];
- if (strokeWidth && strokeWidth !== 'none' && H.svg) {
- this.fakeTS = true; // Fake text shadow
- tspans = [].slice.call(elem.getElementsByTagName('tspan'));
- // In order to get the right y position of the clone,
- // copy over the y setter
- this.ySetter = this.xSetter;
- // Since the stroke is applied on center of the actual outline, we
- // need to double it to get the correct stroke-width outside the
- // glyphs.
- strokeWidth = strokeWidth.replace(
- /(^[\d\.]+)(.*?)$/g,
- function(match, digit, unit) {
- return (2 * digit) + unit;
- }
- );
- // Remove shadows from previous runs. Iterate from the end to
- // support removing items inside the cycle (#6472).
- i = tspans.length;
- while (i--) {
- tspan = tspans[i];
- if (tspan.getAttribute('class') === 'highcharts-text-outline') {
- // Remove then erase
- erase(tspans, elem.removeChild(tspan));
- }
- }
- // For each of the tspans, create a stroked copy behind it.
- firstRealChild = elem.firstChild;
- each(tspans, function(tspan, y) {
- var clone;
- // Let the first line start at the correct X position
- if (y === 0) {
- tspan.setAttribute('x', elem.getAttribute('x'));
- y = elem.getAttribute('y');
- tspan.setAttribute('y', y || 0);
- if (y === null) {
- elem.setAttribute('y', 0);
- }
- }
- // Create the clone and apply outline properties
- clone = tspan.cloneNode(1);
- attr(clone, {
- 'class': 'highcharts-text-outline',
- 'fill': color,
- 'stroke': color,
- 'stroke-width': strokeWidth,
- 'stroke-linejoin': 'round'
- });
- elem.insertBefore(clone, firstRealChild);
- });
- }
- },
- /**
- *
- * @typedef {Object} SVGAttributes An object of key-value pairs for SVG
- * attributes. Attributes in Highcharts elements for the most parts
- * correspond to SVG, but some are specific to Highcharts, like `zIndex`,
- * `rotation`, `translateX`, `translateY`, `scaleX` and `scaleY`. SVG
- * attributes containing a hyphen are _not_ camel-cased, they should be
- * quoted to preserve the hyphen.
- * @example
- * {
- * 'stroke': '#ff0000', // basic
- * 'stroke-width': 2, // hyphenated
- * 'rotation': 45 // custom
- * 'd': ['M', 10, 10, 'L', 30, 30, 'z'] // path definition, note format
- * }
- */
- /**
- * Apply native and custom attributes to the SVG elements.
- *
- * In order to set the rotation center for rotation, set x and y to 0 and
- * use `translateX` and `translateY` attributes to position the element
- * instead.
- *
- * Attributes frequently used in Highcharts are `fill`, `stroke`,
- * `stroke-width`.
- *
- * @param {SVGAttributes|String} hash - The native and custom SVG
- * attributes.
- * @param {string} [val] - If the type of the first argument is `string`,
- * the second can be a value, which will serve as a single attribute
- * setter. If the first argument is a string and the second is undefined,
- * the function serves as a getter and the current value of the property
- * is returned.
- * @param {Function} [complete] - A callback function to execute after setting
- * the attributes. This makes the function compliant and interchangeable
- * with the {@link SVGElement#animate} function.
- * @param {boolean} [continueAnimation=true] Used internally when `.attr` is
- * called as part of an animation step. Otherwise, calling `.attr` for an
- * attribute will stop animation for that attribute.
- *
- * @returns {SVGElement|string|number} If used as a setter, it returns the
- * current {@link SVGElement} so the calls can be chained. If used as a
- * getter, the current value of the attribute is returned.
- *
- * @sample highcharts/members/renderer-rect/
- * Setting some attributes
- *
- * @example
- * // Set multiple attributes
- * element.attr({
- * stroke: 'red',
- * fill: 'blue',
- * x: 10,
- * y: 10
- * });
- *
- * // Set a single attribute
- * element.attr('stroke', 'red');
- *
- * // Get an attribute
- * element.attr('stroke'); // => 'red'
- *
- */
- attr: function(hash, val, complete, continueAnimation) {
- var key,
- element = this.element,
- hasSetSymbolSize,
- ret = this,
- skipAttr,
- setter;
- // single key-value pair
- if (typeof hash === 'string' && val !== undefined) {
- key = hash;
- hash = {};
- hash[key] = val;
- }
- // used as a getter: first argument is a string, second is undefined
- if (typeof hash === 'string') {
- ret = (this[hash + 'Getter'] || this._defaultGetter).call(this, hash, element);
- // setter
- } else {
- objectEach(hash, function(val, key) {
- skipAttr = false;
- // Unless .attr is from the animator update, stop current
- // running animation of this property
- if (!continueAnimation) {
- stop(this, key);
- }
- // Special handling of symbol attributes
- if (
- this.symbolName &&
- /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)$/
- .test(key)
- ) {
- if (!hasSetSymbolSize) {
- this.symbolAttr(hash);
- hasSetSymbolSize = true;
- }
- skipAttr = true;
- }
- if (this.rotation && (key === 'x' || key === 'y')) {
- this.doTransform = true;
- }
- if (!skipAttr) {
- setter = this[key + 'Setter'] || this._defaultSetter;
- setter.call(this, val, key, element);
- // Let the shadow follow the main element
- if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) {
- this.updateShadows(key, val, setter);
- }
- }
- }, this);
- this.afterSetters();
- }
- // In accordance with animate, run a complete callback
- if (complete) {
- complete();
- }
- return ret;
- },
- /**
- * This method is executed in the end of {attr}, after setting all attributes in the hash.
- * In can be used to efficiently consolidate multiple attributes in one SVG property -- e.g.,
- * translate, rotate and scale are merged in one "transform" attribute in the SVG node.
- */
- afterSetters: function() {
- // Update transform. Do this outside the loop to prevent redundant updating for batch setting
- // of attributes.
- if (this.doTransform) {
- this.updateTransform();
- this.doTransform = false;
- }
- },
- /**
- * Update the shadow elements with new attributes.
- *
- * @private
- * @param {String} key - The attribute name.
- * @param {String|Number} value - The value of the attribute.
- * @param {Function} setter - The setter function, inherited from the
- * parent wrapper
- * @returns {void}
- */
- updateShadows: function(key, value, setter) {
- var shadows = this.shadows,
- i = shadows.length;
- while (i--) {
- setter.call(
- shadows[i],
- key === 'height' ?
- Math.max(value - (shadows[i].cutHeight || 0), 0) :
- key === 'd' ? this.d : value,
- key,
- shadows[i]
- );
- }
- },
- /**
- * Add a class name to an element.
- *
- * @param {string} className - The new class name to add.
- * @param {boolean} [replace=false] - When true, the existing class name(s)
- * will be overwritten with the new one. When false, the new one is
- * added.
- * @returns {Highcharts.SVGElement} Return the SVG element for chainability.
- */
- addClass: function(className, replace) {
- var currentClassName = this.attr('class') || '';
- if (currentClassName.indexOf(className) === -1) {
- if (!replace) {
- className =
- (currentClassName + (currentClassName ? ' ' : '') +
- className).replace(' ', ' ');
- }
- this.attr('class', className);
- }
- return this;
- },
- /**
- * Check if an element has the given class name.
- * @param {string} className - The class name to check for.
- * @return {Boolean}
- */
- hasClass: function(className) {
- return attr(this.element, 'class').indexOf(className) !== -1;
- },
- /**
- * Remove a class name from the element.
- * @param {string} className The class name to remove.
- * @return {Highcharts.SVGElement} Returns the SVG element for chainability.
- */
- removeClass: function(className) {
- attr(this.element, 'class', (attr(this.element, 'class') || '').replace(className, ''));
- return this;
- },
- /**
- * If one of the symbol size affecting parameters are changed,
- * check all the others only once for each call to an element's
- * .attr() method
- * @param {Object} hash - The attributes to set.
- * @private
- */
- symbolAttr: function(hash) {
- var wrapper = this;
- each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function(key) {
- wrapper[key] = pick(hash[key], wrapper[key]);
- });
- wrapper.attr({
- d: wrapper.renderer.symbols[wrapper.symbolName](
- wrapper.x,
- wrapper.y,
- wrapper.width,
- wrapper.height,
- wrapper
- )
- });
- },
- /**
- * Apply a clipping rectangle to this element.
- *
- * @param {ClipRect} [clipRect] - The clipping rectangle. If skipped, the
- * current clip is removed.
- * @returns {Highcharts.SVGElement} Returns the SVG element to allow chaining.
- */
- clip: function(clipRect) {
- return this.attr(
- 'clip-path',
- clipRect ?
- 'url(' + this.renderer.url + '#' + clipRect.id + ')' :
- 'none'
- );
- },
- /**
- * Calculate the coordinates needed for drawing a rectangle crisply and
- * return the calculated attributes.
- *
- * @param {Object} rect - A rectangle.
- * @param {number} rect.x - The x position.
- * @param {number} rect.y - The y position.
- * @param {number} rect.width - The width.
- * @param {number} rect.height - The height.
- * @param {number} [strokeWidth] - The stroke width to consider when
- * computing crisp positioning. It can also be set directly on the rect
- * parameter.
- *
- * @returns {{x: Number, y: Number, width: Number, height: Number}} The
- * modified rectangle arguments.
- */
- crisp: function(rect, strokeWidth) {
- var wrapper = this,
- attribs = {},
- normalizer;
- strokeWidth = strokeWidth || rect.strokeWidth || 0;
- normalizer = Math.round(strokeWidth) % 2 / 2; // Math.round because strokeWidth can sometimes have roundoff errors
- // normalize for crisp edges
- rect.x = Math.floor(rect.x || wrapper.x || 0) + normalizer;
- rect.y = Math.floor(rect.y || wrapper.y || 0) + normalizer;
- rect.width = Math.floor((rect.width || wrapper.width || 0) - 2 * normalizer);
- rect.height = Math.floor((rect.height || wrapper.height || 0) - 2 * normalizer);
- if (defined(rect.strokeWidth)) {
- rect.strokeWidth = strokeWidth;
- }
- objectEach(rect, function(val, key) {
- if (wrapper[key] !== val) { // only set attribute if changed
- wrapper[key] = attribs[key] = val;
- }
- });
- return attribs;
- },
- /**
- * Set styles for the element. In addition to CSS styles supported by
- * native SVG and HTML elements, there are also some custom made for
- * Highcharts, like `width`, `ellipsis` and `textOverflow` for SVG text
- * elements.
- * @param {CSSObject} styles The new CSS styles.
- * @returns {Highcharts.SVGElement} Return the SVG element for chaining.
- *
- * @sample highcharts/members/renderer-text-on-chart/
- * Styled text
- */
- css: function(styles) {
- var oldStyles = this.styles,
- newStyles = {},
- elem = this.element,
- textWidth,
- serializedCss = '',
- hyphenate,
- hasNew = !oldStyles,
- // These CSS properties are interpreted internally by the SVG
- // renderer, but are not supported by SVG and should not be added to
- // the DOM. In styled mode, no CSS should find its way to the DOM
- // whatsoever (#6173, #6474).
- svgPseudoProps = ['textOutline', 'textOverflow', 'width'];
- // convert legacy
- if (styles && styles.color) {
- styles.fill = styles.color;
- }
- // Filter out existing styles to increase performance (#2640)
- if (oldStyles) {
- objectEach(styles, function(style, n) {
- if (style !== oldStyles[n]) {
- newStyles[n] = style;
- hasNew = true;
- }
- });
- }
- if (hasNew) {
- // Merge the new styles with the old ones
- if (oldStyles) {
- styles = extend(
- oldStyles,
- newStyles
- );
- }
- // Get the text width from style
- textWidth = this.textWidth = (
- styles &&
- styles.width &&
- styles.width !== 'auto' &&
- elem.nodeName.toLowerCase() === 'text' &&
- pInt(styles.width)
- );
- // store object
- this.styles = styles;
- if (textWidth && (!svg && this.renderer.forExport)) {
- delete styles.width;
- }
- // serialize and set style attribute
- if (isMS && !svg) {
- css(this.element, styles);
- } else {
- hyphenate = function(a, b) {
- return '-' + b.toLowerCase();
- };
- objectEach(styles, function(style, n) {
- if (inArray(n, svgPseudoProps) === -1) {
- serializedCss +=
- n.replace(/([A-Z])/g, hyphenate) + ':' +
- style + ';';
- }
- });
- if (serializedCss) {
- attr(elem, 'style', serializedCss); // #1881
- }
- }
- if (this.added) {
- // Rebuild text after added. Cache mechanisms in the buildText
- // will prevent building if there are no significant changes.
- if (this.element.nodeName === 'text') {
- this.renderer.buildText(this);
- }
- // Apply text outline after added
- if (styles && styles.textOutline) {
- this.applyTextOutline(styles.textOutline);
- }
- }
- }
- return this;
- },
- /**
- * Get the current stroke width. In classic mode, the setter registers it
- * directly on the element.
- * @returns {number} The stroke width in pixels.
- * @ignore
- */
- strokeWidth: function() {
- return this['stroke-width'] || 0;
- },
- /**
- * Add an event listener. This is a simple setter that replaces all other
- * events of the same type, opposed to the {@link Highcharts#addEvent}
- * function.
- * @param {string} eventType - The event type. If the type is `click`,
- * Highcharts will internally translate it to a `touchstart` event on
- * touch devices, to prevent the browser from waiting for a click event
- * from firing.
- * @param {Function} handler - The handler callback.
- * @returns {Highcharts.SVGElement} The SVGElement for chaining.
- *
- * @sample highcharts/members/element-on/
- * A clickable rectangle
- */
- on: function(eventType, handler) {
- var svgElement = this,
- element = svgElement.element;
- // touch
- if (hasTouch && eventType === 'click') {
- element.ontouchstart = function(e) {
- svgElement.touchEventFired = Date.now(); // #2269
- e.preventDefault();
- handler.call(element, e);
- };
- element.onclick = function(e) {
- if (win.navigator.userAgent.indexOf('Android') === -1 ||
- Date.now() - (svgElement.touchEventFired || 0) > 1100) {
- handler.call(element, e);
- }
- };
- } else {
- // simplest possible event model for internal use
- element['on' + eventType] = handler;
- }
- return this;
- },
- /**
- * Set the coordinates needed to draw a consistent radial gradient across
- * a shape regardless of positioning inside the chart. Used on pie slices
- * to make all the slices have the same radial reference point.
- *
- * @param {Array} coordinates The center reference. The format is
- * `[centerX, centerY, diameter]` in pixels.
- * @returns {Highcharts.SVGElement} Returns the SVGElement for chaining.
- */
- setRadialReference: function(coordinates) {
- var existingGradient = this.renderer.gradients[this.element.gradient];
- this.element.radialReference = coordinates;
- // On redrawing objects with an existing gradient, the gradient needs
- // to be repositioned (#3801)
- if (existingGradient && existingGradient.radAttr) {
- existingGradient.animate(
- this.renderer.getRadialAttr(
- coordinates,
- existingGradient.radAttr
- )
- );
- }
- return this;
- },
- /**
- * Move an object and its children by x and y values.
- *
- * @param {number} x - The x value.
- * @param {number} y - The y value.
- */
- translate: function(x, y) {
- return this.attr({
- translateX: x,
- translateY: y
- });
- },
- /**
- * Invert a group, rotate and flip. This is used internally on inverted
- * charts, where the points and graphs are drawn as if not inverted, then
- * the series group elements are inverted.
- *
- * @param {boolean} inverted - Whether to invert or not. An inverted shape
- * can be un-inverted by setting it to false.
- * @returns {Highcharts.SVGElement} Return the SVGElement for chaining.
- */
- invert: function(inverted) {
- var wrapper = this;
- wrapper.inverted = inverted;
- wrapper.updateTransform();
- return wrapper;
- },
- /**
- * Update the transform attribute based on internal properties. Deals with
- * the custom `translateX`, `translateY`, `rotation`, `scaleX` and `scaleY`
- * attributes and updates the SVG `transform` attribute.
- * @private
- * @returns {void}
- */
- updateTransform: function() {
- var wrapper = this,
- translateX = wrapper.translateX || 0,
- translateY = wrapper.translateY || 0,
- scaleX = wrapper.scaleX,
- scaleY = wrapper.scaleY,
- inverted = wrapper.inverted,
- rotation = wrapper.rotation,
- element = wrapper.element,
- transform;
- // flipping affects translate as adjustment for flipping around the group's axis
- if (inverted) {
- translateX += wrapper.width;
- translateY += wrapper.height;
- }
- // Apply translate. Nearly all transformed elements have translation, so instead
- // of checking for translate = 0, do it always (#1767, #1846).
- transform = ['translate(' + translateX + ',' + translateY + ')'];
- // apply rotation
- if (inverted) {
- transform.push('rotate(90) scale(-1,1)');
- } else if (rotation) { // text rotation
- transform.push('rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + ' ' + (element.getAttribute('y') || 0) + ')');
- // Delete bBox memo when the rotation changes
- //delete wrapper.bBox;
- }
- // apply scale
- if (defined(scaleX) || defined(scaleY)) {
- transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')');
- }
- if (transform.length) {
- element.setAttribute('transform', transform.join(' '));
- }
- },
- /**
- * Bring the element to the front. Alternatively, a new zIndex can be set.
- *
- * @returns {Highcharts.SVGElement} Returns the SVGElement for chaining.
- *
- * @sample highcharts/members/element-tofront/
- * Click an element to bring it to front
- */
- toFront: function() {
- var element = this.element;
- element.parentNode.appendChild(element);
- return this;
- },
- /**
- * Align the element relative to the chart or another box.
- *
- * @param {Object} [alignOptions] The alignment options. The function can be
- * called without this parameter in order to re-align an element after the
- * box has been updated.
- * @param {string} [alignOptions.align=left] Horizontal alignment. Can be
- * one of `left`, `center` and `right`.
- * @param {string} [alignOptions.verticalAlign=top] Vertical alignment. Can
- * be one of `top`, `middle` and `bottom`.
- * @param {number} [alignOptions.x=0] Horizontal pixel offset from
- * alignment.
- * @param {number} [alignOptions.y=0] Vertical pixel offset from alignment.
- * @param {Boolean} [alignByTranslate=false] Use the `transform` attribute
- * with translateX and translateY custom attributes to align this elements
- * rather than `x` and `y` attributes.
- * @param {String|Object} box The box to align to, needs a width and height.
- * When the box is a string, it refers to an object in the Renderer. For
- * example, when box is `spacingBox`, it refers to `Renderer.spacingBox`
- * which holds `width`, `height`, `x` and `y` properties.
- * @returns {Highcharts.SVGElement} Returns the SVGElement for chaining.
- */
- align: function(alignOptions, alignByTranslate, box) {
- var align,
- vAlign,
- x,
- y,
- attribs = {},
- alignTo,
- renderer = this.renderer,
- alignedObjects = renderer.alignedObjects,
- alignFactor,
- vAlignFactor;
- // First call on instanciate
- if (alignOptions) {
- this.alignOptions = alignOptions;
- this.alignByTranslate = alignByTranslate;
- if (!box || isString(box)) { // boxes other than renderer handle this internally
- this.alignTo = alignTo = box || 'renderer';
- erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize
- alignedObjects.push(this);
- box = null; // reassign it below
- }
- // When called on resize, no arguments are supplied
- } else {
- alignOptions = this.alignOptions;
- alignByTranslate = this.alignByTranslate;
- alignTo = this.alignTo;
- }
- box = pick(box, renderer[alignTo], renderer);
- // Assign variables
- align = alignOptions.align;
- vAlign = alignOptions.verticalAlign;
- x = (box.x || 0) + (alignOptions.x || 0); // default: left align
- y = (box.y || 0) + (alignOptions.y || 0); // default: top align
- // Align
- if (align === 'right') {
- alignFactor = 1;
- } else if (align === 'center') {
- alignFactor = 2;
- }
- if (alignFactor) {
- x += (box.width - (alignOptions.width || 0)) / alignFactor;
- }
- attribs[alignByTranslate ? 'translateX' : 'x'] = Math.round(x);
- // Vertical align
- if (vAlign === 'bottom') {
- vAlignFactor = 1;
- } else if (vAlign === 'middle') {
- vAlignFactor = 2;
- }
- if (vAlignFactor) {
- y += (box.height - (alignOptions.height || 0)) / vAlignFactor;
- }
- attribs[alignByTranslate ? 'translateY' : 'y'] = Math.round(y);
- // Animate only if already placed
- this[this.placed ? 'animate' : 'attr'](attribs);
- this.placed = true;
- this.alignAttr = attribs;
- return this;
- },
- /**
- * Get the bounding box (width, height, x and y) for the element. Generally
- * used to get rendered text size. Since this is called a lot in charts,
- * the results are cached based on text properties, in order to save DOM
- * traffic. The returned bounding box includes the rotation, so for example
- * a single text line of rotation 90 will report a greater height, and a
- * width corresponding to the line-height.
- *
- * @param {boolean} [reload] Skip the cache and get the updated DOM bouding
- * box.
- * @param {number} [rot] Override the element's rotation. This is internally
- * used on axis labels with a value of 0 to find out what the bounding box
- * would be have been if it were not rotated.
- * @returns {Object} The bounding box with `x`, `y`, `width` and `height`
- * properties.
- *
- * @sample highcharts/members/renderer-on-chart/
- * Draw a rectangle based on a text's bounding box
- */
- getBBox: function(reload, rot) {
- var wrapper = this,
- bBox, // = wrapper.bBox,
- renderer = wrapper.renderer,
- width,
- height,
- rotation,
- rad,
- element = wrapper.element,
- styles = wrapper.styles,
- fontSize,
- textStr = wrapper.textStr,
- toggleTextShadowShim,
- cache = renderer.cache,
- cacheKeys = renderer.cacheKeys,
- cacheKey;
- rotation = pick(rot, wrapper.rotation);
- rad = rotation * deg2rad;
- fontSize = styles && styles.fontSize;
- if (textStr !== undefined) {
- cacheKey = textStr.toString();
- // Since numbers are monospaced, and numerical labels appear a lot
- // in a chart, we assume that a label of n characters has the same
- // bounding box as others of the same length. Unless there is inner
- // HTML in the label. In that case, leave the numbers as is (#5899).
- if (cacheKey.indexOf('<') === -1) {
- cacheKey = cacheKey.replace(/[0-9]/g, '0');
- }
- // Properties that affect bounding box
- cacheKey += [
- '',
- rotation || 0,
- fontSize,
- styles && styles.width,
- styles && styles.textOverflow // #5968
- ]
- .join(',');
- }
- if (cacheKey && !reload) {
- bBox = cache[cacheKey];
- }
- // No cache found
- if (!bBox) {
- // SVG elements
- if (element.namespaceURI === wrapper.SVG_NS || renderer.forExport) {
- try { // Fails in Firefox if the container has display: none.
- // When the text shadow shim is used, we need to hide the fake shadows
- // to get the correct bounding box (#3872)
- toggleTextShadowShim = this.fakeTS && function(display) {
- each(element.querySelectorAll('.highcharts-text-outline'), function(tspan) {
- tspan.style.display = display;
- });
- };
- // Workaround for #3842, Firefox reporting wrong bounding box for shadows
- if (toggleTextShadowShim) {
- toggleTextShadowShim('none');
- }
- bBox = element.getBBox ?
- // SVG: use extend because IE9 is not allowed to change width and height in case
- // of rotation (below)
- extend({}, element.getBBox()) : {
- // Legacy IE in export mode
- width: element.offsetWidth,
- height: element.offsetHeight
- };
- // #3842
- if (toggleTextShadowShim) {
- toggleTextShadowShim('');
- }
- } catch (e) {}
- // If the bBox is not set, the try-catch block above failed. The other condition
- // is for Opera that returns a width of -Infinity on hidden elements.
- if (!bBox || bBox.width < 0) {
- bBox = {
- width: 0,
- height: 0
- };
- }
- // VML Renderer or useHTML within SVG
- } else {
- bBox = wrapper.htmlGetBBox();
- }
- // True SVG elements as well as HTML elements in modern browsers using the .useHTML option
- // need to compensated for rotation
- if (renderer.isSVG) {
- width = bBox.width;
- height = bBox.height;
- // Workaround for wrong bounding box in IE, Edge and Chrome on
- // Windows. With Highcharts' default font, IE and Edge report
- // a box height of 16.899 and Chrome rounds it to 17. If this
- // stands uncorrected, it results in more padding added below
- // the text than above when adding a label border or background.
- // Also vertical positioning is affected.
- // http://jsfiddle.net/highcharts/em37nvuj/
- // (#1101, #1505, #1669, #2568, #6213).
- if (
- styles &&
- styles.fontSize === '11px' &&
- Math.round(height) === 17
- ) {
- bBox.height = height = 14;
- }
- // Adjust for rotated text
- if (rotation) {
- bBox.width = Math.abs(height * Math.sin(rad)) + Math.abs(width * Math.cos(rad));
- bBox.height = Math.abs(height * Math.cos(rad)) + Math.abs(width * Math.sin(rad));
- }
- }
- // Cache it. When loading a chart in a hidden iframe in Firefox and IE/Edge, the
- // bounding box height is 0, so don't cache it (#5620).
- if (cacheKey && bBox.height > 0) {
- // Rotate (#4681)
- while (cacheKeys.length > 250) {
- delete cache[cacheKeys.shift()];
- }
- if (!cache[cacheKey]) {
- cacheKeys.push(cacheKey);
- }
- cache[cacheKey] = bBox;
- }
- }
- return bBox;
- },
- /**
- * Show the element after it has been hidden.
- *
- * @param {boolean} [inherit=false] Set the visibility attribute to
- * `inherit` rather than `visible`. The difference is that an element with
- * `visibility="visible"` will be visible even if the parent is hidden.
- *
- * @returns {Highcharts.SVGElement} Returns the SVGElement for chaining.
- */
- show: function(inherit) {
- return this.attr({
- visibility: inherit ? 'inherit' : 'visible'
- });
- },
- /**
- * Hide the element, equivalent to setting the `visibility` attribute to
- * `hidden`.
- *
- * @returns {Highcharts.SVGElement} Returns the SVGElement for chaining.
- */
- hide: function() {
- return this.attr({
- visibility: 'hidden'
- });
- },
- /**
- * Fade out an element by animating its opacity down to 0, and hide it on
- * complete. Used internally for the tooltip.
- *
- * @param {number} [duration=150] The fade duration in milliseconds.
- */
- fadeOut: function(duration) {
- var elemWrapper = this;
- elemWrapper.animate({
- opacity: 0
- }, {
- duration: duration || 150,
- complete: function() {
- elemWrapper.attr({
- y: -9999
- }); // #3088, assuming we're only using this for tooltips
- }
- });
- },
- /**
- * Add the element to the DOM. All elements must be added this way.
- *
- * @param {Highcharts.SVGElement|SVGDOMElement} [parent] The parent item to add it to.
- * If undefined, the element is added to the {@link
- * Highcharts.SVGRenderer.box}.
- *
- * @returns {Highcharts.SVGElement} Returns the SVGElement for chaining.
- *
- * @sample highcharts/members/renderer-g - Elements added to a group
- */
- add: function(parent) {
- var renderer = this.renderer,
- element = this.element,
- inserted;
- if (parent) {
- this.parentGroup = parent;
- }
- // mark as inverted
- this.parentInverted = parent && parent.inverted;
- // build formatted text
- if (this.textStr !== undefined) {
- renderer.buildText(this);
- }
- // Mark as added
- this.added = true;
- // If we're adding to renderer root, or other elements in the group
- // have a z index, we need to handle it
- if (!parent || parent.handleZ || this.zIndex) {
- inserted = this.zIndexSetter();
- }
- // If zIndex is not handled, append at the end
- if (!inserted) {
- (parent ? parent.element : renderer.box).appendChild(element);
- }
- // fire an event for internal hooks
- if (this.onAdd) {
- this.onAdd();
- }
- return this;
- },
- /**
- * Removes an element from the DOM.
- *
- * @private
- * @param {SVGDOMElement|HTMLDOMElement} element The DOM node to remove.
- */
- safeRemoveChild: function(element) {
- var parentNode = element.parentNode;
- if (parentNode) {
- parentNode.removeChild(element);
- }
- },
- /**
- * Destroy the element and element wrapper and clear up the DOM and event
- * hooks.
- *
- * @returns {void}
- */
- destroy: function() {
- var wrapper = this,
- element = wrapper.element || {},
- parentToClean =
- wrapper.renderer.isSVG &&
- element.nodeName === 'SPAN' &&
- wrapper.parentGroup,
- grandParent,
- ownerSVGElement = element.ownerSVGElement,
- i;
- // remove events
- element.onclick = element.onmouseout = element.onmouseover =
- element.onmousemove = element.point = null;
- stop(wrapper); // stop running animations
- if (wrapper.clipPath && ownerSVGElement) {
- // Look for existing references to this clipPath and remove them
- // before destroying the element (#6196).
- each(
- ownerSVGElement.querySelectorAll('[clip-path]'),
- function(el) {
- // Include the closing paranthesis in the test to rule out
- // id's from 10 and above (#6550)
- if (el.getAttribute('clip-path')
- .indexOf(wrapper.clipPath.element.id + ')') > -1) {
- el.removeAttribute('clip-path');
- }
- }
- );
- wrapper.clipPath = wrapper.clipPath.destroy();
- }
- // Destroy stops in case this is a gradient object
- if (wrapper.stops) {
- for (i = 0; i < wrapper.stops.length; i++) {
- wrapper.stops[i] = wrapper.stops[i].destroy();
- }
- wrapper.stops = null;
- }
- // remove element
- wrapper.safeRemoveChild(element);
- wrapper.destroyShadows();
- // In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393, #2697).
- while (parentToClean && parentToClean.div && parentToClean.div.childNodes.length === 0) {
- grandParent = parentToClean.parentGroup;
- wrapper.safeRemoveChild(parentToClean.div);
- delete parentToClean.div;
- parentToClean = grandParent;
- }
- // remove from alignObjects
- if (wrapper.alignTo) {
- erase(wrapper.renderer.alignedObjects, wrapper);
- }
- objectEach(wrapper, function(val, key) {
- delete wrapper[key];
- });
- return null;
- },
- /**
- * @typedef {Object} ShadowOptions
- * @property {string} [color=#000000] The shadow color.
- * @property {number} [offsetX=1] The horizontal offset from the element.
- * @property {number} [offsetY=1] The vertical offset from the element.
- * @property {number} [opacity=0.15] The shadow opacity.
- * @property {number} [width=3] The shadow width or distance from the
- * element.
- */
- /**
- * Add a shadow to the element. Must be called after the element is added to
- * the DOM. In styled mode, this method is not used, instead use `defs` and
- * filters.
- *
- * @param {boolean|ShadowOptions} shadowOptions The shadow options. If
- * `true`, the default options are applied. If `false`, the current
- * shadow will be removed.
- * @param {Highcharts.SVGElement} [group] The SVG group element where the shadows will
- * be applied. The default is to add it to the same parent as the current
- * element. Internally, this is ised for pie slices, where all the
- * shadows are added to an element behind all the slices.
- * @param {boolean} [cutOff] Used internally for column shadows.
- *
- * @returns {Highcharts.SVGElement} Returns the SVGElement for chaining.
- *
- * @example
- * renderer.rect(10, 100, 100, 100)
- * .attr({ fill: 'red' })
- * .shadow(true);
- */
- shadow: function(shadowOptions, group, cutOff) {
- var shadows = [],
- i,
- shadow,
- element = this.element,
- strokeWidth,
- shadowWidth,
- shadowElementOpacity,
- // compensate for inverted plot area
- transform;
- if (!shadowOptions) {
- this.destroyShadows();
- } else if (!this.shadows) {
- shadowWidth = pick(shadowOptions.width, 3);
- shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
- transform = this.parentInverted ?
- '(-1,-1)' :
- '(' + pick(shadowOptions.offsetX, 1) + ', ' + pick(shadowOptions.offsetY, 1) + ')';
- for (i = 1; i <= shadowWidth; i++) {
- shadow = element.cloneNode(0);
- strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
- attr(shadow, {
- 'isShadow': 'true',
- 'stroke': shadowOptions.color || '#000000',
- 'stroke-opacity': shadowElementOpacity * i,
- 'stroke-width': strokeWidth,
- 'transform': 'translate' + transform,
- 'fill': 'none'
- });
- if (cutOff) {
- attr(shadow, 'height', Math.max(attr(shadow, 'height') - strokeWidth, 0));
- shadow.cutHeight = strokeWidth;
- }
- if (group) {
- group.element.appendChild(shadow);
- } else {
- element.parentNode.insertBefore(shadow, element);
- }
- shadows.push(shadow);
- }
- this.shadows = shadows;
- }
- return this;
- },
- /**
- * Destroy shadows on the element.
- * @private
- */
- destroyShadows: function() {
- each(this.shadows || [], function(shadow) {
- this.safeRemoveChild(shadow);
- }, this);
- this.shadows = undefined;
- },
- xGetter: function(key) {
- if (this.element.nodeName === 'circle') {
- if (key === 'x') {
- key = 'cx';
- } else if (key === 'y') {
- key = 'cy';
- }
- }
- return this._defaultGetter(key);
- },
- /**
- * Get the current value of an attribute or pseudo attribute, used mainly
- * for animation. Called internally from the {@link
- * Highcharts.SVGRenderer#attr}
- * function.
- *
- * @private
- */
- _defaultGetter: function(key) {
- var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0);
- if (/^[\-0-9\.]+$/.test(ret)) { // is numerical
- ret = parseFloat(ret);
- }
- return ret;
- },
- dSetter: function(value, key, element) {
- if (value && value.join) { // join path
- value = value.join(' ');
- }
- if (/(NaN| {2}|^$)/.test(value)) {
- value = 'M 0 0';
- }
- element.setAttribute(key, value);
- this[key] = value;
- },
- dashstyleSetter: function(value) {
- var i,
- strokeWidth = this['stroke-width'];
- // If "inherit", like maps in IE, assume 1 (#4981). With HC5 and the new strokeWidth
- // function, we should be able to use that instead.
- if (strokeWidth === 'inherit') {
- strokeWidth = 1;
- }
- value = value && value.toLowerCase();
- if (value) {
- value = value
- .replace('shortdashdotdot', '3,1,1,1,1,1,')
- .replace('shortdashdot', '3,1,1,1')
- .replace('shortdot', '1,1,')
- .replace('shortdash', '3,1,')
- .replace('longdash', '8,3,')
- .replace(/dot/g, '1,3,')
- .replace('dash', '4,3,')
- .replace(/,$/, '')
- .split(','); // ending comma
- i = value.length;
- while (i--) {
- value[i] = pInt(value[i]) * strokeWidth;
- }
- value = value.join(',')
- .replace(/NaN/g, 'none'); // #3226
- this.element.setAttribute('stroke-dasharray', value);
- }
- },
- alignSetter: function(value) {
- var convert = {
- left: 'start',
- center: 'middle',
- right: 'end'
- };
- this.element.setAttribute('text-anchor', convert[value]);
- },
- opacitySetter: function(value, key, element) {
- this[key] = value;
- element.setAttribute(key, value);
- },
- titleSetter: function(value) {
- var titleNode = this.element.getElementsByTagName('title')[0];
- if (!titleNode) {
- titleNode = doc.createElementNS(this.SVG_NS, 'title');
- this.element.appendChild(titleNode);
- }
- // Remove text content if it exists
- if (titleNode.firstChild) {
- titleNode.removeChild(titleNode.firstChild);
- }
- titleNode.appendChild(
- doc.createTextNode(
- (String(pick(value), '')).replace(/<[^>]*>/g, '') // #3276, #3895
- )
- );
- },
- textSetter: function(value) {
- if (value !== this.textStr) {
- // Delete bBox memo when the text changes
- delete this.bBox;
- this.textStr = value;
- if (this.added) {
- this.renderer.buildText(this);
- }
- }
- },
- fillSetter: function(value, key, element) {
- if (typeof value === 'string') {
- element.setAttribute(key, value);
- } else if (value) {
- this.colorGradient(value, key, element);
- }
- },
- visibilitySetter: function(value, key, element) {
- // IE9-11 doesn't handle visibilty:inherit well, so we remove the attribute instead (#2881, #3909)
- if (value === 'inherit') {
- element.removeAttribute(key);
- } else {
- element.setAttribute(key, value);
- }
- },
- zIndexSetter: function(value, key) {
- var renderer = this.renderer,
- parentGroup = this.parentGroup,
- parentWrapper = parentGroup || renderer,
- parentNode = parentWrapper.element || renderer.box,
- childNodes,
- otherElement,
- otherZIndex,
- element = this.element,
- inserted,
- run = this.added,
- i;
- if (defined(value)) {
- element.zIndex = value; // So we can read it for other elements in the group
- value = +value;
- if (this[key] === value) { // Only update when needed (#3865)
- run = false;
- }
- this[key] = value;
- }
- // Insert according to this and other elements' zIndex. Before .add() is called,
- // nothing is done. Then on add, or by later calls to zIndexSetter, the node
- // is placed on the right place in the DOM.
- if (run) {
- value = this.zIndex;
- if (value && parentGroup) {
- parentGroup.handleZ = true;
- }
- childNodes = parentNode.childNodes;
- for (i = 0; i < childNodes.length && !inserted; i++) {
- otherElement = childNodes[i];
- otherZIndex = otherElement.zIndex;
- if (otherElement !== element && (
- // Insert before the first element with a higher zIndex
- pInt(otherZIndex) > value ||
- // If no zIndex given, insert before the first element with a zIndex
- (!defined(value) && defined(otherZIndex)) ||
- // Negative zIndex versus no zIndex:
- // On all levels except the highest. If the parent is <svg>,
- // then we don't want to put items before <desc> or <defs>
- (value < 0 && !defined(otherZIndex) && parentNode !== renderer.box)
- )) {
- parentNode.insertBefore(element, otherElement);
- inserted = true;
- }
- }
- if (!inserted) {
- parentNode.appendChild(element);
- }
- }
- return inserted;
- },
- _defaultSetter: function(value, key, element) {
- element.setAttribute(key, value);
- }
- });
- // Some shared setters and getters
- SVGElement.prototype.yGetter = SVGElement.prototype.xGetter;
- SVGElement.prototype.translateXSetter = SVGElement.prototype.translateYSetter =
- SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter =
- SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function(value, key) {
- this[key] = value;
- this.doTransform = true;
- };
- // WebKit and Batik have problems with a stroke-width of zero, so in this case we remove the
- // stroke attribute altogether. #1270, #1369, #3065, #3072.
- SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function(value, key, element) {
- this[key] = value;
- // Only apply the stroke attribute if the stroke width is defined and larger than 0
- if (this.stroke && this['stroke-width']) {
- SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element); // use prototype as instance may be overridden
- element.setAttribute('stroke-width', this['stroke-width']);
- this.hasStroke = true;
- } else if (key === 'stroke-width' && value === 0 && this.hasStroke) {
- element.removeAttribute('stroke');
- this.hasStroke = false;
- }
- };
- /**
- * Allows direct access to the Highcharts rendering layer in order to draw
- * primitive shapes like circles, rectangles, paths or text directly on a chart,
- * or independent from any chart. The SVGRenderer represents a wrapper object
- * for SVGin modern browsers and through the VMLRenderer, for VML in IE < 8.
- *
- * An existing chart's renderer can be accessed through {@link Chart#renderer}.
- * The renderer can also be used completely decoupled from a chart.
- *
- * @param {HTMLDOMElement} container - Where to put the SVG in the web page.
- * @param {number} width - The width of the SVG.
- * @param {number} height - The height of the SVG.
- * @param {boolean} [forExport=false] - Whether the rendered content is intended
- * for export.
- * @param {boolean} [allowHTML=true] - Whether the renderer is allowed to
- * include HTML text, which will be projected on top of the SVG.
- *
- * @example
- * // Use directly without a chart object.
- * var renderer = new Highcharts.Renderer(parentNode, 600, 400);
- *
- * @sample highcharts/members/renderer-on-chart - Annotating a chart programmatically.
- * @sample highcharts/members/renderer-basic - Independedt SVG drawing.
- *
- * @class Highcharts.SVGRenderer
- */
- SVGRenderer = H.SVGRenderer = function() {
- this.init.apply(this, arguments);
- };
- extend(SVGRenderer.prototype, /** @lends Highcharts.SVGRenderer.prototype */ {
- /**
- * A pointer to the renderer's associated Element class. The VMLRenderer
- * will have a pointer to VMLElement here.
- * @type {Highcharts.SVGElement}
- */
- Element: SVGElement,
- SVG_NS: SVG_NS,
- /**
- * Initialize the SVGRenderer. Overridable initiator function that takes
- * the same parameters as the constructor.
- */
- init: function(container, width, height, style, forExport, allowHTML) {
- var renderer = this,
- boxWrapper,
- element,
- desc;
- boxWrapper = renderer.createElement('svg')
- .attr({
- 'version': '1.1',
- 'class': 'highcharts-root'
- })
- .css(this.getStyle(style));
- element = boxWrapper.element;
- container.appendChild(element);
- // For browsers other than IE, add the namespace attribute (#1978)
- if (container.innerHTML.indexOf('xmlns') === -1) {
- attr(element, 'xmlns', this.SVG_NS);
- }
- // object properties
- renderer.isSVG = true;
- /**
- * The root `svg` node of the renderer.
- * @type {SVGDOMElement}
- */
- this.box = element;
- /**
- * The wrapper for the root `svg` node of the renderer.
- * @type {Highcharts.SVGElement}
- */
- this.boxWrapper = boxWrapper;
- renderer.alignedObjects = [];
- /**
- * Page url used for internal references.
- * @type {string}
- */
- // #24, #672, #1070
- this.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ?
- win.location.href
- .replace(/#.*?$/, '') // remove the hash
- .replace(/<[^>]*>/g, '') // wing cut HTML
- .replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes
- .replace(/ /g, '%20') : // replace spaces (needed for Safari only)
- '';
- // Add description
- desc = this.createElement('desc').add();
- desc.element.appendChild(doc.createTextNode('Created with Highstock 5.0.12'));
- renderer.defs = this.createElement('defs').add();
- renderer.allowHTML = allowHTML;
- renderer.forExport = forExport;
- renderer.gradients = {}; // Object where gradient SvgElements are stored
- renderer.cache = {}; // Cache for numerical bounding boxes
- renderer.cacheKeys = [];
- renderer.imgCount = 0;
- renderer.setSize(width, height, false);
- // Issue 110 workaround:
- // In Firefox, if a div is positioned by percentage, its pixel position may land
- // between pixels. The container itself doesn't display this, but an SVG element
- // inside this container will be drawn at subpixel precision. In order to draw
- // sharp lines, this must be compensated for. This doesn't seem to work inside
- // iframes though (like in jsFiddle).
- var subPixelFix, rect;
- if (isFirefox && container.getBoundingClientRect) {
- subPixelFix = function() {
- css(container, {
- left: 0,
- top: 0
- });
- rect = container.getBoundingClientRect();
- css(container, {
- left: (Math.ceil(rect.left) - rect.left) + 'px',
- top: (Math.ceil(rect.top) - rect.top) + 'px'
- });
- };
- // run the fix now
- subPixelFix();
- // run it on resize
- renderer.unSubPixelFix = addEvent(win, 'resize', subPixelFix);
- }
- },
- /**
- * Get the global style setting for the renderer.
- * @private
- * @param {CSSObject} style - Style settings.
- * @return {CSSObject} The style settings mixed with defaults.
- */
- getStyle: function(style) {
- this.style = extend({
- fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font
- fontSize: '12px'
- }, style);
- return this.style;
- },
- /**
- * Apply the global style on the renderer, mixed with the default styles.
- * @param {CSSObject} style - CSS to apply.
- */
- setStyle: function(style) {
- this.boxWrapper.css(this.getStyle(style));
- },
- /**
- * Detect whether the renderer is hidden. This happens when one of the
- * parent elements has display: none. Used internally to detect when we need
- * to render preliminarily in another div to get the text bounding boxes
- * right.
- *
- * @returns {boolean} True if it is hidden.
- */
- isHidden: function() { // #608
- return !this.boxWrapper.getBBox().width;
- },
- /**
- * Destroys the renderer and its allocated members.
- */
- destroy: function() {
- var renderer = this,
- rendererDefs = renderer.defs;
- renderer.box = null;
- renderer.boxWrapper = renderer.boxWrapper.destroy();
- // Call destroy on all gradient elements
- destroyObjectProperties(renderer.gradients || {});
- renderer.gradients = null;
- // Defs are null in VMLRenderer
- // Otherwise, destroy them here.
- if (rendererDefs) {
- renderer.defs = rendererDefs.destroy();
- }
- // Remove sub pixel fix handler (#982)
- if (renderer.unSubPixelFix) {
- renderer.unSubPixelFix();
- }
- renderer.alignedObjects = null;
- return null;
- },
- /**
- * Create a wrapper for an SVG element. Serves as a factory for
- * {@link SVGElement}, but this function is itself mostly called from
- * primitive factories like {@link SVGRenderer#path}, {@link
- * SVGRenderer#rect} or {@link SVGRenderer#text}.
- *
- * @param {string} nodeName - The node name, for example `rect`, `g` etc.
- * @returns {Highcharts.SVGElement} The generated SVGElement.
- */
- createElement: function(nodeName) {
- var wrapper = new this.Element();
- wrapper.init(this, nodeName);
- return wrapper;
- },
- /**
- * Dummy function for plugins, called every time the renderer is updated.
- * Prior to Highcharts 5, this was used for the canvg renderer.
- * @function
- */
- draw: noop,
- /**
- * Get converted radial gradient attributes according to the radial
- * reference. Used internally from the {@link SVGElement#colorGradient}
- * function.
- *
- * @private
- */
- getRadialAttr: function(radialReference, gradAttr) {
- return {
- cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2],
- cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2],
- r: gradAttr.r * radialReference[2]
- };
- },
- getSpanWidth: function(wrapper, tspan) {
- var renderer = this,
- bBox = wrapper.getBBox(true),
- actualWidth = bBox.width;
- // Old IE cannot measure the actualWidth for SVG elements (#2314)
- if (!svg && renderer.forExport) {
- actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles);
- }
- return actualWidth;
- },
- applyEllipsis: function(wrapper, tspan, text, width) {
- var renderer = this,
- actualWidth = renderer.getSpanWidth(wrapper, tspan),
- wasTooLong = actualWidth > width,
- str = text,
- currentIndex,
- minIndex = 0,
- maxIndex = text.length,
- updateTSpan = function(s) {
- tspan.removeChild(tspan.firstChild);
- if (s) {
- tspan.appendChild(doc.createTextNode(s));
- }
- };
- if (wasTooLong) {
- while (minIndex <= maxIndex) {
- currentIndex = Math.ceil((minIndex + maxIndex) / 2);
- str = text.substring(0, currentIndex) + '\u2026';
- updateTSpan(str);
- actualWidth = renderer.getSpanWidth(wrapper, tspan);
- if (minIndex === maxIndex) {
- // Complete
- minIndex = maxIndex + 1;
- } else if (actualWidth > width) {
- // Too large. Set max index to current.
- maxIndex = currentIndex - 1;
- } else {
- // Within width. Set min index to current.
- minIndex = currentIndex;
- }
- }
- // If max index was 0 it means just ellipsis was also to large.
- if (maxIndex === 0) {
- // Remove ellipses.
- updateTSpan('');
- }
- }
- return wasTooLong;
- },
- /**
- * Parse a simple HTML string into SVG tspans. Called internally when text
- * is set on an SVGElement. The function supports a subset of HTML tags,
- * CSS text features like `width`, `text-overflow`, `white-space`, and
- * also attributes like `href` and `style`.
- * @private
- * @param {Highcharts.SVGElement} wrapper The parent SVGElement.
- */
- buildText: function(wrapper) {
- var textNode = wrapper.element,
- renderer = this,
- forExport = renderer.forExport,
- textStr = pick(wrapper.textStr, '').toString(),
- hasMarkup = textStr.indexOf('<') !== -1,
- lines,
- childNodes = textNode.childNodes,
- clsRegex,
- styleRegex,
- hrefRegex,
- wasTooLong,
- parentX = attr(textNode, 'x'),
- textStyles = wrapper.styles,
- width = wrapper.textWidth,
- textLineHeight = textStyles && textStyles.lineHeight,
- textOutline = textStyles && textStyles.textOutline,
- ellipsis = textStyles && textStyles.textOverflow === 'ellipsis',
- noWrap = textStyles && textStyles.whiteSpace === 'nowrap',
- fontSize = textStyles && textStyles.fontSize,
- textCache,
- isSubsequentLine,
- i = childNodes.length,
- tempParent = width && !wrapper.added && this.box,
- getLineHeight = function(tspan) {
- var fontSizeStyle;
- fontSizeStyle = /(px|em)$/.test(tspan && tspan.style.fontSize) ?
- tspan.style.fontSize :
- (fontSize || renderer.style.fontSize || 12);
- return textLineHeight ?
- pInt(textLineHeight) :
- renderer.fontMetrics(
- fontSizeStyle,
- // Get the computed size from parent if not explicit
- tspan.getAttribute('style') ? tspan : textNode
- ).h;
- },
- unescapeAngleBrackets = function(inputStr) {
- return inputStr.replace(/</g, '<').replace(/>/g, '>');
- };
- // The buildText code is quite heavy, so if we're not changing something
- // that affects the text, skip it (#6113).
- textCache = [
- textStr,
- ellipsis,
- noWrap,
- textLineHeight,
- textOutline,
- fontSize,
- width
- ].join(',');
- if (textCache === wrapper.textCache) {
- return;
- }
- wrapper.textCache = textCache;
- /// remove old text
- while (i--) {
- textNode.removeChild(childNodes[i]);
- }
- // Skip tspans, add text directly to text node. The forceTSpan is a hook
- // used in text outline hack.
- if (!hasMarkup && !textOutline && !ellipsis && !width && textStr.indexOf(' ') === -1) {
- textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr)));
- // Complex strings, add more logic
- } else {
- clsRegex = /<.*class="([^"]+)".*>/;
- styleRegex = /<.*style="([^"]+)".*>/;
- hrefRegex = /<.*href="([^"]+)".*>/;
- if (tempParent) {
- tempParent.appendChild(textNode); // attach it to the DOM to read offset width
- }
- if (hasMarkup) {
- lines = textStr
- .replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
- .replace(/<(i|em)>/g, '<span style="font-style:italic">')
- .replace(/<a/g, '<span')
- .replace(/<\/(b|strong|i|em|a)>/g, '</span>')
- .split(/<br.*?>/g);
- } else {
- lines = [textStr];
- }
- // Trim empty lines (#5261)
- lines = grep(lines, function(line) {
- return line !== '';
- });
- // build the lines
- each(lines, function buildTextLines(line, lineNo) {
- var spans,
- spanNo = 0;
- line = line
- .replace(/^\s+|\s+$/g, '') // Trim to prevent useless/costly process on the spaces (#5258)
- .replace(/<span/g, '|||<span')
- .replace(/<\/span>/g, '</span>|||');
- spans = line.split('|||');
- each(spans, function buildTextSpans(span) {
- if (span !== '' || spans.length === 1) {
- var attributes = {},
- tspan = doc.createElementNS(renderer.SVG_NS, 'tspan'),
- spanCls,
- spanStyle; // #390
- if (clsRegex.test(span)) {
- spanCls = span.match(clsRegex)[1];
- attr(tspan, 'class', spanCls);
- }
- if (styleRegex.test(span)) {
- spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2');
- attr(tspan, 'style', spanStyle);
- }
- if (hrefRegex.test(span) && !forExport) { // Not for export - #1529
- attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"');
- css(tspan, {
- cursor: 'pointer'
- });
- }
- span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' ');
- // Nested tags aren't supported, and cause crash in Safari (#1596)
- if (span !== ' ') {
- // add the text node
- tspan.appendChild(doc.createTextNode(span));
- if (!spanNo) { // first span in a line, align it to the left
- if (lineNo && parentX !== null) {
- attributes.x = parentX;
- }
- } else {
- attributes.dx = 0; // #16
- }
- // add attributes
- attr(tspan, attributes);
- // Append it
- textNode.appendChild(tspan);
- // first span on subsequent line, add the line height
- if (!spanNo && isSubsequentLine) {
- // allow getting the right offset height in exporting in IE
- if (!svg && forExport) {
- css(tspan, {
- display: 'block'
- });
- }
- // Set the line height based on the font size of either
- // the text element or the tspan element
- attr(
- tspan,
- 'dy',
- getLineHeight(tspan)
- );
- }
- /*if (width) {
- renderer.breakText(wrapper, width);
- }*/
- // Check width and apply soft breaks or ellipsis
- if (width) {
- var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273
- hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && !noWrap),
- tooLong,
- rest = [],
- actualWidth,
- dy = getLineHeight(tspan),
- rotation = wrapper.rotation;
- if (ellipsis) {
- wasTooLong = renderer.applyEllipsis(wrapper, tspan, span, width);
- }
- while (!ellipsis && hasWhiteSpace && (words.length || rest.length)) {
- wrapper.rotation = 0; // discard rotation when computing box
- actualWidth = renderer.getSpanWidth(wrapper, tspan);
- tooLong = actualWidth > width;
- // For ellipsis, do a binary search for the correct string length
- if (wasTooLong === undefined) {
- wasTooLong = tooLong; // First time
- }
- // Looping down, this is the first word sequence that is not too long,
- // so we can move on to build the next line.
- if (!tooLong || words.length === 1) {
- words = rest;
- rest = [];
- if (words.length && !noWrap) {
- tspan = doc.createElementNS(SVG_NS, 'tspan');
- attr(tspan, {
- dy: dy,
- x: parentX
- });
- if (spanStyle) { // #390
- attr(tspan, 'style', spanStyle);
- }
- textNode.appendChild(tspan);
- }
- if (actualWidth > width) { // a single word is pressing it out
- width = actualWidth;
- }
- } else { // append to existing line tspan
- tspan.removeChild(tspan.firstChild);
- rest.unshift(words.pop());
- }
- if (words.length) {
- tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
- }
- }
- wrapper.rotation = rotation;
- }
- spanNo++;
- }
- }
- });
- // To avoid beginning lines that doesn't add to the textNode (#6144)
- isSubsequentLine = isSubsequentLine || textNode.childNodes.length;
- });
- if (wasTooLong) {
- wrapper.attr('title', wrapper.textStr);
- }
- if (tempParent) {
- tempParent.removeChild(textNode); // attach it to the DOM to read offset width
- }
- // Apply the text outline
- if (textOutline && wrapper.applyTextOutline) {
- wrapper.applyTextOutline(textOutline);
- }
- }
- },
- /*
- breakText: function (wrapper, width) {
- var bBox = wrapper.getBBox(),
- node = wrapper.element,
- textLength = node.textContent.length,
- pos = Math.round(width * textLength / bBox.width), // try this position first, based on average character width
- increment = 0,
- finalPos;
- if (bBox.width > width) {
- while (finalPos === undefined) {
- textLength = node.getSubStringLength(0, pos);
- if (textLength <= width) {
- if (increment === -1) {
- finalPos = pos;
- } else {
- increment = 1;
- }
- } else {
- if (increment === 1) {
- finalPos = pos - 1;
- } else {
- increment = -1;
- }
- }
- pos += increment;
- }
- }
- console.log('width', width, 'stringWidth', node.getSubStringLength(0, finalPos))
- },
- */
- /**
- * Returns white for dark colors and black for bright colors.
- *
- * @param {ColorString} rgba - The color to get the contrast for.
- * @returns {string} The contrast color, either `#000000` or `#FFFFFF`.
- */
- getContrast: function(rgba) {
- rgba = color(rgba).rgba;
- // The threshold may be discussed. Here's a proposal for adding
- // different weight to the color channels (#6216)
- /*
- rgba[0] *= 1; // red
- rgba[1] *= 1.2; // green
- rgba[2] *= 0.7; // blue
- */
- return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF';
- },
- /**
- * Create a button with preset states.
- * @param {string} text - The text or HTML to draw.
- * @param {number} x - The x position of the button's left side.
- * @param {number} y - The y position of the button's top side.
- * @param {Function} callback - The function to execute on button click or
- * touch.
- * @param {SVGAttributes} [normalState] - SVG attributes for the normal
- * state.
- * @param {SVGAttributes} [hoverState] - SVG attributes for the hover state.
- * @param {SVGAttributes} [pressedState] - SVG attributes for the pressed
- * state.
- * @param {SVGAttributes} [disabledState] - SVG attributes for the disabled
- * state.
- * @param {Symbol} [shape=rect] - The shape type.
- * @returns {SVGRenderer} The button element.
- */
- button: function(text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) {
- var label = this.label(text, x, y, shape, null, null, null, null, 'button'),
- curState = 0;
- // Default, non-stylable attributes
- label.attr(merge({
- 'padding': 8,
- 'r': 2
- }, normalState));
- // Presentational
- var normalStyle,
- hoverStyle,
- pressedStyle,
- disabledStyle;
- // Normal state - prepare the attributes
- normalState = merge({
- fill: '#f7f7f7',
- stroke: '#cccccc',
- 'stroke-width': 1,
- style: {
- color: '#333333',
- cursor: 'pointer',
- fontWeight: 'normal'
- }
- }, normalState);
- normalStyle = normalState.style;
- delete normalState.style;
- // Hover state
- hoverState = merge(normalState, {
- fill: '#e6e6e6'
- }, hoverState);
- hoverStyle = hoverState.style;
- delete hoverState.style;
- // Pressed state
- pressedState = merge(normalState, {
- fill: '#e6ebf5',
- style: {
- color: '#000000',
- fontWeight: 'bold'
- }
- }, pressedState);
- pressedStyle = pressedState.style;
- delete pressedState.style;
- // Disabled state
- disabledState = merge(normalState, {
- style: {
- color: '#cccccc'
- }
- }, disabledState);
- disabledStyle = disabledState.style;
- delete disabledState.style;
- // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667).
- addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function() {
- if (curState !== 3) {
- label.setState(1);
- }
- });
- addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function() {
- if (curState !== 3) {
- label.setState(curState);
- }
- });
- label.setState = function(state) {
- // Hover state is temporary, don't record it
- if (state !== 1) {
- label.state = curState = state;
- }
- // Update visuals
- label.removeClass(/highcharts-button-(normal|hover|pressed|disabled)/)
- .addClass('highcharts-button-' + ['normal', 'hover', 'pressed', 'disabled'][state || 0]);
- label.attr([normalState, hoverState, pressedState, disabledState][state || 0])
- .css([normalStyle, hoverStyle, pressedStyle, disabledStyle][state || 0]);
- };
- // Presentational attributes
- label
- .attr(normalState)
- .css(extend({
- cursor: 'default'
- }, normalStyle));
- return label
- .on('click', function(e) {
- if (curState !== 3) {
- callback.call(label, e);
- }
- });
- },
- /**
- * Make a straight line crisper by not spilling out to neighbour pixels.
- *
- * @param {Array} points - The original points on the format `['M', 0, 0,
- * 'L', 100, 0]`.
- * @param {number} width - The width of the line.
- * @returns {Array} The original points array, but modified to render
- * crisply.
- */
- crispLine: function(points, width) {
- // normalize to a crisp line
- if (points[1] === points[4]) {
- // Substract due to #1129. Now bottom and left axis gridlines behave the same.
- points[1] = points[4] = Math.round(points[1]) - (width % 2 / 2);
- }
- if (points[2] === points[5]) {
- points[2] = points[5] = Math.round(points[2]) + (width % 2 / 2);
- }
- return points;
- },
- /**
- * Draw a path, wraps the SVG `path` element.
- *
- * @param {Array} [path] An SVG path definition in array form.
- *
- * @example
- * var path = renderer.path(['M', 10, 10, 'L', 30, 30, 'z'])
- * .attr({ stroke: '#ff00ff' })
- * .add();
- * @returns {Highcharts.SVGElement} The generated wrapper element.
- *
- * @sample highcharts/members/renderer-path-on-chart/
- * Draw a path in a chart
- * @sample highcharts/members/renderer-path/
- * Draw a path independent from a chart
- *
- */
- /**
- * Draw a path, wraps the SVG `path` element.
- *
- * @param {SVGAttributes} [attribs] The initial attributes.
- * @returns {Highcharts.SVGElement} The generated wrapper element.
- */
- path: function(path) {
- var attribs = {
- fill: 'none'
- };
- if (isArray(path)) {
- attribs.d = path;
- } else if (isObject(path)) { // attributes
- extend(attribs, path);
- }
- return this.createElement('path').attr(attribs);
- },
- /**
- * Draw a circle, wraps the SVG `circle` element.
- *
- * @param {number} [x] The center x position.
- * @param {number} [y] The center y position.
- * @param {number} [r] The radius.
- * @returns {Highcharts.SVGElement} The generated wrapper element.
- *
- * @sample highcharts/members/renderer-circle/ Drawing a circle
- */
- /**
- * Draw a circle, wraps the SVG `circle` element.
- *
- * @param {SVGAttributes} [attribs] The initial attributes.
- * @returns {Highcharts.SVGElement} The generated wrapper element.
- */
- circle: function(x, y, r) {
- var attribs = isObject(x) ? x : {
- x: x,
- y: y,
- r: r
- },
- wrapper = this.createElement('circle');
- // Setting x or y translates to cx and cy
- wrapper.xSetter = wrapper.ySetter = function(value, key, element) {
- element.setAttribute('c' + key, value);
- };
- return wrapper.attr(attribs);
- },
- /**
- * Draw and return an arc.
- * @param {number} [x=0] Center X position.
- * @param {number} [y=0] Center Y position.
- * @param {number} [r=0] The outer radius of the arc.
- * @param {number} [innerR=0] Inner radius like used in donut charts.
- * @param {number} [start=0] The starting angle of the arc in radians, where
- * 0 is to the right and `-Math.PI/2` is up.
- * @param {number} [end=0] The ending angle of the arc in radians, where 0
- * is to the right and `-Math.PI/2` is up.
- * @returns {Highcharts.SVGElement} The generated wrapper element.
- *
- * @sample highcharts/members/renderer-arc/
- * Drawing an arc
- */
- /**
- * Draw and return an arc. Overloaded function that takes arguments object.
- * @param {SVGAttributes} attribs Initial SVG attributes.
- * @returns {Highcharts.SVGElement} The generated wrapper element.
- */
- arc: function(x, y, r, innerR, start, end) {
- var arc,
- options;
- if (isObject(x)) {
- options = x;
- y = options.y;
- r = options.r;
- innerR = options.innerR;
- start = options.start;
- end = options.end;
- x = options.x;
- } else {
- options = {
- innerR: innerR,
- start: start,
- end: end
- };
- }
- // Arcs are defined as symbols for the ability to set
- // attributes in attr and animate
- arc = this.symbol('arc', x, y, r, r, options);
- arc.r = r; // #959
- return arc;
- },
- /**
- * Draw and return a rectangle.
- * @param {number} [x] Left position.
- * @param {number} [y] Top position.
- * @param {number} [width] Width of the rectangle.
- * @param {number} [height] Height of the rectangle.
- * @param {number} [r] Border corner radius.
- * @param {number} [strokeWidth] A stroke width can be supplied to allow
- * crisp drawing.
- * @returns {Highcharts.SVGElement} The generated wrapper element.
- */
- /**
- * Draw and return a rectangle.
- * @param {SVGAttributes} [attributes]
- * General SVG attributes for the rectangle.
- * @return {Highcharts.SVGElement}
- * The generated wrapper element.
- *
- * @sample highcharts/members/renderer-rect-on-chart/
- * Draw a rectangle in a chart
- * @sample highcharts/members/renderer-rect/
- * Draw a rectangle independent from a chart
- */
- rect: function(x, y, width, height, r, strokeWidth) {
- r = isObject(x) ? x.r : r;
- var wrapper = this.createElement('rect'),
- attribs = isObject(x) ? x : x === undefined ? {} : {
- x: x,
- y: y,
- width: Math.max(width, 0),
- height: Math.max(height, 0)
- };
- if (strokeWidth !== undefined) {
- attribs.strokeWidth = strokeWidth;
- attribs = wrapper.crisp(attribs);
- }
- attribs.fill = 'none';
- if (r) {
- attribs.r = r;
- }
- wrapper.rSetter = function(value, key, element) {
- attr(element, {
- rx: value,
- ry: value
- });
- };
- return wrapper.attr(attribs);
- },
- /**
- * Resize the {@link SVGRenderer#box} and re-align all aligned child
- * elements.
- * @param {number} width The new pixel width.
- * @param {number} height The new pixel height.
- * @param {boolean} animate Whether to animate.
- */
- setSize: function(width, height, animate) {
- var renderer = this,
- alignedObjects = renderer.alignedObjects,
- i = alignedObjects.length;
- renderer.width = width;
- renderer.height = height;
- renderer.boxWrapper.animate({
- width: width,
- height: height
- }, {
- step: function() {
- this.attr({
- viewBox: '0 0 ' + this.attr('width') + ' ' + this.attr('height')
- });
- },
- duration: pick(animate, true) ? undefined : 0
- });
- while (i--) {
- alignedObjects[i].align();
- }
- },
- /**
- * Create and return an svg group element. Child {@link Highcharts.SVGElement}
- * objects are added to the group by using the group as the first parameter
- * in {@link Highcharts.SVGElement#add|add()}.
- *
- * @param {string} [name] The group will be given a class name of
- * `highcharts-{name}`. This can be used for styling and scripting.
- * @returns {Highcharts.SVGElement} The generated wrapper element.
- *
- * @sample highcharts/members/renderer-g/
- * Show and hide grouped objects
- */
- g: function(name) {
- var elem = this.createElement('g');
- return name ? elem.attr({
- 'class': 'highcharts-' + name
- }) : elem;
- },
- /**
- * Display an image.
- * @param {string} src The image source.
- * @param {number} [x] The X position.
- * @param {number} [y] The Y position.
- * @param {number} [width] The image width. If omitted, it defaults to the
- * image file width.
- * @param {number} [height] The image height. If omitted it defaults to the
- * image file height.
- * @returns {Highcharts.SVGElement} The generated wrapper element.
- *
- * @sample highcharts/members/renderer-image-on-chart/
- * Add an image in a chart
- * @sample highcharts/members/renderer-image/
- * Add an image independent of a chart
- */
- image: function(src, x, y, width, height) {
- var attribs = {
- preserveAspectRatio: 'none'
- },
- elemWrapper;
- // optional properties
- if (arguments.length > 1) {
- extend(attribs, {
- x: x,
- y: y,
- width: width,
- height: height
- });
- }
- elemWrapper = this.createElement('image').attr(attribs);
- // set the href in the xlink namespace
- if (elemWrapper.element.setAttributeNS) {
- elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
- 'href', src);
- } else {
- // could be exporting in IE
- // using href throws "not supported" in ie7 and under, requries regex shim to fix later
- elemWrapper.element.setAttribute('hc-svg-href', src);
- }
- return elemWrapper;
- },
- /**
- * Draw a symbol out of pre-defined shape paths from {@SVGRenderer#symbols}.
- * It is used in Highcharts for point makers, which cake a `symbol` option,
- * and label and button backgrounds like in the tooltip and stock flags.
- *
- * @param {Symbol} symbol - The symbol name.
- * @param {number} x - The X coordinate for the top left position.
- * @param {number} y - The Y coordinate for the top left position.
- * @param {number} width - The pixel width.
- * @param {number} height - The pixel height.
- * @param {Object} [options] - Additional options, depending on the actual
- * symbol drawn.
- * @param {number} [options.anchorX] - The anchor X position for the
- * `callout` symbol. This is where the chevron points to.
- * @param {number} [options.anchorY] - The anchor Y position for the
- * `callout` symbol. This is where the chevron points to.
- * @param {number} [options.end] - The end angle of an `arc` symbol.
- * @param {boolean} [options.open] - Whether to draw `arc` symbol open or
- * closed.
- * @param {number} [options.r] - The radius of an `arc` symbol, or the
- * border radius for the `callout` symbol.
- * @param {number} [options.start] - The start angle of an `arc` symbol.
- */
- symbol: function(symbol, x, y, width, height, options) {
- var ren = this,
- obj,
- imageRegex = /^url\((.*?)\)$/,
- isImage = imageRegex.test(symbol),
- sym = !isImage && (this.symbols[symbol] ? symbol : 'circle'),
- // get the symbol definition function
- symbolFn = sym && this.symbols[sym],
- // check if there's a path defined for this symbol
- path = defined(x) && symbolFn && symbolFn.call(
- this.symbols,
- Math.round(x),
- Math.round(y),
- width,
- height,
- options
- ),
- imageSrc,
- centerImage;
- if (symbolFn) {
- obj = this.path(path);
- obj.attr('fill', 'none');
- // expando properties for use in animate and attr
- extend(obj, {
- symbolName: sym,
- x: x,
- y: y,
- width: width,
- height: height
- });
- if (options) {
- extend(obj, options);
- }
- // Image symbols
- } else if (isImage) {
- imageSrc = symbol.match(imageRegex)[1];
- // Create the image synchronously, add attribs async
- obj = this.image(imageSrc);
- // The image width is not always the same as the symbol width. The
- // image may be centered within the symbol, as is the case when
- // image shapes are used as label backgrounds, for example in flags.
- obj.imgwidth = pick(
- symbolSizes[imageSrc] && symbolSizes[imageSrc].width,
- options && options.width
- );
- obj.imgheight = pick(
- symbolSizes[imageSrc] && symbolSizes[imageSrc].height,
- options && options.height
- );
- /**
- * Set the size and position
- */
- centerImage = function() {
- obj.attr({
- width: obj.width,
- height: obj.height
- });
- };
- /**
- * Width and height setters that take both the image's physical size
- * and the label size into consideration, and translates the image
- * to center within the label.
- */
- each(['width', 'height'], function(key) {
- obj[key + 'Setter'] = function(value, key) {
- var attribs = {},
- imgSize = this['img' + key],
- trans = key === 'width' ? 'translateX' : 'translateY';
- this[key] = value;
- if (defined(imgSize)) {
- if (this.element) {
- this.element.setAttribute(key, imgSize);
- }
- if (!this.alignByTranslate) {
- attribs[trans] = ((this[key] || 0) - imgSize) / 2;
- this.attr(attribs);
- }
- }
- };
- });
- if (defined(x)) {
- obj.attr({
- x: x,
- y: y
- });
- }
- obj.isImg = true;
- if (defined(obj.imgwidth) && defined(obj.imgheight)) {
- centerImage();
- } else {
- // Initialize image to be 0 size so export will still function if there's no cached sizes.
- obj.attr({
- width: 0,
- height: 0
- });
- // Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8,
- // the created element must be assigned to a variable in order to load (#292).
- createElement('img', {
- onload: function() {
- var chart = charts[ren.chartIndex];
- // Special case for SVGs on IE11, the width is not accessible until the image is
- // part of the DOM (#2854).
- if (this.width === 0) {
- css(this, {
- position: 'absolute',
- top: '-999em'
- });
- doc.body.appendChild(this);
- }
- // Center the image
- symbolSizes[imageSrc] = { // Cache for next
- width: this.width,
- height: this.height
- };
- obj.imgwidth = this.width;
- obj.imgheight = this.height;
- if (obj.element) {
- centerImage();
- }
- // Clean up after #2854 workaround.
- if (this.parentNode) {
- this.parentNode.removeChild(this);
- }
- // Fire the load event when all external images are loaded
- ren.imgCount--;
- if (!ren.imgCount && chart && chart.onload) {
- chart.onload();
- }
- },
- src: imageSrc
- });
- this.imgCount++;
- }
- }
- return obj;
- },
- /**
- * @typedef {string} Symbol
- *
- * Can be one of `arc`, `callout`, `circle`, `diamond`, `square`,
- * `triangle`, `triangle-down`. Symbols are used internally for point
- * markers, button and label borders and backgrounds, or custom shapes.
- * Extendable by adding to {@link SVGRenderer#symbols}.
- */
- /**
- * An extendable collection of functions for defining symbol paths.
- */
- symbols: {
- 'circle': function(x, y, w, h) {
- // Return a full arc
- return this.arc(x + w / 2, y + h / 2, w / 2, h / 2, {
- start: 0,
- end: Math.PI * 2,
- open: false
- });
- },
- 'square': function(x, y, w, h) {
- return [
- 'M', x, y,
- 'L', x + w, y,
- x + w, y + h,
- x, y + h,
- 'Z'
- ];
- },
- 'triangle': function(x, y, w, h) {
- return [
- 'M', x + w / 2, y,
- 'L', x + w, y + h,
- x, y + h,
- 'Z'
- ];
- },
- 'triangle-down': function(x, y, w, h) {
- return [
- 'M', x, y,
- 'L', x + w, y,
- x + w / 2, y + h,
- 'Z'
- ];
- },
- 'diamond': function(x, y, w, h) {
- return [
- 'M', x + w / 2, y,
- 'L', x + w, y + h / 2,
- x + w / 2, y + h,
- x, y + h / 2,
- 'Z'
- ];
- },
- 'arc': function(x, y, w, h, options) {
- var start = options.start,
- rx = options.r || w,
- ry = options.r || h || w,
- end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561)
- innerRadius = options.innerR,
- open = options.open,
- cosStart = Math.cos(start),
- sinStart = Math.sin(start),
- cosEnd = Math.cos(end),
- sinEnd = Math.sin(end),
- longArc = options.end - start < Math.PI ? 0 : 1,
- arc;
- arc = [
- 'M',
- x + rx * cosStart,
- y + ry * sinStart,
- 'A', // arcTo
- rx, // x radius
- ry, // y radius
- 0, // slanting
- longArc, // long or short arc
- 1, // clockwise
- x + rx * cosEnd,
- y + ry * sinEnd
- ];
- if (defined(innerRadius)) {
- arc.push(
- open ? 'M' : 'L',
- x + innerRadius * cosEnd,
- y + innerRadius * sinEnd,
- 'A', // arcTo
- innerRadius, // x radius
- innerRadius, // y radius
- 0, // slanting
- longArc, // long or short arc
- 0, // clockwise
- x + innerRadius * cosStart,
- y + innerRadius * sinStart
- );
- }
- arc.push(open ? '' : 'Z'); // close
- return arc;
- },
- /**
- * Callout shape used for default tooltips, also used for rounded rectangles in VML
- */
- callout: function(x, y, w, h, options) {
- var arrowLength = 6,
- halfDistance = 6,
- r = Math.min((options && options.r) || 0, w, h),
- safeDistance = r + halfDistance,
- anchorX = options && options.anchorX,
- anchorY = options && options.anchorY,
- path;
- path = [
- 'M', x + r, y,
- 'L', x + w - r, y, // top side
- 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner
- 'L', x + w, y + h - r, // right side
- 'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-right corner
- 'L', x + r, y + h, // bottom side
- 'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner
- 'L', x, y + r, // left side
- 'C', x, y, x, y, x + r, y // top-left corner
- ];
- // Anchor on right side
- if (anchorX && anchorX > w) {
- // Chevron
- if (anchorY > y + safeDistance && anchorY < y + h - safeDistance) {
- path.splice(13, 3,
- 'L', x + w, anchorY - halfDistance,
- x + w + arrowLength, anchorY,
- x + w, anchorY + halfDistance,
- x + w, y + h - r
- );
- // Simple connector
- } else {
- path.splice(13, 3,
- 'L', x + w, h / 2,
- anchorX, anchorY,
- x + w, h / 2,
- x + w, y + h - r
- );
- }
- // Anchor on left side
- } else if (anchorX && anchorX < 0) {
- // Chevron
- if (anchorY > y + safeDistance && anchorY < y + h - safeDistance) {
- path.splice(33, 3,
- 'L', x, anchorY + halfDistance,
- x - arrowLength, anchorY,
- x, anchorY - halfDistance,
- x, y + r
- );
- // Simple connector
- } else {
- path.splice(33, 3,
- 'L', x, h / 2,
- anchorX, anchorY,
- x, h / 2,
- x, y + r
- );
- }
- } else if (anchorY && anchorY > h && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace bottom
- path.splice(23, 3,
- 'L', anchorX + halfDistance, y + h,
- anchorX, y + h + arrowLength,
- anchorX - halfDistance, y + h,
- x + r, y + h
- );
- } else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top
- path.splice(3, 3,
- 'L', anchorX - halfDistance, y,
- anchorX, y - arrowLength,
- anchorX + halfDistance, y,
- w - r, y
- );
- }
- return path;
- }
- },
- /**
- * @typedef {Highcharts.SVGElement} ClipRect - A clipping rectangle that can be applied
- * to one or more {@link SVGElement} instances. It is instanciated with the
- * {@link SVGRenderer#clipRect} function and applied with the {@link
- * SVGElement#clip} function.
- *
- * @example
- * var circle = renderer.circle(100, 100, 100)
- * .attr({ fill: 'red' })
- * .add();
- * var clipRect = renderer.clipRect(100, 100, 100, 100);
- *
- * // Leave only the lower right quarter visible
- * circle.clip(clipRect);
- */
- /**
- * Define a clipping rectangle
- * @param {String} id
- * @param {number} x
- * @param {number} y
- * @param {number} width
- * @param {number} height
- * @returns {ClipRect} A clipping rectangle.
- */
- clipRect: function(x, y, width, height) {
- var wrapper,
- id = H.uniqueKey(),
- clipPath = this.createElement('clipPath').attr({
- id: id
- }).add(this.defs);
- wrapper = this.rect(x, y, width, height, 0).add(clipPath);
- wrapper.id = id;
- wrapper.clipPath = clipPath;
- wrapper.count = 0;
- return wrapper;
- },
- /**
- * Draw text. The text can contain a subset of HTML, like spans and anchors
- * and some basic text styling of these. For more advanced features like
- * border and background, use {@link Highcharts.SVGRenderer#label} instead.
- * To update the text after render, run `text.attr({ text: 'New text' })`.
- * @param {String} str
- * The text of (subset) HTML to draw.
- * @param {number} x
- * The x position of the text's lower left corner.
- * @param {number} y
- * The y position of the text's lower left corner.
- * @param {Boolean} [useHTML=false]
- * Use HTML to render the text.
- *
- * @return {Highcharts.SVGElement} The text object.
- *
- * @sample highcharts/members/renderer-text-on-chart/
- * Annotate the chart freely
- * @sample highcharts/members/renderer-on-chart/
- * Annotate with a border and in response to the data
- * @sample highcharts/members/renderer-text/
- * Formatted text
- */
- text: function(str, x, y, useHTML) {
- // declare variables
- var renderer = this,
- fakeSVG = !svg && renderer.forExport,
- wrapper,
- attribs = {};
- if (useHTML && (renderer.allowHTML || !renderer.forExport)) {
- return renderer.html(str, x, y);
- }
- attribs.x = Math.round(x || 0); // X is always needed for line-wrap logic
- if (y) {
- attribs.y = Math.round(y);
- }
- if (str || str === 0) {
- attribs.text = str;
- }
- wrapper = renderer.createElement('text')
- .attr(attribs);
- // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063)
- if (fakeSVG) {
- wrapper.css({
- position: 'absolute'
- });
- }
- if (!useHTML) {
- wrapper.xSetter = function(value, key, element) {
- var tspans = element.getElementsByTagName('tspan'),
- tspan,
- parentVal = element.getAttribute(key),
- i;
- for (i = 0; i < tspans.length; i++) {
- tspan = tspans[i];
- // If the x values are equal, the tspan represents a linebreak
- if (tspan.getAttribute(key) === parentVal) {
- tspan.setAttribute(key, value);
- }
- }
- element.setAttribute(key, value);
- };
- }
- return wrapper;
- },
- /**
- * Utility to return the baseline offset and total line height from the font
- * size.
- *
- * @param {?string} fontSize The current font size to inspect. If not given,
- * the font size will be found from the DOM element.
- * @param {SVGElement|SVGDOMElement} [elem] The element to inspect for a
- * current font size.
- * @returns {Object} An object containing `h`: the line height, `b`: the
- * baseline relative to the top of the box, and `f`: the font size.
- */
- fontMetrics: function(fontSize, elem) {
- var lineHeight,
- baseline;
- fontSize = fontSize ||
- // When the elem is a DOM element (#5932)
- (elem && elem.style && elem.style.fontSize) ||
- // Fall back on the renderer style default
- (this.style && this.style.fontSize);
- // Handle different units
- if (/px/.test(fontSize)) {
- fontSize = pInt(fontSize);
- } else if (/em/.test(fontSize)) {
- // The em unit depends on parent items
- fontSize = parseFloat(fontSize) *
- (elem ? this.fontMetrics(null, elem.parentNode).f : 16);
- } else {
- fontSize = 12;
- }
- // Empirical values found by comparing font size and bounding box
- // height. Applies to the default font family.
- // http://jsfiddle.net/highcharts/7xvn7/
- lineHeight = fontSize < 24 ? fontSize + 3 : Math.round(fontSize * 1.2);
- baseline = Math.round(lineHeight * 0.8);
- return {
- h: lineHeight,
- b: baseline,
- f: fontSize
- };
- },
- /**
- * Correct X and Y positioning of a label for rotation (#1764)
- */
- rotCorr: function(baseline, rotation, alterY) {
- var y = baseline;
- if (rotation && alterY) {
- y = Math.max(y * Math.cos(rotation * deg2rad), 4);
- }
- return {
- x: (-baseline / 3) * Math.sin(rotation * deg2rad),
- y: y
- };
- },
- /**
- * Draw a label, which is an extended text element with support for border
- * and background. Highcharts creates a `g` element with a text and a `path`
- * or `rect` inside, to make it behave somewhat like a HTML div. Border and
- * background are set through `stroke`, `stroke-width` and `fill` attributes
- * using the {@link Highcharts.SVGElement#attr|attr} method. To update the
- * text after render, run `label.attr({ text: 'New text' })`.
- *
- * @param {string} str
- * The initial text string or (subset) HTML to render.
- * @param {number} x
- * The x position of the label's left side.
- * @param {number} y
- * The y position of the label's top side or baseline, depending on
- * the `baseline` parameter.
- * @param {String} shape
- * The shape of the label's border/background, if any. Defaults to
- * `rect`. Other possible values are `callout` or other shapes
- * defined in {@link Highcharts.SVGRenderer#symbols}.
- * @param {number} anchorX
- * In case the `shape` has a pointer, like a flag, this is the
- * coordinates it should be pinned to.
- * @param {number} anchorY
- * In case the `shape` has a pointer, like a flag, this is the
- * coordinates it should be pinned to.
- * @param {Boolean} baseline
- * Whether to position the label relative to the text baseline,
- * like {@link Highcharts.SVGRenderer#text|renderer.text}, or to the
- * upper border of the rectangle.
- * @param {String} className
- * Class name for the group.
- *
- * @return {Highcharts.SVGElement}
- * The generated label.
- *
- * @sample highcharts/members/renderer-label-on-chart/
- * A label on the chart
- */
- label: function(str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
- var renderer = this,
- wrapper = renderer.g(className !== 'button' && 'label'),
- text = wrapper.text = renderer.text('', 0, 0, useHTML)
- .attr({
- zIndex: 1
- }),
- box,
- bBox,
- alignFactor = 0,
- padding = 3,
- paddingLeft = 0,
- width,
- height,
- wrapperX,
- wrapperY,
- textAlign,
- deferredAttr = {},
- strokeWidth,
- baselineOffset,
- hasBGImage = /^url\((.*?)\)$/.test(shape),
- needsBox = hasBGImage,
- getCrispAdjust,
- updateBoxSize,
- updateTextPadding,
- boxAttr;
- if (className) {
- wrapper.addClass('highcharts-' + className);
- }
- needsBox = hasBGImage;
- getCrispAdjust = function() {
- return (strokeWidth || 0) % 2 / 2;
- };
- /**
- * This function runs after the label is added to the DOM (when the bounding box is
- * available), and after the text of the label is updated to detect the new bounding
- * box and reflect it in the border box.
- */
- updateBoxSize = function() {
- var style = text.element.style,
- crispAdjust,
- attribs = {};
- bBox = (width === undefined || height === undefined || textAlign) && defined(text.textStr) &&
- text.getBBox(); //#3295 && 3514 box failure when string equals 0
- wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft;
- wrapper.height = (height || bBox.height || 0) + 2 * padding;
- // Update the label-scoped y offset
- baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b;
- if (needsBox) {
- // Create the border box if it is not already present
- if (!box) {
- wrapper.box = box = renderer.symbols[shape] || hasBGImage ? // Symbol definition exists (#5324)
- renderer.symbol(shape) :
- renderer.rect();
- box.addClass(
- (className === 'button' ? '' : 'highcharts-label-box') + // Don't use label className for buttons
- (className ? ' highcharts-' + className + '-box' : '')
- );
- box.add(wrapper);
- crispAdjust = getCrispAdjust();
- attribs.x = crispAdjust;
- attribs.y = (baseline ? -baselineOffset : 0) + crispAdjust;
- }
- // Apply the box attributes
- attribs.width = Math.round(wrapper.width);
- attribs.height = Math.round(wrapper.height);
- box.attr(extend(attribs, deferredAttr));
- deferredAttr = {};
- }
- };
- /**
- * This function runs after setting text or padding, but only if padding is changed
- */
- updateTextPadding = function() {
- var textX = paddingLeft + padding,
- textY;
- // determin y based on the baseline
- textY = baseline ? 0 : baselineOffset;
- // compensate for alignment
- if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) {
- textX += {
- center: 0.5,
- right: 1
- }[textAlign] * (width - bBox.width);
- }
- // update if anything changed
- if (textX !== text.x || textY !== text.y) {
- text.attr('x', textX);
- if (textY !== undefined) {
- text.attr('y', textY);
- }
- }
- // record current values
- text.x = textX;
- text.y = textY;
- };
- /**
- * Set a box attribute, or defer it if the box is not yet created
- * @param {Object} key
- * @param {Object} value
- */
- boxAttr = function(key, value) {
- if (box) {
- box.attr(key, value);
- } else {
- deferredAttr[key] = value;
- }
- };
- /**
- * After the text element is added, get the desired size of the border box
- * and add it before the text in the DOM.
- */
- wrapper.onAdd = function() {
- text.add(wrapper);
- wrapper.attr({
- text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value
- x: x,
- y: y
- });
- if (box && defined(anchorX)) {
- wrapper.attr({
- anchorX: anchorX,
- anchorY: anchorY
- });
- }
- };
- /*
- * Add specific attribute setters.
- */
- // only change local variables
- wrapper.widthSetter = function(value) {
- width = H.isNumber(value) ? value : null; // width:auto => null
- };
- wrapper.heightSetter = function(value) {
- height = value;
- };
- wrapper['text-alignSetter'] = function(value) {
- textAlign = value;
- };
- wrapper.paddingSetter = function(value) {
- if (defined(value) && value !== padding) {
- padding = wrapper.padding = value;
- updateTextPadding();
- }
- };
- wrapper.paddingLeftSetter = function(value) {
- if (defined(value) && value !== paddingLeft) {
- paddingLeft = value;
- updateTextPadding();
- }
- };
- // change local variable and prevent setting attribute on the group
- wrapper.alignSetter = function(value) {
- value = {
- left: 0,
- center: 0.5,
- right: 1
- }[value];
- if (value !== alignFactor) {
- alignFactor = value;
- if (bBox) { // Bounding box exists, means we're dynamically changing
- wrapper.attr({
- x: wrapperX
- }); // #5134
- }
- }
- };
- // apply these to the box and the text alike
- wrapper.textSetter = function(value) {
- if (value !== undefined) {
- text.textSetter(value);
- }
- updateBoxSize();
- updateTextPadding();
- };
- // apply these to the box but not to the text
- wrapper['stroke-widthSetter'] = function(value, key) {
- if (value) {
- needsBox = true;
- }
- strokeWidth = this['stroke-width'] = value;
- boxAttr(key, value);
- };
- wrapper.strokeSetter = wrapper.fillSetter = wrapper.rSetter = function(value, key) {
- if (key === 'fill' && value) {
- needsBox = true;
- }
- boxAttr(key, value);
- };
- wrapper.anchorXSetter = function(value, key) {
- anchorX = wrapper.anchorX = value;
- boxAttr(key, Math.round(value) - getCrispAdjust() - wrapperX);
- };
- wrapper.anchorYSetter = function(value, key) {
- anchorY = wrapper.anchorY = value;
- boxAttr(key, value - wrapperY);
- };
- // rename attributes
- wrapper.xSetter = function(value) {
- wrapper.x = value; // for animation getter
- if (alignFactor) {
- value -= alignFactor * ((width || bBox.width) + 2 * padding);
- }
- wrapperX = Math.round(value);
- wrapper.attr('translateX', wrapperX);
- };
- wrapper.ySetter = function(value) {
- wrapperY = wrapper.y = Math.round(value);
- wrapper.attr('translateY', wrapperY);
- };
- // Redirect certain methods to either the box or the text
- var baseCss = wrapper.css;
- return extend(wrapper, {
- /**
- * Pick up some properties and apply them to the text instead of the
- * wrapper.
- * @ignore
- */
- css: function(styles) {
- if (styles) {
- var textStyles = {};
- styles = merge(styles); // create a copy to avoid altering the original object (#537)
- each(wrapper.textProps, function(prop) {
- if (styles[prop] !== undefined) {
- textStyles[prop] = styles[prop];
- delete styles[prop];
- }
- });
- text.css(textStyles);
- }
- return baseCss.call(wrapper, styles);
- },
- /**
- * Return the bounding box of the box, not the group.
- * @ignore
- */
- getBBox: function() {
- return {
- width: bBox.width + 2 * padding,
- height: bBox.height + 2 * padding,
- x: bBox.x - padding,
- y: bBox.y - padding
- };
- },
- /**
- * Apply the shadow to the box.
- * @ignore
- */
- shadow: function(b) {
- if (b) {
- updateBoxSize();
- if (box) {
- box.shadow(b);
- }
- }
- return wrapper;
- },
- /**
- * Destroy and release memory.
- * @ignore
- */
- destroy: function() {
- // Added by button implementation
- removeEvent(wrapper.element, 'mouseenter');
- removeEvent(wrapper.element, 'mouseleave');
- if (text) {
- text = text.destroy();
- }
- if (box) {
- box = box.destroy();
- }
- // Call base implementation to destroy the rest
- SVGElement.prototype.destroy.call(wrapper);
- // Release local pointers (#1298)
- wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null;
- }
- });
- }
- }); // end SVGRenderer
- // general renderer
- H.Renderer = SVGRenderer;
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var attr = H.attr,
- createElement = H.createElement,
- css = H.css,
- defined = H.defined,
- each = H.each,
- extend = H.extend,
- isFirefox = H.isFirefox,
- isMS = H.isMS,
- isWebKit = H.isWebKit,
- pInt = H.pInt,
- SVGElement = H.SVGElement,
- SVGRenderer = H.SVGRenderer,
- win = H.win,
- wrap = H.wrap;
- // Extend SvgElement for useHTML option
- extend(SVGElement.prototype, /** @lends SVGElement.prototype */ {
- /**
- * Apply CSS to HTML elements. This is used in text within SVG rendering and
- * by the VML renderer
- */
- htmlCss: function(styles) {
- var wrapper = this,
- element = wrapper.element,
- textWidth = styles && element.tagName === 'SPAN' && styles.width;
- if (textWidth) {
- delete styles.width;
- wrapper.textWidth = textWidth;
- wrapper.updateTransform();
- }
- if (styles && styles.textOverflow === 'ellipsis') {
- styles.whiteSpace = 'nowrap';
- styles.overflow = 'hidden';
- }
- wrapper.styles = extend(wrapper.styles, styles);
- css(wrapper.element, styles);
- return wrapper;
- },
- /**
- * VML and useHTML method for calculating the bounding box based on offsets
- * @param {Boolean} refresh Whether to force a fresh value from the DOM or to
- * use the cached value
- *
- * @return {Object} A hash containing values for x, y, width and height
- */
- htmlGetBBox: function() {
- var wrapper = this,
- element = wrapper.element;
- // faking getBBox in exported SVG in legacy IE
- // faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?)
- if (element.nodeName === 'text') {
- element.style.position = 'absolute';
- }
- return {
- x: element.offsetLeft,
- y: element.offsetTop,
- width: element.offsetWidth,
- height: element.offsetHeight
- };
- },
- /**
- * VML override private method to update elements based on internal
- * properties based on SVG transform
- */
- htmlUpdateTransform: function() {
- // aligning non added elements is expensive
- if (!this.added) {
- this.alignOnAdd = true;
- return;
- }
- var wrapper = this,
- renderer = wrapper.renderer,
- elem = wrapper.element,
- translateX = wrapper.translateX || 0,
- translateY = wrapper.translateY || 0,
- x = wrapper.x || 0,
- y = wrapper.y || 0,
- align = wrapper.textAlign || 'left',
- alignCorrection = {
- left: 0,
- center: 0.5,
- right: 1
- }[align],
- styles = wrapper.styles;
- // apply translate
- css(elem, {
- marginLeft: translateX,
- marginTop: translateY
- });
- if (wrapper.shadows) { // used in labels/tooltip
- each(wrapper.shadows, function(shadow) {
- css(shadow, {
- marginLeft: translateX + 1,
- marginTop: translateY + 1
- });
- });
- }
- // apply inversion
- if (wrapper.inverted) { // wrapper is a group
- each(elem.childNodes, function(child) {
- renderer.invertChild(child, elem);
- });
- }
- if (elem.tagName === 'SPAN') {
- var rotation = wrapper.rotation,
- baseline,
- textWidth = pInt(wrapper.textWidth),
- whiteSpace = styles && styles.whiteSpace,
- currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(',');
- if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
- baseline = renderer.fontMetrics(elem.style.fontSize).b;
- // Renderer specific handling of span rotation
- if (defined(rotation)) {
- wrapper.setSpanRotation(rotation, alignCorrection, baseline);
- }
- // Reset multiline/ellipsis in order to read width (#4928, #5417)
- css(elem, {
- width: '',
- whiteSpace: whiteSpace || 'nowrap'
- });
- // Update textWidth
- if (elem.offsetWidth > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254
- css(elem, {
- width: textWidth + 'px',
- display: 'block',
- whiteSpace: whiteSpace || 'normal' // #3331
- });
- }
- wrapper.getSpanCorrection(elem.offsetWidth, baseline, alignCorrection, rotation, align);
- }
- // apply position with correction
- css(elem, {
- left: (x + (wrapper.xCorr || 0)) + 'px',
- top: (y + (wrapper.yCorr || 0)) + 'px'
- });
- // force reflow in webkit to apply the left and top on useHTML element (#1249)
- if (isWebKit) {
- baseline = elem.offsetHeight; // assigned to baseline for lint purpose
- }
- // record current text transform
- wrapper.cTT = currentTextTransform;
- }
- },
- /**
- * Set the rotation of an individual HTML span
- */
- setSpanRotation: function(rotation, alignCorrection, baseline) {
- var rotationStyle = {},
- cssTransformKey = isMS ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : win.opera ? '-o-transform' : '';
- rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)';
- rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px';
- css(this.element, rotationStyle);
- },
- /**
- * Get the correction in X and Y positioning as the element is rotated.
- */
- getSpanCorrection: function(width, baseline, alignCorrection) {
- this.xCorr = -width * alignCorrection;
- this.yCorr = -baseline;
- }
- });
- // Extend SvgRenderer for useHTML option.
- extend(SVGRenderer.prototype, /** @lends SVGRenderer.prototype */ {
- /**
- * Create HTML text node. This is used by the VML renderer as well as the SVG
- * renderer through the useHTML option.
- *
- * @param {String} str
- * @param {Number} x
- * @param {Number} y
- */
- html: function(str, x, y) {
- var wrapper = this.createElement('span'),
- element = wrapper.element,
- renderer = wrapper.renderer,
- isSVG = renderer.isSVG,
- addSetters = function(element, style) {
- // These properties are set as attributes on the SVG group, and as
- // identical CSS properties on the div. (#3542)
- each(['opacity', 'visibility'], function(prop) {
- wrap(element, prop + 'Setter', function(proceed, value, key, elem) {
- proceed.call(this, value, key, elem);
- style[key] = value;
- });
- });
- };
- // Text setter
- wrapper.textSetter = function(value) {
- if (value !== element.innerHTML) {
- delete this.bBox;
- }
- element.innerHTML = this.textStr = value;
- wrapper.htmlUpdateTransform();
- };
- // Add setters for the element itself (#4938)
- if (isSVG) { // #4938, only for HTML within SVG
- addSetters(wrapper, wrapper.element.style);
- }
- // Various setters which rely on update transform
- wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function(value, key) {
- if (key === 'align') {
- key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML.
- }
- wrapper[key] = value;
- wrapper.htmlUpdateTransform();
- };
- // Set the default attributes
- wrapper
- .attr({
- text: str,
- x: Math.round(x),
- y: Math.round(y)
- })
- .css({
- fontFamily: this.style.fontFamily,
- fontSize: this.style.fontSize,
- position: 'absolute'
- });
- // Keep the whiteSpace style outside the wrapper.styles collection
- element.style.whiteSpace = 'nowrap';
- // Use the HTML specific .css method
- wrapper.css = wrapper.htmlCss;
- // This is specific for HTML within SVG
- if (isSVG) {
- wrapper.add = function(svgGroupWrapper) {
- var htmlGroup,
- container = renderer.box.parentNode,
- parentGroup,
- parents = [];
- this.parentGroup = svgGroupWrapper;
- // Create a mock group to hold the HTML elements
- if (svgGroupWrapper) {
- htmlGroup = svgGroupWrapper.div;
- if (!htmlGroup) {
- // Read the parent chain into an array and read from top down
- parentGroup = svgGroupWrapper;
- while (parentGroup) {
- parents.push(parentGroup);
- // Move up to the next parent group
- parentGroup = parentGroup.parentGroup;
- }
- // Ensure dynamically updating position when any parent is translated
- each(parents.reverse(), function(parentGroup) {
- var htmlGroupStyle,
- cls = attr(parentGroup.element, 'class');
- if (cls) {
- cls = {
- className: cls
- };
- } // else null
- // Create a HTML div and append it to the parent div to emulate
- // the SVG group structure
- htmlGroup = parentGroup.div = parentGroup.div || createElement('div', cls, {
- position: 'absolute',
- left: (parentGroup.translateX || 0) + 'px',
- top: (parentGroup.translateY || 0) + 'px',
- display: parentGroup.display,
- opacity: parentGroup.opacity, // #5075
- pointerEvents: parentGroup.styles && parentGroup.styles.pointerEvents // #5595
- }, htmlGroup || container); // the top group is appended to container
- // Shortcut
- htmlGroupStyle = htmlGroup.style;
- // Set listeners to update the HTML div's position whenever the SVG group
- // position is changed
- extend(parentGroup, {
- on: function() {
- wrapper.on.apply({
- element: parents[0].div
- }, arguments);
- return parentGroup;
- },
- translateXSetter: function(value, key) {
- htmlGroupStyle.left = value + 'px';
- parentGroup[key] = value;
- parentGroup.doTransform = true;
- },
- translateYSetter: function(value, key) {
- htmlGroupStyle.top = value + 'px';
- parentGroup[key] = value;
- parentGroup.doTransform = true;
- }
- });
- addSetters(parentGroup, htmlGroupStyle);
- });
- }
- } else {
- htmlGroup = container;
- }
- htmlGroup.appendChild(element);
- // Shared with VML:
- wrapper.added = true;
- if (wrapper.alignOnAdd) {
- wrapper.htmlUpdateTransform();
- }
- return wrapper;
- };
- }
- return wrapper;
- }
- });
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var VMLRenderer,
- VMLRendererExtension,
- VMLElement,
- createElement = H.createElement,
- css = H.css,
- defined = H.defined,
- deg2rad = H.deg2rad,
- discardElement = H.discardElement,
- doc = H.doc,
- each = H.each,
- erase = H.erase,
- extend = H.extend,
- extendClass = H.extendClass,
- isArray = H.isArray,
- isNumber = H.isNumber,
- isObject = H.isObject,
- merge = H.merge,
- noop = H.noop,
- pick = H.pick,
- pInt = H.pInt,
- svg = H.svg,
- SVGElement = H.SVGElement,
- SVGRenderer = H.SVGRenderer,
- win = H.win;
- /* ****************************************************************************
- * *
- * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
- * *
- * For applications and websites that don't need IE support, like platform *
- * targeted mobile apps and web apps, this code can be removed. *
- * *
- *****************************************************************************/
- /**
- * @constructor
- */
- if (!svg) {
- /**
- * The VML element wrapper.
- */
- VMLElement = {
- docMode8: doc && doc.documentMode === 8,
- /**
- * Initialize a new VML element wrapper. It builds the markup as a string
- * to minimize DOM traffic.
- * @param {Object} renderer
- * @param {Object} nodeName
- */
- init: function(renderer, nodeName) {
- var wrapper = this,
- markup = ['<', nodeName, ' filled="f" stroked="f"'],
- style = ['position: ', 'absolute', ';'],
- isDiv = nodeName === 'div';
- // divs and shapes need size
- if (nodeName === 'shape' || isDiv) {
- style.push('left:0;top:0;width:1px;height:1px;');
- }
- style.push('visibility: ', isDiv ? 'hidden' : 'visible');
- markup.push(' style="', style.join(''), '"/>');
- // create element with default attributes and style
- if (nodeName) {
- markup = isDiv || nodeName === 'span' || nodeName === 'img' ?
- markup.join('') :
- renderer.prepVML(markup);
- wrapper.element = createElement(markup);
- }
- wrapper.renderer = renderer;
- },
- /**
- * Add the node to the given parent
- * @param {Object} parent
- */
- add: function(parent) {
- var wrapper = this,
- renderer = wrapper.renderer,
- element = wrapper.element,
- box = renderer.box,
- inverted = parent && parent.inverted,
- // get the parent node
- parentNode = parent ?
- parent.element || parent :
- box;
- if (parent) {
- this.parentGroup = parent;
- }
- // if the parent group is inverted, apply inversion on all children
- if (inverted) { // only on groups
- renderer.invertChild(element, parentNode);
- }
- // append it
- parentNode.appendChild(element);
- // align text after adding to be able to read offset
- wrapper.added = true;
- if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
- wrapper.updateTransform();
- }
- // fire an event for internal hooks
- if (wrapper.onAdd) {
- wrapper.onAdd();
- }
- // IE8 Standards can't set the class name before the element is appended
- if (this.className) {
- this.attr('class', this.className);
- }
- return wrapper;
- },
- /**
- * VML always uses htmlUpdateTransform
- */
- updateTransform: SVGElement.prototype.htmlUpdateTransform,
- /**
- * Set the rotation of a span with oldIE's filter
- */
- setSpanRotation: function() {
- // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented
- // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+
- // has support for CSS3 transform. The getBBox method also needs to be updated
- // to compensate for the rotation, like it currently does for SVG.
- // Test case: http://jsfiddle.net/highcharts/Ybt44/
- var rotation = this.rotation,
- costheta = Math.cos(rotation * deg2rad),
- sintheta = Math.sin(rotation * deg2rad);
- css(this.element, {
- filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
- ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
- ', sizingMethod=\'auto expand\')'
- ].join('') : 'none'
- });
- },
- /**
- * Get the positioning correction for the span after rotating.
- */
- getSpanCorrection: function(width, baseline, alignCorrection, rotation, align) {
- var costheta = rotation ? Math.cos(rotation * deg2rad) : 1,
- sintheta = rotation ? Math.sin(rotation * deg2rad) : 0,
- height = pick(this.elemHeight, this.element.offsetHeight),
- quad,
- nonLeft = align && align !== 'left';
- // correct x and y
- this.xCorr = costheta < 0 && -width;
- this.yCorr = sintheta < 0 && -height;
- // correct for baseline and corners spilling out after rotation
- quad = costheta * sintheta < 0;
- this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection);
- this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
- // correct for the length/height of the text
- if (nonLeft) {
- this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
- if (rotation) {
- this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
- }
- css(this.element, {
- textAlign: align
- });
- }
- },
- /**
- * Converts a subset of an SVG path definition to its VML counterpart. Takes an array
- * as the parameter and returns a string.
- */
- pathToVML: function(value) {
- // convert paths
- var i = value.length,
- path = [];
- while (i--) {
- // Multiply by 10 to allow subpixel precision.
- // Substracting half a pixel seems to make the coordinates
- // align with SVG, but this hasn't been tested thoroughly
- if (isNumber(value[i])) {
- path[i] = Math.round(value[i] * 10) - 5;
- } else if (value[i] === 'Z') { // close the path
- path[i] = 'x';
- } else {
- path[i] = value[i];
- // When the start X and end X coordinates of an arc are too close,
- // they are rounded to the same value above. In this case, substract or
- // add 1 from the end X and Y positions. #186, #760, #1371, #1410.
- if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) {
- // Start and end X
- if (path[i + 5] === path[i + 7]) {
- path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1;
- }
- // Start and end Y
- if (path[i + 6] === path[i + 8]) {
- path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1;
- }
- }
- }
- }
- // Loop up again to handle path shortcuts (#2132)
- /*while (i++ < path.length) {
- if (path[i] === 'H') { // horizontal line to
- path[i] = 'L';
- path.splice(i + 2, 0, path[i - 1]);
- } else if (path[i] === 'V') { // vertical line to
- path[i] = 'L';
- path.splice(i + 1, 0, path[i - 2]);
- }
- }*/
- return path.join(' ') || 'x';
- },
- /**
- * Set the element's clipping to a predefined rectangle
- *
- * @param {String} id The id of the clip rectangle
- */
- clip: function(clipRect) {
- var wrapper = this,
- clipMembers,
- cssRet;
- if (clipRect) {
- clipMembers = clipRect.members;
- erase(clipMembers, wrapper); // Ensure unique list of elements (#1258)
- clipMembers.push(wrapper);
- wrapper.destroyClip = function() {
- erase(clipMembers, wrapper);
- };
- cssRet = clipRect.getCSS(wrapper);
- } else {
- if (wrapper.destroyClip) {
- wrapper.destroyClip();
- }
- cssRet = {
- clip: wrapper.docMode8 ? 'inherit' : 'rect(auto)'
- }; // #1214
- }
- return wrapper.css(cssRet);
- },
- /**
- * Set styles for the element
- * @param {Object} styles
- */
- css: SVGElement.prototype.htmlCss,
- /**
- * Removes a child either by removeChild or move to garbageBin.
- * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
- */
- safeRemoveChild: function(element) {
- // discardElement will detach the node from its parent before attaching it
- // to the garbage bin. Therefore it is important that the node is attached and have parent.
- if (element.parentNode) {
- discardElement(element);
- }
- },
- /**
- * Extend element.destroy by removing it from the clip members array
- */
- destroy: function() {
- if (this.destroyClip) {
- this.destroyClip();
- }
- return SVGElement.prototype.destroy.apply(this);
- },
- /**
- * Add an event listener. VML override for normalizing event parameters.
- * @param {String} eventType
- * @param {Function} handler
- */
- on: function(eventType, handler) {
- // simplest possible event model for internal use
- this.element['on' + eventType] = function() {
- var evt = win.event;
- evt.target = evt.srcElement;
- handler(evt);
- };
- return this;
- },
- /**
- * In stacked columns, cut off the shadows so that they don't overlap
- */
- cutOffPath: function(path, length) {
- var len;
- path = path.split(/[ ,]/); // The extra comma tricks the trailing comma remover in "gulp scripts" task
- len = path.length;
- if (len === 9 || len === 11) {
- path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length;
- }
- return path.join(' ');
- },
- /**
- * Apply a drop shadow by copying elements and giving them different strokes
- * @param {Boolean|Object} shadowOptions
- */
- shadow: function(shadowOptions, group, cutOff) {
- var shadows = [],
- i,
- element = this.element,
- renderer = this.renderer,
- shadow,
- elemStyle = element.style,
- markup,
- path = element.path,
- strokeWidth,
- modifiedPath,
- shadowWidth,
- shadowElementOpacity;
- // some times empty paths are not strings
- if (path && typeof path.value !== 'string') {
- path = 'x';
- }
- modifiedPath = path;
- if (shadowOptions) {
- shadowWidth = pick(shadowOptions.width, 3);
- shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
- for (i = 1; i <= 3; i++) {
- strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
- // Cut off shadows for stacked column items
- if (cutOff) {
- modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5);
- }
- markup = ['<shape isShadow="true" strokeweight="', strokeWidth,
- '" filled="false" path="', modifiedPath,
- '" coordsize="10 10" style="', element.style.cssText, '" />'
- ];
- shadow = createElement(renderer.prepVML(markup),
- null, {
- left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1),
- top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1)
- }
- );
- if (cutOff) {
- shadow.cutOff = strokeWidth + 1;
- }
- // apply the opacity
- markup = [
- '<stroke color="',
- shadowOptions.color || '#000000',
- '" opacity="', shadowElementOpacity * i, '"/>'
- ];
- createElement(renderer.prepVML(markup), null, null, shadow);
- // insert it
- if (group) {
- group.element.appendChild(shadow);
- } else {
- element.parentNode.insertBefore(shadow, element);
- }
- // record it
- shadows.push(shadow);
- }
- this.shadows = shadows;
- }
- return this;
- },
- updateShadows: noop, // Used in SVG only
- setAttr: function(key, value) {
- if (this.docMode8) { // IE8 setAttribute bug
- this.element[key] = value;
- } else {
- this.element.setAttribute(key, value);
- }
- },
- classSetter: function(value) {
- // IE8 Standards mode has problems retrieving the className unless set like this.
- // IE8 Standards can't set the class name before the element is appended.
- (this.added ? this.element : this).className = value;
- },
- dashstyleSetter: function(value, key, element) {
- var strokeElem = element.getElementsByTagName('stroke')[0] ||
- createElement(this.renderer.prepVML(['<stroke/>']), null, null, element);
- strokeElem[key] = value || 'solid';
- this[key] = value;
- /* because changing stroke-width will change the dash length
- and cause an epileptic effect */
- },
- dSetter: function(value, key, element) {
- var i,
- shadows = this.shadows;
- value = value || [];
- this.d = value.join && value.join(' '); // used in getter for animation
- element.path = value = this.pathToVML(value);
- // update shadows
- if (shadows) {
- i = shadows.length;
- while (i--) {
- shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value;
- }
- }
- this.setAttr(key, value);
- },
- fillSetter: function(value, key, element) {
- var nodeName = element.nodeName;
- if (nodeName === 'SPAN') { // text color
- element.style.color = value;
- } else if (nodeName !== 'IMG') { // #1336
- element.filled = value !== 'none';
- this.setAttr('fillcolor', this.renderer.color(value, element, key, this));
- }
- },
- 'fill-opacitySetter': function(value, key, element) {
- createElement(
- this.renderer.prepVML(['<', key.split('-')[0], ' opacity="', value, '"/>']),
- null,
- null,
- element
- );
- },
- opacitySetter: noop, // Don't bother - animation is too slow and filters introduce artifacts
- rotationSetter: function(value, key, element) {
- var style = element.style;
- this[key] = style[key] = value; // style is for #1873
- // Correction for the 1x1 size of the shape container. Used in gauge needles.
- style.left = -Math.round(Math.sin(value * deg2rad) + 1) + 'px';
- style.top = Math.round(Math.cos(value * deg2rad)) + 'px';
- },
- strokeSetter: function(value, key, element) {
- this.setAttr('strokecolor', this.renderer.color(value, element, key, this));
- },
- 'stroke-widthSetter': function(value, key, element) {
- element.stroked = !!value; // VML "stroked" attribute
- this[key] = value; // used in getter, issue #113
- if (isNumber(value)) {
- value += 'px';
- }
- this.setAttr('strokeweight', value);
- },
- titleSetter: function(value, key) {
- this.setAttr(key, value);
- },
- visibilitySetter: function(value, key, element) {
- // Handle inherited visibility
- if (value === 'inherit') {
- value = 'visible';
- }
- // Let the shadow follow the main element
- if (this.shadows) {
- each(this.shadows, function(shadow) {
- shadow.style[key] = value;
- });
- }
- // Instead of toggling the visibility CSS property, move the div out of the viewport.
- // This works around #61 and #586
- if (element.nodeName === 'DIV') {
- value = value === 'hidden' ? '-999em' : 0;
- // In order to redraw, IE7 needs the div to be visible when tucked away
- // outside the viewport. So the visibility is actually opposite of
- // the expected value. This applies to the tooltip only.
- if (!this.docMode8) {
- element.style[key] = value ? 'visible' : 'hidden';
- }
- key = 'top';
- }
- element.style[key] = value;
- },
- xSetter: function(value, key, element) {
- this[key] = value; // used in getter
- if (key === 'x') {
- key = 'left';
- } else if (key === 'y') {
- key = 'top';
- }
- /* else {
- value = Math.max(0, value); // don't set width or height below zero (#311)
- }*/
- // clipping rectangle special
- if (this.updateClipping) {
- this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y'
- this.updateClipping();
- } else {
- // normal
- element.style[key] = value;
- }
- },
- zIndexSetter: function(value, key, element) {
- element.style[key] = value;
- }
- };
- VMLElement['stroke-opacitySetter'] = VMLElement['fill-opacitySetter'];
- H.VMLElement = VMLElement = extendClass(SVGElement, VMLElement);
- // Some shared setters
- VMLElement.prototype.ySetter =
- VMLElement.prototype.widthSetter =
- VMLElement.prototype.heightSetter =
- VMLElement.prototype.xSetter;
- /**
- * The VML renderer
- */
- VMLRendererExtension = { // inherit SVGRenderer
- Element: VMLElement,
- isIE8: win.navigator.userAgent.indexOf('MSIE 8.0') > -1,
- /**
- * Initialize the VMLRenderer
- * @param {Object} container
- * @param {Number} width
- * @param {Number} height
- */
- init: function(container, width, height) {
- var renderer = this,
- boxWrapper,
- box,
- css;
- renderer.alignedObjects = [];
- boxWrapper = renderer.createElement('div')
- .css({
- position: 'relative'
- });
- box = boxWrapper.element;
- container.appendChild(boxWrapper.element);
- // generate the containing box
- renderer.isVML = true;
- renderer.box = box;
- renderer.boxWrapper = boxWrapper;
- renderer.gradients = {};
- renderer.cache = {}; // Cache for numerical bounding boxes
- renderer.cacheKeys = [];
- renderer.imgCount = 0;
- renderer.setSize(width, height, false);
- // The only way to make IE6 and IE7 print is to use a global namespace. However,
- // with IE8 the only way to make the dynamic shapes visible in screen and print mode
- // seems to be to add the xmlns attribute and the behaviour style inline.
- if (!doc.namespaces.hcv) {
- doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
- // Setup default CSS (#2153, #2368, #2384)
- css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' +
- '{ behavior:url(#default#VML); display: inline-block; } ';
- try {
- doc.createStyleSheet().cssText = css;
- } catch (e) {
- doc.styleSheets[0].cssText += css;
- }
- }
- },
- /**
- * Detect whether the renderer is hidden. This happens when one of the parent elements
- * has display: none
- */
- isHidden: function() {
- return !this.box.offsetWidth;
- },
- /**
- * Define a clipping rectangle. In VML it is accomplished by storing the values
- * for setting the CSS style to all associated members.
- *
- * @param {Number} x
- * @param {Number} y
- * @param {Number} width
- * @param {Number} height
- */
- clipRect: function(x, y, width, height) {
- // create a dummy element
- var clipRect = this.createElement(),
- isObj = isObject(x);
- // mimic a rectangle with its style object for automatic updating in attr
- return extend(clipRect, {
- members: [],
- count: 0,
- left: (isObj ? x.x : x) + 1,
- top: (isObj ? x.y : y) + 1,
- width: (isObj ? x.width : width) - 1,
- height: (isObj ? x.height : height) - 1,
- getCSS: function(wrapper) {
- var element = wrapper.element,
- nodeName = element.nodeName,
- isShape = nodeName === 'shape',
- inverted = wrapper.inverted,
- rect = this,
- top = rect.top - (isShape ? element.offsetTop : 0),
- left = rect.left,
- right = left + rect.width,
- bottom = top + rect.height,
- ret = {
- clip: 'rect(' +
- Math.round(inverted ? left : top) + 'px,' +
- Math.round(inverted ? bottom : right) + 'px,' +
- Math.round(inverted ? right : bottom) + 'px,' +
- Math.round(inverted ? top : left) + 'px)'
- };
- // issue 74 workaround
- if (!inverted && wrapper.docMode8 && nodeName === 'DIV') {
- extend(ret, {
- width: right + 'px',
- height: bottom + 'px'
- });
- }
- return ret;
- },
- // used in attr and animation to update the clipping of all members
- updateClipping: function() {
- each(clipRect.members, function(member) {
- // Member.element is falsy on deleted series, like in
- // stock/members/series-remove demo. Should be removed
- // from members, but this will do.
- if (member.element) {
- member.css(clipRect.getCSS(member));
- }
- });
- }
- });
- },
- /**
- * Take a color and return it if it's a string, make it a gradient if it's a
- * gradient configuration object, and apply opacity.
- *
- * @param {Object} color The color or config object
- */
- color: function(color, elem, prop, wrapper) {
- var renderer = this,
- colorObject,
- regexRgba = /^rgba/,
- markup,
- fillType,
- ret = 'none';
- // Check for linear or radial gradient
- if (color && color.linearGradient) {
- fillType = 'gradient';
- } else if (color && color.radialGradient) {
- fillType = 'pattern';
- }
- if (fillType) {
- var stopColor,
- stopOpacity,
- gradient = color.linearGradient || color.radialGradient,
- x1,
- y1,
- x2,
- y2,
- opacity1,
- opacity2,
- color1,
- color2,
- fillAttr = '',
- stops = color.stops,
- firstStop,
- lastStop,
- colors = [],
- addFillNode = function() {
- // Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2
- // are reversed.
- markup = ['<fill colors="' + colors.join(',') +
- '" opacity="', opacity2, '" o:opacity2="',
- opacity1, '" type="', fillType, '" ', fillAttr,
- 'focus="100%" method="any" />'
- ];
- createElement(renderer.prepVML(markup), null, null, elem);
- };
- // Extend from 0 to 1
- firstStop = stops[0];
- lastStop = stops[stops.length - 1];
- if (firstStop[0] > 0) {
- stops.unshift([
- 0,
- firstStop[1]
- ]);
- }
- if (lastStop[0] < 1) {
- stops.push([
- 1,
- lastStop[1]
- ]);
- }
- // Compute the stops
- each(stops, function(stop, i) {
- if (regexRgba.test(stop[1])) {
- colorObject = H.color(stop[1]);
- stopColor = colorObject.get('rgb');
- stopOpacity = colorObject.get('a');
- } else {
- stopColor = stop[1];
- stopOpacity = 1;
- }
- // Build the color attribute
- colors.push((stop[0] * 100) + '% ' + stopColor);
- // Only start and end opacities are allowed, so we use the first and the last
- if (!i) {
- opacity1 = stopOpacity;
- color2 = stopColor;
- } else {
- opacity2 = stopOpacity;
- color1 = stopColor;
- }
- });
- // Apply the gradient to fills only.
- if (prop === 'fill') {
- // Handle linear gradient angle
- if (fillType === 'gradient') {
- x1 = gradient.x1 || gradient[0] || 0;
- y1 = gradient.y1 || gradient[1] || 0;
- x2 = gradient.x2 || gradient[2] || 0;
- y2 = gradient.y2 || gradient[3] || 0;
- fillAttr = 'angle="' + (90 - Math.atan(
- (y2 - y1) / // y vector
- (x2 - x1) // x vector
- ) * 180 / Math.PI) + '"';
- addFillNode();
- // Radial (circular) gradient
- } else {
- var r = gradient.r,
- sizex = r * 2,
- sizey = r * 2,
- cx = gradient.cx,
- cy = gradient.cy,
- radialReference = elem.radialReference,
- bBox,
- applyRadialGradient = function() {
- if (radialReference) {
- bBox = wrapper.getBBox();
- cx += (radialReference[0] - bBox.x) / bBox.width - 0.5;
- cy += (radialReference[1] - bBox.y) / bBox.height - 0.5;
- sizex *= radialReference[2] / bBox.width;
- sizey *= radialReference[2] / bBox.height;
- }
- fillAttr = 'src="' + H.getOptions().global.VMLRadialGradientURL + '" ' +
- 'size="' + sizex + ',' + sizey + '" ' +
- 'origin="0.5,0.5" ' +
- 'position="' + cx + ',' + cy + '" ' +
- 'color2="' + color2 + '" ';
- addFillNode();
- };
- // Apply radial gradient
- if (wrapper.added) {
- applyRadialGradient();
- } else {
- // We need to know the bounding box to get the size and position right
- wrapper.onAdd = applyRadialGradient;
- }
- // The fill element's color attribute is broken in IE8 standards mode, so we
- // need to set the parent shape's fillcolor attribute instead.
- ret = color1;
- }
- // Gradients are not supported for VML stroke, return the first color. #722.
- } else {
- ret = stopColor;
- }
- // If the color is an rgba color, split it and add a fill node
- // to hold the opacity component
- } else if (regexRgba.test(color) && elem.tagName !== 'IMG') {
- colorObject = H.color(color);
- wrapper[prop + '-opacitySetter'](colorObject.get('a'), prop, elem);
- ret = colorObject.get('rgb');
- } else {
- var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node
- if (propNodes.length) {
- propNodes[0].opacity = 1;
- propNodes[0].type = 'solid';
- }
- ret = color;
- }
- return ret;
- },
- /**
- * Take a VML string and prepare it for either IE8 or IE6/IE7.
- * @param {Array} markup A string array of the VML markup to prepare
- */
- prepVML: function(markup) {
- var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
- isIE8 = this.isIE8;
- markup = markup.join('');
- if (isIE8) { // add xmlns and style inline
- markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');
- if (markup.indexOf('style="') === -1) {
- markup = markup.replace('/>', ' style="' + vmlStyle + '" />');
- } else {
- markup = markup.replace('style="', 'style="' + vmlStyle);
- }
- } else { // add namespace
- markup = markup.replace('<', '<hcv:');
- }
- return markup;
- },
- /**
- * Create rotated and aligned text
- * @param {String} str
- * @param {Number} x
- * @param {Number} y
- */
- text: SVGRenderer.prototype.html,
- /**
- * Create and return a path element
- * @param {Array} path
- */
- path: function(path) {
- var attr = {
- // subpixel precision down to 0.1 (width and height = 1px)
- coordsize: '10 10'
- };
- if (isArray(path)) {
- attr.d = path;
- } else if (isObject(path)) { // attributes
- extend(attr, path);
- }
- // create the shape
- return this.createElement('shape').attr(attr);
- },
- /**
- * Create and return a circle element. In VML circles are implemented as
- * shapes, which is faster than v:oval
- * @param {Number} x
- * @param {Number} y
- * @param {Number} r
- */
- circle: function(x, y, r) {
- var circle = this.symbol('circle');
- if (isObject(x)) {
- r = x.r;
- y = x.y;
- x = x.x;
- }
- circle.isCircle = true; // Causes x and y to mean center (#1682)
- circle.r = r;
- return circle.attr({
- x: x,
- y: y
- });
- },
- /**
- * Create a group using an outer div and an inner v:group to allow rotating
- * and flipping. A simple v:group would have problems with positioning
- * child HTML elements and CSS clip.
- *
- * @param {String} name The name of the group
- */
- g: function(name) {
- var wrapper,
- attribs;
- // set the class name
- if (name) {
- attribs = {
- 'className': 'highcharts-' + name,
- 'class': 'highcharts-' + name
- };
- }
- // the div to hold HTML and clipping
- wrapper = this.createElement('div').attr(attribs);
- return wrapper;
- },
- /**
- * VML override to create a regular HTML image
- * @param {String} src
- * @param {Number} x
- * @param {Number} y
- * @param {Number} width
- * @param {Number} height
- */
- image: function(src, x, y, width, height) {
- var obj = this.createElement('img')
- .attr({
- src: src
- });
- if (arguments.length > 1) {
- obj.attr({
- x: x,
- y: y,
- width: width,
- height: height
- });
- }
- return obj;
- },
- /**
- * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems
- */
- createElement: function(nodeName) {
- return nodeName === 'rect' ?
- this.symbol(nodeName) :
- SVGRenderer.prototype.createElement.call(this, nodeName);
- },
- /**
- * In the VML renderer, each child of an inverted div (group) is inverted
- * @param {Object} element
- * @param {Object} parentNode
- */
- invertChild: function(element, parentNode) {
- var ren = this,
- parentStyle = parentNode.style,
- imgStyle = element.tagName === 'IMG' && element.style; // #1111
- css(element, {
- flip: 'x',
- left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1),
- top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1),
- rotation: -90
- });
- // Recursively invert child elements, needed for nested composite
- // shapes like box plots and error bars. #1680, #1806.
- each(element.childNodes, function(child) {
- ren.invertChild(child, element);
- });
- },
- /**
- * Symbol definitions that override the parent SVG renderer's symbols
- *
- */
- symbols: {
- // VML specific arc function
- arc: function(x, y, w, h, options) {
- var start = options.start,
- end = options.end,
- radius = options.r || w || h,
- innerRadius = options.innerR,
- cosStart = Math.cos(start),
- sinStart = Math.sin(start),
- cosEnd = Math.cos(end),
- sinEnd = Math.sin(end),
- ret;
- if (end - start === 0) { // no angle, don't show it.
- return ['x'];
- }
- ret = [
- 'wa', // clockwise arc to
- x - radius, // left
- y - radius, // top
- x + radius, // right
- y + radius, // bottom
- x + radius * cosStart, // start x
- y + radius * sinStart, // start y
- x + radius * cosEnd, // end x
- y + radius * sinEnd // end y
- ];
- if (options.open && !innerRadius) {
- ret.push(
- 'e',
- 'M',
- x, // - innerRadius,
- y // - innerRadius
- );
- }
- ret.push(
- 'at', // anti clockwise arc to
- x - innerRadius, // left
- y - innerRadius, // top
- x + innerRadius, // right
- y + innerRadius, // bottom
- x + innerRadius * cosEnd, // start x
- y + innerRadius * sinEnd, // start y
- x + innerRadius * cosStart, // end x
- y + innerRadius * sinStart, // end y
- 'x', // finish path
- 'e' // close
- );
- ret.isArc = true;
- return ret;
- },
- // Add circle symbol path. This performs significantly faster than v:oval.
- circle: function(x, y, w, h, wrapper) {
- if (wrapper && defined(wrapper.r)) {
- w = h = 2 * wrapper.r;
- }
- // Center correction, #1682
- if (wrapper && wrapper.isCircle) {
- x -= w / 2;
- y -= h / 2;
- }
- // Return the path
- return [
- 'wa', // clockwisearcto
- x, // left
- y, // top
- x + w, // right
- y + h, // bottom
- x + w, // start x
- y + h / 2, // start y
- x + w, // end x
- y + h / 2, // end y
- //'x', // finish path
- 'e' // close
- ];
- },
- /**
- * Add rectangle symbol path which eases rotation and omits arcsize problems
- * compared to the built-in VML roundrect shape. When borders are not rounded,
- * use the simpler square path, else use the callout path without the arrow.
- */
- rect: function(x, y, w, h, options) {
- return SVGRenderer.prototype.symbols[!defined(options) || !options.r ? 'square' : 'callout'].call(0, x, y, w, h, options);
- }
- }
- };
- H.VMLRenderer = VMLRenderer = function() {
- this.init.apply(this, arguments);
- };
- VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension);
- // general renderer
- H.Renderer = VMLRenderer;
- }
- // This method is used with exporting in old IE, when emulating SVG (see #2314)
- SVGRenderer.prototype.measureSpanWidth = function(text, styles) {
- var measuringSpan = doc.createElement('span'),
- offsetWidth,
- textNode = doc.createTextNode(text);
- measuringSpan.appendChild(textNode);
- css(measuringSpan, styles);
- this.box.appendChild(measuringSpan);
- offsetWidth = measuringSpan.offsetWidth;
- discardElement(measuringSpan); // #2463
- return offsetWidth;
- };
- /* ****************************************************************************
- * *
- * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
- * *
- *****************************************************************************/
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var color = H.color,
- each = H.each,
- getTZOffset = H.getTZOffset,
- isTouchDevice = H.isTouchDevice,
- merge = H.merge,
- pick = H.pick,
- svg = H.svg,
- win = H.win;
- /* ****************************************************************************
- * Handle the options *
- *****************************************************************************/
- H.defaultOptions = {
- colors: '#7cb5ec #434348 #90ed7d #f7a35c #8085e9 #f15c80 #e4d354 #2b908f #f45b5b #91e8e1'.split(' '),
- symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
- lang: {
- loading: 'Loading...',
- months: [
- 'January', 'February', 'March', 'April', 'May', 'June', 'July',
- 'August', 'September', 'October', 'November', 'December'
- ],
- shortMonths: [
- 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
- 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
- ],
- weekdays: [
- 'Sunday', 'Monday', 'Tuesday', 'Wednesday',
- 'Thursday', 'Friday', 'Saturday'
- ],
- // invalidDate: '',
- decimalPoint: '.',
- numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels
- resetZoom: 'Reset zoom',
- resetZoomTitle: 'Reset zoom level 1:1',
- thousandsSep: ' '
- },
- global: {
- useUTC: true,
- //timezoneOffset: 0,
- VMLRadialGradientURL: 'http://code.highcharts.com/5.0.12/gfx/vml-radial-gradient.png'
- },
- chart: {
- //animation: true,
- //alignTicks: false,
- //reflow: true,
- //className: null,
- //events: { load, selection },
- //margin: [null],
- //marginTop: null,
- //marginRight: null,
- //marginBottom: null,
- //marginLeft: null,
- borderRadius: 0,
- defaultSeriesType: 'line',
- ignoreHiddenSeries: true,
- //inverted: false,
- spacing: [10, 10, 15, 10],
- //spacingTop: 10,
- //spacingRight: 10,
- //spacingBottom: 15,
- //spacingLeft: 10,
- //zoomType: ''
- resetZoomButton: {
- theme: {
- zIndex: 20
- },
- position: {
- align: 'right',
- x: -10,
- //verticalAlign: 'top',
- y: 10
- }
- // relativeTo: 'plot'
- },
- width: null,
- height: null,
- borderColor: '#335cad',
- //borderWidth: 0,
- //style: {
- // fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
- // fontSize: '12px'
- //},
- backgroundColor: '#ffffff',
- //plotBackgroundColor: null,
- plotBorderColor: '#cccccc'
- //plotBorderWidth: 0,
- //plotShadow: false
- },
- title: {
- text: 'Chart title',
- align: 'center',
- // floating: false,
- margin: 15,
- // x: 0,
- // verticalAlign: 'top',
- // y: null,
- // style: {}, // defined inline
- widthAdjust: -44
- },
- subtitle: {
- text: '',
- align: 'center',
- // floating: false
- // x: 0,
- // verticalAlign: 'top',
- // y: null,
- // style: {}, // defined inline
- widthAdjust: -44
- },
- plotOptions: {},
- labels: {
- //items: [],
- style: {
- //font: defaultFont,
- position: 'absolute',
- color: '#333333'
- }
- },
- legend: {
- enabled: true,
- align: 'center',
- //floating: false,
- layout: 'horizontal',
- labelFormatter: function() {
- return this.name;
- },
- //borderWidth: 0,
- borderColor: '#999999',
- borderRadius: 0,
- navigation: {
- activeColor: '#003399',
- inactiveColor: '#cccccc'
- // animation: true,
- // arrowSize: 12
- // style: {} // text styles
- },
- // margin: 20,
- // reversed: false,
- // backgroundColor: null,
- /*style: {
- padding: '5px'
- },*/
- itemStyle: {
- color: '#333333',
- fontSize: '12px',
- fontWeight: 'bold',
- textOverflow: 'ellipsis'
- },
- itemHoverStyle: {
- //cursor: 'pointer', removed as of #601
- color: '#000000'
- },
- itemHiddenStyle: {
- color: '#cccccc'
- },
- shadow: false,
- itemCheckboxStyle: {
- position: 'absolute',
- width: '13px', // for IE precision
- height: '13px'
- },
- // itemWidth: undefined,
- squareSymbol: true,
- // symbolRadius: 0,
- // symbolWidth: 16,
- symbolPadding: 5,
- verticalAlign: 'bottom',
- // width: undefined,
- x: 0,
- y: 0,
- title: {
- //text: null,
- style: {
- fontWeight: 'bold'
- }
- }
- },
- loading: {
- // hideDuration: 100,
- // showDuration: 0,
- labelStyle: {
- fontWeight: 'bold',
- position: 'relative',
- top: '45%'
- },
- style: {
- position: 'absolute',
- backgroundColor: '#ffffff',
- opacity: 0.5,
- textAlign: 'center'
- }
- },
- tooltip: {
- enabled: true,
- animation: svg,
- //crosshairs: null,
- borderRadius: 3,
- dateTimeLabelFormats: {
- millisecond: '%A, %b %e, %H:%M:%S.%L',
- second: '%A, %b %e, %H:%M:%S',
- minute: '%A, %b %e, %H:%M',
- hour: '%A, %b %e, %H:%M',
- day: '%A, %b %e, %Y',
- week: 'Week from %A, %b %e, %Y',
- month: '%B %Y',
- year: '%Y'
- },
- footerFormat: '',
- //formatter: defaultFormatter,
- /* todo: em font-size when finished comparing against HC4
- headerFormat: '<span style="font-size: 0.85em">{point.key}</span><br/>',
- */
- padding: 8,
- //shape: 'callout',
- //shared: false,
- snap: isTouchDevice ? 25 : 10,
- backgroundColor: color('#f7f7f7').setOpacity(0.85).get(),
- borderWidth: 1,
- headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>',
- pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
- shadow: true,
- style: {
- color: '#333333',
- cursor: 'default',
- fontSize: '12px',
- pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events
- whiteSpace: 'nowrap'
- }
- //xDateFormat: '%A, %b %e, %Y',
- //valueDecimals: null,
- //valuePrefix: '',
- //valueSuffix: ''
- },
- credits: {
- enabled: true,
- href: 'http://www.highcharts.com',
- position: {
- align: 'right',
- x: -10,
- verticalAlign: 'bottom',
- y: -5
- },
- style: {
- cursor: 'pointer',
- color: '#999999',
- fontSize: '9px'
- },
- text: 'Highcharts.com'
- }
- };
- /**
- * Sets the getTimezoneOffset function. If the timezone option is set, a default
- * getTimezoneOffset function with that timezone is returned. If not, the
- * specified getTimezoneOffset function is returned. If neither are specified,
- * undefined is returned.
- * @return {function} a getTimezoneOffset function or undefined
- */
- function getTimezoneOffsetOption() {
- var globalOptions = H.defaultOptions.global,
- moment = win.moment;
- if (globalOptions.timezone) {
- if (!moment) {
- // getTimezoneOffset-function stays undefined because it depends on
- // Moment.js
- H.error(25);
- } else {
- return function(timestamp) {
- return -moment.tz(
- timestamp,
- globalOptions.timezone
- ).utcOffset();
- };
- }
- }
- // If not timezone is set, look for the getTimezoneOffset callback
- return globalOptions.useUTC && globalOptions.getTimezoneOffset;
- }
- /**
- * Set the time methods globally based on the useUTC option. Time method can be
- * either local time or UTC (default). It is called internally on initiating
- * Highcharts and after running `Highcharts.setOptions`.
- *
- * @private
- */
- function setTimeMethods() {
- var globalOptions = H.defaultOptions.global,
- Date,
- useUTC = globalOptions.useUTC,
- GET = useUTC ? 'getUTC' : 'get',
- SET = useUTC ? 'setUTC' : 'set';
- H.Date = Date = globalOptions.Date || win.Date; // Allow using a different Date class
- Date.hcTimezoneOffset = useUTC && globalOptions.timezoneOffset;
- Date.hcGetTimezoneOffset = getTimezoneOffsetOption();
- Date.hcMakeTime = function(year, month, date, hours, minutes, seconds) {
- var d;
- if (useUTC) {
- d = Date.UTC.apply(0, arguments);
- d += getTZOffset(d);
- } else {
- d = new Date(
- year,
- month,
- pick(date, 1),
- pick(hours, 0),
- pick(minutes, 0),
- pick(seconds, 0)
- ).getTime();
- }
- return d;
- };
- each(['Minutes', 'Hours', 'Day', 'Date', 'Month', 'FullYear'], function(s) {
- Date['hcGet' + s] = GET + s;
- });
- each(['Milliseconds', 'Seconds', 'Minutes', 'Hours', 'Date', 'Month', 'FullYear'], function(s) {
- Date['hcSet' + s] = SET + s;
- });
- }
- /**
- * Merge the default options with custom options and return the new options
- * structure. Commonly used for defining reusable templates.
- *
- * @function #setOptions
- * @memberOf Highcharts
- * @sample highcharts/global/useutc-false Setting a global option
- * @sample highcharts/members/setoptions Applying a global theme
- * @param {Object} options The new custom chart options.
- * @returns {Object} Updated options.
- */
- H.setOptions = function(options) {
- // Copy in the default options
- H.defaultOptions = merge(true, H.defaultOptions, options);
- // Apply UTC
- setTimeMethods();
- return H.defaultOptions;
- };
- /**
- * Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules
- * wasn't enough because the setOptions method created a new object.
- */
- H.getOptions = function() {
- return H.defaultOptions;
- };
- // Series defaults
- H.defaultPlotOptions = H.defaultOptions.plotOptions;
- // set the default time methods
- setTimeMethods();
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var correctFloat = H.correctFloat,
- defined = H.defined,
- destroyObjectProperties = H.destroyObjectProperties,
- isNumber = H.isNumber,
- merge = H.merge,
- pick = H.pick,
- deg2rad = H.deg2rad;
- /**
- * The Tick class
- */
- H.Tick = function(axis, pos, type, noLabel) {
- this.axis = axis;
- this.pos = pos;
- this.type = type || '';
- this.isNew = true;
- this.isNewLabel = true;
- if (!type && !noLabel) {
- this.addLabel();
- }
- };
- H.Tick.prototype = {
- /**
- * Write the tick label
- */
- addLabel: function() {
- var tick = this,
- axis = tick.axis,
- options = axis.options,
- chart = axis.chart,
- categories = axis.categories,
- names = axis.names,
- pos = tick.pos,
- labelOptions = options.labels,
- str,
- tickPositions = axis.tickPositions,
- isFirst = pos === tickPositions[0],
- isLast = pos === tickPositions[tickPositions.length - 1],
- value = categories ?
- pick(categories[pos], names[pos], pos) :
- pos,
- label = tick.label,
- tickPositionInfo = tickPositions.info,
- dateTimeLabelFormat;
- // Set the datetime label format. If a higher rank is set for this position, use that. If not,
- // use the general format.
- if (axis.isDatetimeAxis && tickPositionInfo) {
- dateTimeLabelFormat =
- options.dateTimeLabelFormats[
- tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName
- ];
- }
- // set properties for access in render method
- tick.isFirst = isFirst;
- tick.isLast = isLast;
- // get the string
- str = axis.labelFormatter.call({
- axis: axis,
- chart: chart,
- isFirst: isFirst,
- isLast: isLast,
- dateTimeLabelFormat: dateTimeLabelFormat,
- value: axis.isLog ? correctFloat(axis.lin2log(value)) : value
- });
- // prepare CSS
- //css = width && { width: Math.max(1, Math.round(width - 2 * (labelOptions.padding || 10))) + 'px' };
- // first call
- if (!defined(label)) {
- tick.label = label =
- defined(str) && labelOptions.enabled ?
- chart.renderer.text(
- str,
- 0,
- 0,
- labelOptions.useHTML
- )
- // without position absolute, IE export sometimes is wrong
- .css(merge(labelOptions.style))
- .add(axis.labelGroup) :
- null;
- tick.labelLength = label && label.getBBox().width; // Un-rotated length
- tick.rotation = 0; // Base value to detect change for new calls to getBBox
- // update
- } else if (label) {
- label.attr({
- text: str
- });
- }
- },
- /**
- * Get the offset height or width of the label
- */
- getLabelSize: function() {
- return this.label ?
- this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] :
- 0;
- },
- /**
- * Handle the label overflow by adjusting the labels to the left and right edge, or
- * hide them if they collide into the neighbour label.
- */
- handleOverflow: function(xy) {
- var axis = this.axis,
- pxPos = xy.x,
- chartWidth = axis.chart.chartWidth,
- spacing = axis.chart.spacing,
- leftBound = pick(axis.labelLeft, Math.min(axis.pos, spacing[3])),
- rightBound = pick(axis.labelRight, Math.max(axis.pos + axis.len, chartWidth - spacing[1])),
- label = this.label,
- rotation = this.rotation,
- factor = {
- left: 0,
- center: 0.5,
- right: 1
- }[axis.labelAlign],
- labelWidth = label.getBBox().width,
- slotWidth = axis.getSlotWidth(),
- modifiedSlotWidth = slotWidth,
- xCorrection = factor,
- goRight = 1,
- leftPos,
- rightPos,
- textWidth,
- css = {};
- // Check if the label overshoots the chart spacing box. If it does, move it.
- // If it now overshoots the slotWidth, add ellipsis.
- if (!rotation) {
- leftPos = pxPos - factor * labelWidth;
- rightPos = pxPos + (1 - factor) * labelWidth;
- if (leftPos < leftBound) {
- modifiedSlotWidth = xy.x + modifiedSlotWidth * (1 - factor) - leftBound;
- } else if (rightPos > rightBound) {
- modifiedSlotWidth = rightBound - xy.x + modifiedSlotWidth * factor;
- goRight = -1;
- }
- modifiedSlotWidth = Math.min(slotWidth, modifiedSlotWidth); // #4177
- if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') {
- xy.x += goRight * (slotWidth - modifiedSlotWidth - xCorrection *
- (slotWidth - Math.min(labelWidth, modifiedSlotWidth)));
- }
- // If the label width exceeds the available space, set a text width to be
- // picked up below. Also, if a width has been set before, we need to set a new
- // one because the reported labelWidth will be limited by the box (#3938).
- if (labelWidth > modifiedSlotWidth || (axis.autoRotation && (label.styles || {}).width)) {
- textWidth = modifiedSlotWidth;
- }
- // Add ellipsis to prevent rotated labels to be clipped against the edge of the chart
- } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) {
- textWidth = Math.round(pxPos / Math.cos(rotation * deg2rad) - leftBound);
- } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) {
- textWidth = Math.round((chartWidth - pxPos) / Math.cos(rotation * deg2rad));
- }
- if (textWidth) {
- css.width = textWidth;
- if (!(axis.options.labels.style || {}).textOverflow) {
- css.textOverflow = 'ellipsis';
- }
- label.css(css);
- }
- },
- /**
- * Get the x and y position for ticks and labels
- */
- getPosition: function(horiz, pos, tickmarkOffset, old) {
- var axis = this.axis,
- chart = axis.chart,
- cHeight = (old && chart.oldChartHeight) || chart.chartHeight;
- return {
- x: horiz ?
- axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB : axis.left + axis.offset +
- (axis.opposite ?
- ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left :
- 0
- ),
- y: horiz ?
- cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB
- };
- },
- /**
- * Get the x, y position of the tick label
- */
- getLabelPosition: function(x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
- var axis = this.axis,
- transA = axis.transA,
- reversed = axis.reversed,
- staggerLines = axis.staggerLines,
- rotCorr = axis.tickRotCorr || {
- x: 0,
- y: 0
- },
- yOffset = labelOptions.y,
- line;
- if (!defined(yOffset)) {
- if (axis.side === 0) {
- yOffset = label.rotation ? -8 : -label.getBBox().height;
- } else if (axis.side === 2) {
- yOffset = rotCorr.y + 8;
- } else {
- // #3140, #3140
- yOffset = Math.cos(label.rotation * deg2rad) * (rotCorr.y - label.getBBox(false, 0).height / 2);
- }
- }
- x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ?
- tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
- y = y + yOffset - (tickmarkOffset && !horiz ?
- tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
- // Correct for staggered labels
- if (staggerLines) {
- line = (index / (step || 1) % staggerLines);
- if (axis.opposite) {
- line = staggerLines - line - 1;
- }
- y += line * (axis.labelOffset / staggerLines);
- }
- return {
- x: x,
- y: Math.round(y)
- };
- },
- /**
- * Extendible method to return the path of the marker
- */
- getMarkPath: function(x, y, tickLength, tickWidth, horiz, renderer) {
- return renderer.crispLine([
- 'M',
- x,
- y,
- 'L',
- x + (horiz ? 0 : -tickLength),
- y + (horiz ? tickLength : 0)
- ], tickWidth);
- },
- /**
- * Renders the gridLine.
- * @param {Boolean} old Whether or not the tick is old
- * @param {number} opacity The opacity of the grid line
- * @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1
- * @return {undefined}
- */
- renderGridLine: function(old, opacity, reverseCrisp) {
- var tick = this,
- axis = tick.axis,
- options = axis.options,
- gridLine = tick.gridLine,
- gridLinePath,
- attribs = {},
- pos = tick.pos,
- type = tick.type,
- tickmarkOffset = axis.tickmarkOffset,
- renderer = axis.chart.renderer;
- var gridPrefix = type ? type + 'Grid' : 'grid',
- gridLineWidth = options[gridPrefix + 'LineWidth'],
- gridLineColor = options[gridPrefix + 'LineColor'],
- dashStyle = options[gridPrefix + 'LineDashStyle'];
- if (!gridLine) {
- attribs.stroke = gridLineColor;
- attribs['stroke-width'] = gridLineWidth;
- if (dashStyle) {
- attribs.dashstyle = dashStyle;
- }
- if (!type) {
- attribs.zIndex = 1;
- }
- if (old) {
- attribs.opacity = 0;
- }
- tick.gridLine = gridLine = renderer.path()
- .attr(attribs)
- .addClass(
- 'highcharts-' + (type ? type + '-' : '') + 'grid-line'
- )
- .add(axis.gridGroup);
- }
- // If the parameter 'old' is set, the current call will be followed
- // by another call, therefore do not do any animations this time
- if (!old && gridLine) {
- gridLinePath = axis.getPlotLinePath(
- pos + tickmarkOffset,
- gridLine.strokeWidth() * reverseCrisp,
- old, true
- );
- if (gridLinePath) {
- gridLine[tick.isNew ? 'attr' : 'animate']({
- d: gridLinePath,
- opacity: opacity
- });
- }
- }
- },
- /**
- * Renders the tick mark.
- * @param {Object} xy The position vector of the mark
- * @param {number} xy.x The x position of the mark
- * @param {number} xy.y The y position of the mark
- * @param {number} opacity The opacity of the mark
- * @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1
- * @return {undefined}
- */
- renderMark: function(xy, opacity, reverseCrisp) {
- var tick = this,
- axis = tick.axis,
- options = axis.options,
- renderer = axis.chart.renderer,
- type = tick.type,
- tickPrefix = type ? type + 'Tick' : 'tick',
- tickSize = axis.tickSize(tickPrefix),
- mark = tick.mark,
- isNewMark = !mark,
- x = xy.x,
- y = xy.y;
- var tickWidth = pick(
- options[tickPrefix + 'Width'], !type && axis.isXAxis ? 1 : 0
- ), // X axis defaults to 1
- tickColor = options[tickPrefix + 'Color'];
- if (tickSize) {
- // negate the length
- if (axis.opposite) {
- tickSize[0] = -tickSize[0];
- }
- // First time, create it
- if (isNewMark) {
- tick.mark = mark = renderer.path()
- .addClass('highcharts-' + (type ? type + '-' : '') + 'tick')
- .add(axis.axisGroup);
- mark.attr({
- stroke: tickColor,
- 'stroke-width': tickWidth
- });
- }
- mark[isNewMark ? 'attr' : 'animate']({
- d: tick.getMarkPath(
- x,
- y,
- tickSize[0],
- mark.strokeWidth() * reverseCrisp,
- axis.horiz,
- renderer),
- opacity: opacity
- });
- }
- },
- /**
- * Renders the tick label.
- * Note: The label should already be created in init(), so it should only
- * have to be moved into place.
- * @param {Object} xy The position vector of the label
- * @param {number} xy.x The x position of the label
- * @param {number} xy.y The y position of the label
- * @param {Boolean} old Whether or not the tick is old
- * @param {number} opacity The opacity of the label
- * @param {number} index The index of the tick
- * @return {undefined}
- */
- renderLabel: function(xy, old, opacity, index) {
- var tick = this,
- axis = tick.axis,
- horiz = axis.horiz,
- options = axis.options,
- label = tick.label,
- labelOptions = options.labels,
- step = labelOptions.step,
- tickmarkOffset = axis.tickmarkOffset,
- show = true,
- x = xy.x,
- y = xy.y;
- if (label && isNumber(x)) {
- label.xy = xy = tick.getLabelPosition(
- x,
- y,
- label,
- horiz,
- labelOptions,
- tickmarkOffset,
- index,
- step
- );
- // Apply show first and show last. If the tick is both first and
- // last, it is a single centered tick, in which case we show the
- // label anyway (#2100).
- if (
- (
- tick.isFirst &&
- !tick.isLast &&
- !pick(options.showFirstLabel, 1)
- ) ||
- (
- tick.isLast &&
- !tick.isFirst &&
- !pick(options.showLastLabel, 1)
- )
- ) {
- show = false;
- // Handle label overflow and show or hide accordingly
- } else if (horiz && !axis.isRadial && !labelOptions.step &&
- !labelOptions.rotation && !old && opacity !== 0) {
- tick.handleOverflow(xy);
- }
- // apply step
- if (step && index % step) {
- // show those indices dividable by step
- show = false;
- }
- // Set the new position, and show or hide
- if (show && isNumber(xy.y)) {
- xy.opacity = opacity;
- label[tick.isNewLabel ? 'attr' : 'animate'](xy);
- tick.isNewLabel = false;
- } else {
- label.attr('y', -9999); // #1338
- tick.isNewLabel = true;
- }
- tick.isNew = false;
- }
- },
- /**
- * Put everything in place
- *
- * @param index {Number}
- * @param old {Boolean} Use old coordinates to prepare an animation into new
- * position
- */
- render: function(index, old, opacity) {
- var tick = this,
- axis = tick.axis,
- horiz = axis.horiz,
- pos = tick.pos,
- tickmarkOffset = axis.tickmarkOffset,
- xy = tick.getPosition(horiz, pos, tickmarkOffset, old),
- x = xy.x,
- y = xy.y,
- reverseCrisp = ((horiz && x === axis.pos + axis.len) ||
- (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
- opacity = pick(opacity, 1);
- this.isActive = true;
- // Create the grid line
- this.renderGridLine(old, opacity, reverseCrisp);
- // create the tick mark
- this.renderMark(xy, opacity, reverseCrisp);
- // the label is created on init - now move it into place
- this.renderLabel(xy, old, opacity, index);
- },
- /**
- * Destructor for the tick prototype
- */
- destroy: function() {
- destroyObjectProperties(this, this.axis);
- }
- };
- }(Highcharts));
- var Axis = (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- animObject = H.animObject,
- arrayMax = H.arrayMax,
- arrayMin = H.arrayMin,
- color = H.color,
- correctFloat = H.correctFloat,
- defaultOptions = H.defaultOptions,
- defined = H.defined,
- deg2rad = H.deg2rad,
- destroyObjectProperties = H.destroyObjectProperties,
- each = H.each,
- extend = H.extend,
- fireEvent = H.fireEvent,
- format = H.format,
- getMagnitude = H.getMagnitude,
- grep = H.grep,
- inArray = H.inArray,
- isArray = H.isArray,
- isNumber = H.isNumber,
- isString = H.isString,
- merge = H.merge,
- normalizeTickInterval = H.normalizeTickInterval,
- objectEach = H.objectEach,
- pick = H.pick,
- removeEvent = H.removeEvent,
- splat = H.splat,
- syncTimeout = H.syncTimeout,
- Tick = H.Tick;
- /**
- * Create a new axis object. Called internally when instanciating a new chart or
- * adding axes by {@link Highcharts.Chart#addAxis}.
- *
- * A chart can have from 0 axes (pie chart) to multiples. In a normal, single
- * series cartesian chart, there is one X axis and one Y axis.
- *
- * The X axis or axes are referenced by {@link Highcharts.Chart.xAxis}, which is
- * an array of Axis objects. If there is only one axis, it can be referenced
- * through `chart.xAxis[0]`, and multiple axes have increasing indices. The same
- * pattern goes for Y axes.
- *
- * If you need to get the axes from a series object, use the `series.xAxis` and
- * `series.yAxis` properties. These are not arrays, as one series can only be
- * associated to one X and one Y axis.
- *
- * A third way to reference the axis programmatically is by `id`. Add an `id` in
- * the axis configuration options, and get the axis by
- * {@link Highcharts.Chart#get}.
- *
- * Configuration options for the axes are given in options.xAxis and
- * options.yAxis.
- *
- * @class Highcharts.Axis
- * @memberOf Highcharts
- * @param {Highcharts.Chart} chart - The Chart instance to apply the axis on.
- * @param {Object} options - Axis options
- */
- var Axis = function() {
- this.init.apply(this, arguments);
- };
- H.extend(Axis.prototype, /** @lends Highcharts.Axis.prototype */ {
- /**
- * Default options for the X axis - the Y axis has extended defaults.
- *
- * @private
- * @type {Object}
- */
- defaultOptions: {
- // allowDecimals: null,
- // alternateGridColor: null,
- // categories: [],
- dateTimeLabelFormats: {
- millisecond: '%H:%M:%S.%L',
- second: '%H:%M:%S',
- minute: '%H:%M',
- hour: '%H:%M',
- day: '%e. %b',
- week: '%e. %b',
- month: '%b \'%y',
- year: '%Y'
- },
- endOnTick: false,
- // reversed: false,
- labels: {
- enabled: true,
- // rotation: 0,
- // align: 'center',
- // step: null,
- style: {
- color: '#666666',
- cursor: 'default',
- fontSize: '11px'
- },
- x: 0
- //y: undefined
- /*formatter: function () {
- return this.value;
- },*/
- },
- //linkedTo: null,
- //max: undefined,
- //min: undefined,
- minPadding: 0.01,
- maxPadding: 0.01,
- //minRange: null,
- //minorTickInterval: null,
- minorTickLength: 2,
- minorTickPosition: 'outside', // inside or outside
- //opposite: false,
- //offset: 0,
- //plotBands: [{
- // events: {},
- // zIndex: 1,
- // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
- //}],
- //plotLines: [{
- // events: {}
- // dashStyle: {}
- // zIndex:
- // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
- //}],
- //reversed: false,
- // showFirstLabel: true,
- // showLastLabel: true,
- startOfWeek: 1,
- startOnTick: false,
- //tickInterval: null,
- tickLength: 10,
- tickmarkPlacement: 'between', // on or between
- tickPixelInterval: 100,
- tickPosition: 'outside',
- title: {
- //text: null,
- align: 'middle', // low, middle or high
- //margin: 0 for horizontal, 10 for vertical axes,
- // reserveSpace: true,
- //rotation: 0,
- //side: 'outside',
- style: {
- color: '#666666'
- }
- //x: 0,
- //y: 0
- },
- type: 'linear', // linear, logarithmic or datetime
- //visible: true
- minorGridLineColor: '#f2f2f2',
- // minorGridLineDashStyle: null,
- minorGridLineWidth: 1,
- minorTickColor: '#999999',
- //minorTickWidth: 0,
- lineColor: '#ccd6eb',
- lineWidth: 1,
- gridLineColor: '#e6e6e6',
- // gridLineDashStyle: 'solid',
- // gridLineWidth: 0,
- tickColor: '#ccd6eb'
- // tickWidth: 1
- },
- /**
- * This options set extends the defaultOptions for Y axes.
- *
- * @private
- * @type {Object}
- */
- defaultYAxisOptions: {
- endOnTick: true,
- tickPixelInterval: 72,
- showLastLabel: true,
- labels: {
- x: -8
- },
- maxPadding: 0.05,
- minPadding: 0.05,
- startOnTick: true,
- title: {
- rotation: 270,
- text: 'Values'
- },
- stackLabels: {
- enabled: false,
- //align: dynamic,
- //y: dynamic,
- //x: dynamic,
- //verticalAlign: dynamic,
- //textAlign: dynamic,
- //rotation: 0,
- formatter: function() {
- return H.numberFormat(this.total, -1);
- },
- style: {
- fontSize: '11px',
- fontWeight: 'bold',
- color: '#000000',
- textOutline: '1px contrast'
- }
- },
- gridLineWidth: 1,
- lineWidth: 0
- // tickWidth: 0
- },
- /**
- * These options extend the defaultOptions for left axes.
- *
- * @private
- * @type {Object}
- */
- defaultLeftAxisOptions: {
- labels: {
- x: -15
- },
- title: {
- rotation: 270
- }
- },
- /**
- * These options extend the defaultOptions for right axes.
- *
- * @private
- * @type {Object}
- */
- defaultRightAxisOptions: {
- labels: {
- x: 15
- },
- title: {
- rotation: 90
- }
- },
- /**
- * These options extend the defaultOptions for bottom axes.
- *
- * @private
- * @type {Object}
- */
- defaultBottomAxisOptions: {
- labels: {
- autoRotation: [-45],
- x: 0
- // overflow: undefined,
- // staggerLines: null
- },
- title: {
- rotation: 0
- }
- },
- /**
- * These options extend the defaultOptions for top axes.
- *
- * @private
- * @type {Object}
- */
- defaultTopAxisOptions: {
- labels: {
- autoRotation: [-45],
- x: 0
- // overflow: undefined
- // staggerLines: null
- },
- title: {
- rotation: 0
- }
- },
- /**
- * Initialize the axis
- */
- init: function(chart, userOptions) {
- var isXAxis = userOptions.isX,
- axis = this;
- axis.chart = chart;
- // Flag, is the axis horizontal
- axis.horiz = chart.inverted && !axis.isZAxis ? !isXAxis : isXAxis;
- // Flag, isXAxis
- axis.isXAxis = isXAxis;
- axis.coll = axis.coll || (isXAxis ? 'xAxis' : 'yAxis');
- axis.opposite = userOptions.opposite; // needed in setOptions
- axis.side = userOptions.side || (axis.horiz ?
- (axis.opposite ? 0 : 2) : // top : bottom
- (axis.opposite ? 1 : 3)); // right : left
- axis.setOptions(userOptions);
- var options = this.options,
- type = options.type,
- isDatetimeAxis = type === 'datetime';
- axis.labelFormatter = options.labels.formatter ||
- axis.defaultLabelFormatter; // can be overwritten by dynamic format
- // Flag, stagger lines or not
- axis.userOptions = userOptions;
- //axis.axisTitleMargin = undefined,// = options.title.margin,
- axis.minPixelPadding = 0;
- axis.reversed = options.reversed;
- axis.visible = options.visible !== false;
- axis.zoomEnabled = options.zoomEnabled !== false;
- // Initial categories
- axis.hasNames = type === 'category' || options.categories === true;
- axis.categories = options.categories || axis.hasNames;
- axis.names = axis.names || []; // Preserve on update (#3830)
- // Elements
- //axis.axisGroup = undefined;
- //axis.gridGroup = undefined;
- //axis.axisTitle = undefined;
- //axis.axisLine = undefined;
- // Placeholder for plotlines and plotbands groups
- axis.plotLinesAndBandsGroups = {};
- // Shorthand types
- axis.isLog = type === 'logarithmic';
- axis.isDatetimeAxis = isDatetimeAxis;
- axis.positiveValuesOnly = axis.isLog && !axis.allowNegativeLog;
- // Flag, if axis is linked to another axis
- axis.isLinked = defined(options.linkedTo);
- // Linked axis.
- //axis.linkedParent = undefined;
- // Major ticks
- axis.ticks = {};
- axis.labelEdge = [];
- // Minor ticks
- axis.minorTicks = {};
- // List of plotLines/Bands
- axis.plotLinesAndBands = [];
- // Alternate bands
- axis.alternateBands = {};
- // Axis metrics
- //axis.left = undefined;
- //axis.top = undefined;
- //axis.width = undefined;
- //axis.height = undefined;
- //axis.bottom = undefined;
- //axis.right = undefined;
- //axis.transA = undefined;
- //axis.transB = undefined;
- //axis.oldTransA = undefined;
- axis.len = 0;
- //axis.oldMin = undefined;
- //axis.oldMax = undefined;
- //axis.oldUserMin = undefined;
- //axis.oldUserMax = undefined;
- //axis.oldAxisLength = undefined;
- axis.minRange = axis.userMinRange = options.minRange || options.maxZoom;
- axis.range = options.range;
- axis.offset = options.offset || 0;
- // Dictionary for stacks
- axis.stacks = {};
- axis.oldStacks = {};
- axis.stacksTouched = 0;
- // Min and max in the data
- //axis.dataMin = undefined,
- //axis.dataMax = undefined,
- // The axis range
- axis.max = null;
- axis.min = null;
- // User set min and max
- //axis.userMin = undefined,
- //axis.userMax = undefined,
- // Crosshair options
- axis.crosshair = pick(
- options.crosshair,
- splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1],
- false
- );
- var events = axis.options.events;
- // Register. Don't add it again on Axis.update().
- if (inArray(axis, chart.axes) === -1) { //
- if (isXAxis) { // #2713
- chart.axes.splice(chart.xAxis.length, 0, axis);
- } else {
- chart.axes.push(axis);
- }
- chart[axis.coll].push(axis);
- }
- axis.series = axis.series || []; // populated by Series
- // inverted charts have reversed xAxes as default
- if (chart.inverted && !axis.isZAxis && isXAxis && axis.reversed === undefined) {
- axis.reversed = true;
- }
- // register event listeners
- objectEach(events, function(event, eventType) {
- addEvent(axis, eventType, event);
- });
- // extend logarithmic axis
- axis.lin2log = options.linearToLogConverter || axis.lin2log;
- if (axis.isLog) {
- axis.val2lin = axis.log2lin;
- axis.lin2val = axis.lin2log;
- }
- },
- /**
- * Merge and set options
- */
- setOptions: function(userOptions) {
- this.options = merge(
- this.defaultOptions,
- this.coll === 'yAxis' && this.defaultYAxisOptions, [
- this.defaultTopAxisOptions,
- this.defaultRightAxisOptions,
- this.defaultBottomAxisOptions,
- this.defaultLeftAxisOptions
- ][this.side],
- merge(
- defaultOptions[this.coll], // if set in setOptions (#1053)
- userOptions
- )
- );
- },
- /**
- * The default label formatter. The context is a special config object for
- * the label. In apps, use the {@link
- * https://api.highcharts.com/highcharts/xAxis.labels.formatter|
- * labels.formatter} instead except when a modification is needed.
- *
- * @private
- */
- defaultLabelFormatter: function() {
- var axis = this.axis,
- value = this.value,
- categories = axis.categories,
- dateTimeLabelFormat = this.dateTimeLabelFormat,
- lang = defaultOptions.lang,
- numericSymbols = lang.numericSymbols,
- numSymMagnitude = lang.numericSymbolMagnitude || 1000,
- i = numericSymbols && numericSymbols.length,
- multi,
- ret,
- formatOption = axis.options.labels.format,
- // make sure the same symbol is added for all labels on a linear
- // axis
- numericSymbolDetector = axis.isLog ?
- Math.abs(value) :
- axis.tickInterval;
- if (formatOption) {
- ret = format(formatOption, this);
- } else if (categories) {
- ret = value;
- } else if (dateTimeLabelFormat) { // datetime axis
- ret = H.dateFormat(dateTimeLabelFormat, value);
- } else if (i && numericSymbolDetector >= 1000) {
- // Decide whether we should add a numeric symbol like k (thousands)
- // or M (millions). If we are to enable this in tooltip or other
- // places as well, we can move this logic to the numberFormatter and
- // enable it by a parameter.
- while (i-- && ret === undefined) {
- multi = Math.pow(numSymMagnitude, i + 1);
- if (
- numericSymbolDetector >= multi &&
- (value * 10) % multi === 0 &&
- numericSymbols[i] !== null &&
- value !== 0
- ) { // #5480
- ret = H.numberFormat(value / multi, -1) + numericSymbols[i];
- }
- }
- }
- if (ret === undefined) {
- if (Math.abs(value) >= 10000) { // add thousands separators
- ret = H.numberFormat(value, -1);
- } else { // small numbers
- ret = H.numberFormat(value, -1, undefined, ''); // #2466
- }
- }
- return ret;
- },
- /**
- * Get the minimum and maximum for the series of each axis
- */
- getSeriesExtremes: function() {
- var axis = this,
- chart = axis.chart;
- axis.hasVisibleSeries = false;
- // Reset properties in case we're redrawing (#3353)
- axis.dataMin = axis.dataMax = axis.threshold = null;
- axis.softThreshold = !axis.isXAxis;
- if (axis.buildStacks) {
- axis.buildStacks();
- }
- // loop through this axis' series
- each(axis.series, function(series) {
- if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
- var seriesOptions = series.options,
- xData,
- threshold = seriesOptions.threshold,
- seriesDataMin,
- seriesDataMax;
- axis.hasVisibleSeries = true;
- // Validate threshold in logarithmic axes
- if (axis.positiveValuesOnly && threshold <= 0) {
- threshold = null;
- }
- // Get dataMin and dataMax for X axes
- if (axis.isXAxis) {
- xData = series.xData;
- if (xData.length) {
- // If xData contains values which is not numbers, then
- // filter them out. To prevent performance hit, we only
- // do this after we have already found seriesDataMin
- // because in most cases all data is valid. #5234.
- seriesDataMin = arrayMin(xData);
- if (!isNumber(seriesDataMin) &&
- !(seriesDataMin instanceof Date) // #5010
- ) {
- xData = grep(xData, function(x) {
- return isNumber(x);
- });
- seriesDataMin = arrayMin(xData); // Do it again with valid data
- }
- axis.dataMin = Math.min(
- pick(axis.dataMin, xData[0]),
- seriesDataMin
- );
- axis.dataMax = Math.max(
- pick(axis.dataMax, xData[0]),
- arrayMax(xData)
- );
- }
- // Get dataMin and dataMax for Y axes, as well as handle
- // stacking and processed data
- } else {
- // Get this particular series extremes
- series.getExtremes();
- seriesDataMax = series.dataMax;
- seriesDataMin = series.dataMin;
- // Get the dataMin and dataMax so far. If percentage is
- // used, the min and max are always 0 and 100. If
- // seriesDataMin and seriesDataMax is null, then series
- // doesn't have active y data, we continue with nulls
- if (defined(seriesDataMin) && defined(seriesDataMax)) {
- axis.dataMin = Math.min(
- pick(axis.dataMin, seriesDataMin),
- seriesDataMin
- );
- axis.dataMax = Math.max(
- pick(axis.dataMax, seriesDataMax),
- seriesDataMax
- );
- }
- // Adjust to threshold
- if (defined(threshold)) {
- axis.threshold = threshold;
- }
- // If any series has a hard threshold, it takes precedence
- if (!seriesOptions.softThreshold ||
- axis.positiveValuesOnly
- ) {
- axis.softThreshold = false;
- }
- }
- }
- });
- },
- /**
- * Translate from axis value to pixel position on the chart, or back
- *
- */
- translate: function(val, backwards, cvsCoord, old, handleLog, pointPlacement) {
- var axis = this.linkedParent || this, // #1417
- sign = 1,
- cvsOffset = 0,
- localA = old ? axis.oldTransA : axis.transA,
- localMin = old ? axis.oldMin : axis.min,
- returnValue,
- minPixelPadding = axis.minPixelPadding,
- doPostTranslate = (axis.isOrdinal || axis.isBroken || (axis.isLog && handleLog)) && axis.lin2val;
- if (!localA) {
- localA = axis.transA;
- }
- // In vertical axes, the canvas coordinates start from 0 at the top like in
- // SVG.
- if (cvsCoord) {
- sign *= -1; // canvas coordinates inverts the value
- cvsOffset = axis.len;
- }
- // Handle reversed axis
- if (axis.reversed) {
- sign *= -1;
- cvsOffset -= sign * (axis.sector || axis.len);
- }
- // From pixels to value
- if (backwards) { // reverse translation
- val = val * sign + cvsOffset;
- val -= minPixelPadding;
- returnValue = val / localA + localMin; // from chart pixel to value
- if (doPostTranslate) { // log and ordinal axes
- returnValue = axis.lin2val(returnValue);
- }
- // From value to pixels
- } else {
- if (doPostTranslate) { // log and ordinal axes
- val = axis.val2lin(val);
- }
- returnValue = sign * (val - localMin) * localA + cvsOffset +
- (sign * minPixelPadding) +
- (isNumber(pointPlacement) ? localA * pointPlacement : 0);
- }
- return returnValue;
- },
- /**
- * Translate a value in terms of axis units into pixels within the chart.
- *
- * @param {Number} value
- * A value in terms of axis units.
- * @param {Boolean} paneCoordinates
- * Whether to return the pixel coordinate relative to the chart or
- * just the axis/pane itself.
- * @return {Number} Pixel position of the value on the chart or axis.
- */
- toPixels: function(value, paneCoordinates) {
- return this.translate(value, false, !this.horiz, null, true) +
- (paneCoordinates ? 0 : this.pos);
- },
- /**
- * Translate a pixel position along the axis to a value in terms of axis
- * units.
- * @param {Number} pixel
- * The pixel value coordinate.
- * @param {Boolean} paneCoordiantes
- * Whether the input pixel is relative to the chart or just the
- * axis/pane itself.
- * @return {Number} The axis value.
- */
- toValue: function(pixel, paneCoordinates) {
- return this.translate(
- pixel - (paneCoordinates ? 0 : this.pos),
- true, !this.horiz,
- null,
- true
- );
- },
- /**
- * Create the path for a plot line that goes from the given value on
- * this axis, across the plot to the opposite side
- * @param {Number} value
- * @param {Number} lineWidth Used for calculation crisp line
- * @param {Number] old Use old coordinates (for resizing and rescaling)
- */
- getPlotLinePath: function(value, lineWidth, old, force, translatedValue) {
- var axis = this,
- chart = axis.chart,
- axisLeft = axis.left,
- axisTop = axis.top,
- x1,
- y1,
- x2,
- y2,
- cHeight = (old && chart.oldChartHeight) || chart.chartHeight,
- cWidth = (old && chart.oldChartWidth) || chart.chartWidth,
- skip,
- transB = axis.transB,
- /**
- * Check if x is between a and b. If not, either move to a/b or skip,
- * depending on the force parameter.
- */
- between = function(x, a, b) {
- if (x < a || x > b) {
- if (force) {
- x = Math.min(Math.max(a, x), b);
- } else {
- skip = true;
- }
- }
- return x;
- };
- translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
- x1 = x2 = Math.round(translatedValue + transB);
- y1 = y2 = Math.round(cHeight - translatedValue - transB);
- if (!isNumber(translatedValue)) { // no min or max
- skip = true;
- } else if (axis.horiz) {
- y1 = axisTop;
- y2 = cHeight - axis.bottom;
- x1 = x2 = between(x1, axisLeft, axisLeft + axis.width);
- } else {
- x1 = axisLeft;
- x2 = cWidth - axis.right;
- y1 = y2 = between(y1, axisTop, axisTop + axis.height);
- }
- return skip && !force ?
- null :
- chart.renderer.crispLine(['M', x1, y1, 'L', x2, y2], lineWidth || 1);
- },
- /**
- * Internal function to et the tick positions of a linear axis to round
- * values like whole tens or every five.
- *
- * @param {Number} tickInterval
- * The normalized tick interval
- * @param {Number} min
- * Axis minimum.
- * @param {Number} max
- * Axis maximum.
- *
- * @return {Array.<Number>}
- * An array of numbers where ticks should be placed.
- */
- getLinearTickPositions: function(tickInterval, min, max) {
- var pos,
- lastPos,
- roundedMin = correctFloat(Math.floor(min / tickInterval) * tickInterval),
- roundedMax = correctFloat(Math.ceil(max / tickInterval) * tickInterval),
- tickPositions = [];
- // For single points, add a tick regardless of the relative position
- // (#2662, #6274)
- if (this.single) {
- return [min];
- }
- // Populate the intermediate values
- pos = roundedMin;
- while (pos <= roundedMax) {
- // Place the tick on the rounded value
- tickPositions.push(pos);
- // Always add the raw tickInterval, not the corrected one.
- pos = correctFloat(pos + tickInterval);
- // If the interval is not big enough in the current min - max range to actually increase
- // the loop variable, we need to break out to prevent endless loop. Issue #619
- if (pos === lastPos) {
- break;
- }
- // Record the last value
- lastPos = pos;
- }
- return tickPositions;
- },
- /**
- * Return the minor tick positions. For logarithmic axes, reuse the same logic
- * as for major ticks.
- */
- getMinorTickPositions: function() {
- var axis = this,
- options = axis.options,
- tickPositions = axis.tickPositions,
- minorTickInterval = axis.minorTickInterval,
- minorTickPositions = [],
- pos,
- pointRangePadding = axis.pointRangePadding || 0,
- min = axis.min - pointRangePadding, // #1498
- max = axis.max + pointRangePadding, // #1498
- range = max - min;
- // If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them.
- if (range && range / minorTickInterval < axis.len / 3) { // #3875
- if (axis.isLog) {
- // For each interval in the major ticks, compute the minor ticks
- // separately.
- each(this.paddedTicks, function(pos, i, paddedTicks) {
- if (i) {
- minorTickPositions.push.apply(
- minorTickPositions,
- axis.getLogTickPositions(
- minorTickInterval,
- paddedTicks[i - 1],
- paddedTicks[i],
- true
- )
- );
- }
- });
- } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314
- minorTickPositions = minorTickPositions.concat(
- axis.getTimeTicks(
- axis.normalizeTimeTickInterval(minorTickInterval),
- min,
- max,
- options.startOfWeek
- )
- );
- } else {
- for (
- pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval
- ) {
- // Very, very, tight grid lines (#5771)
- if (pos === minorTickPositions[0]) {
- break;
- }
- minorTickPositions.push(pos);
- }
- }
- }
- if (minorTickPositions.length !== 0) {
- axis.trimTicks(minorTickPositions); // #3652 #3743 #1498 #6330
- }
- return minorTickPositions;
- },
- /**
- * Adjust the min and max for the minimum range. Keep in mind that the series data is
- * not yet processed, so we don't have information on data cropping and grouping, or
- * updated axis.pointRange or series.pointRange. The data can't be processed until
- * we have finally established min and max.
- *
- * @private
- */
- adjustForMinRange: function() {
- var axis = this,
- options = axis.options,
- min = axis.min,
- max = axis.max,
- zoomOffset,
- spaceAvailable,
- closestDataRange,
- i,
- distance,
- xData,
- loopLength,
- minArgs,
- maxArgs,
- minRange;
- // Set the automatic minimum range based on the closest point distance
- if (axis.isXAxis && axis.minRange === undefined && !axis.isLog) {
- if (defined(options.min) || defined(options.max)) {
- axis.minRange = null; // don't do this again
- } else {
- // Find the closest distance between raw data points, as opposed to
- // closestPointRange that applies to processed points (cropped and grouped)
- each(axis.series, function(series) {
- xData = series.xData;
- loopLength = series.xIncrement ? 1 : xData.length - 1;
- for (i = loopLength; i > 0; i--) {
- distance = xData[i] - xData[i - 1];
- if (closestDataRange === undefined || distance < closestDataRange) {
- closestDataRange = distance;
- }
- }
- });
- axis.minRange = Math.min(closestDataRange * 5, axis.dataMax - axis.dataMin);
- }
- }
- // if minRange is exceeded, adjust
- if (max - min < axis.minRange) {
- spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange;
- minRange = axis.minRange;
- zoomOffset = (minRange - max + min) / 2;
- // if min and max options have been set, don't go beyond it
- minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)];
- if (spaceAvailable) { // if space is available, stay within the data range
- minArgs[2] = axis.isLog ? axis.log2lin(axis.dataMin) : axis.dataMin;
- }
- min = arrayMax(minArgs);
- maxArgs = [min + minRange, pick(options.max, min + minRange)];
- if (spaceAvailable) { // if space is availabe, stay within the data range
- maxArgs[2] = axis.isLog ? axis.log2lin(axis.dataMax) : axis.dataMax;
- }
- max = arrayMin(maxArgs);
- // now if the max is adjusted, adjust the min back
- if (max - min < minRange) {
- minArgs[0] = max - minRange;
- minArgs[1] = pick(options.min, max - minRange);
- min = arrayMax(minArgs);
- }
- }
- // Record modified extremes
- axis.min = min;
- axis.max = max;
- },
- /**
- * Find the closestPointRange across all series.
- *
- * @private
- */
- getClosest: function() {
- var ret;
- if (this.categories) {
- ret = 1;
- } else {
- each(this.series, function(series) {
- var seriesClosest = series.closestPointRange,
- visible = series.visible ||
- !series.chart.options.chart.ignoreHiddenSeries;
- if (!series.noSharedTooltip &&
- defined(seriesClosest) &&
- visible
- ) {
- ret = defined(ret) ?
- Math.min(ret, seriesClosest) :
- seriesClosest;
- }
- });
- }
- return ret;
- },
- /**
- * When a point name is given and no x, search for the name in the existing categories,
- * or if categories aren't provided, search names or create a new category (#2522).
- */
- nameToX: function(point) {
- var explicitCategories = isArray(this.categories),
- names = explicitCategories ? this.categories : this.names,
- nameX = point.options.x,
- x;
- point.series.requireSorting = false;
- if (!defined(nameX)) {
- nameX = this.options.uniqueNames === false ?
- point.series.autoIncrement() :
- inArray(point.name, names);
- }
- if (nameX === -1) { // The name is not found in currenct categories
- if (!explicitCategories) {
- x = names.length;
- }
- } else {
- x = nameX;
- }
- // Write the last point's name to the names array
- if (x !== undefined) {
- this.names[x] = point.name;
- }
- return x;
- },
- /**
- * When changes have been done to series data, update the axis.names.
- */
- updateNames: function() {
- var axis = this;
- if (this.names.length > 0) {
- this.names.length = 0;
- this.minRange = this.userMinRange; // Reset
- each(this.series || [], function(series) {
- // Reset incrementer (#5928)
- series.xIncrement = null;
- // When adding a series, points are not yet generated
- if (!series.points || series.isDirtyData) {
- series.processData();
- series.generatePoints();
- }
- each(series.points, function(point, i) {
- var x;
- if (point.options) {
- x = axis.nameToX(point);
- if (x !== undefined && x !== point.x) {
- point.x = x;
- series.xData[i] = x;
- }
- }
- });
- });
- }
- },
- /**
- * Update translation information
- */
- setAxisTranslation: function(saveOld) {
- var axis = this,
- range = axis.max - axis.min,
- pointRange = axis.axisPointRange || 0,
- closestPointRange,
- minPointOffset = 0,
- pointRangePadding = 0,
- linkedParent = axis.linkedParent,
- ordinalCorrection,
- hasCategories = !!axis.categories,
- transA = axis.transA,
- isXAxis = axis.isXAxis;
- // Adjust translation for padding. Y axis with categories need to go through the same (#1784).
- if (isXAxis || hasCategories || pointRange) {
- // Get the closest points
- closestPointRange = axis.getClosest();
- if (linkedParent) {
- minPointOffset = linkedParent.minPointOffset;
- pointRangePadding = linkedParent.pointRangePadding;
- } else {
- each(axis.series, function(series) {
- var seriesPointRange = hasCategories ?
- 1 :
- (isXAxis ?
- pick(series.options.pointRange, closestPointRange, 0) :
- (axis.axisPointRange || 0)), // #2806
- pointPlacement = series.options.pointPlacement;
- pointRange = Math.max(pointRange, seriesPointRange);
- if (!axis.single) {
- // minPointOffset is the value padding to the left of the axis in order to make
- // room for points with a pointRange, typically columns. When the pointPlacement option
- // is 'between' or 'on', this padding does not apply.
- minPointOffset = Math.max(
- minPointOffset,
- isString(pointPlacement) ? 0 : seriesPointRange / 2
- );
- // Determine the total padding needed to the length of the axis to make room for the
- // pointRange. If the series' pointPlacement is 'on', no padding is added.
- pointRangePadding = Math.max(
- pointRangePadding,
- pointPlacement === 'on' ? 0 : seriesPointRange
- );
- }
- });
- }
- // Record minPointOffset and pointRangePadding
- ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853
- axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection;
- axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection;
- // pointRange means the width reserved for each point, like in a column chart
- axis.pointRange = Math.min(pointRange, range);
- // closestPointRange means the closest distance between points. In columns
- // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange
- // is some other value
- if (isXAxis) {
- axis.closestPointRange = closestPointRange;
- }
- }
- // Secondary values
- if (saveOld) {
- axis.oldTransA = transA;
- }
- axis.translationSlope = axis.transA = transA =
- axis.options.staticScale ||
- axis.len / ((range + pointRangePadding) || 1);
- axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend
- axis.minPixelPadding = transA * minPointOffset;
- },
- minFromRange: function() {
- return this.max - this.range;
- },
- /**
- * Set the tick positions to round values and optionally extend the extremes
- * to the nearest tick
- */
- setTickInterval: function(secondPass) {
- var axis = this,
- chart = axis.chart,
- options = axis.options,
- isLog = axis.isLog,
- log2lin = axis.log2lin,
- isDatetimeAxis = axis.isDatetimeAxis,
- isXAxis = axis.isXAxis,
- isLinked = axis.isLinked,
- maxPadding = options.maxPadding,
- minPadding = options.minPadding,
- length,
- linkedParentExtremes,
- tickIntervalOption = options.tickInterval,
- minTickInterval,
- tickPixelIntervalOption = options.tickPixelInterval,
- categories = axis.categories,
- threshold = axis.threshold,
- softThreshold = axis.softThreshold,
- thresholdMin,
- thresholdMax,
- hardMin,
- hardMax;
- if (!isDatetimeAxis && !categories && !isLinked) {
- this.getTickAmount();
- }
- // Min or max set either by zooming/setExtremes or initial options
- hardMin = pick(axis.userMin, options.min);
- hardMax = pick(axis.userMax, options.max);
- // Linked axis gets the extremes from the parent axis
- if (isLinked) {
- axis.linkedParent = chart[axis.coll][options.linkedTo];
- linkedParentExtremes = axis.linkedParent.getExtremes();
- axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
- axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
- if (options.type !== axis.linkedParent.options.type) {
- H.error(11, 1); // Can't link axes of different type
- }
- // Initial min and max from the extreme data values
- } else {
- // Adjust to hard threshold
- if (!softThreshold && defined(threshold)) {
- if (axis.dataMin >= threshold) {
- thresholdMin = threshold;
- minPadding = 0;
- } else if (axis.dataMax <= threshold) {
- thresholdMax = threshold;
- maxPadding = 0;
- }
- }
- axis.min = pick(hardMin, thresholdMin, axis.dataMin);
- axis.max = pick(hardMax, thresholdMax, axis.dataMax);
- }
- if (isLog) {
- if (
- axis.positiveValuesOnly &&
- !secondPass &&
- Math.min(axis.min, pick(axis.dataMin, axis.min)) <= 0
- ) { // #978
- H.error(10, 1); // Can't plot negative values on log axis
- }
- // The correctFloat cures #934, float errors on full tens. But it
- // was too aggressive for #4360 because of conversion back to lin,
- // therefore use precision 15.
- axis.min = correctFloat(log2lin(axis.min), 15);
- axis.max = correctFloat(log2lin(axis.max), 15);
- }
- // handle zoomed range
- if (axis.range && defined(axis.max)) {
- axis.userMin = axis.min = hardMin = Math.max(axis.min, axis.minFromRange()); // #618
- axis.userMax = hardMax = axis.max;
- axis.range = null; // don't use it when running setExtremes
- }
- // Hook for Highstock Scroller. Consider combining with beforePadding.
- fireEvent(axis, 'foundExtremes');
- // Hook for adjusting this.min and this.max. Used by bubble series.
- if (axis.beforePadding) {
- axis.beforePadding();
- }
- // adjust min and max for the minimum range
- axis.adjustForMinRange();
- // Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding
- // into account, we do this after computing tick interval (#1337).
- if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) {
- length = axis.max - axis.min;
- if (length) {
- if (!defined(hardMin) && minPadding) {
- axis.min -= length * minPadding;
- }
- if (!defined(hardMax) && maxPadding) {
- axis.max += length * maxPadding;
- }
- }
- }
- // Handle options for floor, ceiling, softMin and softMax (#6359)
- if (isNumber(options.softMin)) {
- axis.min = Math.min(axis.min, options.softMin);
- }
- if (isNumber(options.softMax)) {
- axis.max = Math.max(axis.max, options.softMax);
- }
- if (isNumber(options.floor)) {
- axis.min = Math.max(axis.min, options.floor);
- }
- if (isNumber(options.ceiling)) {
- axis.max = Math.min(axis.max, options.ceiling);
- }
- // When the threshold is soft, adjust the extreme value only if
- // the data extreme and the padded extreme land on either side of the threshold. For example,
- // a series of [0, 1, 2, 3] would make the yAxis add a tick for -1 because of the
- // default minPadding and startOnTick options. This is prevented by the softThreshold
- // option.
- if (softThreshold && defined(axis.dataMin)) {
- threshold = threshold || 0;
- if (!defined(hardMin) && axis.min < threshold && axis.dataMin >= threshold) {
- axis.min = threshold;
- } else if (!defined(hardMax) && axis.max > threshold && axis.dataMax <= threshold) {
- axis.max = threshold;
- }
- }
- // get tickInterval
- if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) {
- axis.tickInterval = 1;
- } else if (isLinked && !tickIntervalOption &&
- tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) {
- axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval;
- } else {
- axis.tickInterval = pick(
- tickIntervalOption,
- this.tickAmount ? ((axis.max - axis.min) / Math.max(this.tickAmount - 1, 1)) : undefined,
- categories ? // for categoried axis, 1 is default, for linear axis use tickPix
- 1 :
- // don't let it be more than the data range
- (axis.max - axis.min) * tickPixelIntervalOption / Math.max(axis.len, tickPixelIntervalOption)
- );
- }
- // Now we're finished detecting min and max, crop and group series data. This
- // is in turn needed in order to find tick positions in ordinal axes.
- if (isXAxis && !secondPass) {
- each(axis.series, function(series) {
- series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax);
- });
- }
- // set the translation factor used in translate function
- axis.setAxisTranslation(true);
- // hook for ordinal axes and radial axes
- if (axis.beforeSetTickPositions) {
- axis.beforeSetTickPositions();
- }
- // hook for extensions, used in Highstock ordinal axes
- if (axis.postProcessTickInterval) {
- axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval);
- }
- // In column-like charts, don't cramp in more ticks than there are points (#1943, #4184)
- if (axis.pointRange && !tickIntervalOption) {
- axis.tickInterval = Math.max(axis.pointRange, axis.tickInterval);
- }
- // Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined.
- minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange);
- if (!tickIntervalOption && axis.tickInterval < minTickInterval) {
- axis.tickInterval = minTickInterval;
- }
- // for linear axes, get magnitude and normalize the interval
- if (!isDatetimeAxis && !isLog && !tickIntervalOption) {
- axis.tickInterval = normalizeTickInterval(
- axis.tickInterval,
- null,
- getMagnitude(axis.tickInterval),
- // If the tick interval is between 0.5 and 5 and the axis max is in the order of
- // thousands, chances are we are dealing with years. Don't allow decimals. #3363.
- pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)), !!this.tickAmount
- );
- }
- // Prevent ticks from getting so close that we can't draw the labels
- if (!this.tickAmount) {
- axis.tickInterval = axis.unsquish();
- }
- this.setTickPositions();
- },
- /**
- * Now we have computed the normalized tickInterval, get the tick positions
- */
- setTickPositions: function() {
- var options = this.options,
- tickPositions,
- tickPositionsOption = options.tickPositions,
- tickPositioner = options.tickPositioner,
- startOnTick = options.startOnTick,
- endOnTick = options.endOnTick;
- // Set the tickmarkOffset
- this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' &&
- this.tickInterval === 1) ? 0.5 : 0; // #3202
- // get minorTickInterval
- this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ?
- this.tickInterval / 5 : options.minorTickInterval;
- // When there is only one point, or all points have the same value on
- // this axis, then min and max are equal and tickPositions.length is 0
- // or 1. In this case, add some padding in order to center the point,
- // but leave it with one tick. #1337.
- this.single =
- this.min === this.max &&
- defined(this.min) &&
- !this.tickAmount &&
- (
- // Data is on integer (#6563)
- parseInt(this.min, 10) === this.min ||
- // Between integers and decimals are not allowed (#6274)
- options.allowDecimals !== false
- );
- // Find the tick positions
- this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565)
- if (!tickPositions) {
- if (this.isDatetimeAxis) {
- tickPositions = this.getTimeTicks(
- this.normalizeTimeTickInterval(
- this.tickInterval,
- options.units
- ),
- this.min,
- this.max,
- options.startOfWeek,
- this.ordinalPositions,
- this.closestPointRange,
- true
- );
- } else if (this.isLog) {
- tickPositions = this.getLogTickPositions(
- this.tickInterval,
- this.min,
- this.max
- );
- } else {
- tickPositions = this.getLinearTickPositions(
- this.tickInterval,
- this.min,
- this.max
- );
- }
- // Too dense ticks, keep only the first and last (#4477)
- if (tickPositions.length > this.len) {
- tickPositions = [tickPositions[0], tickPositions.pop()];
- }
- this.tickPositions = tickPositions;
- // Run the tick positioner callback, that allows modifying auto tick positions.
- if (tickPositioner) {
- tickPositioner = tickPositioner.apply(this, [this.min, this.max]);
- if (tickPositioner) {
- this.tickPositions = tickPositions = tickPositioner;
- }
- }
- }
- // Reset min/max or remove extremes based on start/end on tick
- this.paddedTicks = tickPositions.slice(0); // Used for logarithmic minor
- this.trimTicks(tickPositions, startOnTick, endOnTick);
- if (!this.isLinked) {
- // Substract half a unit (#2619, #2846, #2515, #3390)
- if (this.single) {
- this.min -= 0.5;
- this.max += 0.5;
- }
- if (!tickPositionsOption && !tickPositioner) {
- this.adjustTickAmount();
- }
- }
- },
- /**
- * Handle startOnTick and endOnTick by either adapting to padding min/max or rounded min/max
- */
- trimTicks: function(tickPositions, startOnTick, endOnTick) {
- var roundedMin = tickPositions[0],
- roundedMax = tickPositions[tickPositions.length - 1],
- minPointOffset = this.minPointOffset || 0;
- if (!this.isLinked) {
- if (startOnTick && roundedMin !== -Infinity) { // #6502
- this.min = roundedMin;
- } else {
- while (this.min - minPointOffset > tickPositions[0]) {
- tickPositions.shift();
- }
- }
- if (endOnTick) {
- this.max = roundedMax;
- } else {
- while (this.max + minPointOffset < tickPositions[tickPositions.length - 1]) {
- tickPositions.pop();
- }
- }
- // If no tick are left, set one tick in the middle (#3195)
- if (tickPositions.length === 0 && defined(roundedMin)) {
- tickPositions.push((roundedMax + roundedMin) / 2);
- }
- }
- },
- /**
- * Check if there are multiple axes in the same pane.
- *
- * @private
- * @return {Boolean}
- * True if there are other axes.
- */
- alignToOthers: function() {
- var others = {}, // Whether there is another axis to pair with this one
- hasOther,
- options = this.options;
- if (
- // Only if alignTicks is true
- this.chart.options.chart.alignTicks !== false &&
- options.alignTicks !== false &&
- // Don't try to align ticks on a log axis, they are not evenly
- // spaced (#6021)
- !this.isLog
- ) {
- each(this.chart[this.coll], function(axis) {
- var otherOptions = axis.options,
- horiz = axis.horiz,
- key = [
- horiz ? otherOptions.left : otherOptions.top,
- otherOptions.width,
- otherOptions.height,
- otherOptions.pane
- ].join(',');
- if (axis.series.length) { // #4442
- if (others[key]) {
- hasOther = true; // #4201
- } else {
- others[key] = 1;
- }
- }
- });
- }
- return hasOther;
- },
- /**
- * Set the max ticks of either the x and y axis collection
- */
- getTickAmount: function() {
- var options = this.options,
- tickAmount = options.tickAmount,
- tickPixelInterval = options.tickPixelInterval;
- if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial &&
- !this.isLog && options.startOnTick && options.endOnTick) {
- tickAmount = 2;
- }
- if (!tickAmount && this.alignToOthers()) {
- // Add 1 because 4 tick intervals require 5 ticks (including first and last)
- tickAmount = Math.ceil(this.len / tickPixelInterval) + 1;
- }
- // For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This
- // prevents the axis from adding ticks that are too far away from the data extremes.
- if (tickAmount < 4) {
- this.finalTickAmt = tickAmount;
- tickAmount = 5;
- }
- this.tickAmount = tickAmount;
- },
- /**
- * When using multiple axes, adjust the number of ticks to match the highest
- * number of ticks in that group.
- *
- * @private
- */
- adjustTickAmount: function() {
- var tickInterval = this.tickInterval,
- tickPositions = this.tickPositions,
- tickAmount = this.tickAmount,
- finalTickAmt = this.finalTickAmt,
- currentTickAmount = tickPositions && tickPositions.length,
- i,
- len;
- if (currentTickAmount < tickAmount) {
- while (tickPositions.length < tickAmount) {
- tickPositions.push(correctFloat(
- tickPositions[tickPositions.length - 1] + tickInterval
- ));
- }
- this.transA *= (currentTickAmount - 1) / (tickAmount - 1);
- this.max = tickPositions[tickPositions.length - 1];
- // We have too many ticks, run second pass to try to reduce ticks
- } else if (currentTickAmount > tickAmount) {
- this.tickInterval *= 2;
- this.setTickPositions();
- }
- // The finalTickAmt property is set in getTickAmount
- if (defined(finalTickAmt)) {
- i = len = tickPositions.length;
- while (i--) {
- if (
- (finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick
- (finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last
- ) {
- tickPositions.splice(i, 1);
- }
- }
- this.finalTickAmt = undefined;
- }
- },
- /**
- * Set the scale based on data min and max, user set min and max or options
- *
- */
- setScale: function() {
- var axis = this,
- isDirtyData,
- isDirtyAxisLength;
- axis.oldMin = axis.min;
- axis.oldMax = axis.max;
- axis.oldAxisLength = axis.len;
- // set the new axisLength
- axis.setAxisSize();
- //axisLength = horiz ? axisWidth : axisHeight;
- isDirtyAxisLength = axis.len !== axis.oldAxisLength;
- // is there new data?
- each(axis.series, function(series) {
- if (series.isDirtyData || series.isDirty ||
- series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well
- isDirtyData = true;
- }
- });
- // do we really need to go through all this?
- if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw ||
- axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax || axis.alignToOthers()) {
- if (axis.resetStacks) {
- axis.resetStacks();
- }
- axis.forceRedraw = false;
- // get data extremes if needed
- axis.getSeriesExtremes();
- // get fixed positions based on tickInterval
- axis.setTickInterval();
- // record old values to decide whether a rescale is necessary later on (#540)
- axis.oldUserMin = axis.userMin;
- axis.oldUserMax = axis.userMax;
- // Mark as dirty if it is not already set to dirty and extremes have changed. #595.
- if (!axis.isDirty) {
- axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax;
- }
- } else if (axis.cleanStacks) {
- axis.cleanStacks();
- }
- },
- /**
- * Set the minimum and maximum of the axes after render time. If the
- * `startOnTick` and `endOnTick` options are true, the minimum and maximum
- * values are rounded off to the nearest tick. To prevent this, these
- * options can be set to false before calling setExtremes. Also, setExtremes
- * will not allow a range lower than the `minRange` option, which by default
- * is the range of five points.
- *
- * @param {Number} [newMin]
- * The new minimum value.
- * @param {Number} [newMax]
- * The new maximum value.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart or wait for an explicit call to
- * {@link Highcharts.Chart#redraw}
- * @param {AnimationOptions} [animation=true]
- * Enable or modify animations.
- * @param {Object} [eventArguments]
- * Arguments to be accessed in event handler.
- *
- * @sample highcharts/members/axis-setextremes/
- * Set extremes from a button
- * @sample highcharts/members/axis-setextremes-datetime/
- * Set extremes on a datetime axis
- * @sample highcharts/members/axis-setextremes-off-ticks/
- * Set extremes off ticks
- * @sample stock/members/axis-setextremes/
- * Set extremes in Highstock
- * @sample maps/members/axis-setextremes/
- * Set extremes in Highmaps
- */
- setExtremes: function(newMin, newMax, redraw, animation, eventArguments) {
- var axis = this,
- chart = axis.chart;
- redraw = pick(redraw, true); // defaults to true
- each(axis.series, function(serie) {
- delete serie.kdTree;
- });
- // Extend the arguments with min and max
- eventArguments = extend(eventArguments, {
- min: newMin,
- max: newMax
- });
- // Fire the event
- fireEvent(axis, 'setExtremes', eventArguments, function() { // the default event handler
- axis.userMin = newMin;
- axis.userMax = newMax;
- axis.eventArgs = eventArguments;
- if (redraw) {
- chart.redraw(animation);
- }
- });
- },
- /**
- * Overridable method for zooming chart. Pulled out in a separate method to allow overriding
- * in stock charts.
- */
- zoom: function(newMin, newMax) {
- var dataMin = this.dataMin,
- dataMax = this.dataMax,
- options = this.options,
- min = Math.min(dataMin, pick(options.min, dataMin)),
- max = Math.max(dataMax, pick(options.max, dataMax));
- if (newMin !== this.min || newMax !== this.max) { // #5790
- // Prevent pinch zooming out of range. Check for defined is for #1946. #1734.
- if (!this.allowZoomOutside) {
- // #6014, sometimes newMax will be smaller than min (or newMin will be larger than max).
- if (defined(dataMin)) {
- if (newMin < min) {
- newMin = min;
- }
- if (newMin > max) {
- newMin = max;
- }
- }
- if (defined(dataMax)) {
- if (newMax < min) {
- newMax = min;
- }
- if (newMax > max) {
- newMax = max;
- }
- }
- }
- // In full view, displaying the reset zoom button is not required
- this.displayBtn = newMin !== undefined || newMax !== undefined;
- // Do it
- this.setExtremes(
- newMin,
- newMax,
- false,
- undefined, {
- trigger: 'zoom'
- }
- );
- }
- return true;
- },
- /**
- * Update the axis metrics
- */
- setAxisSize: function() {
- var chart = this.chart,
- options = this.options,
- offsets = options.offsets || [0, 0, 0, 0], // top / right / bottom / left
- horiz = this.horiz,
- width = pick(options.width, chart.plotWidth - offsets[3] + offsets[1]),
- height = pick(options.height, chart.plotHeight - offsets[0] + offsets[2]),
- top = pick(options.top, chart.plotTop + offsets[0]),
- left = pick(options.left, chart.plotLeft + offsets[3]),
- percentRegex = /%$/;
- // Check for percentage based input values. Rounding fixes problems with
- // column overflow and plot line filtering (#4898, #4899)
- if (percentRegex.test(height)) {
- height = Math.round(parseFloat(height) / 100 * chart.plotHeight);
- }
- if (percentRegex.test(top)) {
- top = Math.round(parseFloat(top) / 100 * chart.plotHeight + chart.plotTop);
- }
- // Expose basic values to use in Series object and navigator
- this.left = left;
- this.top = top;
- this.width = width;
- this.height = height;
- this.bottom = chart.chartHeight - height - top;
- this.right = chart.chartWidth - width - left;
- // Direction agnostic properties
- this.len = Math.max(horiz ? width : height, 0); // Math.max fixes #905
- this.pos = horiz ? left : top; // distance from SVG origin
- },
- /**
- * The returned object literal from the {@link Highcharts.Axis#getExtremes}
- * function.
- * @typedef {Object} Extremes
- * @property {Number} dataMax
- * The maximum value of the axis' associated series.
- * @property {Number} dataMin
- * The minimum value of the axis' associated series.
- * @property {Number} max
- * The maximum axis value, either automatic or set manually. If the
- * `max` option is not set, `maxPadding` is 0 and `endOnTick` is
- * false, this value will be the same as `dataMax`.
- * @property {Number} min
- * The minimum axis value, either automatic or set manually. If the
- * `min` option is not set, `minPadding` is 0 and `startOnTick` is
- * false, this value will be the same as `dataMin`.
- */
- /**
- * Get the current extremes for the axis.
- *
- * @returns {Extremes}
- * An object containing extremes information.
- *
- * @sample members/axis-getextremes/
- * Report extremes by click on a button
- * @sample maps/members/axis-getextremes/
- * Get extremes in Highmaps
- */
- getExtremes: function() {
- var axis = this,
- isLog = axis.isLog,
- lin2log = axis.lin2log;
- return {
- min: isLog ? correctFloat(lin2log(axis.min)) : axis.min,
- max: isLog ? correctFloat(lin2log(axis.max)) : axis.max,
- dataMin: axis.dataMin,
- dataMax: axis.dataMax,
- userMin: axis.userMin,
- userMax: axis.userMax
- };
- },
- /**
- * Get the zero plane either based on zero or on the min or max value.
- * Used in bar and area plots
- */
- getThreshold: function(threshold) {
- var axis = this,
- isLog = axis.isLog,
- lin2log = axis.lin2log,
- realMin = isLog ? lin2log(axis.min) : axis.min,
- realMax = isLog ? lin2log(axis.max) : axis.max;
- if (threshold === null) {
- threshold = realMin;
- } else if (realMin > threshold) {
- threshold = realMin;
- } else if (realMax < threshold) {
- threshold = realMax;
- }
- return axis.translate(threshold, 0, 1, 0, 1);
- },
- /**
- * Compute auto alignment for the axis label based on which side the axis is
- * on and the given rotation for the label.
- *
- * @param {Number} rotation
- * The rotation in degrees as set by either the `rotation` or
- * `autoRotation` options.
- * @private
- */
- autoLabelAlign: function(rotation) {
- var ret,
- angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360;
- if (angle > 15 && angle < 165) {
- ret = 'right';
- } else if (angle > 195 && angle < 345) {
- ret = 'left';
- } else {
- ret = 'center';
- }
- return ret;
- },
- /**
- * Get the tick length and width for the axis.
- * @param {String} prefix 'tick' or 'minorTick'
- * @returns {Array} An array of tickLength and tickWidth
- */
- tickSize: function(prefix) {
- var options = this.options,
- tickLength = options[prefix + 'Length'],
- tickWidth = pick(options[prefix + 'Width'], prefix === 'tick' && this.isXAxis ? 1 : 0); // X axis defaults to 1
- if (tickWidth && tickLength) {
- // Negate the length
- if (options[prefix + 'Position'] === 'inside') {
- tickLength = -tickLength;
- }
- return [tickLength, tickWidth];
- }
- },
- /**
- * Return the size of the labels
- */
- labelMetrics: function() {
- var index = this.tickPositions && this.tickPositions[0] || 0;
- return this.chart.renderer.fontMetrics(
- this.options.labels.style && this.options.labels.style.fontSize,
- this.ticks[index] && this.ticks[index].label
- );
- },
- /**
- * Prevent the ticks from getting so close we can't draw the labels. On a horizontal
- * axis, this is handled by rotating the labels, removing ticks and adding ellipsis.
- * On a vertical axis remove ticks and add ellipsis.
- */
- unsquish: function() {
- var labelOptions = this.options.labels,
- horiz = this.horiz,
- tickInterval = this.tickInterval,
- newTickInterval = tickInterval,
- slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval),
- rotation,
- rotationOption = labelOptions.rotation,
- labelMetrics = this.labelMetrics(),
- step,
- bestScore = Number.MAX_VALUE,
- autoRotation,
- // Return the multiple of tickInterval that is needed to avoid collision
- getStep = function(spaceNeeded) {
- var step = spaceNeeded / (slotSize || 1);
- step = step > 1 ? Math.ceil(step) : 1;
- return step * tickInterval;
- };
- if (horiz) {
- autoRotation = !labelOptions.staggerLines && !labelOptions.step && ( // #3971
- defined(rotationOption) ? [rotationOption] :
- slotSize < pick(labelOptions.autoRotationLimit, 80) && labelOptions.autoRotation
- );
- if (autoRotation) {
- // Loop over the given autoRotation options, and determine which gives the best score. The
- // best score is that with the lowest number of steps and a rotation closest to horizontal.
- each(autoRotation, function(rot) {
- var score;
- if (rot === rotationOption || (rot && rot >= -90 && rot <= 90)) { // #3891
- step = getStep(Math.abs(labelMetrics.h / Math.sin(deg2rad * rot)));
- score = step + Math.abs(rot / 360);
- if (score < bestScore) {
- bestScore = score;
- rotation = rot;
- newTickInterval = step;
- }
- }
- });
- }
- } else if (!labelOptions.step) { // #4411
- newTickInterval = getStep(labelMetrics.h);
- }
- this.autoRotation = autoRotation;
- this.labelRotation = pick(rotation, rotationOption);
- return newTickInterval;
- },
- /**
- * Get the general slot width for this axis. This may change between the pre-render (from Axis.getOffset)
- * and the final tick rendering and placement (#5086).
- */
- getSlotWidth: function() {
- var chart = this.chart,
- horiz = this.horiz,
- labelOptions = this.options.labels,
- slotCount = Math.max(this.tickPositions.length - (this.categories ? 0 : 1), 1),
- marginLeft = chart.margin[3];
- return (
- horiz &&
- (labelOptions.step || 0) < 2 &&
- !labelOptions.rotation && // #4415
- ((this.staggerLines || 1) * this.len) / slotCount
- ) || (!horiz && (
- (marginLeft && (marginLeft - chart.spacing[3])) ||
- chart.chartWidth * 0.33
- )); // #1580, #1931
- },
- /**
- * Render the axis labels and determine whether ellipsis or rotation need to be applied
- */
- renderUnsquish: function() {
- var chart = this.chart,
- renderer = chart.renderer,
- tickPositions = this.tickPositions,
- ticks = this.ticks,
- labelOptions = this.options.labels,
- horiz = this.horiz,
- slotWidth = this.getSlotWidth(),
- innerWidth = Math.max(1, Math.round(slotWidth - 2 * (labelOptions.padding || 5))),
- attr = {},
- labelMetrics = this.labelMetrics(),
- textOverflowOption = labelOptions.style && labelOptions.style.textOverflow,
- css,
- maxLabelLength = 0,
- label,
- i,
- pos;
- // Set rotation option unless it is "auto", like in gauges
- if (!isString(labelOptions.rotation)) {
- attr.rotation = labelOptions.rotation || 0; // #4443
- }
- // Get the longest label length
- each(tickPositions, function(tick) {
- tick = ticks[tick];
- if (tick && tick.labelLength > maxLabelLength) {
- maxLabelLength = tick.labelLength;
- }
- });
- this.maxLabelLength = maxLabelLength;
- // Handle auto rotation on horizontal axis
- if (this.autoRotation) {
- // Apply rotation only if the label is too wide for the slot, and
- // the label is wider than its height.
- if (maxLabelLength > innerWidth && maxLabelLength > labelMetrics.h) {
- attr.rotation = this.labelRotation;
- } else {
- this.labelRotation = 0;
- }
- // Handle word-wrap or ellipsis on vertical axis
- } else if (slotWidth) {
- // For word-wrap or ellipsis
- css = {
- width: innerWidth + 'px'
- };
- if (!textOverflowOption) {
- css.textOverflow = 'clip';
- // On vertical axis, only allow word wrap if there is room for more lines.
- i = tickPositions.length;
- while (!horiz && i--) {
- pos = tickPositions[i];
- label = ticks[pos].label;
- if (label) {
- // Reset ellipsis in order to get the correct bounding box (#4070)
- if (label.styles && label.styles.textOverflow === 'ellipsis') {
- label.css({
- textOverflow: 'clip'
- });
- // Set the correct width in order to read the bounding box height (#4678, #5034)
- } else if (ticks[pos].labelLength > slotWidth) {
- label.css({
- width: slotWidth + 'px'
- });
- }
- if (label.getBBox().height > this.len / tickPositions.length - (labelMetrics.h - labelMetrics.f)) {
- label.specCss = {
- textOverflow: 'ellipsis'
- };
- }
- }
- }
- }
- }
- // Add ellipsis if the label length is significantly longer than ideal
- if (attr.rotation) {
- css = {
- width: (maxLabelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + 'px'
- };
- if (!textOverflowOption) {
- css.textOverflow = 'ellipsis';
- }
- }
- // Set the explicit or automatic label alignment
- this.labelAlign = labelOptions.align || this.autoLabelAlign(this.labelRotation);
- if (this.labelAlign) {
- attr.align = this.labelAlign;
- }
- // Apply general and specific CSS
- each(tickPositions, function(pos) {
- var tick = ticks[pos],
- label = tick && tick.label;
- if (label) {
- label.attr(attr); // This needs to go before the CSS in old IE (#4502)
- if (css) {
- label.css(merge(css, label.specCss));
- }
- delete label.specCss;
- tick.rotation = attr.rotation;
- }
- });
- // Note: Why is this not part of getLabelPosition?
- this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side !== 0);
- },
- /**
- * Return true if the axis has associated data
- */
- hasData: function() {
- return this.hasVisibleSeries || (defined(this.min) && defined(this.max) && !!this.tickPositions);
- },
- /**
- * Adds the title defined in axis.options.title.
- * @param {Boolean} display - whether or not to display the title
- */
- addTitle: function(display) {
- var axis = this,
- renderer = axis.chart.renderer,
- horiz = axis.horiz,
- opposite = axis.opposite,
- options = axis.options,
- axisTitleOptions = options.title,
- textAlign;
- if (!axis.axisTitle) {
- textAlign = axisTitleOptions.textAlign;
- if (!textAlign) {
- textAlign = (horiz ? {
- low: 'left',
- middle: 'center',
- high: 'right'
- } : {
- low: opposite ? 'right' : 'left',
- middle: 'center',
- high: opposite ? 'left' : 'right'
- })[axisTitleOptions.align];
- }
- axis.axisTitle = renderer.text(
- axisTitleOptions.text,
- 0,
- 0,
- axisTitleOptions.useHTML
- )
- .attr({
- zIndex: 7,
- rotation: axisTitleOptions.rotation || 0,
- align: textAlign
- })
- .addClass('highcharts-axis-title')
- .css(axisTitleOptions.style)
- .add(axis.axisGroup);
- axis.axisTitle.isNew = true;
- }
- // hide or show the title depending on whether showEmpty is set
- axis.axisTitle[display ? 'show' : 'hide'](true);
- },
- /**
- * Generates a tick for initial positioning.
- *
- * @private
- * @param {number} pos
- * The tick position in axis values.
- * @param {number} i
- * The index of the tick in {@link Axis.tickPositions}.
- */
- generateTick: function(pos) {
- var ticks = this.ticks;
- if (!ticks[pos]) {
- ticks[pos] = new Tick(this, pos);
- } else {
- ticks[pos].addLabel(); // update labels depending on tick interval
- }
- },
- /**
- * Render the tick labels to a preliminary position to get their sizes
- */
- getOffset: function() {
- var axis = this,
- chart = axis.chart,
- renderer = chart.renderer,
- options = axis.options,
- tickPositions = axis.tickPositions,
- ticks = axis.ticks,
- horiz = axis.horiz,
- side = axis.side,
- invertedSide = chart.inverted && !axis.isZAxis ? [1, 0, 3, 2][side] : side,
- hasData,
- showAxis,
- titleOffset = 0,
- titleOffsetOption,
- titleMargin = 0,
- axisTitleOptions = options.title,
- labelOptions = options.labels,
- labelOffset = 0, // reset
- labelOffsetPadded,
- axisOffset = chart.axisOffset,
- clipOffset = chart.clipOffset,
- clip,
- directionFactor = [-1, 1, 1, -1][side],
- className = options.className,
- axisParent = axis.axisParent, // Used in color axis
- lineHeightCorrection,
- tickSize = this.tickSize('tick');
- // For reuse in Axis.render
- hasData = axis.hasData();
- axis.showAxis = showAxis = hasData || pick(options.showEmpty, true);
- // Set/reset staggerLines
- axis.staggerLines = axis.horiz && labelOptions.staggerLines;
- // Create the axisGroup and gridGroup elements on first iteration
- if (!axis.axisGroup) {
- axis.gridGroup = renderer.g('grid')
- .attr({
- zIndex: options.gridZIndex || 1
- })
- .addClass('highcharts-' + this.coll.toLowerCase() + '-grid ' + (className || ''))
- .add(axisParent);
- axis.axisGroup = renderer.g('axis')
- .attr({
- zIndex: options.zIndex || 2
- })
- .addClass('highcharts-' + this.coll.toLowerCase() + ' ' + (className || ''))
- .add(axisParent);
- axis.labelGroup = renderer.g('axis-labels')
- .attr({
- zIndex: labelOptions.zIndex || 7
- })
- .addClass('highcharts-' + axis.coll.toLowerCase() + '-labels ' + (className || ''))
- .add(axisParent);
- }
- if (hasData || axis.isLinked) {
- // Generate ticks
- each(tickPositions, function(pos, i) {
- // i is not used here, but may be used in overrides
- axis.generateTick(pos, i);
- });
- axis.renderUnsquish();
- // Left side must be align: right and right side must have align: left for labels
- if (labelOptions.reserveSpace !== false && (side === 0 || side === 2 || {
- 1: 'left',
- 3: 'right'
- }[side] === axis.labelAlign || axis.labelAlign === 'center')) {
- each(tickPositions, function(pos) {
- // get the highest offset
- labelOffset = Math.max(
- ticks[pos].getLabelSize(),
- labelOffset
- );
- });
- }
- if (axis.staggerLines) {
- labelOffset *= axis.staggerLines;
- axis.labelOffset = labelOffset * (axis.opposite ? -1 : 1);
- }
- } else { // doesn't have data
- objectEach(ticks, function(tick, n) {
- tick.destroy();
- delete ticks[n];
- });
- }
- if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) {
- axis.addTitle(showAxis);
- if (showAxis && axisTitleOptions.reserveSpace !== false) {
- axis.titleOffset = titleOffset =
- axis.axisTitle.getBBox()[horiz ? 'height' : 'width'];
- titleOffsetOption = axisTitleOptions.offset;
- titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10);
- }
- }
- // Render the axis line
- axis.renderLine();
- // handle automatic or user set offset
- axis.offset = directionFactor * pick(options.offset, axisOffset[side]);
- axis.tickRotCorr = axis.tickRotCorr || {
- x: 0,
- y: 0
- }; // polar
- if (side === 0) {
- lineHeightCorrection = -axis.labelMetrics().h;
- } else if (side === 2) {
- lineHeightCorrection = axis.tickRotCorr.y;
- } else {
- lineHeightCorrection = 0;
- }
- // Find the padded label offset
- labelOffsetPadded = Math.abs(labelOffset) + titleMargin;
- if (labelOffset) {
- labelOffsetPadded -= lineHeightCorrection;
- labelOffsetPadded += directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + directionFactor * 8) : labelOptions.x);
- }
- axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded);
- axisOffset[side] = Math.max(
- axisOffset[side],
- axis.axisTitleMargin + titleOffset + directionFactor * axis.offset,
- labelOffsetPadded, // #3027
- hasData && tickPositions.length && tickSize ?
- tickSize[0] + directionFactor * axis.offset :
- 0 // #4866
- );
- // Decide the clipping needed to keep the graph inside the plot area and
- // axis lines
- clip = Math.floor(axis.axisLine.strokeWidth() / 2) * 2; // #4308, #4371
- if (options.offset > 0) {
- clip -= options.offset * 2;
- }
- clipOffset[invertedSide] = Math.max(
- clipOffset[invertedSide] || clip,
- clip
- );
- },
- /**
- * Internal function to get the path for the axis line. Extended for polar
- * charts.
- *
- * @param {Number} lineWidth
- * The line width in pixels.
- * @return {Array}
- * The SVG path definition in array form.
- */
- getLinePath: function(lineWidth) {
- var chart = this.chart,
- opposite = this.opposite,
- offset = this.offset,
- horiz = this.horiz,
- lineLeft = this.left + (opposite ? this.width : 0) + offset,
- lineTop = chart.chartHeight - this.bottom -
- (opposite ? this.height : 0) + offset;
- if (opposite) {
- lineWidth *= -1; // crispify the other way - #1480, #1687
- }
- return chart.renderer
- .crispLine([
- 'M',
- horiz ?
- this.left :
- lineLeft,
- horiz ?
- lineTop :
- this.top,
- 'L',
- horiz ?
- chart.chartWidth - this.right :
- lineLeft,
- horiz ?
- lineTop :
- chart.chartHeight - this.bottom
- ], lineWidth);
- },
- /**
- * Render the axis line
- */
- renderLine: function() {
- if (!this.axisLine) {
- this.axisLine = this.chart.renderer.path()
- .addClass('highcharts-axis-line')
- .add(this.axisGroup);
- this.axisLine.attr({
- stroke: this.options.lineColor,
- 'stroke-width': this.options.lineWidth,
- zIndex: 7
- });
- }
- },
- /**
- * Position the title
- */
- getTitlePosition: function() {
- // compute anchor points for each of the title align options
- var horiz = this.horiz,
- axisLeft = this.left,
- axisTop = this.top,
- axisLength = this.len,
- axisTitleOptions = this.options.title,
- margin = horiz ? axisLeft : axisTop,
- opposite = this.opposite,
- offset = this.offset,
- xOption = axisTitleOptions.x || 0,
- yOption = axisTitleOptions.y || 0,
- fontSize = this.chart.renderer.fontMetrics(axisTitleOptions.style && axisTitleOptions.style.fontSize, this.axisTitle).f,
- // the position in the length direction of the axis
- alongAxis = {
- low: margin + (horiz ? 0 : axisLength),
- middle: margin + axisLength / 2,
- high: margin + (horiz ? axisLength : 0)
- }[axisTitleOptions.align],
- // the position in the perpendicular direction of the axis
- offAxis = (horiz ? axisTop + this.height : axisLeft) +
- (horiz ? 1 : -1) * // horizontal axis reverses the margin
- (opposite ? -1 : 1) * // so does opposite axes
- this.axisTitleMargin +
- (this.side === 2 ? fontSize : 0);
- return {
- x: horiz ?
- alongAxis + xOption : offAxis + (opposite ? this.width : 0) + offset + xOption,
- y: horiz ?
- offAxis + yOption - (opposite ? this.height : 0) + offset : alongAxis + yOption
- };
- },
- /**
- * Render a minor tick into the given position. If a minor tick already
- * exists in this position, move it.
- * @param {number} pos - The position in axis values.
- */
- renderMinorTick: function(pos) {
- var slideInTicks = this.chart.hasRendered && isNumber(this.oldMin),
- minorTicks = this.minorTicks;
- if (!minorTicks[pos]) {
- minorTicks[pos] = new Tick(this, pos, 'minor');
- }
- // Render new ticks in old position
- if (slideInTicks && minorTicks[pos].isNew) {
- minorTicks[pos].render(null, true);
- }
- minorTicks[pos].render(null, false, 1);
- },
- /**
- * Render a major tick into the given position. If a tick already exists
- * in this position, move it.
- * @param {number} pos - The position in axis values
- * @param {number} i - The tick index
- */
- renderTick: function(pos, i) {
- var isLinked = this.isLinked,
- ticks = this.ticks,
- slideInTicks = this.chart.hasRendered && isNumber(this.oldMin);
- // Linked axes need an extra check to find out if
- if (!isLinked || (pos >= this.min && pos <= this.max)) {
- if (!ticks[pos]) {
- ticks[pos] = new Tick(this, pos);
- }
- // render new ticks in old position
- if (slideInTicks && ticks[pos].isNew) {
- ticks[pos].render(i, true, 0.1);
- }
- ticks[pos].render(i);
- }
- },
- /**
- * Render the axis
- */
- render: function() {
- var axis = this,
- chart = axis.chart,
- renderer = chart.renderer,
- options = axis.options,
- isLog = axis.isLog,
- lin2log = axis.lin2log,
- isLinked = axis.isLinked,
- tickPositions = axis.tickPositions,
- axisTitle = axis.axisTitle,
- ticks = axis.ticks,
- minorTicks = axis.minorTicks,
- alternateBands = axis.alternateBands,
- stackLabelOptions = options.stackLabels,
- alternateGridColor = options.alternateGridColor,
- tickmarkOffset = axis.tickmarkOffset,
- axisLine = axis.axisLine,
- showAxis = axis.showAxis,
- animation = animObject(renderer.globalAnimation),
- from,
- to;
- // Reset
- axis.labelEdge.length = 0;
- //axis.justifyToPlot = overflow === 'justify';
- axis.overlap = false;
- // Mark all elements inActive before we go over and mark the active ones
- each([ticks, minorTicks, alternateBands], function(coll) {
- objectEach(coll, function(tick) {
- tick.isActive = false;
- });
- });
- // If the series has data draw the ticks. Else only the line and title
- if (axis.hasData() || isLinked) {
- // minor ticks
- if (axis.minorTickInterval && !axis.categories) {
- each(axis.getMinorTickPositions(), function(pos) {
- axis.renderMinorTick(pos);
- });
- }
- // Major ticks. Pull out the first item and render it last so that
- // we can get the position of the neighbour label. #808.
- if (tickPositions.length) { // #1300
- each(tickPositions, function(pos, i) {
- axis.renderTick(pos, i);
- });
- // In a categorized axis, the tick marks are displayed between labels. So
- // we need to add a tick mark and grid line at the left edge of the X axis.
- if (tickmarkOffset && (axis.min === 0 || axis.single)) {
- if (!ticks[-1]) {
- ticks[-1] = new Tick(axis, -1, null, true);
- }
- ticks[-1].render(-1);
- }
- }
- // alternate grid color
- if (alternateGridColor) {
- each(tickPositions, function(pos, i) {
- to = tickPositions[i + 1] !== undefined ? tickPositions[i + 1] + tickmarkOffset : axis.max - tickmarkOffset;
- if (i % 2 === 0 && pos < axis.max && to <= axis.max + (chart.polar ? -tickmarkOffset : tickmarkOffset)) { // #2248, #4660
- if (!alternateBands[pos]) {
- alternateBands[pos] = new H.PlotLineOrBand(axis);
- }
- from = pos + tickmarkOffset; // #949
- alternateBands[pos].options = {
- from: isLog ? lin2log(from) : from,
- to: isLog ? lin2log(to) : to,
- color: alternateGridColor
- };
- alternateBands[pos].render();
- alternateBands[pos].isActive = true;
- }
- });
- }
- // custom plot lines and bands
- if (!axis._addedPlotLB) { // only first time
- each((options.plotLines || []).concat(options.plotBands || []), function(plotLineOptions) {
- axis.addPlotBandOrLine(plotLineOptions);
- });
- axis._addedPlotLB = true;
- }
- } // end if hasData
- // Remove inactive ticks
- each([ticks, minorTicks, alternateBands], function(coll) {
- var i,
- forDestruction = [],
- delay = animation.duration,
- destroyInactiveItems = function() {
- i = forDestruction.length;
- while (i--) {
- // When resizing rapidly, the same items may be destroyed in different timeouts,
- // or the may be reactivated
- if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) {
- coll[forDestruction[i]].destroy();
- delete coll[forDestruction[i]];
- }
- }
- };
- objectEach(coll, function(tick, pos) {
- if (!tick.isActive) {
- // Render to zero opacity
- tick.render(pos, false, 0);
- tick.isActive = false;
- forDestruction.push(pos);
- }
- });
- // When the objects are finished fading out, destroy them
- syncTimeout(
- destroyInactiveItems,
- coll === alternateBands || !chart.hasRendered || !delay ? 0 : delay
- );
- });
- // Set the axis line path
- if (axisLine) {
- axisLine[axisLine.isPlaced ? 'animate' : 'attr']({
- d: this.getLinePath(axisLine.strokeWidth())
- });
- axisLine.isPlaced = true;
- // Show or hide the line depending on options.showEmpty
- axisLine[showAxis ? 'show' : 'hide'](true);
- }
- if (axisTitle && showAxis) {
- var titleXy = axis.getTitlePosition();
- if (isNumber(titleXy.y)) {
- axisTitle[axisTitle.isNew ? 'attr' : 'animate'](titleXy);
- axisTitle.isNew = false;
- } else {
- axisTitle.attr('y', -9999);
- axisTitle.isNew = true;
- }
- }
- // Stacked totals:
- if (stackLabelOptions && stackLabelOptions.enabled) {
- axis.renderStackTotals();
- }
- // End stacked totals
- axis.isDirty = false;
- },
- /**
- * Redraw the axis to reflect changes in the data or axis extremes
- */
- redraw: function() {
- if (this.visible) {
- // render the axis
- this.render();
- // move plot lines and bands
- each(this.plotLinesAndBands, function(plotLine) {
- plotLine.render();
- });
- }
- // mark associated series as dirty and ready for redraw
- each(this.series, function(series) {
- series.isDirty = true;
- });
- },
- // Properties to survive after destroy, needed for Axis.update (#4317,
- // #5773, #5881).
- keepProps: ['extKey', 'hcEvents', 'names', 'series', 'userMax', 'userMin'],
- /**
- * Destroys an Axis instance. See {@link Axis#remove} for the API endpoint
- * to fully remove the axis.
- *
- * @private
- * @param {Boolean} keepEvents
- * Whether to preserve events, used internally in Axis.update.
- */
- destroy: function(keepEvents) {
- var axis = this,
- stacks = axis.stacks,
- plotLinesAndBands = axis.plotLinesAndBands,
- plotGroup,
- i;
- // Remove the events
- if (!keepEvents) {
- removeEvent(axis);
- }
- // Destroy each stack total
- objectEach(stacks, function(stack, stackKey) {
- destroyObjectProperties(stack);
- stacks[stackKey] = null;
- });
- // Destroy collections
- each([axis.ticks, axis.minorTicks, axis.alternateBands], function(coll) {
- destroyObjectProperties(coll);
- });
- if (plotLinesAndBands) {
- i = plotLinesAndBands.length;
- while (i--) { // #1975
- plotLinesAndBands[i].destroy();
- }
- }
- // Destroy local variables
- each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'gridGroup', 'labelGroup', 'cross'], function(prop) {
- if (axis[prop]) {
- axis[prop] = axis[prop].destroy();
- }
- });
- // Destroy each generated group for plotlines and plotbands
- for (plotGroup in axis.plotLinesAndBandsGroups) {
- axis.plotLinesAndBandsGroups[plotGroup] = axis.plotLinesAndBandsGroups[plotGroup].destroy();
- }
- // Delete all properties and fall back to the prototype.
- objectEach(axis, function(val, key) {
- if (inArray(key, axis.keepProps) === -1) {
- delete axis[key];
- }
- });
- },
- /**
- * Internal function to draw a crosshair.
- *
- * @param {PointerEvent} [e]
- * The event arguments from the modified pointer event, extended
- * with `chartX` and `chartY`
- * @param {Point} [point]
- * The Point object if the crosshair snaps to points.
- */
- drawCrosshair: function(e, point) {
- var path,
- options = this.crosshair,
- snap = pick(options.snap, true),
- pos,
- categorized,
- graphic = this.cross;
- // Use last available event when updating non-snapped crosshairs without
- // mouse interaction (#5287)
- if (!e) {
- e = this.cross && this.cross.e;
- }
- if (
- // Disabled in options
- !this.crosshair ||
- // Snap
- ((defined(point) || !snap) === false)
- ) {
- this.hideCrosshair();
- } else {
- // Get the path
- if (!snap) {
- pos = e && (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos);
- } else if (defined(point)) {
- pos = this.isXAxis ? point.plotX : this.len - point.plotY; // #3834
- }
- if (defined(pos)) {
- path = this.getPlotLinePath(
- // First argument, value, only used on radial
- point && (this.isXAxis ? point.x : pick(point.stackY, point.y)),
- null,
- null,
- null,
- pos // Translated position
- ) || null; // #3189
- }
- if (!defined(path)) {
- this.hideCrosshair();
- return;
- }
- categorized = this.categories && !this.isRadial;
- // Draw the cross
- if (!graphic) {
- this.cross = graphic = this.chart.renderer
- .path()
- .addClass('highcharts-crosshair highcharts-crosshair-' +
- (categorized ? 'category ' : 'thin ') + options.className)
- .attr({
- zIndex: pick(options.zIndex, 2)
- })
- .add();
- // Presentational attributes
- graphic.attr({
- 'stroke': options.color || (categorized ? color('#ccd6eb').setOpacity(0.25).get() : '#cccccc'),
- 'stroke-width': pick(options.width, 1)
- });
- if (options.dashStyle) {
- graphic.attr({
- dashstyle: options.dashStyle
- });
- }
- }
- graphic.show().attr({
- d: path
- });
- if (categorized && !options.width) {
- graphic.attr({
- 'stroke-width': this.transA
- });
- }
- this.cross.e = e;
- }
- },
- /**
- * Hide the crosshair.
- */
- hideCrosshair: function() {
- if (this.cross) {
- this.cross.hide();
- }
- }
- }); // end Axis
- H.Axis = Axis;
- return Axis;
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var Axis = H.Axis,
- Date = H.Date,
- dateFormat = H.dateFormat,
- defaultOptions = H.defaultOptions,
- defined = H.defined,
- each = H.each,
- extend = H.extend,
- getMagnitude = H.getMagnitude,
- getTZOffset = H.getTZOffset,
- normalizeTickInterval = H.normalizeTickInterval,
- pick = H.pick,
- timeUnits = H.timeUnits;
- /**
- * Set the tick positions to a time unit that makes sense, for example
- * on the first of each month or on every Monday. Return an array
- * with the time positions. Used in datetime axes as well as for grouping
- * data on a datetime axis.
- *
- * @param {Object} normalizedInterval The interval in axis values (ms) and the count
- * @param {Number} min The minimum in axis values
- * @param {Number} max The maximum in axis values
- * @param {Number} startOfWeek
- */
- Axis.prototype.getTimeTicks = function(normalizedInterval, min, max, startOfWeek) {
- var tickPositions = [],
- i,
- higherRanks = {},
- useUTC = defaultOptions.global.useUTC,
- minYear, // used in months and years as a basis for Date.UTC()
- // When crossing DST, use the max. Resolves #6278.
- minDate = new Date(min - Math.max(getTZOffset(min), getTZOffset(max))),
- makeTime = Date.hcMakeTime,
- interval = normalizedInterval.unitRange,
- count = normalizedInterval.count,
- variableDayLength;
- if (defined(min)) { // #1300
- minDate[Date.hcSetMilliseconds](interval >= timeUnits.second ? 0 : // #3935
- count * Math.floor(minDate.getMilliseconds() / count)); // #3652, #3654
- if (interval >= timeUnits.second) { // second
- minDate[Date.hcSetSeconds](interval >= timeUnits.minute ? 0 : // #3935
- count * Math.floor(minDate.getSeconds() / count));
- }
- if (interval >= timeUnits.minute) { // minute
- minDate[Date.hcSetMinutes](interval >= timeUnits.hour ? 0 :
- count * Math.floor(minDate[Date.hcGetMinutes]() / count));
- }
- if (interval >= timeUnits.hour) { // hour
- minDate[Date.hcSetHours](interval >= timeUnits.day ? 0 :
- count * Math.floor(minDate[Date.hcGetHours]() / count));
- }
- if (interval >= timeUnits.day) { // day
- minDate[Date.hcSetDate](interval >= timeUnits.month ? 1 :
- count * Math.floor(minDate[Date.hcGetDate]() / count));
- }
- if (interval >= timeUnits.month) { // month
- minDate[Date.hcSetMonth](interval >= timeUnits.year ? 0 :
- count * Math.floor(minDate[Date.hcGetMonth]() / count));
- minYear = minDate[Date.hcGetFullYear]();
- }
- if (interval >= timeUnits.year) { // year
- minYear -= minYear % count;
- minDate[Date.hcSetFullYear](minYear);
- }
- // week is a special case that runs outside the hierarchy
- if (interval === timeUnits.week) {
- // get start of current week, independent of count
- minDate[Date.hcSetDate](minDate[Date.hcGetDate]() - minDate[Date.hcGetDay]() +
- pick(startOfWeek, 1));
- }
- // Get basics for variable time spans
- minYear = minDate[Date.hcGetFullYear]();
- var minMonth = minDate[Date.hcGetMonth](),
- minDateDate = minDate[Date.hcGetDate](),
- minHours = minDate[Date.hcGetHours]();
- // Handle local timezone offset
- if (Date.hcTimezoneOffset || Date.hcGetTimezoneOffset) {
- // Detect whether we need to take the DST crossover into
- // consideration. If we're crossing over DST, the day length may be
- // 23h or 25h and we need to compute the exact clock time for each
- // tick instead of just adding hours. This comes at a cost, so first
- // we found out if it is needed. #4951.
- variableDayLength =
- (!useUTC || !!Date.hcGetTimezoneOffset) &&
- (
- // Long range, assume we're crossing over.
- max - min > 4 * timeUnits.month ||
- // Short range, check if min and max are in different time
- // zones.
- getTZOffset(min) !== getTZOffset(max)
- );
- // Adjust minDate to the offset date
- minDate = minDate.getTime();
- minDate = new Date(minDate + getTZOffset(minDate));
- }
- // Iterate and add tick positions at appropriate values
- var time = minDate.getTime();
- i = 1;
- while (time < max) {
- tickPositions.push(time);
- // if the interval is years, use Date.UTC to increase years
- if (interval === timeUnits.year) {
- time = makeTime(minYear + i * count, 0);
- // if the interval is months, use Date.UTC to increase months
- } else if (interval === timeUnits.month) {
- time = makeTime(minYear, minMonth + i * count);
- // if we're using global time, the interval is not fixed as it jumps
- // one hour at the DST crossover
- } else if (variableDayLength && (interval === timeUnits.day || interval === timeUnits.week)) {
- time = makeTime(minYear, minMonth, minDateDate +
- i * count * (interval === timeUnits.day ? 1 : 7));
- } else if (variableDayLength && interval === timeUnits.hour) {
- time = makeTime(minYear, minMonth, minDateDate, minHours + i * count);
- // else, the interval is fixed and we use simple addition
- } else {
- time += interval * count;
- }
- i++;
- }
- // push the last time
- tickPositions.push(time);
- // Handle higher ranks. Mark new days if the time is on midnight
- // (#950, #1649, #1760, #3349). Use a reasonable dropout threshold to
- // prevent looping over dense data grouping (#6156).
- if (interval <= timeUnits.hour && tickPositions.length < 10000) {
- each(tickPositions, function(time) {
- if (
- // Speed optimization, no need to run dateFormat unless
- // we're on a full or half hour
- time % 1800000 === 0 &&
- // Check for local or global midnight
- dateFormat('%H%M%S%L', time) === '000000000'
- ) {
- higherRanks[time] = 'day';
- }
- });
- }
- }
- // record information on the chosen unit - for dynamic label formatter
- tickPositions.info = extend(normalizedInterval, {
- higherRanks: higherRanks,
- totalRange: interval * count
- });
- return tickPositions;
- };
- /**
- * Get a normalized tick interval for dates. Returns a configuration object with
- * unit range (interval), count and name. Used to prepare data for getTimeTicks.
- * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs
- * of segments in stock charts, the normalizing logic was extracted in order to
- * prevent it for running over again for each segment having the same interval.
- * #662, #697.
- */
- Axis.prototype.normalizeTimeTickInterval = function(tickInterval, unitsOption) {
- var units = unitsOption || [
- [
- 'millisecond', // unit name
- [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
- ],
- [
- 'second', [1, 2, 5, 10, 15, 30]
- ],
- [
- 'minute', [1, 2, 5, 10, 15, 30]
- ],
- [
- 'hour', [1, 2, 3, 4, 6, 8, 12]
- ],
- [
- 'day', [1, 2]
- ],
- [
- 'week', [1, 2]
- ],
- [
- 'month', [1, 2, 3, 4, 6]
- ],
- [
- 'year',
- null
- ]
- ],
- unit = units[units.length - 1], // default unit is years
- interval = timeUnits[unit[0]],
- multiples = unit[1],
- count,
- i;
- // loop through the units to find the one that best fits the tickInterval
- for (i = 0; i < units.length; i++) {
- unit = units[i];
- interval = timeUnits[unit[0]];
- multiples = unit[1];
- if (units[i + 1]) {
- // lessThan is in the middle between the highest multiple and the next unit.
- var lessThan = (interval * multiples[multiples.length - 1] +
- timeUnits[units[i + 1][0]]) / 2;
- // break and keep the current unit
- if (tickInterval <= lessThan) {
- break;
- }
- }
- }
- // prevent 2.5 years intervals, though 25, 250 etc. are allowed
- if (interval === timeUnits.year && tickInterval < 5 * interval) {
- multiples = [1, 2, 5];
- }
- // get the count
- count = normalizeTickInterval(
- tickInterval / interval,
- multiples,
- unit[0] === 'year' ? Math.max(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360
- );
- return {
- unitRange: interval,
- count: count,
- unitName: unit[0]
- };
- };
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var Axis = H.Axis,
- getMagnitude = H.getMagnitude,
- map = H.map,
- normalizeTickInterval = H.normalizeTickInterval,
- pick = H.pick;
- /**
- * Methods defined on the Axis prototype
- */
- /**
- * Set the tick positions of a logarithmic axis
- */
- Axis.prototype.getLogTickPositions = function(interval, min, max, minor) {
- var axis = this,
- options = axis.options,
- axisLength = axis.len,
- lin2log = axis.lin2log,
- log2lin = axis.log2lin,
- // Since we use this method for both major and minor ticks,
- // use a local variable and return the result
- positions = [];
- // Reset
- if (!minor) {
- axis._minorAutoInterval = null;
- }
- // First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
- if (interval >= 0.5) {
- interval = Math.round(interval);
- positions = axis.getLinearTickPositions(interval, min, max);
- // Second case: We need intermediary ticks. For example
- // 1, 2, 4, 6, 8, 10, 20, 40 etc.
- } else if (interval >= 0.08) {
- var roundedMin = Math.floor(min),
- intermediate,
- i,
- j,
- len,
- pos,
- lastPos,
- break2;
- if (interval > 0.3) {
- intermediate = [1, 2, 4];
- } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
- intermediate = [1, 2, 4, 6, 8];
- } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
- intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
- }
- for (i = roundedMin; i < max + 1 && !break2; i++) {
- len = intermediate.length;
- for (j = 0; j < len && !break2; j++) {
- pos = log2lin(lin2log(i) * intermediate[j]);
- if (pos > min && (!minor || lastPos <= max) && lastPos !== undefined) { // #1670, lastPos is #3113
- positions.push(lastPos);
- }
- if (lastPos > max) {
- break2 = true;
- }
- lastPos = pos;
- }
- }
- // Third case: We are so deep in between whole logarithmic values that
- // we might as well handle the tick positions like a linear axis. For
- // example 1.01, 1.02, 1.03, 1.04.
- } else {
- var realMin = lin2log(min),
- realMax = lin2log(max),
- tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
- filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
- tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
- totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength;
- interval = pick(
- filteredTickIntervalOption,
- axis._minorAutoInterval,
- (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
- );
- interval = normalizeTickInterval(
- interval,
- null,
- getMagnitude(interval)
- );
- positions = map(axis.getLinearTickPositions(
- interval,
- realMin,
- realMax
- ), log2lin);
- if (!minor) {
- axis._minorAutoInterval = interval / 5;
- }
- }
- // Set the axis-level tickInterval variable
- if (!minor) {
- axis.tickInterval = interval;
- }
- return positions;
- };
- Axis.prototype.log2lin = function(num) {
- return Math.log(num) / Math.LN10;
- };
- Axis.prototype.lin2log = function(num) {
- return Math.pow(10, num);
- };
- }(Highcharts));
- (function(H, Axis) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var arrayMax = H.arrayMax,
- arrayMin = H.arrayMin,
- defined = H.defined,
- destroyObjectProperties = H.destroyObjectProperties,
- each = H.each,
- erase = H.erase,
- merge = H.merge,
- pick = H.pick;
- /*
- * The object wrapper for plot lines and plot bands
- * @param {Object} options
- */
- H.PlotLineOrBand = function(axis, options) {
- this.axis = axis;
- if (options) {
- this.options = options;
- this.id = options.id;
- }
- };
- H.PlotLineOrBand.prototype = {
- /**
- * Render the plot line or plot band. If it is already existing,
- * move it.
- */
- render: function() {
- var plotLine = this,
- axis = plotLine.axis,
- horiz = axis.horiz,
- options = plotLine.options,
- optionsLabel = options.label,
- label = plotLine.label,
- to = options.to,
- from = options.from,
- value = options.value,
- isBand = defined(from) && defined(to),
- isLine = defined(value),
- svgElem = plotLine.svgElem,
- isNew = !svgElem,
- path = [],
- color = options.color,
- zIndex = pick(options.zIndex, 0),
- events = options.events,
- attribs = {
- 'class': 'highcharts-plot-' + (isBand ? 'band ' : 'line ') + (options.className || '')
- },
- groupAttribs = {},
- renderer = axis.chart.renderer,
- groupName = isBand ? 'bands' : 'lines',
- group,
- log2lin = axis.log2lin;
- // logarithmic conversion
- if (axis.isLog) {
- from = log2lin(from);
- to = log2lin(to);
- value = log2lin(value);
- }
- // Set the presentational attributes
- if (isLine) {
- attribs = {
- stroke: color,
- 'stroke-width': options.width
- };
- if (options.dashStyle) {
- attribs.dashstyle = options.dashStyle;
- }
- } else if (isBand) { // plot band
- if (color) {
- attribs.fill = color;
- }
- if (options.borderWidth) {
- attribs.stroke = options.borderColor;
- attribs['stroke-width'] = options.borderWidth;
- }
- }
- // Grouping and zIndex
- groupAttribs.zIndex = zIndex;
- groupName += '-' + zIndex;
- group = axis.plotLinesAndBandsGroups[groupName];
- if (!group) {
- axis.plotLinesAndBandsGroups[groupName] = group = renderer.g('plot-' + groupName)
- .attr(groupAttribs).add();
- }
- // Create the path
- if (isNew) {
- plotLine.svgElem = svgElem =
- renderer
- .path()
- .attr(attribs).add(group);
- }
- // Set the path or return
- if (isLine) {
- path = axis.getPlotLinePath(value, svgElem.strokeWidth());
- } else if (isBand) { // plot band
- path = axis.getPlotBandPath(from, to, options);
- } else {
- return;
- }
- // common for lines and bands
- if (isNew && path && path.length) {
- svgElem.attr({
- d: path
- });
- // events
- if (events) {
- H.objectEach(events, function(event, eventType) {
- svgElem.on(eventType, function(e) {
- events[eventType].apply(plotLine, [e]);
- });
- });
- }
- } else if (svgElem) {
- if (path) {
- svgElem.show();
- svgElem.animate({
- d: path
- });
- } else {
- svgElem.hide();
- if (label) {
- plotLine.label = label = label.destroy();
- }
- }
- }
- // the plot band/line label
- if (optionsLabel && defined(optionsLabel.text) && path && path.length &&
- axis.width > 0 && axis.height > 0 && !path.flat) {
- // apply defaults
- optionsLabel = merge({
- align: horiz && isBand && 'center',
- x: horiz ? !isBand && 4 : 10,
- verticalAlign: !horiz && isBand && 'middle',
- y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4,
- rotation: horiz && !isBand && 90
- }, optionsLabel);
- this.renderLabel(optionsLabel, path, isBand, zIndex);
- } else if (label) { // move out of sight
- label.hide();
- }
- // chainable
- return plotLine;
- },
- /**
- * Render and align label for plot line or band.
- */
- renderLabel: function(optionsLabel, path, isBand, zIndex) {
- var plotLine = this,
- label = plotLine.label,
- renderer = plotLine.axis.chart.renderer,
- attribs,
- xs,
- ys,
- x,
- y;
- // add the SVG element
- if (!label) {
- attribs = {
- align: optionsLabel.textAlign || optionsLabel.align,
- rotation: optionsLabel.rotation,
- 'class': 'highcharts-plot-' + (isBand ? 'band' : 'line') + '-label ' + (optionsLabel.className || '')
- };
- attribs.zIndex = zIndex;
- plotLine.label = label = renderer.text(
- optionsLabel.text,
- 0,
- 0,
- optionsLabel.useHTML
- )
- .attr(attribs)
- .add();
- label.css(optionsLabel.style);
- }
- // get the bounding box and align the label
- // #3000 changed to better handle choice between plotband or plotline
- xs = [path[1], path[4], (isBand ? path[6] : path[1])];
- ys = [path[2], path[5], (isBand ? path[7] : path[2])];
- x = arrayMin(xs);
- y = arrayMin(ys);
- label.align(optionsLabel, false, {
- x: x,
- y: y,
- width: arrayMax(xs) - x,
- height: arrayMax(ys) - y
- });
- label.show();
- },
- /**
- * Remove the plot line or band
- */
- destroy: function() {
- // remove it from the lookup
- erase(this.axis.plotLinesAndBands, this);
- delete this.axis;
- destroyObjectProperties(this);
- }
- };
- /**
- * Object with members for extending the Axis prototype
- * @todo Extend directly instead of adding object to Highcharts first
- */
- H.extend(Axis.prototype, /** @lends Highcharts.Axis.prototype */ {
- /**
- * Create the path for a plot band
- */
- getPlotBandPath: function(from, to) {
- var toPath = this.getPlotLinePath(to, null, null, true),
- path = this.getPlotLinePath(from, null, null, true),
- // #4964 check if chart is inverted or plotband is on yAxis
- horiz = this.horiz,
- plus = 1,
- outside =
- (from < this.min && to < this.min) ||
- (from > this.max && to > this.max);
- if (path && toPath) {
- // Flat paths don't need labels (#3836)
- if (outside) {
- path.flat = path.toString() === toPath.toString();
- plus = 0;
- }
- // Add 1 pixel, when coordinates are the same
- path.push(
- horiz && toPath[4] === path[4] ? toPath[4] + plus : toPath[4], !horiz && toPath[5] === path[5] ? toPath[5] + plus : toPath[5],
- horiz && toPath[1] === path[1] ? toPath[1] + plus : toPath[1], !horiz && toPath[2] === path[2] ? toPath[2] + plus : toPath[2]
- );
- } else { // outside the axis area
- path = null;
- }
- return path;
- },
- /**
- * Add a plot band after render time.
- *
- * @param {AxisPlotBandsOptions} options
- * A configuration object for the plot band, as defined in {@link
- * https://api.highcharts.com/highcharts/xAxis.plotBands|
- * xAxis.plotBands}.
- * @return {Object}
- * The added plot band.
- * @sample highcharts/members/axis-addplotband/
- * Toggle the plot band from a button
- */
- addPlotBand: function(options) {
- return this.addPlotBandOrLine(options, 'plotBands');
- },
- /**
- * Add a plot line after render time.
- *
- * @param {AxisPlotLinesOptions} options
- * A configuration object for the plot line, as defined in {@link
- * https://api.highcharts.com/highcharts/xAxis.plotLines|
- * xAxis.plotLines}.
- * @return {Object}
- * The added plot line.
- * @sample highcharts/members/axis-addplotline/
- * Toggle the plot line from a button
- */
- addPlotLine: function(options) {
- return this.addPlotBandOrLine(options, 'plotLines');
- },
- /**
- * Add a plot band or plot line after render time. Called from addPlotBand
- * and addPlotLine internally.
- *
- * @private
- * @param options {AxisPlotLinesOptions|AxisPlotBandsOptions}
- * The plotBand or plotLine configuration object.
- */
- addPlotBandOrLine: function(options, coll) {
- var obj = new H.PlotLineOrBand(this, options).render(),
- userOptions = this.userOptions;
- if (obj) { // #2189
- // Add it to the user options for exporting and Axis.update
- if (coll) {
- userOptions[coll] = userOptions[coll] || [];
- userOptions[coll].push(options);
- }
- this.plotLinesAndBands.push(obj);
- }
- return obj;
- },
- /**
- * Remove a plot band or plot line from the chart by id. Called internally
- * from `removePlotBand` and `removePlotLine`.
- *
- * @private
- * @param {String} id
- */
- removePlotBandOrLine: function(id) {
- var plotLinesAndBands = this.plotLinesAndBands,
- options = this.options,
- userOptions = this.userOptions,
- i = plotLinesAndBands.length;
- while (i--) {
- if (plotLinesAndBands[i].id === id) {
- plotLinesAndBands[i].destroy();
- }
- }
- each([
- options.plotLines || [],
- userOptions.plotLines || [],
- options.plotBands || [],
- userOptions.plotBands || []
- ], function(arr) {
- i = arr.length;
- while (i--) {
- if (arr[i].id === id) {
- erase(arr, arr[i]);
- }
- }
- });
- },
- /**
- * Remove a plot band by its id.
- *
- * @param {String} id
- * The plot band's `id` as given in the original configuration
- * object or in the `addPlotBand` option.
- * @sample highcharts/members/axis-removeplotband/
- * Remove plot band by id
- * @sample highcharts/members/axis-addplotband/
- * Toggle the plot band from a button
- */
- removePlotBand: function(id) {
- this.removePlotBandOrLine(id);
- },
- /**
- * Remove a plot line by its id.
- * @param {String} id
- * The plot line's `id` as given in the original configuration
- * object or in the `addPlotLine` option.
- * @sample highcharts/xaxis/plotlines-id/
- * Remove plot line by id
- * @sample highcharts/members/axis-addplotline/
- * Toggle the plot line from a button
- */
- removePlotLine: function(id) {
- this.removePlotBandOrLine(id);
- }
- });
- }(Highcharts, Axis));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var dateFormat = H.dateFormat,
- each = H.each,
- extend = H.extend,
- format = H.format,
- isNumber = H.isNumber,
- map = H.map,
- merge = H.merge,
- pick = H.pick,
- splat = H.splat,
- syncTimeout = H.syncTimeout,
- timeUnits = H.timeUnits;
- /**
- * The tooltip object
- * @param {Object} chart The chart instance
- * @param {Object} options Tooltip options
- */
- H.Tooltip = function() {
- this.init.apply(this, arguments);
- };
- H.Tooltip.prototype = {
- init: function(chart, options) {
- // Save the chart and options
- this.chart = chart;
- this.options = options;
- // Keep track of the current series
- //this.currentSeries = undefined;
- // List of crosshairs
- this.crosshairs = [];
- // Current values of x and y when animating
- this.now = {
- x: 0,
- y: 0
- };
- // The tooltip is initially hidden
- this.isHidden = true;
- // Public property for getting the shared state.
- this.split = options.split && !chart.inverted;
- this.shared = options.shared || this.split;
- },
- /**
- * Destroy the single tooltips in a split tooltip.
- * If the tooltip is active then it is not destroyed, unless forced to.
- * @param {boolean} force Force destroy all tooltips.
- * @return {undefined}
- */
- cleanSplit: function(force) {
- each(this.chart.series, function(series) {
- var tt = series && series.tt;
- if (tt) {
- if (!tt.isActive || force) {
- series.tt = tt.destroy();
- } else {
- tt.isActive = false;
- }
- }
- });
- },
- /**
- * Create the Tooltip label element if it doesn't exist, then return the
- * label.
- */
- getLabel: function() {
- var renderer = this.chart.renderer,
- options = this.options;
- if (!this.label) {
- // Create the label
- if (this.split) {
- this.label = renderer.g('tooltip');
- } else {
- this.label = renderer.label(
- '',
- 0,
- 0,
- options.shape || 'callout',
- null,
- null,
- options.useHTML,
- null,
- 'tooltip'
- )
- .attr({
- padding: options.padding,
- r: options.borderRadius
- });
- this.label
- .attr({
- 'fill': options.backgroundColor,
- 'stroke-width': options.borderWidth
- })
- // #2301, #2657
- .css(options.style)
- .shadow(options.shadow);
- }
- this.label
- .attr({
- zIndex: 8
- })
- .add();
- }
- return this.label;
- },
- update: function(options) {
- this.destroy();
- // Update user options (#6218)
- merge(true, this.chart.options.tooltip.userOptions, options);
- this.init(this.chart, merge(true, this.options, options));
- },
- /**
- * Destroy the tooltip and its elements.
- */
- destroy: function() {
- // Destroy and clear local variables
- if (this.label) {
- this.label = this.label.destroy();
- }
- if (this.split && this.tt) {
- this.cleanSplit(this.chart, true);
- this.tt = this.tt.destroy();
- }
- clearTimeout(this.hideTimer);
- clearTimeout(this.tooltipTimeout);
- },
- /**
- * Provide a soft movement for the tooltip
- *
- * @param {Number} x
- * @param {Number} y
- * @private
- */
- move: function(x, y, anchorX, anchorY) {
- var tooltip = this,
- now = tooltip.now,
- animate = tooltip.options.animation !== false && !tooltip.isHidden &&
- // When we get close to the target position, abort animation and land on the right place (#3056)
- (Math.abs(x - now.x) > 1 || Math.abs(y - now.y) > 1),
- skipAnchor = tooltip.followPointer || tooltip.len > 1;
- // Get intermediate values for animation
- extend(now, {
- x: animate ? (2 * now.x + x) / 3 : x,
- y: animate ? (now.y + y) / 2 : y,
- anchorX: skipAnchor ? undefined : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX,
- anchorY: skipAnchor ? undefined : animate ? (now.anchorY + anchorY) / 2 : anchorY
- });
- // Move to the intermediate value
- tooltip.getLabel().attr(now);
- // Run on next tick of the mouse tracker
- if (animate) {
- // Never allow two timeouts
- clearTimeout(this.tooltipTimeout);
- // Set the fixed interval ticking for the smooth tooltip
- this.tooltipTimeout = setTimeout(function() {
- // The interval function may still be running during destroy,
- // so check that the chart is really there before calling.
- if (tooltip) {
- tooltip.move(x, y, anchorX, anchorY);
- }
- }, 32);
- }
- },
- /**
- * Hide the tooltip
- */
- hide: function(delay) {
- var tooltip = this;
- clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766)
- delay = pick(delay, this.options.hideDelay, 500);
- if (!this.isHidden) {
- this.hideTimer = syncTimeout(function() {
- tooltip.getLabel()[delay ? 'fadeOut' : 'hide']();
- tooltip.isHidden = true;
- }, delay);
- }
- },
- /**
- * Extendable method to get the anchor position of the tooltip
- * from a point or set of points
- */
- getAnchor: function(points, mouseEvent) {
- var ret,
- chart = this.chart,
- inverted = chart.inverted,
- plotTop = chart.plotTop,
- plotLeft = chart.plotLeft,
- plotX = 0,
- plotY = 0,
- yAxis,
- xAxis;
- points = splat(points);
- // Pie uses a special tooltipPos
- ret = points[0].tooltipPos;
- // When tooltip follows mouse, relate the position to the mouse
- if (this.followPointer && mouseEvent) {
- if (mouseEvent.chartX === undefined) {
- mouseEvent = chart.pointer.normalize(mouseEvent);
- }
- ret = [
- mouseEvent.chartX - chart.plotLeft,
- mouseEvent.chartY - plotTop
- ];
- }
- // When shared, use the average position
- if (!ret) {
- each(points, function(point) {
- yAxis = point.series.yAxis;
- xAxis = point.series.xAxis;
- plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0);
- plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
- (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
- });
- plotX /= points.length;
- plotY /= points.length;
- ret = [
- inverted ? chart.plotWidth - plotY : plotX,
- this.shared && !inverted && points.length > 1 && mouseEvent ?
- mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424)
- inverted ? chart.plotHeight - plotX : plotY
- ];
- }
- return map(ret, Math.round);
- },
- /**
- * Place the tooltip in a chart without spilling over
- * and not covering the point it self.
- */
- getPosition: function(boxWidth, boxHeight, point) {
- var chart = this.chart,
- distance = this.distance,
- ret = {},
- h = point.h || 0, // #4117
- swapped,
- first = ['y', chart.chartHeight, boxHeight,
- point.plotY + chart.plotTop, chart.plotTop,
- chart.plotTop + chart.plotHeight
- ],
- second = ['x', chart.chartWidth, boxWidth,
- point.plotX + chart.plotLeft, chart.plotLeft,
- chart.plotLeft + chart.plotWidth
- ],
- // The far side is right or bottom
- preferFarSide = !this.followPointer && pick(point.ttBelow, !chart.inverted === !!point.negative), // #4984
- /**
- * Handle the preferred dimension. When the preferred dimension is tooltip
- * on top or bottom of the point, it will look for space there.
- */
- firstDimension = function(dim, outerSize, innerSize, point, min, max) {
- var roomLeft = innerSize < point - distance,
- roomRight = point + distance + innerSize < outerSize,
- alignedLeft = point - distance - innerSize,
- alignedRight = point + distance;
- if (preferFarSide && roomRight) {
- ret[dim] = alignedRight;
- } else if (!preferFarSide && roomLeft) {
- ret[dim] = alignedLeft;
- } else if (roomLeft) {
- ret[dim] = Math.min(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h);
- } else if (roomRight) {
- ret[dim] = Math.max(
- min,
- alignedRight + h + innerSize > outerSize ?
- alignedRight :
- alignedRight + h
- );
- } else {
- return false;
- }
- },
- /**
- * Handle the secondary dimension. If the preferred dimension is tooltip
- * on top or bottom of the point, the second dimension is to align the tooltip
- * above the point, trying to align center but allowing left or right
- * align within the chart box.
- */
- secondDimension = function(dim, outerSize, innerSize, point) {
- var retVal;
- // Too close to the edge, return false and swap dimensions
- if (point < distance || point > outerSize - distance) {
- retVal = false;
- // Align left/top
- } else if (point < innerSize / 2) {
- ret[dim] = 1;
- // Align right/bottom
- } else if (point > outerSize - innerSize / 2) {
- ret[dim] = outerSize - innerSize - 2;
- // Align center
- } else {
- ret[dim] = point - innerSize / 2;
- }
- return retVal;
- },
- /**
- * Swap the dimensions
- */
- swap = function(count) {
- var temp = first;
- first = second;
- second = temp;
- swapped = count;
- },
- run = function() {
- if (firstDimension.apply(0, first) !== false) {
- if (secondDimension.apply(0, second) === false && !swapped) {
- swap(true);
- run();
- }
- } else if (!swapped) {
- swap(true);
- run();
- } else {
- ret.x = ret.y = 0;
- }
- };
- // Under these conditions, prefer the tooltip on the side of the point
- if (chart.inverted || this.len > 1) {
- swap();
- }
- run();
- return ret;
- },
- /**
- * In case no user defined formatter is given, this will be used. Note that the context
- * here is an object holding point, series, x, y etc.
- *
- * @returns {String|Array<String>}
- */
- defaultFormatter: function(tooltip) {
- var items = this.points || splat(this),
- s;
- // Build the header
- s = [tooltip.tooltipFooterHeaderFormatter(items[0])];
- // build the values
- s = s.concat(tooltip.bodyFormatter(items));
- // footer
- s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true));
- return s;
- },
- /**
- * Refresh the tooltip's text and position.
- * @param {Object|Array} pointOrPoints Rither a point or an array of points
- */
- refresh: function(pointOrPoints, mouseEvent) {
- var tooltip = this,
- label,
- options = tooltip.options,
- x,
- y,
- point = pointOrPoints,
- anchor,
- textConfig = {},
- text,
- pointConfig = [],
- formatter = options.formatter || tooltip.defaultFormatter,
- shared = tooltip.shared,
- currentSeries;
- clearTimeout(this.hideTimer);
- // get the reference point coordinates (pie charts use tooltipPos)
- tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer;
- anchor = tooltip.getAnchor(point, mouseEvent);
- x = anchor[0];
- y = anchor[1];
- // shared tooltip, array is sent over
- if (shared && !(point.series && point.series.noSharedTooltip)) {
- each(point, function(item) {
- item.setState('hover');
- pointConfig.push(item.getLabelConfig());
- });
- textConfig = {
- x: point[0].category,
- y: point[0].y
- };
- textConfig.points = pointConfig;
- point = point[0];
- // single point tooltip
- } else {
- textConfig = point.getLabelConfig();
- }
- this.len = pointConfig.length; // #6128
- text = formatter.call(textConfig, tooltip);
- // register the current series
- currentSeries = point.series;
- this.distance = pick(currentSeries.tooltipOptions.distance, 16);
- // update the inner HTML
- if (text === false) {
- this.hide();
- } else {
- label = tooltip.getLabel();
- // show it
- if (tooltip.isHidden) {
- label.attr({
- opacity: 1
- }).show();
- }
- // update text
- if (tooltip.split) {
- this.renderSplit(text, pointOrPoints);
- } else {
- // Prevent the tooltip from flowing over the chart box (#6659)
- if (!options.style.width) {
- label.css({
- width: this.chart.spacingBox.width
- });
- }
- label.attr({
- text: text && text.join ? text.join('') : text
- });
- // Set the stroke color of the box to reflect the point
- label.removeClass(/highcharts-color-[\d]+/g)
- .addClass('highcharts-color-' + pick(point.colorIndex, currentSeries.colorIndex));
- label.attr({
- stroke: options.borderColor || point.color || currentSeries.color || '#666666'
- });
- tooltip.updatePosition({
- plotX: x,
- plotY: y,
- negative: point.negative,
- ttBelow: point.ttBelow,
- h: anchor[2] || 0
- });
- }
- this.isHidden = false;
- }
- },
- /**
- * Render the split tooltip. Loops over each point's text and adds
- * a label next to the point, then uses the distribute function to
- * find best non-overlapping positions.
- */
- renderSplit: function(labels, points) {
- var tooltip = this,
- boxes = [],
- chart = this.chart,
- ren = chart.renderer,
- rightAligned = true,
- options = this.options,
- headerHeight,
- tooltipLabel = this.getLabel();
- // Create the individual labels for header and points, ignore footer
- each(labels.slice(0, points.length + 1), function(str, i) {
- var point = points[i - 1] ||
- // Item 0 is the header. Instead of this, we could also use the crosshair label
- {
- isHeader: true,
- plotX: points[0].plotX
- },
- owner = point.series || tooltip,
- tt = owner.tt,
- series = point.series || {},
- colorClass = 'highcharts-color-' + pick(point.colorIndex, series.colorIndex, 'none'),
- target,
- x,
- bBox,
- boxWidth;
- // Store the tooltip referance on the series
- if (!tt) {
- owner.tt = tt = ren.label(null, null, null, 'callout')
- .addClass('highcharts-tooltip-box ' + colorClass)
- .attr({
- 'padding': options.padding,
- 'r': options.borderRadius,
- 'fill': options.backgroundColor,
- 'stroke': point.color || series.color || '#333333',
- 'stroke-width': options.borderWidth
- })
- .add(tooltipLabel);
- }
- tt.isActive = true;
- tt.attr({
- text: str
- });
- tt.css(options.style);
- // Get X position now, so we can move all to the other side in case of overflow
- bBox = tt.getBBox();
- boxWidth = bBox.width + tt.strokeWidth();
- if (point.isHeader) {
- headerHeight = bBox.height;
- x = Math.max(
- 0, // No left overflow
- Math.min(
- point.plotX + chart.plotLeft - boxWidth / 2,
- chart.chartWidth - boxWidth // No right overflow (#5794)
- )
- );
- } else {
- x = point.plotX + chart.plotLeft - pick(options.distance, 16) -
- boxWidth;
- }
- // If overflow left, we don't use this x in the next loop
- if (x < 0) {
- rightAligned = false;
- }
- // Prepare for distribution
- target = (point.series && point.series.yAxis && point.series.yAxis.pos) + (point.plotY || 0);
- target -= chart.plotTop;
- boxes.push({
- target: point.isHeader ? chart.plotHeight + headerHeight : target,
- rank: point.isHeader ? 1 : 0,
- size: owner.tt.getBBox().height + 1,
- point: point,
- x: x,
- tt: tt
- });
- });
- // Clean previous run (for missing points)
- this.cleanSplit();
- // Distribute and put in place
- H.distribute(boxes, chart.plotHeight + headerHeight);
- each(boxes, function(box) {
- var point = box.point,
- series = point.series;
- // Put the label in place
- box.tt.attr({
- visibility: box.pos === undefined ? 'hidden' : 'inherit',
- x: (rightAligned || point.isHeader ?
- box.x :
- point.plotX + chart.plotLeft + pick(options.distance, 16)),
- y: box.pos + chart.plotTop,
- anchorX: point.isHeader ?
- point.plotX + chart.plotLeft : point.plotX + series.xAxis.pos,
- anchorY: point.isHeader ?
- box.pos + chart.plotTop - 15 : point.plotY + series.yAxis.pos
- });
- });
- },
- /**
- * Find the new position and perform the move
- */
- updatePosition: function(point) {
- var chart = this.chart,
- label = this.getLabel(),
- pos = (this.options.positioner || this.getPosition).call(
- this,
- label.width,
- label.height,
- point
- );
- // do the move
- this.move(
- Math.round(pos.x),
- Math.round(pos.y || 0), // can be undefined (#3977)
- point.plotX + chart.plotLeft,
- point.plotY + chart.plotTop
- );
- },
- /**
- * Get the optimal date format for a point, based on a range.
- * @param {number} range - The time range
- * @param {number|Date} date - The date of the point in question
- * @param {number} startOfWeek - An integer representing the first day of
- * the week, where 0 is Sunday
- * @param {Object} dateTimeLabelFormats - A map of time units to formats
- * @return {string} - the optimal date format for a point
- */
- getDateFormat: function(range, date, startOfWeek, dateTimeLabelFormats) {
- var dateStr = dateFormat('%m-%d %H:%M:%S.%L', date),
- format,
- n,
- blank = '01-01 00:00:00.000',
- strpos = {
- millisecond: 15,
- second: 12,
- minute: 9,
- hour: 6,
- day: 3
- },
- lastN = 'millisecond'; // for sub-millisecond data, #4223
- for (n in timeUnits) {
- // If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format
- if (range === timeUnits.week && +dateFormat('%w', date) === startOfWeek &&
- dateStr.substr(6) === blank.substr(6)) {
- n = 'week';
- break;
- }
- // The first format that is too great for the range
- if (timeUnits[n] > range) {
- n = lastN;
- break;
- }
- // If the point is placed every day at 23:59, we need to show
- // the minutes as well. #2637.
- if (strpos[n] && dateStr.substr(strpos[n]) !== blank.substr(strpos[n])) {
- break;
- }
- // Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition
- if (n !== 'week') {
- lastN = n;
- }
- }
- if (n) {
- format = dateTimeLabelFormats[n];
- }
- return format;
- },
- /**
- * Get the best X date format based on the closest point range on the axis.
- */
- getXDateFormat: function(point, options, xAxis) {
- var xDateFormat,
- dateTimeLabelFormats = options.dateTimeLabelFormats,
- closestPointRange = xAxis && xAxis.closestPointRange;
- if (closestPointRange) {
- xDateFormat = this.getDateFormat(
- closestPointRange,
- point.x,
- xAxis.options.startOfWeek,
- dateTimeLabelFormats
- );
- } else {
- xDateFormat = dateTimeLabelFormats.day;
- }
- return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
- },
- /**
- * Format the footer/header of the tooltip
- * #3397: abstraction to enable formatting of footer and header
- */
- tooltipFooterHeaderFormatter: function(labelConfig, isFooter) {
- var footOrHead = isFooter ? 'footer' : 'header',
- series = labelConfig.series,
- tooltipOptions = series.tooltipOptions,
- xDateFormat = tooltipOptions.xDateFormat,
- xAxis = series.xAxis,
- isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(labelConfig.key),
- formatString = tooltipOptions[footOrHead + 'Format'];
- // Guess the best date format based on the closest point distance (#568, #3418)
- if (isDateTime && !xDateFormat) {
- xDateFormat = this.getXDateFormat(labelConfig, tooltipOptions, xAxis);
- }
- // Insert the footer date format if any
- if (isDateTime && xDateFormat) {
- formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}');
- }
- return format(formatString, {
- point: labelConfig,
- series: series
- });
- },
- /**
- * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item,
- * abstracting this functionality allows to easily overwrite and extend it.
- */
- bodyFormatter: function(items) {
- return map(items, function(item) {
- var tooltipOptions = item.series.tooltipOptions;
- return (tooltipOptions.pointFormatter || item.point.tooltipFormatter)
- .call(item.point, tooltipOptions.pointFormat);
- });
- }
- };
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- attr = H.attr,
- charts = H.charts,
- color = H.color,
- css = H.css,
- defined = H.defined,
- doc = H.doc,
- each = H.each,
- extend = H.extend,
- fireEvent = H.fireEvent,
- offset = H.offset,
- pick = H.pick,
- removeEvent = H.removeEvent,
- splat = H.splat,
- Tooltip = H.Tooltip,
- win = H.win;
- /**
- * The mouse tracker object. All methods starting with "on" are primary DOM
- * event handlers. Subsequent methods should be named differently from what they
- * are doing.
- *
- * @constructor Pointer
- * @param {Object} chart The Chart instance
- * @param {Object} options The root options object
- */
- H.Pointer = function(chart, options) {
- this.init(chart, options);
- };
- H.Pointer.prototype = {
- /**
- * Initialize Pointer
- */
- init: function(chart, options) {
- // Store references
- this.options = options;
- this.chart = chart;
- // Do we need to handle click on a touch device?
- this.runChartClick = options.chart.events && !!options.chart.events.click;
- this.pinchDown = [];
- this.lastValidTouch = {};
- if (Tooltip && options.tooltip.enabled) {
- chart.tooltip = new Tooltip(chart, options.tooltip);
- this.followTouchMove = pick(options.tooltip.followTouchMove, true);
- }
- this.setDOMEvents();
- },
- /**
- * Resolve the zoomType option, this is reset on all touch start and mouse
- * down events.
- */
- zoomOption: function(e) {
- var chart = this.chart,
- options = chart.options.chart,
- zoomType = options.zoomType || '',
- inverted = chart.inverted,
- zoomX,
- zoomY;
- // Look for the pinchType option
- if (/touch/.test(e.type)) {
- zoomType = pick(options.pinchType, zoomType);
- }
- this.zoomX = zoomX = /x/.test(zoomType);
- this.zoomY = zoomY = /y/.test(zoomType);
- this.zoomHor = (zoomX && !inverted) || (zoomY && inverted);
- this.zoomVert = (zoomY && !inverted) || (zoomX && inverted);
- this.hasZoom = zoomX || zoomY;
- },
- /**
- * @typedef {Object} PointerEvent
- * A native browser mouse or touch event, extended with position
- * information relative to the {@link Chart.container}.
- * @property {Number} chartX
- * The X coordinate of the pointer interaction relative to the
- * chart.
- * @property {Number} chartY
- * The Y coordinate of the pointer interaction relative to the
- * chart.
- *
- */
- /**
- * Add crossbrowser support for chartX and chartY.
- *
- * @param {Object} e
- * The event object in standard browsers.
- *
- * @return {PointerEvent}
- * A browser event with extended properties `chartX` and `chartY`
- */
- normalize: function(e, chartPosition) {
- var chartX,
- chartY,
- ePos;
- // IE normalizing
- e = e || win.event;
- if (!e.target) {
- e.target = e.srcElement;
- }
- // iOS (#2757)
- ePos = e.touches ? (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e;
- // Get mouse position
- if (!chartPosition) {
- this.chartPosition = chartPosition = offset(this.chart.container);
- }
- // chartX and chartY
- if (ePos.pageX === undefined) { // IE < 9. #886.
- chartX = Math.max(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is
- // for IE10 quirks mode within framesets
- chartY = e.y;
- } else {
- chartX = ePos.pageX - chartPosition.left;
- chartY = ePos.pageY - chartPosition.top;
- }
- return extend(e, {
- chartX: Math.round(chartX),
- chartY: Math.round(chartY)
- });
- },
- /**
- * Get the click position in terms of axis values.
- *
- * @param {Object} e A pointer event
- */
- getCoordinates: function(e) {
- var coordinates = {
- xAxis: [],
- yAxis: []
- };
- each(this.chart.axes, function(axis) {
- coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
- axis: axis,
- value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
- });
- });
- return coordinates;
- },
- /**
- * Collects the points closest to a mouseEvent
- * @param {Array} series Array of series to gather points from
- * @param {Boolean} shared True if shared tooltip, otherwise false
- * @param {Object} e Mouse event which possess a position to compare against
- * @return {Array} KDPoints sorted by distance
- */
- getKDPoints: function(series, shared, e) {
- var kdpoints = [],
- noSharedTooltip,
- directTouch,
- kdpointT,
- i;
- // Find nearest points on all series
- each(series, function(s) {
- // Skip hidden series
- noSharedTooltip = s.noSharedTooltip && shared;
- directTouch = !shared && s.directTouch;
- if (s.visible && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821
- // #3828
- kdpointT = s.searchPoint(
- e, !noSharedTooltip && s.options.findNearestPointBy.indexOf('y') < 0
- );
- if (kdpointT && kdpointT.series) { // Point.series becomes null when reset and before redraw (#5197)
- kdpoints.push(kdpointT);
- }
- }
- });
- // Sort kdpoints by distance to mouse pointer
- kdpoints.sort(function(p1, p2) {
- var isCloserX = p1.distX - p2.distX,
- isCloser = p1.dist - p2.dist,
- isAbove =
- (p2.series.group && p2.series.group.zIndex) -
- (p1.series.group && p1.series.group.zIndex),
- result;
- // We have two points which are not in the same place on xAxis and shared tooltip:
- if (isCloserX !== 0 && shared) { // #5721
- result = isCloserX;
- // Points are not exactly in the same place on x/yAxis:
- } else if (isCloser !== 0) {
- result = isCloser;
- // The same xAxis and yAxis position, sort by z-index:
- } else if (isAbove !== 0) {
- result = isAbove;
- // The same zIndex, sort by array index:
- } else {
- result = p1.series.index > p2.series.index ? -1 : 1;
- }
- return result;
- });
- // Remove points with different x-positions, required for shared tooltip and crosshairs (#4645):
- if (shared && kdpoints[0] && !kdpoints[0].series.noSharedTooltip) {
- i = kdpoints.length;
- while (i--) {
- if (kdpoints[i].x !== kdpoints[0].x || kdpoints[i].series.noSharedTooltip) {
- kdpoints.splice(i, 1);
- }
- }
- }
- return kdpoints;
- },
- getPointFromEvent: function(e) {
- var target = e.target,
- point;
- while (target && !point) {
- point = target.point;
- target = target.parentNode;
- }
- return point;
- },
- getChartCoordinatesFromPoint: function(point, inverted) {
- var series = point.series,
- xAxis = series.xAxis,
- yAxis = series.yAxis;
- if (xAxis && yAxis) {
- return inverted ? {
- chartX: xAxis.len + xAxis.pos - point.clientX,
- chartY: yAxis.len + yAxis.pos - point.plotY
- } : {
- chartX: point.clientX + xAxis.pos,
- chartY: point.plotY + yAxis.pos
- };
- }
- },
- /**
- * Calculates what is the current hovered point/points and series.
- *
- * @private
- *
- * @param {undefined|Point} existingHoverPoint
- * The point currrently beeing hovered.
- * @param {undefined|Series} existingHoverSeries
- * The series currently beeing hovered.
- * @param {Array<.Series>} series
- * All the series in the chart.
- * @param {boolean} isDirectTouch
- * Is the pointer directly hovering the point.
- * @param {boolean} shared
- * Whether it is a shared tooltip or not.
- * @param {object} coordinates
- * Chart coordinates of the pointer.
- * @param {number} coordinates.chartX
- * @param {number} coordinates.chartY
- *
- * @return {object}
- * Object containing resulting hover data.
- */
- getHoverData: function(
- existingHoverPoint,
- existingHoverSeries,
- series,
- isDirectTouch,
- shared,
- coordinates
- ) {
- var hoverPoint = existingHoverPoint,
- hoverSeries = existingHoverSeries,
- searchSeries = shared ? series : [hoverSeries],
- useExisting = !!(isDirectTouch && existingHoverPoint),
- notSticky = hoverSeries && !hoverSeries.stickyTracking,
- isHoverPoint = function(point, i) {
- return i === 0;
- },
- hoverPoints;
- // If there is a hoverPoint and its series requires direct touch (like
- // columns, #3899), or we're on a noSharedTooltip series among shared
- // tooltip series (#4546), use the existing hoverPoint.
- if (useExisting) {
- isHoverPoint = function(p) {
- return p === existingHoverPoint;
- };
- } else if (notSticky) {
- isHoverPoint = function(p) {
- return p.series === hoverSeries;
- };
- } else {
- // Avoid series with stickyTracking false
- searchSeries = H.grep(series, function(s) {
- return s.stickyTracking;
- });
- }
- hoverPoints = (useExisting && !shared) ?
- // Non-shared tooltips with directTouch don't use the k-d-tree
- [existingHoverPoint] :
- this.getKDPoints(searchSeries, shared, coordinates);
- hoverPoint = H.find(hoverPoints, isHoverPoint);
- hoverSeries = hoverPoint && hoverPoint.series;
- // In this case we could only look for the hoverPoint in series with
- // stickyTracking, but we should still include all series in the shared
- // tooltip.
- if (!useExisting && !notSticky && shared) {
- hoverPoints = this.getKDPoints(series, shared, coordinates);
- }
- // Keep the order of series in tooltip
- // Must be done after assigning of hoverPoint
- hoverPoints.sort(function(p1, p2) {
- return p1.series.index - p2.series.index;
- });
- return {
- hoverPoint: hoverPoint,
- hoverSeries: hoverSeries,
- hoverPoints: hoverPoints
- };
- },
- /**
- * With line type charts with a single tracker, get the point closest to the mouse.
- * Run Point.onMouseOver and display tooltip for the point or points.
- */
- runPointActions: function(e, p) {
- var pointer = this,
- chart = pointer.chart,
- series = chart.series,
- tooltip = chart.tooltip,
- shared = tooltip ? tooltip.shared : false,
- hoverPoint = p || chart.hoverPoint,
- hoverSeries = hoverPoint && hoverPoint.series || chart.hoverSeries,
- // onMouseOver or already hovering a series with directTouch
- isDirectTouch = !!p || (
- (hoverSeries && hoverSeries.directTouch) &&
- pointer.isDirectTouch
- ),
- hoverData = this.getHoverData(
- hoverPoint,
- hoverSeries,
- series,
- isDirectTouch,
- shared,
- e
- ),
- useSharedTooltip,
- followPointer,
- anchor,
- points;
- // Update variables from hoverData.
- hoverPoint = hoverData.hoverPoint;
- hoverSeries = hoverData.hoverSeries;
- followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
- useSharedTooltip = (
- shared &&
- hoverPoint &&
- !hoverPoint.series.noSharedTooltip
- );
- points = (useSharedTooltip ?
- hoverData.hoverPoints :
- (hoverPoint ? [hoverPoint] : [])
- );
- // Refresh tooltip for kdpoint if new hover point or tooltip was hidden
- // #3926, #4200
- if (
- hoverPoint &&
- // !(hoverSeries && hoverSeries.directTouch) &&
- (hoverPoint !== chart.hoverPoint || (tooltip && tooltip.isHidden))
- ) {
- each(chart.hoverPoints || [], function(p) {
- if (H.inArray(p, points) === -1) {
- p.setState();
- }
- });
- // Do mouseover on all points (#3919, #3985, #4410, #5622)
- each(points || [], function(p) {
- p.setState('hover');
- });
- // set normal state to previous series
- if (chart.hoverSeries !== hoverSeries) {
- hoverSeries.onMouseOver();
- }
- // If tracking is on series in stead of on each point,
- // fire mouseOver on hover point. // #4448
- if (chart.hoverPoint) {
- chart.hoverPoint.firePointEvent('mouseOut');
- }
- hoverPoint.firePointEvent('mouseOver');
- chart.hoverPoints = points;
- chart.hoverPoint = hoverPoint;
- // Draw tooltip if necessary
- if (tooltip) {
- tooltip.refresh(useSharedTooltip ? points : hoverPoint, e);
- }
- // Update positions (regardless of kdpoint or hoverPoint)
- } else if (followPointer && tooltip && !tooltip.isHidden) {
- anchor = tooltip.getAnchor([{}], e);
- tooltip.updatePosition({
- plotX: anchor[0],
- plotY: anchor[1]
- });
- }
- // Start the event listener to pick up the tooltip and crosshairs
- if (!pointer.unDocMouseMove) {
- pointer.unDocMouseMove = addEvent(doc, 'mousemove', function(e) {
- var chart = charts[H.hoverChartIndex];
- if (chart) {
- chart.pointer.onDocumentMouseMove(e);
- }
- });
- }
- // Issues related to crosshair #4927, #5269 #5066, #5658
- each(chart.axes, function drawAxisCrosshair(axis) {
- var snap = pick(axis.crosshair.snap, true);
- if (!snap) {
- axis.drawCrosshair(e);
- // Axis has snapping crosshairs, and one of the hover points belongs
- // to axis
- } else if (H.find(points, function(p) {
- return p.series[axis.coll] === axis;
- })) {
- axis.drawCrosshair(e, hoverPoint);
- // Axis has snapping crosshairs, but no hover point belongs to axis
- } else {
- axis.hideCrosshair();
- }
- });
- },
- /**
- * Reset the tracking by hiding the tooltip, the hover series state and the
- * hover point
- *
- * @param allowMove {Boolean}
- * Instead of destroying the tooltip altogether, allow moving it if
- * possible
- */
- reset: function(allowMove, delay) {
- var pointer = this,
- chart = pointer.chart,
- hoverSeries = chart.hoverSeries,
- hoverPoint = chart.hoverPoint,
- hoverPoints = chart.hoverPoints,
- tooltip = chart.tooltip,
- tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint;
- // Check if the points have moved outside the plot area (#1003, #4736, #5101)
- if (allowMove && tooltipPoints) {
- each(splat(tooltipPoints), function(point) {
- if (point.series.isCartesian && point.plotX === undefined) {
- allowMove = false;
- }
- });
- }
- // Just move the tooltip, #349
- if (allowMove) {
- if (tooltip && tooltipPoints) {
- tooltip.refresh(tooltipPoints);
- if (hoverPoint) { // #2500
- hoverPoint.setState(hoverPoint.state, true);
- each(chart.axes, function(axis) {
- if (axis.crosshair) {
- axis.drawCrosshair(null, hoverPoint);
- }
- });
- }
- }
- // Full reset
- } else {
- if (hoverPoint) {
- hoverPoint.onMouseOut();
- }
- if (hoverPoints) {
- each(hoverPoints, function(point) {
- point.setState();
- });
- }
- if (hoverSeries) {
- hoverSeries.onMouseOut();
- }
- if (tooltip) {
- tooltip.hide(delay);
- }
- if (pointer.unDocMouseMove) {
- pointer.unDocMouseMove = pointer.unDocMouseMove();
- }
- // Remove crosshairs
- each(chart.axes, function(axis) {
- axis.hideCrosshair();
- });
- pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null;
- }
- },
- /**
- * Scale series groups to a certain scale and translation
- */
- scaleGroups: function(attribs, clip) {
- var chart = this.chart,
- seriesAttribs;
- // Scale each series
- each(chart.series, function(series) {
- seriesAttribs = attribs || series.getPlotBox(); // #1701
- if (series.xAxis && series.xAxis.zoomEnabled && series.group) {
- series.group.attr(seriesAttribs);
- if (series.markerGroup) {
- series.markerGroup.attr(seriesAttribs);
- series.markerGroup.clip(clip ? chart.clipRect : null);
- }
- if (series.dataLabelsGroup) {
- series.dataLabelsGroup.attr(seriesAttribs);
- }
- }
- });
- // Clip
- chart.clipRect.attr(clip || chart.clipBox);
- },
- /**
- * Start a drag operation
- */
- dragStart: function(e) {
- var chart = this.chart;
- // Record the start position
- chart.mouseIsDown = e.type;
- chart.cancelClick = false;
- chart.mouseDownX = this.mouseDownX = e.chartX;
- chart.mouseDownY = this.mouseDownY = e.chartY;
- },
- /**
- * Perform a drag operation in response to a mousemove event while the mouse is down
- */
- drag: function(e) {
- var chart = this.chart,
- chartOptions = chart.options.chart,
- chartX = e.chartX,
- chartY = e.chartY,
- zoomHor = this.zoomHor,
- zoomVert = this.zoomVert,
- plotLeft = chart.plotLeft,
- plotTop = chart.plotTop,
- plotWidth = chart.plotWidth,
- plotHeight = chart.plotHeight,
- clickedInside,
- size,
- selectionMarker = this.selectionMarker,
- mouseDownX = this.mouseDownX,
- mouseDownY = this.mouseDownY,
- panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key'];
- // If the device supports both touch and mouse (like IE11), and we are touch-dragging
- // inside the plot area, don't handle the mouse event. #4339.
- if (selectionMarker && selectionMarker.touch) {
- return;
- }
- // If the mouse is outside the plot area, adjust to cooordinates
- // inside to prevent the selection marker from going outside
- if (chartX < plotLeft) {
- chartX = plotLeft;
- } else if (chartX > plotLeft + plotWidth) {
- chartX = plotLeft + plotWidth;
- }
- if (chartY < plotTop) {
- chartY = plotTop;
- } else if (chartY > plotTop + plotHeight) {
- chartY = plotTop + plotHeight;
- }
- // determine if the mouse has moved more than 10px
- this.hasDragged = Math.sqrt(
- Math.pow(mouseDownX - chartX, 2) +
- Math.pow(mouseDownY - chartY, 2)
- );
- if (this.hasDragged > 10) {
- clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
- // make a selection
- if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) {
- if (!selectionMarker) {
- this.selectionMarker = selectionMarker = chart.renderer.rect(
- plotLeft,
- plotTop,
- zoomHor ? 1 : plotWidth,
- zoomVert ? 1 : plotHeight,
- 0
- )
- .attr({
- fill: chartOptions.selectionMarkerFill || color('#335cad').setOpacity(0.25).get(),
- 'class': 'highcharts-selection-marker',
- 'zIndex': 7
- })
- .add();
- }
- }
- // adjust the width of the selection marker
- if (selectionMarker && zoomHor) {
- size = chartX - mouseDownX;
- selectionMarker.attr({
- width: Math.abs(size),
- x: (size > 0 ? 0 : size) + mouseDownX
- });
- }
- // adjust the height of the selection marker
- if (selectionMarker && zoomVert) {
- size = chartY - mouseDownY;
- selectionMarker.attr({
- height: Math.abs(size),
- y: (size > 0 ? 0 : size) + mouseDownY
- });
- }
- // panning
- if (clickedInside && !selectionMarker && chartOptions.panning) {
- chart.pan(e, chartOptions.panning);
- }
- }
- },
- /**
- * On mouse up or touch end across the entire document, drop the selection.
- */
- drop: function(e) {
- var pointer = this,
- chart = this.chart,
- hasPinched = this.hasPinched;
- if (this.selectionMarker) {
- var selectionData = {
- originalEvent: e, // #4890
- xAxis: [],
- yAxis: []
- },
- selectionBox = this.selectionMarker,
- selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x,
- selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y,
- selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width,
- selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height,
- runZoom;
- // a selection has been made
- if (this.hasDragged || hasPinched) {
- // record each axis' min and max
- each(chart.axes, function(axis) {
- if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{
- xAxis: 'zoomX',
- yAxis: 'zoomY'
- }[axis.coll]])) { // #859, #3569
- var horiz = axis.horiz,
- minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding : 0, // #1207, #3075
- selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding),
- selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding);
- selectionData[axis.coll].push({
- axis: axis,
- min: Math.min(selectionMin, selectionMax), // for reversed axes
- max: Math.max(selectionMin, selectionMax)
- });
- runZoom = true;
- }
- });
- if (runZoom) {
- fireEvent(chart, 'selection', selectionData, function(args) {
- chart.zoom(extend(args, hasPinched ? {
- animation: false
- } : null));
- });
- }
- }
- this.selectionMarker = this.selectionMarker.destroy();
- // Reset scaling preview
- if (hasPinched) {
- this.scaleGroups();
- }
- }
- // Reset all
- if (chart) { // it may be destroyed on mouse up - #877
- css(chart.container, {
- cursor: chart._cursor
- });
- chart.cancelClick = this.hasDragged > 10; // #370
- chart.mouseIsDown = this.hasDragged = this.hasPinched = false;
- this.pinchDown = [];
- }
- },
- onContainerMouseDown: function(e) {
- e = this.normalize(e);
- this.zoomOption(e);
- // issue #295, dragging not always working in Firefox
- if (e.preventDefault) {
- e.preventDefault();
- }
- this.dragStart(e);
- },
- onDocumentMouseUp: function(e) {
- if (charts[H.hoverChartIndex]) {
- charts[H.hoverChartIndex].pointer.drop(e);
- }
- },
- /**
- * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
- * Issue #149 workaround. The mouseleave event does not always fire.
- */
- onDocumentMouseMove: function(e) {
- var chart = this.chart,
- chartPosition = this.chartPosition;
- e = this.normalize(e, chartPosition);
- // If we're outside, hide the tooltip
- if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') &&
- !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
- this.reset();
- }
- },
- /**
- * When mouse leaves the container, hide the tooltip.
- */
- onContainerMouseLeave: function(e) {
- var chart = charts[H.hoverChartIndex];
- if (chart && (e.relatedTarget || e.toElement)) { // #4886, MS Touch end fires mouseleave but with no related target
- chart.pointer.reset();
- chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix
- }
- },
- // The mousemove, touchmove and touchstart event handler
- onContainerMouseMove: function(e) {
- var chart = this.chart;
- if (!defined(H.hoverChartIndex) || !charts[H.hoverChartIndex] || !charts[H.hoverChartIndex].mouseIsDown) {
- H.hoverChartIndex = chart.index;
- }
- e = this.normalize(e);
- e.returnValue = false; // #2251, #3224
- if (chart.mouseIsDown === 'mousedown') {
- this.drag(e);
- }
- // Show the tooltip and run mouse over events (#977)
- if ((this.inClass(e.target, 'highcharts-tracker') ||
- chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) {
- this.runPointActions(e);
- }
- },
- /**
- * Utility to detect whether an element has, or has a parent with, a specific
- * class name. Used on detection of tracker objects and on deciding whether
- * hovering the tooltip should cause the active series to mouse out.
- */
- inClass: function(element, className) {
- var elemClassName;
- while (element) {
- elemClassName = attr(element, 'class');
- if (elemClassName) {
- if (elemClassName.indexOf(className) !== -1) {
- return true;
- }
- if (elemClassName.indexOf('highcharts-container') !== -1) {
- return false;
- }
- }
- element = element.parentNode;
- }
- },
- onTrackerMouseOut: function(e) {
- var series = this.chart.hoverSeries,
- relatedTarget = e.relatedTarget || e.toElement;
- this.isDirectTouch = false;
- if (series && relatedTarget && !series.stickyTracking &&
- !this.inClass(relatedTarget, 'highcharts-tooltip') &&
- (!this.inClass(relatedTarget, 'highcharts-series-' + series.index) || // #2499, #4465
- !this.inClass(relatedTarget, 'highcharts-tracker') // #5553
- )
- ) {
- series.onMouseOut();
- }
- },
- onContainerClick: function(e) {
- var chart = this.chart,
- hoverPoint = chart.hoverPoint,
- plotLeft = chart.plotLeft,
- plotTop = chart.plotTop;
- e = this.normalize(e);
- if (!chart.cancelClick) {
- // On tracker click, fire the series and point events. #783, #1583
- if (hoverPoint && this.inClass(e.target, 'highcharts-tracker')) {
- // the series click event
- fireEvent(hoverPoint.series, 'click', extend(e, {
- point: hoverPoint
- }));
- // the point click event
- if (chart.hoverPoint) { // it may be destroyed (#1844)
- hoverPoint.firePointEvent('click', e);
- }
- // When clicking outside a tracker, fire a chart event
- } else {
- extend(e, this.getCoordinates(e));
- // fire a click event in the chart
- if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
- fireEvent(chart, 'click', e);
- }
- }
- }
- },
- /**
- * Set the JS DOM events on the container and document. This method should contain
- * a one-to-one assignment between methods and their handlers. Any advanced logic should
- * be moved to the handler reflecting the event's name.
- */
- setDOMEvents: function() {
- var pointer = this,
- container = pointer.chart.container;
- container.onmousedown = function(e) {
- pointer.onContainerMouseDown(e);
- };
- container.onmousemove = function(e) {
- pointer.onContainerMouseMove(e);
- };
- container.onclick = function(e) {
- pointer.onContainerClick(e);
- };
- addEvent(container, 'mouseleave', pointer.onContainerMouseLeave);
- if (H.chartCount === 1) {
- addEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
- }
- if (H.hasTouch) {
- container.ontouchstart = function(e) {
- pointer.onContainerTouchStart(e);
- };
- container.ontouchmove = function(e) {
- pointer.onContainerTouchMove(e);
- };
- if (H.chartCount === 1) {
- addEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
- }
- }
- },
- /**
- * Destroys the Pointer object and disconnects DOM events.
- */
- destroy: function() {
- var pointer = this;
- if (pointer.unDocMouseMove) {
- pointer.unDocMouseMove();
- }
- removeEvent(
- pointer.chart.container,
- 'mouseleave',
- pointer.onContainerMouseLeave
- );
- if (!H.chartCount) {
- removeEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
- removeEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
- }
- // memory and CPU leak
- clearInterval(pointer.tooltipTimeout);
- H.objectEach(pointer, function(val, prop) {
- pointer[prop] = null;
- });
- }
- };
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var charts = H.charts,
- each = H.each,
- extend = H.extend,
- map = H.map,
- noop = H.noop,
- pick = H.pick,
- Pointer = H.Pointer;
- /* Support for touch devices */
- extend(Pointer.prototype, /** @lends Pointer.prototype */ {
- /**
- * Run translation operations
- */
- pinchTranslate: function(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
- if (this.zoomHor) {
- this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
- }
- if (this.zoomVert) {
- this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
- }
- },
- /**
- * Run translation operations for each direction (horizontal and vertical) independently
- */
- pinchTranslateDirection: function(horiz, pinchDown, touches, transform,
- selectionMarker, clip, lastValidTouch, forcedScale) {
- var chart = this.chart,
- xy = horiz ? 'x' : 'y',
- XY = horiz ? 'X' : 'Y',
- sChartXY = 'chart' + XY,
- wh = horiz ? 'width' : 'height',
- plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
- selectionWH,
- selectionXY,
- clipXY,
- scale = forcedScale || 1,
- inverted = chart.inverted,
- bounds = chart.bounds[horiz ? 'h' : 'v'],
- singleTouch = pinchDown.length === 1,
- touch0Start = pinchDown[0][sChartXY],
- touch0Now = touches[0][sChartXY],
- touch1Start = !singleTouch && pinchDown[1][sChartXY],
- touch1Now = !singleTouch && touches[1][sChartXY],
- outOfBounds,
- transformScale,
- scaleKey,
- setScale = function() {
- // Don't zoom if fingers are too close on this axis
- if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) {
- scale = forcedScale || Math.abs(touch0Now - touch1Now) / Math.abs(touch0Start - touch1Start);
- }
- clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
- selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
- };
- // Set the scale, first pass
- setScale();
- selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
- // Out of bounds
- if (selectionXY < bounds.min) {
- selectionXY = bounds.min;
- outOfBounds = true;
- } else if (selectionXY + selectionWH > bounds.max) {
- selectionXY = bounds.max - selectionWH;
- outOfBounds = true;
- }
- // Is the chart dragged off its bounds, determined by dataMin and dataMax?
- if (outOfBounds) {
- // Modify the touchNow position in order to create an elastic drag movement. This indicates
- // to the user that the chart is responsive but can't be dragged further.
- touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
- if (!singleTouch) {
- touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
- }
- // Set the scale, second pass to adapt to the modified touchNow positions
- setScale();
- } else {
- lastValidTouch[xy] = [touch0Now, touch1Now];
- }
- // Set geometry for clipping, selection and transformation
- if (!inverted) {
- clip[xy] = clipXY - plotLeftTop;
- clip[wh] = selectionWH;
- }
- scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
- transformScale = inverted ? 1 / scale : scale;
- selectionMarker[wh] = selectionWH;
- selectionMarker[xy] = selectionXY;
- transform[scaleKey] = scale;
- transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
- },
- /**
- * Handle touch events with two touches
- */
- pinch: function(e) {
- var self = this,
- chart = self.chart,
- pinchDown = self.pinchDown,
- touches = e.touches,
- touchesLength = touches.length,
- lastValidTouch = self.lastValidTouch,
- hasZoom = self.hasZoom,
- selectionMarker = self.selectionMarker,
- transform = {},
- fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, 'highcharts-tracker') &&
- chart.runTrackerClick) || self.runChartClick),
- clip = {};
- // Don't initiate panning until the user has pinched. This prevents us from
- // blocking page scrolling as users scroll down a long page (#4210).
- if (touchesLength > 1) {
- self.initiated = true;
- }
- // On touch devices, only proceed to trigger click if a handler is defined
- if (hasZoom && self.initiated && !fireClickEvent) {
- e.preventDefault();
- }
- // Normalize each touch
- map(touches, function(e) {
- return self.normalize(e);
- });
- // Register the touch start position
- if (e.type === 'touchstart') {
- each(touches, function(e, i) {
- pinchDown[i] = {
- chartX: e.chartX,
- chartY: e.chartY
- };
- });
- lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
- lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
- // Identify the data bounds in pixels
- each(chart.axes, function(axis) {
- if (axis.zoomEnabled) {
- var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
- minPixelPadding = axis.minPixelPadding,
- min = axis.toPixels(pick(axis.options.min, axis.dataMin)),
- max = axis.toPixels(pick(axis.options.max, axis.dataMax)),
- absMin = Math.min(min, max),
- absMax = Math.max(min, max);
- // Store the bounds for use in the touchmove handler
- bounds.min = Math.min(axis.pos, absMin - minPixelPadding);
- bounds.max = Math.max(axis.pos + axis.len, absMax + minPixelPadding);
- }
- });
- self.res = true; // reset on next move
- // Optionally move the tooltip on touchmove
- } else if (self.followTouchMove && touchesLength === 1) {
- this.runPointActions(self.normalize(e));
- // Event type is touchmove, handle panning and pinching
- } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
- // Set the marker
- if (!selectionMarker) {
- self.selectionMarker = selectionMarker = extend({
- destroy: noop,
- touch: true
- }, chart.plotBox);
- }
- self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
- self.hasPinched = hasZoom;
- // Scale and translate the groups to provide visual feedback during pinching
- self.scaleGroups(transform, clip);
- if (self.res) {
- self.res = false;
- this.reset(false, 0);
- }
- }
- },
- /**
- * General touch handler shared by touchstart and touchmove.
- */
- touch: function(e, start) {
- var chart = this.chart,
- hasMoved,
- pinchDown,
- isInside;
- if (chart.index !== H.hoverChartIndex) {
- this.onContainerMouseLeave({
- relatedTarget: true
- });
- }
- H.hoverChartIndex = chart.index;
- if (e.touches.length === 1) {
- e = this.normalize(e);
- isInside = chart.isInsidePlot(
- e.chartX - chart.plotLeft,
- e.chartY - chart.plotTop
- );
- if (isInside && !chart.openMenu) {
- // Run mouse events and display tooltip etc
- if (start) {
- this.runPointActions(e);
- }
- // Android fires touchmove events after the touchstart even if the
- // finger hasn't moved, or moved only a pixel or two. In iOS however,
- // the touchmove doesn't fire unless the finger moves more than ~4px.
- // So we emulate this behaviour in Android by checking how much it
- // moved, and cancelling on small distances. #3450.
- if (e.type === 'touchmove') {
- pinchDown = this.pinchDown;
- hasMoved = pinchDown[0] ? Math.sqrt( // #5266
- Math.pow(pinchDown[0].chartX - e.chartX, 2) +
- Math.pow(pinchDown[0].chartY - e.chartY, 2)
- ) >= 4 : false;
- }
- if (pick(hasMoved, true)) {
- this.pinch(e);
- }
- } else if (start) {
- // Hide the tooltip on touching outside the plot area (#1203)
- this.reset();
- }
- } else if (e.touches.length === 2) {
- this.pinch(e);
- }
- },
- onContainerTouchStart: function(e) {
- this.zoomOption(e);
- this.touch(e, true);
- },
- onContainerTouchMove: function(e) {
- this.touch(e);
- },
- onDocumentTouchEnd: function(e) {
- if (charts[H.hoverChartIndex]) {
- charts[H.hoverChartIndex].pointer.drop(e);
- }
- }
- });
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- charts = H.charts,
- css = H.css,
- doc = H.doc,
- extend = H.extend,
- hasTouch = H.hasTouch,
- noop = H.noop,
- Pointer = H.Pointer,
- removeEvent = H.removeEvent,
- win = H.win,
- wrap = H.wrap;
- if (!hasTouch && (win.PointerEvent || win.MSPointerEvent)) {
- // The touches object keeps track of the points being touched at all times
- var touches = {},
- hasPointerEvent = !!win.PointerEvent,
- getWebkitTouches = function() {
- var fake = [];
- fake.item = function(i) {
- return this[i];
- };
- H.objectEach(touches, function(touch) {
- fake.push({
- pageX: touch.pageX,
- pageY: touch.pageY,
- target: touch.target
- });
- });
- return fake;
- },
- translateMSPointer = function(e, method, wktype, func) {
- var p;
- if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[H.hoverChartIndex]) {
- func(e);
- p = charts[H.hoverChartIndex].pointer;
- p[method]({
- type: wktype,
- target: e.currentTarget,
- preventDefault: noop,
- touches: getWebkitTouches()
- });
- }
- };
- /**
- * Extend the Pointer prototype with methods for each event handler and more
- */
- extend(Pointer.prototype, /** @lends Pointer.prototype */ {
- onContainerPointerDown: function(e) {
- translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function(e) {
- touches[e.pointerId] = {
- pageX: e.pageX,
- pageY: e.pageY,
- target: e.currentTarget
- };
- });
- },
- onContainerPointerMove: function(e) {
- translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function(e) {
- touches[e.pointerId] = {
- pageX: e.pageX,
- pageY: e.pageY
- };
- if (!touches[e.pointerId].target) {
- touches[e.pointerId].target = e.currentTarget;
- }
- });
- },
- onDocumentPointerUp: function(e) {
- translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function(e) {
- delete touches[e.pointerId];
- });
- },
- /**
- * Add or remove the MS Pointer specific events
- */
- batchMSEvents: function(fn) {
- fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown);
- fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove);
- fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp);
- }
- });
- // Disable default IE actions for pinch and such on chart element
- wrap(Pointer.prototype, 'init', function(proceed, chart, options) {
- proceed.call(this, chart, options);
- if (this.hasZoom) { // #4014
- css(chart.container, {
- '-ms-touch-action': 'none',
- 'touch-action': 'none'
- });
- }
- });
- // Add IE specific touch events to chart
- wrap(Pointer.prototype, 'setDOMEvents', function(proceed) {
- proceed.apply(this);
- if (this.hasZoom || this.followTouchMove) {
- this.batchMSEvents(addEvent);
- }
- });
- // Destroy MS events also
- wrap(Pointer.prototype, 'destroy', function(proceed) {
- this.batchMSEvents(removeEvent);
- proceed.call(this);
- });
- }
- }(Highcharts));
- (function(Highcharts) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var H = Highcharts,
- addEvent = H.addEvent,
- css = H.css,
- discardElement = H.discardElement,
- defined = H.defined,
- each = H.each,
- isFirefox = H.isFirefox,
- marginNames = H.marginNames,
- merge = H.merge,
- pick = H.pick,
- setAnimation = H.setAnimation,
- stableSort = H.stableSort,
- win = H.win,
- wrap = H.wrap;
- /**
- * The overview of the chart's series. The legend object is instanciated
- * internally in the chart constructor, and available from `chart.legend`. Each
- * chart has only one legend.
- *
- * @class
- */
- Highcharts.Legend = function(chart, options) {
- this.init(chart, options);
- };
- Highcharts.Legend.prototype = {
- /**
- * Initialize the legend
- */
- init: function(chart, options) {
- this.chart = chart;
- this.setOptions(options);
- if (options.enabled) {
- // Render it
- this.render();
- // move checkboxes
- addEvent(this.chart, 'endResize', function() {
- this.legend.positionCheckboxes();
- });
- }
- },
- setOptions: function(options) {
- var padding = pick(options.padding, 8);
- this.options = options;
- this.itemStyle = options.itemStyle;
- this.itemHiddenStyle = merge(this.itemStyle, options.itemHiddenStyle);
- this.itemMarginTop = options.itemMarginTop || 0;
- this.padding = padding;
- this.initialItemY = padding - 5; // 5 is pixels above the text
- this.maxItemWidth = 0;
- this.itemHeight = 0;
- this.symbolWidth = pick(options.symbolWidth, 16);
- this.pages = [];
- },
- /**
- * Update the legend with new options. Equivalent to running `chart.update`
- * with a legend configuration option.
- * @param {LegendOptions} options
- * Legend options.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart.
- *
- * @sample highcharts/legend/legend-update/
- * Legend update
- */
- update: function(options, redraw) {
- var chart = this.chart;
- this.setOptions(merge(true, this.options, options));
- this.destroy();
- chart.isDirtyLegend = chart.isDirtyBox = true;
- if (pick(redraw, true)) {
- chart.redraw();
- }
- },
- /**
- * Set the colors for the legend item
- * @param {Object} item A Series or Point instance
- * @param {Object} visible Dimmed or colored
- */
- colorizeItem: function(item, visible) {
- item.legendGroup[visible ? 'removeClass' : 'addClass'](
- 'highcharts-legend-item-hidden'
- );
- var legend = this,
- options = legend.options,
- legendItem = item.legendItem,
- legendLine = item.legendLine,
- legendSymbol = item.legendSymbol,
- hiddenColor = legend.itemHiddenStyle.color,
- textColor = visible ? options.itemStyle.color : hiddenColor,
- symbolColor = visible ? (item.color || hiddenColor) : hiddenColor,
- markerOptions = item.options && item.options.marker,
- symbolAttr = {
- fill: symbolColor
- };
- if (legendItem) {
- legendItem.css({
- fill: textColor,
- color: textColor // #1553, oldIE
- });
- }
- if (legendLine) {
- legendLine.attr({
- stroke: symbolColor
- });
- }
- if (legendSymbol) {
- // Apply marker options
- if (markerOptions && legendSymbol.isMarker) { // #585
- //symbolAttr.stroke = symbolColor;
- symbolAttr = item.pointAttribs();
- if (!visible) {
- H.objectEach(symbolAttr, function(val, key) {
- symbolAttr[key] = hiddenColor;
- });
- }
- }
- legendSymbol.attr(symbolAttr);
- }
- },
- /**
- * Position the legend item
- * @param {Object} item A Series or Point instance
- */
- positionItem: function(item) {
- var legend = this,
- options = legend.options,
- symbolPadding = options.symbolPadding,
- ltr = !options.rtl,
- legendItemPos = item._legendItemPos,
- itemX = legendItemPos[0],
- itemY = legendItemPos[1],
- checkbox = item.checkbox,
- legendGroup = item.legendGroup;
- if (legendGroup && legendGroup.element) {
- legendGroup.translate(
- ltr ?
- itemX :
- legend.legendWidth - itemX - 2 * symbolPadding - 4,
- itemY
- );
- }
- if (checkbox) {
- checkbox.x = itemX;
- checkbox.y = itemY;
- }
- },
- /**
- * Destroy a single legend item
- * @param {Object} item The series or point
- */
- destroyItem: function(item) {
- var checkbox = item.checkbox;
- // destroy SVG elements
- each(
- ['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'],
- function(key) {
- if (item[key]) {
- item[key] = item[key].destroy();
- }
- }
- );
- if (checkbox) {
- discardElement(item.checkbox);
- }
- },
- /**
- * Destroys the legend.
- */
- destroy: function() {
- function destroyItems(key) {
- if (this[key]) {
- this[key] = this[key].destroy();
- }
- }
- // Destroy items
- each(this.getAllItems(), function(item) {
- each(['legendItem', 'legendGroup'], destroyItems, item);
- });
- // Destroy legend elements
- each([
- 'clipRect',
- 'up',
- 'down',
- 'pager',
- 'nav',
- 'box',
- 'title',
- 'group'
- ], destroyItems, this);
- this.display = null; // Reset in .render on update.
- },
- /**
- * Position the checkboxes after the width is determined
- */
- positionCheckboxes: function(scrollOffset) {
- var alignAttr = this.group && this.group.alignAttr,
- translateY,
- clipHeight = this.clipHeight || this.legendHeight,
- titleHeight = this.titleHeight;
- if (alignAttr) {
- translateY = alignAttr.translateY;
- each(this.allItems, function(item) {
- var checkbox = item.checkbox,
- top;
- if (checkbox) {
- top = translateY + titleHeight + checkbox.y +
- (scrollOffset || 0) + 3;
- css(checkbox, {
- left: (alignAttr.translateX + item.checkboxOffset +
- checkbox.x - 20) + 'px',
- top: top + 'px',
- display: top > translateY - 6 && top < translateY +
- clipHeight - 6 ? '' : 'none'
- });
- }
- });
- }
- },
- /**
- * Render the legend title on top of the legend
- */
- renderTitle: function() {
- var options = this.options,
- padding = this.padding,
- titleOptions = options.title,
- titleHeight = 0,
- bBox;
- if (titleOptions.text) {
- if (!this.title) {
- this.title = this.chart.renderer.label(
- titleOptions.text,
- padding - 3,
- padding - 4,
- null,
- null,
- null,
- options.useHTML,
- null,
- 'legend-title'
- )
- .attr({
- zIndex: 1
- })
- .css(titleOptions.style)
- .add(this.group);
- }
- bBox = this.title.getBBox();
- titleHeight = bBox.height;
- this.offsetWidth = bBox.width; // #1717
- this.contentGroup.attr({
- translateY: titleHeight
- });
- }
- this.titleHeight = titleHeight;
- },
- /**
- * Set the legend item text
- */
- setText: function(item) {
- var options = this.options;
- item.legendItem.attr({
- text: options.labelFormat ?
- H.format(options.labelFormat, item) : options.labelFormatter.call(item)
- });
- },
- /**
- * Render a single specific legend item
- * @param {Object} item A series or point
- */
- renderItem: function(item) {
- var legend = this,
- chart = legend.chart,
- renderer = chart.renderer,
- options = legend.options,
- horizontal = options.layout === 'horizontal',
- symbolWidth = legend.symbolWidth,
- symbolPadding = options.symbolPadding,
- itemStyle = legend.itemStyle,
- itemHiddenStyle = legend.itemHiddenStyle,
- padding = legend.padding,
- itemDistance = horizontal ? pick(options.itemDistance, 20) : 0,
- ltr = !options.rtl,
- itemHeight,
- widthOption = options.width,
- itemMarginBottom = options.itemMarginBottom || 0,
- itemMarginTop = legend.itemMarginTop,
- bBox,
- itemWidth,
- li = item.legendItem,
- isSeries = !item.series,
- series = !isSeries && item.series.drawLegendSymbol ?
- item.series :
- item,
- seriesOptions = series.options,
- showCheckbox = legend.createCheckboxForItem &&
- seriesOptions &&
- seriesOptions.showCheckbox,
- // full width minus text width
- itemExtraWidth = symbolWidth + symbolPadding + itemDistance +
- (showCheckbox ? 20 : 0),
- useHTML = options.useHTML,
- fontSize = 12,
- itemClassName = item.options.className;
- if (!li) { // generate it once, later move it
- // Generate the group box, a group to hold the symbol and text. Text
- // is to be appended in Legend class.
- item.legendGroup = renderer.g('legend-item')
- .addClass(
- 'highcharts-' + series.type + '-series ' +
- 'highcharts-color-' + item.colorIndex +
- (itemClassName ? ' ' + itemClassName : '') +
- (isSeries ? ' highcharts-series-' + item.index : '')
- )
- .attr({
- zIndex: 1
- })
- .add(legend.scrollGroup);
- // Generate the list item text and add it to the group
- item.legendItem = li = renderer.text(
- '',
- ltr ? symbolWidth + symbolPadding : -symbolPadding,
- legend.baseline || 0,
- useHTML
- )
- // merge to prevent modifying original (#1021)
- .css(merge(item.visible ? itemStyle : itemHiddenStyle))
- .attr({
- align: ltr ? 'left' : 'right',
- zIndex: 2
- })
- .add(item.legendGroup);
- // Get the baseline for the first item - the font size is equal for
- // all
- if (!legend.baseline) {
- fontSize = itemStyle.fontSize;
- legend.fontMetrics = renderer.fontMetrics(
- fontSize,
- li
- );
- legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop;
- li.attr('y', legend.baseline);
- }
- // Draw the legend symbol inside the group box
- legend.symbolHeight = options.symbolHeight || legend.fontMetrics.f;
- series.drawLegendSymbol(legend, item);
- if (legend.setItemEvents) {
- legend.setItemEvents(item, li, useHTML);
- }
- // add the HTML checkbox on top
- if (showCheckbox) {
- legend.createCheckboxForItem(item);
- }
- }
- // Colorize the items
- legend.colorizeItem(item, item.visible);
- // Take care of max width and text overflow (#6659)
- if (!itemStyle.width) {
- li.css({
- width: (options.itemWidth || chart.spacingBox.width) -
- itemExtraWidth
- });
- }
- // Always update the text
- legend.setText(item);
- // calculate the positions for the next line
- bBox = li.getBBox();
- itemWidth = item.checkboxOffset =
- options.itemWidth ||
- item.legendItemWidth ||
- bBox.width + itemExtraWidth;
- legend.itemHeight = itemHeight = Math.round(
- item.legendItemHeight || bBox.height || legend.symbolHeight
- );
- // If the item exceeds the width, start a new line
- if (
- horizontal &&
- legend.itemX - padding + itemWidth > (
- widthOption || (
- chart.spacingBox.width - 2 * padding - options.x
- )
- )
- ) {
- legend.itemX = padding;
- legend.itemY += itemMarginTop + legend.lastLineHeight +
- itemMarginBottom;
- legend.lastLineHeight = 0; // reset for next line (#915, #3976)
- }
- // If the item exceeds the height, start a new column
- /*if (!horizontal && legend.itemY + options.y +
- itemHeight > chart.chartHeight - spacingTop - spacingBottom) {
- legend.itemY = legend.initialItemY;
- legend.itemX += legend.maxItemWidth;
- legend.maxItemWidth = 0;
- }*/
- // Set the edge positions
- legend.maxItemWidth = Math.max(legend.maxItemWidth, itemWidth);
- legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom;
- legend.lastLineHeight = Math.max( // #915
- itemHeight,
- legend.lastLineHeight
- );
- // cache the position of the newly generated or reordered items
- item._legendItemPos = [legend.itemX, legend.itemY];
- // advance
- if (horizontal) {
- legend.itemX += itemWidth;
- } else {
- legend.itemY += itemMarginTop + itemHeight + itemMarginBottom;
- legend.lastLineHeight = itemHeight;
- }
- // the width of the widest item
- legend.offsetWidth = widthOption || Math.max(
- (horizontal ? legend.itemX - padding - itemDistance : itemWidth) +
- padding,
- legend.offsetWidth
- );
- },
- /**
- * Get all items, which is one item per series for normal series and one
- * item per point for pie series.
- */
- getAllItems: function() {
- var allItems = [];
- each(this.chart.series, function(series) {
- var seriesOptions = series && series.options;
- // Handle showInLegend. If the series is linked to another series,
- // defaults to false.
- if (series && pick(
- seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? undefined : false, true
- )) {
- // Use points or series for the legend item depending on
- // legendType
- allItems = allItems.concat(
- series.legendItems ||
- (
- seriesOptions.legendType === 'point' ?
- series.data :
- series
- )
- );
- }
- });
- return allItems;
- },
- /**
- * Adjust the chart margins by reserving space for the legend on only one
- * side of the chart. If the position is set to a corner, top or bottom is
- * reserved for horizontal legends and left or right for vertical ones.
- */
- adjustMargins: function(margin, spacing) {
- var chart = this.chart,
- options = this.options,
- // Use the first letter of each alignment option in order to detect
- // the side. (#4189 - use charAt(x) notation instead of [x] for IE7)
- alignment = options.align.charAt(0) +
- options.verticalAlign.charAt(0) +
- options.layout.charAt(0);
- if (!options.floating) {
- each([
- /(lth|ct|rth)/,
- /(rtv|rm|rbv)/,
- /(rbh|cb|lbh)/,
- /(lbv|lm|ltv)/
- ], function(alignments, side) {
- if (alignments.test(alignment) && !defined(margin[side])) {
- // Now we have detected on which side of the chart we should
- // reserve space for the legend
- chart[marginNames[side]] = Math.max(
- chart[marginNames[side]],
- (
- chart.legend[
- (side + 1) % 2 ? 'legendHeight' : 'legendWidth'
- ] + [1, -1, -1, 1][side] * options[
- (side % 2) ? 'x' : 'y'
- ] +
- pick(options.margin, 12) +
- spacing[side]
- )
- );
- }
- });
- }
- },
- /**
- * Render the legend. This method can be called both before and after
- * chart.render. If called after, it will only rearrange items instead
- * of creating new ones.
- */
- render: function() {
- var legend = this,
- chart = legend.chart,
- renderer = chart.renderer,
- legendGroup = legend.group,
- allItems,
- display,
- legendWidth,
- legendHeight,
- box = legend.box,
- options = legend.options,
- padding = legend.padding;
- legend.itemX = padding;
- legend.itemY = legend.initialItemY;
- legend.offsetWidth = 0;
- legend.lastItemY = 0;
- if (!legendGroup) {
- legend.group = legendGroup = renderer.g('legend')
- .attr({
- zIndex: 7
- })
- .add();
- legend.contentGroup = renderer.g()
- .attr({
- zIndex: 1
- }) // above background
- .add(legendGroup);
- legend.scrollGroup = renderer.g()
- .add(legend.contentGroup);
- }
- legend.renderTitle();
- // add each series or point
- allItems = legend.getAllItems();
- // sort by legendIndex
- stableSort(allItems, function(a, b) {
- return ((a.options && a.options.legendIndex) || 0) -
- ((b.options && b.options.legendIndex) || 0);
- });
- // reversed legend
- if (options.reversed) {
- allItems.reverse();
- }
- legend.allItems = allItems;
- legend.display = display = !!allItems.length;
- // render the items
- legend.lastLineHeight = 0;
- each(allItems, function(item) {
- legend.renderItem(item);
- });
- // Get the box
- legendWidth = (options.width || legend.offsetWidth) + padding;
- legendHeight = legend.lastItemY + legend.lastLineHeight +
- legend.titleHeight;
- legendHeight = legend.handleOverflow(legendHeight);
- legendHeight += padding;
- // Draw the border and/or background
- if (!box) {
- legend.box = box = renderer.rect()
- .addClass('highcharts-legend-box')
- .attr({
- r: options.borderRadius
- })
- .add(legendGroup);
- box.isNew = true;
- }
- // Presentational
- box
- .attr({
- stroke: options.borderColor,
- 'stroke-width': options.borderWidth || 0,
- fill: options.backgroundColor || 'none'
- })
- .shadow(options.shadow);
- if (legendWidth > 0 && legendHeight > 0) {
- box[box.isNew ? 'attr' : 'animate'](
- box.crisp({
- x: 0,
- y: 0,
- width: legendWidth,
- height: legendHeight
- }, box.strokeWidth())
- );
- box.isNew = false;
- }
- // hide the border if no items
- box[display ? 'show' : 'hide']();
- legend.legendWidth = legendWidth;
- legend.legendHeight = legendHeight;
- // Now that the legend width and height are established, put the items
- // in the final position
- each(allItems, function(item) {
- legend.positionItem(item);
- });
- // 1.x compatibility: positioning based on style
- /*var props = ['left', 'right', 'top', 'bottom'],
- prop,
- i = 4;
- while (i--) {
- prop = props[i];
- if (options.style[prop] && options.style[prop] !== 'auto') {
- options[i < 2 ? 'align' : 'verticalAlign'] = prop;
- options[i < 2 ? 'x' : 'y'] =
- pInt(options.style[prop]) * (i % 2 ? -1 : 1);
- }
- }*/
- if (display) {
- legendGroup.align(merge(options, {
- width: legendWidth,
- height: legendHeight
- }), true, 'spacingBox');
- }
- if (!chart.isResizing) {
- this.positionCheckboxes();
- }
- },
- /**
- * Set up the overflow handling by adding navigation with up and down arrows
- * below the legend.
- */
- handleOverflow: function(legendHeight) {
- var legend = this,
- chart = this.chart,
- renderer = chart.renderer,
- options = this.options,
- optionsY = options.y,
- alignTop = options.verticalAlign === 'top',
- padding = this.padding,
- spaceHeight = chart.spacingBox.height +
- (alignTop ? -optionsY : optionsY) - padding,
- maxHeight = options.maxHeight,
- clipHeight,
- clipRect = this.clipRect,
- navOptions = options.navigation,
- animation = pick(navOptions.animation, true),
- arrowSize = navOptions.arrowSize || 12,
- nav = this.nav,
- pages = this.pages,
- lastY,
- allItems = this.allItems,
- clipToHeight = function(height) {
- if (typeof height === 'number') {
- clipRect.attr({
- height: height
- });
- } else if (clipRect) { // Reset (#5912)
- legend.clipRect = clipRect.destroy();
- legend.contentGroup.clip();
- }
- // useHTML
- if (legend.contentGroup.div) {
- legend.contentGroup.div.style.clip = height ?
- 'rect(' + padding + 'px,9999px,' +
- (padding + height) + 'px,0)' :
- 'auto';
- }
- };
- // Adjust the height
- if (
- options.layout === 'horizontal' &&
- options.verticalAlign !== 'middle' &&
- !options.floating
- ) {
- spaceHeight /= 2;
- }
- if (maxHeight) {
- spaceHeight = Math.min(spaceHeight, maxHeight);
- }
- // Reset the legend height and adjust the clipping rectangle
- pages.length = 0;
- if (legendHeight > spaceHeight && navOptions.enabled !== false) {
- this.clipHeight = clipHeight =
- Math.max(spaceHeight - 20 - this.titleHeight - padding, 0);
- this.currentPage = pick(this.currentPage, 1);
- this.fullHeight = legendHeight;
- // Fill pages with Y positions so that the top of each a legend item
- // defines the scroll top for each page (#2098)
- each(allItems, function(item, i) {
- var y = item._legendItemPos[1],
- h = Math.round(item.legendItem.getBBox().height),
- len = pages.length;
- if (!len || (y - pages[len - 1] > clipHeight &&
- (lastY || y) !== pages[len - 1])) {
- pages.push(lastY || y);
- len++;
- }
- if (i === allItems.length - 1 &&
- y + h - pages[len - 1] > clipHeight) {
- pages.push(y);
- }
- if (y !== lastY) {
- lastY = y;
- }
- });
- // Only apply clipping if needed. Clipping causes blurred legend in
- // PDF export (#1787)
- if (!clipRect) {
- clipRect = legend.clipRect =
- renderer.clipRect(0, padding, 9999, 0);
- legend.contentGroup.clip(clipRect);
- }
- clipToHeight(clipHeight);
- // Add navigation elements
- if (!nav) {
- this.nav = nav = renderer.g()
- .attr({
- zIndex: 1
- })
- .add(this.group);
- this.up = renderer
- .symbol(
- 'triangle',
- 0,
- 0,
- arrowSize,
- arrowSize
- )
- .on('click', function() {
- legend.scroll(-1, animation);
- })
- .add(nav);
- this.pager = renderer.text('', 15, 10)
- .addClass('highcharts-legend-navigation')
- .css(navOptions.style)
- .add(nav);
- this.down = renderer
- .symbol(
- 'triangle-down',
- 0,
- 0,
- arrowSize,
- arrowSize
- )
- .on('click', function() {
- legend.scroll(1, animation);
- })
- .add(nav);
- }
- // Set initial position
- legend.scroll(0);
- legendHeight = spaceHeight;
- // Reset
- } else if (nav) {
- clipToHeight();
- this.nav = nav.destroy(); // #6322
- this.scrollGroup.attr({
- translateY: 1
- });
- this.clipHeight = 0; // #1379
- }
- return legendHeight;
- },
- /**
- * Scroll the legend by a number of pages
- * @param {Object} scrollBy
- * @param {Object} animation
- */
- scroll: function(scrollBy, animation) {
- var pages = this.pages,
- pageCount = pages.length,
- currentPage = this.currentPage + scrollBy,
- clipHeight = this.clipHeight,
- navOptions = this.options.navigation,
- pager = this.pager,
- padding = this.padding,
- scrollOffset;
- // When resizing while looking at the last page
- if (currentPage > pageCount) {
- currentPage = pageCount;
- }
- if (currentPage > 0) {
- if (animation !== undefined) {
- setAnimation(animation, this.chart);
- }
- this.nav.attr({
- translateX: padding,
- translateY: clipHeight + this.padding + 7 + this.titleHeight,
- visibility: 'visible'
- });
- this.up.attr({
- 'class': currentPage === 1 ?
- 'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active'
- });
- pager.attr({
- text: currentPage + '/' + pageCount
- });
- this.down.attr({
- 'x': 18 + this.pager.getBBox().width, // adjust to text width
- 'class': currentPage === pageCount ?
- 'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active'
- });
- this.up
- .attr({
- fill: currentPage === 1 ?
- navOptions.inactiveColor : navOptions.activeColor
- })
- .css({
- cursor: currentPage === 1 ? 'default' : 'pointer'
- });
- this.down
- .attr({
- fill: currentPage === pageCount ?
- navOptions.inactiveColor : navOptions.activeColor
- })
- .css({
- cursor: currentPage === pageCount ? 'default' : 'pointer'
- });
- scrollOffset = -pages[currentPage - 1] + this.initialItemY;
- this.scrollGroup.animate({
- translateY: scrollOffset
- });
- this.currentPage = currentPage;
- this.positionCheckboxes(scrollOffset);
- }
- }
- };
- /*
- * LegendSymbolMixin
- */
- H.LegendSymbolMixin = {
- /**
- * Get the series' symbol in the legend
- *
- * @param {Object} legend The legend object
- * @param {Object} item The series (this) or point
- */
- drawRectangle: function(legend, item) {
- var options = legend.options,
- symbolHeight = legend.symbolHeight,
- square = options.squareSymbol,
- symbolWidth = square ? symbolHeight : legend.symbolWidth;
- item.legendSymbol = this.chart.renderer.rect(
- square ? (legend.symbolWidth - symbolHeight) / 2 : 0,
- legend.baseline - symbolHeight + 1, // #3988
- symbolWidth,
- symbolHeight,
- pick(legend.options.symbolRadius, symbolHeight / 2)
- )
- .addClass('highcharts-point')
- .attr({
- zIndex: 3
- }).add(item.legendGroup);
- },
- /**
- * Get the series' symbol in the legend. This method should be overridable
- * to create custom symbols through
- * Highcharts.seriesTypes[type].prototype.drawLegendSymbols.
- *
- * @param {Object} legend The legend object
- */
- drawLineMarker: function(legend) {
- var options = this.options,
- markerOptions = options.marker,
- radius,
- legendSymbol,
- symbolWidth = legend.symbolWidth,
- symbolHeight = legend.symbolHeight,
- generalRadius = symbolHeight / 2,
- renderer = this.chart.renderer,
- legendItemGroup = this.legendGroup,
- verticalCenter = legend.baseline -
- Math.round(legend.fontMetrics.b * 0.3),
- attr = {};
- // Draw the line
- attr = {
- 'stroke-width': options.lineWidth || 0
- };
- if (options.dashStyle) {
- attr.dashstyle = options.dashStyle;
- }
- this.legendLine = renderer.path([
- 'M',
- 0,
- verticalCenter,
- 'L',
- symbolWidth,
- verticalCenter
- ])
- .addClass('highcharts-graph')
- .attr(attr)
- .add(legendItemGroup);
- // Draw the marker
- if (markerOptions && markerOptions.enabled !== false) {
- // Do not allow the marker to be larger than the symbolHeight
- radius = Math.min(
- pick(markerOptions.radius, generalRadius),
- generalRadius
- );
- // Restrict symbol markers size
- if (this.symbol.indexOf('url') === 0) {
- markerOptions = merge(markerOptions, {
- width: symbolHeight,
- height: symbolHeight
- });
- radius = 0;
- }
- this.legendSymbol = legendSymbol = renderer.symbol(
- this.symbol,
- (symbolWidth / 2) - radius,
- verticalCenter - radius,
- 2 * radius,
- 2 * radius,
- markerOptions
- )
- .addClass('highcharts-point')
- .add(legendItemGroup);
- legendSymbol.isMarker = true;
- }
- }
- };
- // Workaround for #2030, horizontal legend items not displaying in IE11 Preview,
- // and for #2580, a similar drawing flaw in Firefox 26.
- // Explore if there's a general cause for this. The problem may be related
- // to nested group elements, as the legend item texts are within 4 group
- // elements.
- if (/Trident\/7\.0/.test(win.navigator.userAgent) || isFirefox) {
- wrap(Highcharts.Legend.prototype, 'positionItem', function(proceed, item) {
- var legend = this,
- // If chart destroyed in sync, this is undefined (#2030)
- runPositionItem = function() {
- if (item._legendItemPos) {
- proceed.call(legend, item);
- }
- };
- // Do it now, for export and to get checkbox placement
- runPositionItem();
- // Do it after to work around the core issue
- setTimeout(runPositionItem);
- });
- }
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- animate = H.animate,
- animObject = H.animObject,
- attr = H.attr,
- doc = H.doc,
- Axis = H.Axis, // @todo add as requirement
- createElement = H.createElement,
- defaultOptions = H.defaultOptions,
- discardElement = H.discardElement,
- charts = H.charts,
- css = H.css,
- defined = H.defined,
- each = H.each,
- extend = H.extend,
- find = H.find,
- fireEvent = H.fireEvent,
- getStyle = H.getStyle,
- grep = H.grep,
- isNumber = H.isNumber,
- isObject = H.isObject,
- isString = H.isString,
- Legend = H.Legend, // @todo add as requirement
- marginNames = H.marginNames,
- merge = H.merge,
- objectEach = H.objectEach,
- Pointer = H.Pointer, // @todo add as requirement
- pick = H.pick,
- pInt = H.pInt,
- removeEvent = H.removeEvent,
- seriesTypes = H.seriesTypes,
- splat = H.splat,
- svg = H.svg,
- syncTimeout = H.syncTimeout,
- win = H.win,
- Renderer = H.Renderer;
- /**
- * The Chart class. The recommended constructor is {@link Highcharts#chart}.
- * @class Highcharts.Chart
- * @param {String|HTMLDOMElement} renderTo
- * The DOM element to render to, or its id.
- * @param {Options} options
- * The chart options structure.
- * @param {Function} [callback]
- * Function to run when the chart has loaded and and all external images
- * are loaded. Defining a {@link
- * https://api.highcharts.com/highcharts/chart.events.load|chart.event.load}
- * handler is equivalent.
- *
- * @example
- * var chart = new Highcharts.Chart('container', {
- * title: {
- * text: 'My chart'
- * },
- * series: [{
- * data: [1, 3, 2, 4]
- * }]
- * })
- */
- var Chart = H.Chart = function() {
- this.getArgs.apply(this, arguments);
- };
- /**
- * Factory function for basic charts.
- *
- * @function #chart
- * @memberOf Highcharts
- * @param {String|HTMLDOMElement} renderTo - The DOM element to render to, or
- * its id.
- * @param {Options} options - The chart options structure.
- * @param {Function} [callback] - Function to run when the chart has loaded and
- * and all external images are loaded. Defining a {@link
- * https://api.highcharts.com/highcharts/chart.events.load|chart.event.load}
- * handler is equivalent.
- * @return {Highcharts.Chart} - Returns the Chart object.
- *
- * @example
- * // Render a chart in to div#container
- * var chart = Highcharts.chart('container', {
- * title: {
- * text: 'My chart'
- * },
- * series: [{
- * data: [1, 3, 2, 4]
- * }]
- * });
- */
- H.chart = function(a, b, c) {
- return new Chart(a, b, c);
- };
- extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ {
- /**
- * Hook for modules
- */
- callbacks: [],
- /**
- * Handle the arguments passed to the constructor
- * @returns {Array} Arguments without renderTo
- */
- getArgs: function() {
- var args = [].slice.call(arguments);
- // Remove the optional first argument, renderTo, and
- // set it on this.
- if (isString(args[0]) || args[0].nodeName) {
- this.renderTo = args.shift();
- }
- this.init(args[0], args[1]);
- },
- /**
- * Initialize the chart
- */
- init: function(userOptions, callback) {
- // Handle regular options
- var options,
- type,
- seriesOptions = userOptions.series, // skip merging data points to increase performance
- userPlotOptions = userOptions.plotOptions || {};
- userOptions.series = null;
- options = merge(defaultOptions, userOptions); // do the merge
- // Override (by copy of user options) or clear tooltip options
- // in chart.options.plotOptions (#6218)
- for (type in options.plotOptions) {
- options.plotOptions[type].tooltip = (
- userPlotOptions[type] &&
- merge(userPlotOptions[type].tooltip) // override by copy
- ) || undefined; // or clear
- }
- // User options have higher priority than default options (#6218).
- // In case of exporting: path is changed
- options.tooltip.userOptions = (userOptions.chart &&
- userOptions.chart.forExport && userOptions.tooltip.userOptions) ||
- userOptions.tooltip;
- options.series = userOptions.series = seriesOptions; // set back the series data
- this.userOptions = userOptions;
- var optionsChart = options.chart;
- var chartEvents = optionsChart.events;
- this.margin = [];
- this.spacing = [];
- //this.runChartClick = chartEvents && !!chartEvents.click;
- this.bounds = {
- h: {},
- v: {}
- }; // Pixel data bounds for touch zoom
- this.callback = callback;
- this.isResizing = 0;
- /**
- * The options structure for the chart. It contains members for the sub
- * elements like series, legend, tooltip etc.
- *
- * @memberof Highcharts.Chart
- * @name options
- * @type {Options}
- */
- this.options = options;
- /**
- * All the axes in the chart.
- *
- * @memberof Highcharts.Chart
- * @name axes
- * @see Highcharts.Chart.xAxis
- * @see Highcharts.Chart.yAxis
- * @type {Array.<Highcharts.Axis>}
- */
- this.axes = [];
- /**
- * All the current series in the chart.
- *
- * @memberof Highcharts.Chart
- * @name series
- * @type {Array.<Highcharts.Series>}
- */
- this.series = [];
- /**
- * The chart title. The title has an `update` method that allows
- * modifying the options directly or indirectly via `chart.update`.
- *
- * @memberof Highcharts.Chart
- * @name title
- * @type Object
- *
- * @sample highcharts/members/title-update/
- * Updating titles
- */
- /**
- * The chart subtitle. The subtitle has an `update` method that allows
- * modifying the options directly or indirectly via `chart.update`.
- *
- * @memberof Highcharts.Chart
- * @name subtitle
- * @type Object
- */
- this.hasCartesianSeries = optionsChart.showAxes;
- //this.axisOffset = undefined;
- //this.inverted = undefined;
- //this.loadingShown = undefined;
- //this.container = undefined;
- //this.chartWidth = undefined;
- //this.chartHeight = undefined;
- //this.marginRight = undefined;
- //this.marginBottom = undefined;
- //this.containerWidth = undefined;
- //this.containerHeight = undefined;
- //this.oldChartWidth = undefined;
- //this.oldChartHeight = undefined;
- //this.renderTo = undefined;
- //this.spacingBox = undefined
- //this.legend = undefined;
- // Elements
- //this.chartBackground = undefined;
- //this.plotBackground = undefined;
- //this.plotBGImage = undefined;
- //this.plotBorder = undefined;
- //this.loadingDiv = undefined;
- //this.loadingSpan = undefined;
- var chart = this;
- // Add the chart to the global lookup
- chart.index = charts.length;
- charts.push(chart);
- H.chartCount++;
- // Chart event handlers
- if (chartEvents) {
- objectEach(chartEvents, function(event, eventType) {
- addEvent(chart, eventType, event);
- });
- }
- /**
- * A collection of the X axes in the chart.
- * @type {Array.<Highcharts.Axis>}
- * @name xAxis
- * @memberOf Highcharts.Chart
- */
- chart.xAxis = [];
- /**
- * A collection of the Y axes in the chart.
- * @type {Array.<Highcharts.Axis>}
- * @name yAxis
- * @memberOf Highcharts.Chart
- */
- chart.yAxis = [];
- chart.pointCount = chart.colorCounter = chart.symbolCounter = 0;
- chart.firstRender();
- },
- /**
- * Initialize an individual series, called internally before render time
- */
- initSeries: function(options) {
- var chart = this,
- optionsChart = chart.options.chart,
- type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
- series,
- Constr = seriesTypes[type];
- // No such series type
- if (!Constr) {
- H.error(17, true);
- }
- series = new Constr();
- series.init(this, options);
- return series;
- },
- /**
- * Order all series above a given index. When series are added and ordered
- * by configuration, only the last series is handled (#248, #1123, #2456,
- * #6112). This function is called on series initialization and destroy.
- *
- * @param {number} fromIndex - If this is given, only the series above this
- * index are handled.
- */
- orderSeries: function(fromIndex) {
- var series = this.series,
- i = fromIndex || 0;
- for (; i < series.length; i++) {
- if (series[i]) {
- series[i].index = i;
- series[i].name = series[i].name ||
- 'Series ' + (series[i].index + 1);
- }
- }
- },
- /**
- * Check whether a given point is within the plot area
- *
- * @param {Number} plotX Pixel x relative to the plot area
- * @param {Number} plotY Pixel y relative to the plot area
- * @param {Boolean} inverted Whether the chart is inverted
- */
- isInsidePlot: function(plotX, plotY, inverted) {
- var x = inverted ? plotY : plotX,
- y = inverted ? plotX : plotY;
- return x >= 0 &&
- x <= this.plotWidth &&
- y >= 0 &&
- y <= this.plotHeight;
- },
- /**
- * Redraw the chart after changes have been done to the data, axis extremes
- * chart size or chart elements. All methods for updating axes, series or
- * points have a parameter for redrawing the chart. This is `true` by
- * default. But in many cases you want to do more than one operation on the
- * chart before redrawing, for example add a number of points. In those
- * cases it is a waste of resources to redraw the chart for each new point
- * added. So you add the points and call `chart.redraw()` after.
- *
- * @param {AnimationOptions} animation
- * If or how to apply animation to the redraw.
- */
- redraw: function(animation) {
- var chart = this,
- axes = chart.axes,
- series = chart.series,
- pointer = chart.pointer,
- legend = chart.legend,
- redrawLegend = chart.isDirtyLegend,
- hasStackedSeries,
- hasDirtyStacks,
- hasCartesianSeries = chart.hasCartesianSeries,
- isDirtyBox = chart.isDirtyBox,
- i,
- serie,
- renderer = chart.renderer,
- isHiddenChart = renderer.isHidden(),
- afterRedraw = [];
- // Handle responsive rules, not only on resize (#6130)
- if (chart.setResponsive) {
- chart.setResponsive(false);
- }
- H.setAnimation(animation, chart);
- if (isHiddenChart) {
- chart.temporaryDisplay();
- }
- // Adjust title layout (reflow multiline text)
- chart.layOutTitles();
- // link stacked series
- i = series.length;
- while (i--) {
- serie = series[i];
- if (serie.options.stacking) {
- hasStackedSeries = true;
- if (serie.isDirty) {
- hasDirtyStacks = true;
- break;
- }
- }
- }
- if (hasDirtyStacks) { // mark others as dirty
- i = series.length;
- while (i--) {
- serie = series[i];
- if (serie.options.stacking) {
- serie.isDirty = true;
- }
- }
- }
- // Handle updated data in the series
- each(series, function(serie) {
- if (serie.isDirty) {
- if (serie.options.legendType === 'point') {
- if (serie.updateTotals) {
- serie.updateTotals();
- }
- redrawLegend = true;
- }
- }
- if (serie.isDirtyData) {
- fireEvent(serie, 'updatedData');
- }
- });
- // handle added or removed series
- if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed
- // draw legend graphics
- legend.render();
- chart.isDirtyLegend = false;
- }
- // reset stacks
- if (hasStackedSeries) {
- chart.getStacks();
- }
- if (hasCartesianSeries) {
- // set axes scales
- each(axes, function(axis) {
- axis.updateNames();
- axis.setScale();
- });
- }
- chart.getMargins(); // #3098
- if (hasCartesianSeries) {
- // If one axis is dirty, all axes must be redrawn (#792, #2169)
- each(axes, function(axis) {
- if (axis.isDirty) {
- isDirtyBox = true;
- }
- });
- // redraw axes
- each(axes, function(axis) {
- // Fire 'afterSetExtremes' only if extremes are set
- var key = axis.min + ',' + axis.max;
- if (axis.extKey !== key) { // #821, #4452
- axis.extKey = key;
- afterRedraw.push(function() { // prevent a recursive call to chart.redraw() (#1119)
- fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751
- delete axis.eventArgs;
- });
- }
- if (isDirtyBox || hasStackedSeries) {
- axis.redraw();
- }
- });
- }
- // the plot areas size has changed
- if (isDirtyBox) {
- chart.drawChartBox();
- }
- // Fire an event before redrawing series, used by the boost module to
- // clear previous series renderings.
- fireEvent(chart, 'predraw');
- // redraw affected series
- each(series, function(serie) {
- if ((isDirtyBox || serie.isDirty) && serie.visible) {
- serie.redraw();
- }
- // Set it here, otherwise we will have unlimited 'updatedData' calls
- // for a hidden series after setData(). Fixes #6012
- serie.isDirtyData = false;
- });
- // move tooltip or reset
- if (pointer) {
- pointer.reset(true);
- }
- // redraw if canvas
- renderer.draw();
- // Fire the events
- fireEvent(chart, 'redraw');
- fireEvent(chart, 'render');
- if (isHiddenChart) {
- chart.temporaryDisplay(true);
- }
- // Fire callbacks that are put on hold until after the redraw
- each(afterRedraw, function(callback) {
- callback.call();
- });
- },
- /**
- * Get an axis, series or point object by `id` as given in the configuration
- * options. Returns `undefined` if no item is found.
- * @param id {String} The id as given in the configuration options.
- * @return {Highcharts.Axis|Highcharts.Series|Highcharts.Point|undefined}
- * The retrieved item.
- * @sample highcharts/plotoptions/series-id/
- * Get series by id
- */
- get: function(id) {
- var ret,
- series = this.series,
- i;
- function itemById(item) {
- return item.id === id || (item.options && item.options.id === id);
- }
- ret =
- // Search axes
- find(this.axes, itemById) ||
- // Search series
- find(this.series, itemById);
- // Search points
- for (i = 0; !ret && i < series.length; i++) {
- ret = find(series[i].points || [], itemById);
- }
- return ret;
- },
- /**
- * Create the Axis instances based on the config options
- */
- getAxes: function() {
- var chart = this,
- options = this.options,
- xAxisOptions = options.xAxis = splat(options.xAxis || {}),
- yAxisOptions = options.yAxis = splat(options.yAxis || {}),
- optionsArray;
- // make sure the options are arrays and add some members
- each(xAxisOptions, function(axis, i) {
- axis.index = i;
- axis.isX = true;
- });
- each(yAxisOptions, function(axis, i) {
- axis.index = i;
- });
- // concatenate all axis options into one array
- optionsArray = xAxisOptions.concat(yAxisOptions);
- each(optionsArray, function(axisOptions) {
- new Axis(chart, axisOptions); // eslint-disable-line no-new
- });
- },
- /**
- * Returns an array of all currently selected points in the chart. Points
- * can be selected by clicking or programmatically by the {@link
- * Highcharts.Point#select} function.
- *
- * @return {Array.<Highcharts.Point>}
- * The currently selected points.
- *
- * @sample highcharts/plotoptions/series-allowpointselect-line/
- * Get selected points
- */
- getSelectedPoints: function() {
- var points = [];
- each(this.series, function(serie) {
- // series.data - for points outside of viewed range (#6445)
- points = points.concat(grep(serie.data || [], function(point) {
- return point.selected;
- }));
- });
- return points;
- },
- /**
- * Returns an array of all currently selected series in the chart. Series
- * can be selected either programmatically by the {@link
- * Highcharts.Series#select} function or by checking the checkbox next to
- * the legend item if {@link
- * https://api.highcharts.com/highcharts/plotOptions.series.showCheckbox|
- * series.showCheckBox} is true.
- *
- * @return {Array.<Highcharts.Series>}
- * The currently selected series.
- *
- * @sample highcharts/members/chart-getselectedseries/
- * Get selected series
- */
- getSelectedSeries: function() {
- return grep(this.series, function(serie) {
- return serie.selected;
- });
- },
- /**
- * Set a new title or subtitle for the chart.
- *
- * @param titleOptions {TitleOptions}
- * New title options.
- * @param subtitleOptions {SubtitleOptions}
- * New subtitle options.
- * @param redraw {Boolean}
- * Whether to redraw the chart or wait for a later call to
- * `chart.redraw()`.
- *
- * @sample highcharts/members/chart-settitle/ Set title text and styles
- *
- */
- setTitle: function(titleOptions, subtitleOptions, redraw) {
- var chart = this,
- options = chart.options,
- chartTitleOptions,
- chartSubtitleOptions;
- chartTitleOptions = options.title = merge(
- // Default styles
- {
- style: {
- color: '#333333',
- fontSize: options.isStock ? '16px' : '18px' // #2944
- }
- },
- options.title,
- titleOptions
- );
- chartSubtitleOptions = options.subtitle = merge(
- // Default styles
- {
- style: {
- color: '#666666'
- }
- },
- options.subtitle,
- subtitleOptions
- );
- // add title and subtitle
- each([
- ['title', titleOptions, chartTitleOptions],
- ['subtitle', subtitleOptions, chartSubtitleOptions]
- ], function(arr, i) {
- var name = arr[0],
- title = chart[name],
- titleOptions = arr[1],
- chartTitleOptions = arr[2];
- if (title && titleOptions) {
- chart[name] = title = title.destroy(); // remove old
- }
- if (chartTitleOptions && chartTitleOptions.text && !title) {
- chart[name] = chart.renderer.text(
- chartTitleOptions.text,
- 0,
- 0,
- chartTitleOptions.useHTML
- )
- .attr({
- align: chartTitleOptions.align,
- 'class': 'highcharts-' + name,
- zIndex: chartTitleOptions.zIndex || 4
- })
- .add();
- // Update methods, shortcut to Chart.setTitle
- chart[name].update = function(o) {
- chart.setTitle(!i && o, i && o);
- };
- // Presentational
- chart[name].css(chartTitleOptions.style);
- }
- });
- chart.layOutTitles(redraw);
- },
- /**
- * Lay out the chart titles and cache the full offset height for use
- * in getMargins
- */
- layOutTitles: function(redraw) {
- var titleOffset = 0,
- requiresDirtyBox,
- renderer = this.renderer,
- spacingBox = this.spacingBox;
- // Lay out the title and the subtitle respectively
- each(['title', 'subtitle'], function(key) {
- var title = this[key],
- titleOptions = this.options[key],
- offset = key === 'title' ? -3 :
- // Floating subtitle (#6574)
- titleOptions.verticalAlign ? 0 : titleOffset + 2,
- titleSize;
- if (title) {
- titleSize = titleOptions.style.fontSize;
- titleSize = renderer.fontMetrics(titleSize, title).b;
- title
- .css({
- width: (titleOptions.width ||
- spacingBox.width + titleOptions.widthAdjust) + 'px'
- })
- .align(extend({
- y: offset + titleSize
- }, titleOptions), false, 'spacingBox');
- if (!titleOptions.floating && !titleOptions.verticalAlign) {
- titleOffset = Math.ceil(
- titleOffset +
- // Skip the cache for HTML (#3481)
- title.getBBox(titleOptions.useHTML).height
- );
- }
- }
- }, this);
- requiresDirtyBox = this.titleOffset !== titleOffset;
- this.titleOffset = titleOffset; // used in getMargins
- if (!this.isDirtyBox && requiresDirtyBox) {
- this.isDirtyBox = requiresDirtyBox;
- // Redraw if necessary (#2719, #2744)
- if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
- this.redraw();
- }
- }
- },
- /**
- * Get chart width and height according to options and container size
- */
- getChartSize: function() {
- var chart = this,
- optionsChart = chart.options.chart,
- widthOption = optionsChart.width,
- heightOption = optionsChart.height,
- renderTo = chart.renderTo;
- // Get inner width and height
- if (!defined(widthOption)) {
- chart.containerWidth = getStyle(renderTo, 'width');
- }
- if (!defined(heightOption)) {
- chart.containerHeight = getStyle(renderTo, 'height');
- }
- chart.chartWidth = Math.max( // #1393
- 0,
- widthOption || chart.containerWidth || 600 // #1460
- );
- chart.chartHeight = Math.max(
- 0,
- H.relativeLength(
- heightOption,
- chart.chartWidth
- ) || chart.containerHeight || 400
- );
- },
- /**
- * If the renderTo element has no offsetWidth, most likely one or more of
- * its parents are hidden. Loop up the DOM tree to temporarily display the
- * parents, then save the original display properties, and when the true
- * size is retrieved, reset them. Used on first render and on redraws.
- *
- * @param {Boolean} revert - Revert to the saved original styles.
- */
- temporaryDisplay: function(revert) {
- var node = this.renderTo,
- tempStyle;
- if (!revert) {
- while (node && node.style) {
- if (getStyle(node, 'display', false) === 'none') {
- node.hcOrigStyle = {
- display: node.style.display,
- height: node.style.height,
- overflow: node.style.overflow
- };
- tempStyle = {
- display: 'block',
- overflow: 'hidden'
- };
- if (node !== this.renderTo) {
- tempStyle.height = 0;
- }
- H.css(node, tempStyle);
- if (node.style.setProperty) { // #2631
- node.style.setProperty('display', 'block', 'important');
- }
- }
- node = node.parentNode;
- }
- } else {
- while (node && node.style) {
- if (node.hcOrigStyle) {
- H.css(node, node.hcOrigStyle);
- delete node.hcOrigStyle;
- }
- node = node.parentNode;
- }
- }
- },
- /**
- * Setter for the chart class name
- */
- setClassName: function(className) {
- this.container.className = 'highcharts-container ' + (className || '');
- },
- /**
- * Get the containing element, determine the size and create the inner
- * container div to hold the chart
- */
- getContainer: function() {
- var chart = this,
- container,
- options = chart.options,
- optionsChart = options.chart,
- chartWidth,
- chartHeight,
- renderTo = chart.renderTo,
- indexAttrName = 'data-highcharts-chart',
- oldChartIndex,
- Ren,
- containerId = H.uniqueKey(),
- containerStyle,
- key;
- if (!renderTo) {
- chart.renderTo = renderTo = optionsChart.renderTo;
- }
- if (isString(renderTo)) {
- chart.renderTo = renderTo = doc.getElementById(renderTo);
- }
- // Display an error if the renderTo is wrong
- if (!renderTo) {
- H.error(13, true);
- }
- // If the container already holds a chart, destroy it. The check for
- // hasRendered is there because web pages that are saved to disk from
- // the browser, will preserve the data-highcharts-chart attribute and
- // the SVG contents, but not an interactive chart. So in this case,
- // charts[oldChartIndex] will point to the wrong chart if any (#2609).
- oldChartIndex = pInt(attr(renderTo, indexAttrName));
- if (
- isNumber(oldChartIndex) &&
- charts[oldChartIndex] &&
- charts[oldChartIndex].hasRendered
- ) {
- charts[oldChartIndex].destroy();
- }
- // Make a reference to the chart from the div
- attr(renderTo, indexAttrName, chart.index);
- // remove previous chart
- renderTo.innerHTML = '';
- // If the container doesn't have an offsetWidth, it has or is a child of
- // a node that has display:none. We need to temporarily move it out to a
- // visible state to determine the size, else the legend and tooltips
- // won't render properly. The skipClone option is used in sparklines as
- // a micro optimization, saving about 1-2 ms each chart.
- if (!optionsChart.skipClone && !renderTo.offsetWidth) {
- chart.temporaryDisplay();
- }
- // get the width and height
- chart.getChartSize();
- chartWidth = chart.chartWidth;
- chartHeight = chart.chartHeight;
- // Create the inner container
- containerStyle = extend({
- position: 'relative',
- overflow: 'hidden', // needed for context menu (avoid scrollbars)
- // and content overflow in IE
- width: chartWidth + 'px',
- height: chartHeight + 'px',
- textAlign: 'left',
- lineHeight: 'normal', // #427
- zIndex: 0, // #1072
- '-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
- }, optionsChart.style);
- /**
- * The containing HTML element of the chart. The container is
- * dynamically inserted into the element given as the `renderTo`
- * parameterin the {@link Highcharts#chart} constructor.
- *
- * @memberOf Highcharts.Chart
- * @type {HTMLDOMElement}
- */
- container = createElement(
- 'div', {
- id: containerId
- },
- containerStyle,
- renderTo
- );
- chart.container = container;
- // cache the cursor (#1650)
- chart._cursor = container.style.cursor;
- // Initialize the renderer
- Ren = H[optionsChart.renderer] || Renderer;
- chart.renderer = new Ren(
- container,
- chartWidth,
- chartHeight,
- null,
- optionsChart.forExport,
- options.exporting && options.exporting.allowHTML
- );
- chart.setClassName(optionsChart.className);
- chart.renderer.setStyle(optionsChart.style);
- // Add a reference to the charts index
- chart.renderer.chartIndex = chart.index;
- },
- /**
- * Calculate margins by rendering axis labels in a preliminary position.
- * Title, subtitle and legend have already been rendered at this stage, but
- * will be moved into their final positions
- */
- getMargins: function(skipAxes) {
- var chart = this,
- spacing = chart.spacing,
- margin = chart.margin,
- titleOffset = chart.titleOffset;
- chart.resetMargins();
- // Adjust for title and subtitle
- if (titleOffset && !defined(margin[0])) {
- chart.plotTop = Math.max(
- chart.plotTop,
- titleOffset + chart.options.title.margin + spacing[0]
- );
- }
- // Adjust for legend
- if (chart.legend.display) {
- chart.legend.adjustMargins(margin, spacing);
- }
- // adjust for scroller
- if (chart.extraMargin) {
- chart[chart.extraMargin.type] =
- (chart[chart.extraMargin.type] || 0) + chart.extraMargin.value;
- }
- if (chart.extraTopMargin) {
- chart.plotTop += chart.extraTopMargin;
- }
- if (!skipAxes) {
- this.getAxisMargins();
- }
- },
- getAxisMargins: function() {
- var chart = this,
- // [top, right, bottom, left]
- axisOffset = chart.axisOffset = [0, 0, 0, 0],
- margin = chart.margin;
- // pre-render axes to get labels offset width
- if (chart.hasCartesianSeries) {
- each(chart.axes, function(axis) {
- if (axis.visible) {
- axis.getOffset();
- }
- });
- }
- // Add the axis offsets
- each(marginNames, function(m, side) {
- if (!defined(margin[side])) {
- chart[m] += axisOffset[side];
- }
- });
- chart.setChartSize();
- },
- /**
- * Reflows the chart to its container. By default, the chart reflows
- * automatically to its container following a `window.resize` event, as per
- * the {@link https://api.highcharts/highcharts/chart.reflow|chart.reflow}
- * option. However, there are no reliable events for div resize, so if the
- * container is resized without a window resize event, this must be called
- * explicitly.
- *
- * @param {Object} e
- * Event arguments. Used primarily when the function is called
- * internally as a response to window resize.
- *
- * @sample highcharts/members/chart-reflow/
- * Resize div and reflow
- * @sample highcharts/chart/events-container/
- * Pop up and reflow
- */
- reflow: function(e) {
- var chart = this,
- optionsChart = chart.options.chart,
- renderTo = chart.renderTo,
- hasUserWidth = defined(optionsChart.width),
- width = optionsChart.width || getStyle(renderTo, 'width'),
- height = optionsChart.height || getStyle(renderTo, 'height'),
- target = e ? e.target : win;
- // Width and height checks for display:none. Target is doc in IE8 and
- // Opera, win in Firefox, Chrome and IE9.
- if (!hasUserWidth &&
- !chart.isPrinting &&
- width &&
- height &&
- (target === win || target === doc)
- ) {
- if (
- width !== chart.containerWidth ||
- height !== chart.containerHeight
- ) {
- clearTimeout(chart.reflowTimeout);
- // When called from window.resize, e is set, else it's called
- // directly (#2224)
- chart.reflowTimeout = syncTimeout(function() {
- // Set size, it may have been destroyed in the meantime
- // (#1257)
- if (chart.container) {
- chart.setSize(undefined, undefined, false);
- }
- }, e ? 100 : 0);
- }
- chart.containerWidth = width;
- chart.containerHeight = height;
- }
- },
- /**
- * Add the event handlers necessary for auto resizing
- */
- initReflow: function() {
- var chart = this,
- unbind;
- unbind = addEvent(win, 'resize', function(e) {
- chart.reflow(e);
- });
- addEvent(chart, 'destroy', unbind);
- // The following will add listeners to re-fit the chart before and after
- // printing (#2284). However it only works in WebKit. Should have worked
- // in Firefox, but not supported in IE.
- /*
- if (win.matchMedia) {
- win.matchMedia('print').addListener(function reflow() {
- chart.reflow();
- });
- }
- */
- },
- /**
- * Resize the chart to a given width and height. In order to set the width
- * only, the height argument may be skipped. To set the height only, pass
- * `undefined for the width.
- * @param {Number|undefined|null} [width]
- * The new pixel width of the chart. Since v4.2.6, the argument can
- * be `undefined` in order to preserve the current value (when
- * setting height only), or `null` to adapt to the width of the
- * containing element.
- * @param {Number|undefined|null} [height]
- * The new pixel height of the chart. Since v4.2.6, the argument can
- * be `undefined` in order to preserve the current value, or `null`
- * in order to adapt to the height of the containing element.
- * @param {AnimationOptions} [animation=true]
- * Whether and how to apply animation.
- *
- * @sample highcharts/members/chart-setsize-button/
- * Test resizing from buttons
- * @sample highcharts/members/chart-setsize-jquery-resizable/
- * Add a jQuery UI resizable
- * @sample stock/members/chart-setsize/
- * Highstock with UI resizable
- */
- setSize: function(width, height, animation) {
- var chart = this,
- renderer = chart.renderer,
- globalAnimation;
- // Handle the isResizing counter
- chart.isResizing += 1;
- // set the animation for the current process
- H.setAnimation(animation, chart);
- chart.oldChartHeight = chart.chartHeight;
- chart.oldChartWidth = chart.chartWidth;
- if (width !== undefined) {
- chart.options.chart.width = width;
- }
- if (height !== undefined) {
- chart.options.chart.height = height;
- }
- chart.getChartSize();
- // Resize the container with the global animation applied if enabled
- // (#2503)
- globalAnimation = renderer.globalAnimation;
- (globalAnimation ? animate : css)(chart.container, {
- width: chart.chartWidth + 'px',
- height: chart.chartHeight + 'px'
- }, globalAnimation);
- chart.setChartSize(true);
- renderer.setSize(chart.chartWidth, chart.chartHeight, animation);
- // handle axes
- each(chart.axes, function(axis) {
- axis.isDirty = true;
- axis.setScale();
- });
- chart.isDirtyLegend = true; // force legend redraw
- chart.isDirtyBox = true; // force redraw of plot and chart border
- chart.layOutTitles(); // #2857
- chart.getMargins();
- chart.redraw(animation);
- chart.oldChartHeight = null;
- fireEvent(chart, 'resize');
- // Fire endResize and set isResizing back. If animation is disabled,
- // fire without delay
- syncTimeout(function() {
- if (chart) {
- fireEvent(chart, 'endResize', null, function() {
- chart.isResizing -= 1;
- });
- }
- }, animObject(globalAnimation).duration);
- },
- /**
- * Set the public chart properties. This is done before and after the
- * pre-render to determine margin sizes
- */
- setChartSize: function(skipAxes) {
- var chart = this,
- inverted = chart.inverted,
- renderer = chart.renderer,
- chartWidth = chart.chartWidth,
- chartHeight = chart.chartHeight,
- optionsChart = chart.options.chart,
- spacing = chart.spacing,
- clipOffset = chart.clipOffset,
- clipX,
- clipY,
- plotLeft,
- plotTop,
- plotWidth,
- plotHeight,
- plotBorderWidth;
- function clipOffsetSide(side) {
- var offset = clipOffset[side] || 0;
- return Math.max(plotBorderWidth || offset, offset) / 2;
- }
- chart.plotLeft = plotLeft = Math.round(chart.plotLeft);
- chart.plotTop = plotTop = Math.round(chart.plotTop);
- chart.plotWidth = plotWidth = Math.max(
- 0,
- Math.round(chartWidth - plotLeft - chart.marginRight)
- );
- chart.plotHeight = plotHeight = Math.max(
- 0,
- Math.round(chartHeight - plotTop - chart.marginBottom)
- );
- chart.plotSizeX = inverted ? plotHeight : plotWidth;
- chart.plotSizeY = inverted ? plotWidth : plotHeight;
- chart.plotBorderWidth = optionsChart.plotBorderWidth || 0;
- // Set boxes used for alignment
- chart.spacingBox = renderer.spacingBox = {
- x: spacing[3],
- y: spacing[0],
- width: chartWidth - spacing[3] - spacing[1],
- height: chartHeight - spacing[0] - spacing[2]
- };
- chart.plotBox = renderer.plotBox = {
- x: plotLeft,
- y: plotTop,
- width: plotWidth,
- height: plotHeight
- };
- plotBorderWidth = 2 * Math.floor(chart.plotBorderWidth / 2);
- clipX = Math.ceil(clipOffsetSide(3));
- clipY = Math.ceil(clipOffsetSide(0));
- chart.clipBox = {
- x: clipX,
- y: clipY,
- width: Math.floor(
- chart.plotSizeX -
- clipOffsetSide(1) -
- clipX
- ),
- height: Math.max(
- 0,
- Math.floor(
- chart.plotSizeY -
- clipOffsetSide(2) -
- clipY
- )
- )
- };
- if (!skipAxes) {
- each(chart.axes, function(axis) {
- axis.setAxisSize();
- axis.setAxisTranslation();
- });
- }
- },
- /**
- * Initial margins before auto size margins are applied
- */
- resetMargins: function() {
- var chart = this,
- chartOptions = chart.options.chart;
- // Create margin and spacing array
- each(['margin', 'spacing'], function splashArrays(target) {
- var value = chartOptions[target],
- values = isObject(value) ? value : [value, value, value, value];
- each(['Top', 'Right', 'Bottom', 'Left'], function(sideName, side) {
- chart[target][side] = pick(
- chartOptions[target + sideName],
- values[side]
- );
- });
- });
- // Set margin names like chart.plotTop, chart.plotLeft,
- // chart.marginRight, chart.marginBottom.
- each(marginNames, function(m, side) {
- chart[m] = pick(chart.margin[side], chart.spacing[side]);
- });
- chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
- chart.clipOffset = [];
- },
- /**
- * Draw the borders and backgrounds for chart and plot area
- */
- drawChartBox: function() {
- var chart = this,
- optionsChart = chart.options.chart,
- renderer = chart.renderer,
- chartWidth = chart.chartWidth,
- chartHeight = chart.chartHeight,
- chartBackground = chart.chartBackground,
- plotBackground = chart.plotBackground,
- plotBorder = chart.plotBorder,
- chartBorderWidth,
- plotBGImage = chart.plotBGImage,
- chartBackgroundColor = optionsChart.backgroundColor,
- plotBackgroundColor = optionsChart.plotBackgroundColor,
- plotBackgroundImage = optionsChart.plotBackgroundImage,
- mgn,
- bgAttr,
- plotLeft = chart.plotLeft,
- plotTop = chart.plotTop,
- plotWidth = chart.plotWidth,
- plotHeight = chart.plotHeight,
- plotBox = chart.plotBox,
- clipRect = chart.clipRect,
- clipBox = chart.clipBox,
- verb = 'animate';
- // Chart area
- if (!chartBackground) {
- chart.chartBackground = chartBackground = renderer.rect()
- .addClass('highcharts-background')
- .add();
- verb = 'attr';
- }
- // Presentational
- chartBorderWidth = optionsChart.borderWidth || 0;
- mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
- bgAttr = {
- fill: chartBackgroundColor || 'none'
- };
- if (chartBorderWidth || chartBackground['stroke-width']) { // #980
- bgAttr.stroke = optionsChart.borderColor;
- bgAttr['stroke-width'] = chartBorderWidth;
- }
- chartBackground
- .attr(bgAttr)
- .shadow(optionsChart.shadow);
- chartBackground[verb]({
- x: mgn / 2,
- y: mgn / 2,
- width: chartWidth - mgn - chartBorderWidth % 2,
- height: chartHeight - mgn - chartBorderWidth % 2,
- r: optionsChart.borderRadius
- });
- // Plot background
- verb = 'animate';
- if (!plotBackground) {
- verb = 'attr';
- chart.plotBackground = plotBackground = renderer.rect()
- .addClass('highcharts-plot-background')
- .add();
- }
- plotBackground[verb](plotBox);
- // Presentational attributes for the background
- plotBackground
- .attr({
- fill: plotBackgroundColor || 'none'
- })
- .shadow(optionsChart.plotShadow);
- // Create the background image
- if (plotBackgroundImage) {
- if (!plotBGImage) {
- chart.plotBGImage = renderer.image(
- plotBackgroundImage,
- plotLeft,
- plotTop,
- plotWidth,
- plotHeight
- ).add();
- } else {
- plotBGImage.animate(plotBox);
- }
- }
- // Plot clip
- if (!clipRect) {
- chart.clipRect = renderer.clipRect(clipBox);
- } else {
- clipRect.animate({
- width: clipBox.width,
- height: clipBox.height
- });
- }
- // Plot area border
- verb = 'animate';
- if (!plotBorder) {
- verb = 'attr';
- chart.plotBorder = plotBorder = renderer.rect()
- .addClass('highcharts-plot-border')
- .attr({
- zIndex: 1 // Above the grid
- })
- .add();
- }
- // Presentational
- plotBorder.attr({
- stroke: optionsChart.plotBorderColor,
- 'stroke-width': optionsChart.plotBorderWidth || 0,
- fill: 'none'
- });
- plotBorder[verb](plotBorder.crisp({
- x: plotLeft,
- y: plotTop,
- width: plotWidth,
- height: plotHeight
- }, -plotBorder.strokeWidth())); //#3282 plotBorder should be negative;
- // reset
- chart.isDirtyBox = false;
- },
- /**
- * Detect whether a certain chart property is needed based on inspecting its
- * options and series. This mainly applies to the chart.inverted property,
- * and in extensions to the chart.angular and chart.polar properties.
- */
- propFromSeries: function() {
- var chart = this,
- optionsChart = chart.options.chart,
- klass,
- seriesOptions = chart.options.series,
- i,
- value;
- each(['inverted', 'angular', 'polar'], function(key) {
- // The default series type's class
- klass = seriesTypes[optionsChart.type ||
- optionsChart.defaultSeriesType];
- // Get the value from available chart-wide properties
- value =
- optionsChart[key] || // It is set in the options
- (klass && klass.prototype[key]); // The default series class
- // requires it
- // 4. Check if any the chart's series require it
- i = seriesOptions && seriesOptions.length;
- while (!value && i--) {
- klass = seriesTypes[seriesOptions[i].type];
- if (klass && klass.prototype[key]) {
- value = true;
- }
- }
- // Set the chart property
- chart[key] = value;
- });
- },
- /**
- * Link two or more series together. This is done initially from
- * Chart.render, and after Chart.addSeries and Series.remove.
- */
- linkSeries: function() {
- var chart = this,
- chartSeries = chart.series;
- // Reset links
- each(chartSeries, function(series) {
- series.linkedSeries.length = 0;
- });
- // Apply new links
- each(chartSeries, function(series) {
- var linkedTo = series.options.linkedTo;
- if (isString(linkedTo)) {
- if (linkedTo === ':previous') {
- linkedTo = chart.series[series.index - 1];
- } else {
- linkedTo = chart.get(linkedTo);
- }
- // #3341 avoid mutual linking
- if (linkedTo && linkedTo.linkedParent !== series) {
- linkedTo.linkedSeries.push(series);
- series.linkedParent = linkedTo;
- series.visible = pick(
- series.options.visible,
- linkedTo.options.visible,
- series.visible
- ); // #3879
- }
- }
- });
- },
- /**
- * Render series for the chart
- */
- renderSeries: function() {
- each(this.series, function(serie) {
- serie.translate();
- serie.render();
- });
- },
- /**
- * Render labels for the chart
- */
- renderLabels: function() {
- var chart = this,
- labels = chart.options.labels;
- if (labels.items) {
- each(labels.items, function(label) {
- var style = extend(labels.style, label.style),
- x = pInt(style.left) + chart.plotLeft,
- y = pInt(style.top) + chart.plotTop + 12;
- // delete to prevent rewriting in IE
- delete style.left;
- delete style.top;
- chart.renderer.text(
- label.html,
- x,
- y
- )
- .attr({
- zIndex: 2
- })
- .css(style)
- .add();
- });
- }
- },
- /**
- * Render all graphics for the chart
- */
- render: function() {
- var chart = this,
- axes = chart.axes,
- renderer = chart.renderer,
- options = chart.options,
- tempWidth,
- tempHeight,
- redoHorizontal,
- redoVertical;
- // Title
- chart.setTitle();
- // Legend
- chart.legend = new Legend(chart, options.legend);
- // Get stacks
- if (chart.getStacks) {
- chart.getStacks();
- }
- // Get chart margins
- chart.getMargins(true);
- chart.setChartSize();
- // Record preliminary dimensions for later comparison
- tempWidth = chart.plotWidth;
- tempHeight = chart.plotHeight = chart.plotHeight - 21; // 21 is the most common correction for X axis labels
- // Get margins by pre-rendering axes
- each(axes, function(axis) {
- axis.setScale();
- });
- chart.getAxisMargins();
- // If the plot area size has changed significantly, calculate tick positions again
- redoHorizontal = tempWidth / chart.plotWidth > 1.1;
- redoVertical = tempHeight / chart.plotHeight > 1.05; // Height is more sensitive
- if (redoHorizontal || redoVertical) {
- each(axes, function(axis) {
- if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) {
- axis.setTickInterval(true); // update to reflect the new margins
- }
- });
- chart.getMargins(); // second pass to check for new labels
- }
- // Draw the borders and backgrounds
- chart.drawChartBox();
- // Axes
- if (chart.hasCartesianSeries) {
- each(axes, function(axis) {
- if (axis.visible) {
- axis.render();
- }
- });
- }
- // The series
- if (!chart.seriesGroup) {
- chart.seriesGroup = renderer.g('series-group')
- .attr({
- zIndex: 3
- })
- .add();
- }
- chart.renderSeries();
- // Labels
- chart.renderLabels();
- // Credits
- chart.addCredits();
- // Handle responsiveness
- if (chart.setResponsive) {
- chart.setResponsive();
- }
- // Set flag
- chart.hasRendered = true;
- },
- /**
- * Set a new credits label for the chart.
- *
- * @param {CreditOptions} options
- * A configuration object for the new credits.
- * @sample highcharts/credits/credits-update/ Add and update credits
- */
- addCredits: function(credits) {
- var chart = this;
- credits = merge(true, this.options.credits, credits);
- if (credits.enabled && !this.credits) {
- /**
- * The chart's credits label. The label has an `update` method that
- * allows setting new options as per the {@link
- * https://api.highcharts.com/highcharts/credits|
- * credits options set}.
- *
- * @memberof Highcharts.Chart
- * @name credits
- * @type {Highcharts.SVGElement}
- */
- this.credits = this.renderer.text(
- credits.text + (this.mapCredits || ''),
- 0,
- 0
- )
- .addClass('highcharts-credits')
- .on('click', function() {
- if (credits.href) {
- win.location.href = credits.href;
- }
- })
- .attr({
- align: credits.position.align,
- zIndex: 8
- })
- .css(credits.style)
- .add()
- .align(credits.position);
- // Dynamically update
- this.credits.update = function(options) {
- chart.credits = chart.credits.destroy();
- chart.addCredits(options);
- };
- }
- },
- /**
- * Remove the chart and purge memory. This method is called internally
- * before adding a second chart into the same container, as well as on
- * window unload to prevent leaks.
- *
- * @sample highcharts/members/chart-destroy/
- * Destroy the chart from a button
- * @sample stock/members/chart-destroy/
- * Destroy with Highstock
- */
- destroy: function() {
- var chart = this,
- axes = chart.axes,
- series = chart.series,
- container = chart.container,
- i,
- parentNode = container && container.parentNode;
- // fire the chart.destoy event
- fireEvent(chart, 'destroy');
- // Delete the chart from charts lookup array
- if (chart.renderer.forExport) {
- H.erase(charts, chart); // #6569
- } else {
- charts[chart.index] = undefined;
- }
- H.chartCount--;
- chart.renderTo.removeAttribute('data-highcharts-chart');
- // remove events
- removeEvent(chart);
- // ==== Destroy collections:
- // Destroy axes
- i = axes.length;
- while (i--) {
- axes[i] = axes[i].destroy();
- }
- // Destroy scroller & scroller series before destroying base series
- if (this.scroller && this.scroller.destroy) {
- this.scroller.destroy();
- }
- // Destroy each series
- i = series.length;
- while (i--) {
- series[i] = series[i].destroy();
- }
- // ==== Destroy chart properties:
- each([
- 'title', 'subtitle', 'chartBackground', 'plotBackground',
- 'plotBGImage', 'plotBorder', 'seriesGroup', 'clipRect', 'credits',
- 'pointer', 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip',
- 'renderer'
- ], function(name) {
- var prop = chart[name];
- if (prop && prop.destroy) {
- chart[name] = prop.destroy();
- }
- });
- // remove container and all SVG
- if (container) { // can break in IE when destroyed before finished loading
- container.innerHTML = '';
- removeEvent(container);
- if (parentNode) {
- discardElement(container);
- }
- }
- // clean it all up
- objectEach(chart, function(val, key) {
- delete chart[key];
- });
- },
- /**
- * VML namespaces can't be added until after complete. Listening
- * for Perini's doScroll hack is not enough.
- */
- isReadyToRender: function() {
- var chart = this;
- // Note: win == win.top is required
- if ((!svg && (win == win.top && doc.readyState !== 'complete'))) { // eslint-disable-line eqeqeq
- doc.attachEvent('onreadystatechange', function() {
- doc.detachEvent('onreadystatechange', chart.firstRender);
- if (doc.readyState === 'complete') {
- chart.firstRender();
- }
- });
- return false;
- }
- return true;
- },
- /**
- * Prepare for first rendering after all data are loaded
- */
- firstRender: function() {
- var chart = this,
- options = chart.options;
- // Check whether the chart is ready to render
- if (!chart.isReadyToRender()) {
- return;
- }
- // Create the container
- chart.getContainer();
- // Run an early event after the container and renderer are established
- fireEvent(chart, 'init');
- chart.resetMargins();
- chart.setChartSize();
- // Set the common chart properties (mainly invert) from the given series
- chart.propFromSeries();
- // get axes
- chart.getAxes();
- // Initialize the series
- each(options.series || [], function(serieOptions) {
- chart.initSeries(serieOptions);
- });
- chart.linkSeries();
- // Run an event after axes and series are initialized, but before render. At this stage,
- // the series data is indexed and cached in the xData and yData arrays, so we can access
- // those before rendering. Used in Highstock.
- fireEvent(chart, 'beforeRender');
- // depends on inverted and on margins being set
- if (Pointer) {
- chart.pointer = new Pointer(chart, options);
- }
- chart.render();
- // Fire the load event if there are no external images
- if (!chart.renderer.imgCount && chart.onload) {
- chart.onload();
- }
- // If the chart was rendered outside the top container, put it back in (#3679)
- chart.temporaryDisplay(true);
- },
- /**
- * On chart load
- */
- onload: function() {
- // Run callbacks
- each([this.callback].concat(this.callbacks), function(fn) {
- if (fn && this.index !== undefined) { // Chart destroyed in its own callback (#3600)
- fn.apply(this, [this]);
- }
- }, this);
- fireEvent(this, 'load');
- fireEvent(this, 'render');
- // Set up auto resize, check for not destroyed (#6068)
- if (defined(this.index) && this.options.chart.reflow !== false) {
- this.initReflow();
- }
- // Don't run again
- this.onload = null;
- }
- }); // end Chart
- }(Highcharts));
- (function(Highcharts) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var Point,
- H = Highcharts,
- each = H.each,
- extend = H.extend,
- erase = H.erase,
- fireEvent = H.fireEvent,
- format = H.format,
- isArray = H.isArray,
- isNumber = H.isNumber,
- pick = H.pick,
- removeEvent = H.removeEvent;
- /**
- * The Point object. The point objects are generated from the `series.data`
- * configuration objects or raw numbers. They can be accessed from the
- * `Series.points` array. Other ways to instaniate points are through {@link
- * Highcharts.Series#addPoint} or {@link Highcharts.Series#setData}.
- *
- * @class
- */
- Highcharts.Point = Point = function() {};
- Highcharts.Point.prototype = {
- /**
- * Initialize the point. Called internally based on the series.data option.
- * @param {Object} series The series object containing this point.
- * @param {Object} options The data in either number, array or object
- * format.
- * @param {Number} x Optionally, the X value of the.
- * @returns {Object} The Point instance.
- */
- init: function(series, options, x) {
- var point = this,
- colors,
- colorCount = series.chart.options.chart.colorCount,
- colorIndex;
- /**
- * The series object associated with the point.
- *
- * @name series
- * @memberof Highcharts.Point
- * @type Highcharts.Series
- */
- point.series = series;
- /**
- * The point's current color.
- * @name color
- * @memberof Highcharts.Point
- * @type {Color}
- */
- point.color = series.color; // #3445
- point.applyOptions(options, x);
- if (series.options.colorByPoint) {
- colors = series.options.colors || series.chart.options.colors;
- point.color = point.color || colors[series.colorCounter];
- colorCount = colors.length;
- colorIndex = series.colorCounter;
- series.colorCounter++;
- // loop back to zero
- if (series.colorCounter === colorCount) {
- series.colorCounter = 0;
- }
- } else {
- colorIndex = series.colorIndex;
- }
- point.colorIndex = pick(point.colorIndex, colorIndex);
- series.chart.pointCount++;
- return point;
- },
- /**
- * Apply the options containing the x and y data and possible some extra
- * properties. Called on point init or from point.update.
- *
- * @param {Object} options The point options as defined in series.data.
- * @param {Number} x Optionally, the X value.
- * @returns {Object} The Point instance.
- */
- applyOptions: function(options, x) {
- var point = this,
- series = point.series,
- pointValKey = series.options.pointValKey || series.pointValKey;
- options = Point.prototype.optionsToObject.call(this, options);
- // copy options directly to point
- extend(point, options);
- point.options = point.options ? extend(point.options, options) : options;
- // Since options are copied into the Point instance, some accidental options must be shielded (#5681)
- if (options.group) {
- delete point.group;
- }
- // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low.
- if (pointValKey) {
- point.y = point[pointValKey];
- }
- point.isNull = pick(
- point.isValid && !point.isValid(),
- point.x === null || !isNumber(point.y, true)
- ); // #3571, check for NaN
- // The point is initially selected by options (#5777)
- if (point.selected) {
- point.state = 'select';
- }
- // If no x is set by now, get auto incremented value. All points must have an
- // x value, however the y value can be null to create a gap in the series
- if ('name' in point && x === undefined && series.xAxis && series.xAxis.hasNames) {
- point.x = series.xAxis.nameToX(point);
- }
- if (point.x === undefined && series) {
- if (x === undefined) {
- point.x = series.autoIncrement(point);
- } else {
- point.x = x;
- }
- }
- return point;
- },
- /**
- * Transform number or array configs into objects
- */
- optionsToObject: function(options) {
- var ret = {},
- series = this.series,
- keys = series.options.keys,
- pointArrayMap = keys || series.pointArrayMap || ['y'],
- valueCount = pointArrayMap.length,
- firstItemType,
- i = 0,
- j = 0;
- if (isNumber(options) || options === null) {
- ret[pointArrayMap[0]] = options;
- } else if (isArray(options)) {
- // with leading x value
- if (!keys && options.length > valueCount) {
- firstItemType = typeof options[0];
- if (firstItemType === 'string') {
- ret.name = options[0];
- } else if (firstItemType === 'number') {
- ret.x = options[0];
- }
- i++;
- }
- while (j < valueCount) {
- if (!keys || options[i] !== undefined) { // Skip undefined positions for keys
- ret[pointArrayMap[j]] = options[i];
- }
- i++;
- j++;
- }
- } else if (typeof options === 'object') {
- ret = options;
- // This is the fastest way to detect if there are individual point dataLabels that need
- // to be considered in drawDataLabels. These can only occur in object configs.
- if (options.dataLabels) {
- series._hasPointLabels = true;
- }
- // Same approach as above for markers
- if (options.marker) {
- series._hasPointMarkers = true;
- }
- }
- return ret;
- },
- /**
- * Get the CSS class names for individual points
- * @returns {String} The class name
- */
- getClassName: function() {
- return 'highcharts-point' +
- (this.selected ? ' highcharts-point-select' : '') +
- (this.negative ? ' highcharts-negative' : '') +
- (this.isNull ? ' highcharts-null-point' : '') +
- (this.colorIndex !== undefined ? ' highcharts-color-' +
- this.colorIndex : '') +
- (this.options.className ? ' ' + this.options.className : '') +
- (this.zone && this.zone.className ? ' ' +
- this.zone.className.replace('highcharts-negative', '') : '');
- },
- /**
- * Return the zone that the point belongs to
- */
- getZone: function() {
- var series = this.series,
- zones = series.zones,
- zoneAxis = series.zoneAxis || 'y',
- i = 0,
- zone;
- zone = zones[i];
- while (this[zoneAxis] >= zone.value) {
- zone = zones[++i];
- }
- if (zone && zone.color && !this.options.color) {
- this.color = zone.color;
- }
- return zone;
- },
- /**
- * Destroy a point to clear memory. Its reference still stays in series.data.
- */
- destroy: function() {
- var point = this,
- series = point.series,
- chart = series.chart,
- hoverPoints = chart.hoverPoints,
- prop;
- chart.pointCount--;
- if (hoverPoints) {
- point.setState();
- erase(hoverPoints, point);
- if (!hoverPoints.length) {
- chart.hoverPoints = null;
- }
- }
- if (point === chart.hoverPoint) {
- point.onMouseOut();
- }
- // remove all events
- if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
- removeEvent(point);
- point.destroyElements();
- }
- if (point.legendItem) { // pies have legend items
- chart.legend.destroyItem(point);
- }
- for (prop in point) {
- point[prop] = null;
- }
- },
- /**
- * Destroy SVG elements associated with the point
- */
- destroyElements: function() {
- var point = this,
- props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'],
- prop,
- i = 6;
- while (i--) {
- prop = props[i];
- if (point[prop]) {
- point[prop] = point[prop].destroy();
- }
- }
- },
- /**
- * Return the configuration hash needed for the data label and tooltip formatters
- */
- getLabelConfig: function() {
- return {
- x: this.category,
- y: this.y,
- color: this.color,
- colorIndex: this.colorIndex,
- key: this.name || this.category,
- series: this.series,
- point: this,
- percentage: this.percentage,
- total: this.total || this.stackTotal
- };
- },
- /**
- * Extendable method for formatting each point's tooltip line
- *
- * @return {String} A string to be concatenated in to the common tooltip text
- */
- tooltipFormatter: function(pointFormat) {
- // Insert options for valueDecimals, valuePrefix, and valueSuffix
- var series = this.series,
- seriesTooltipOptions = series.tooltipOptions,
- valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''),
- valuePrefix = seriesTooltipOptions.valuePrefix || '',
- valueSuffix = seriesTooltipOptions.valueSuffix || '';
- // Loop over the point array map and replace unformatted values with sprintf formatting markup
- each(series.pointArrayMap || ['y'], function(key) {
- key = '{point.' + key; // without the closing bracket
- if (valuePrefix || valueSuffix) {
- pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix);
- }
- pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}');
- });
- return format(pointFormat, {
- point: this,
- series: this.series
- });
- },
- /**
- * Fire an event on the Point object.
- * @param {String} eventType
- * @param {Object} eventArgs Additional event arguments
- * @param {Function} defaultFunction Default event handler
- */
- firePointEvent: function(eventType, eventArgs, defaultFunction) {
- var point = this,
- series = this.series,
- seriesOptions = series.options;
- // load event handlers on demand to save time on mouseover/out
- if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
- this.importEvents();
- }
- // add default handler if in selection mode
- if (eventType === 'click' && seriesOptions.allowPointSelect) {
- defaultFunction = function(event) {
- // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
- if (point.select) { // Could be destroyed by prior event handlers (#2911)
- point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
- }
- };
- }
- fireEvent(this, eventType, eventArgs, defaultFunction);
- },
- /**
- * For certain series types, like pie charts, where individual points can
- * be shown or hidden.
- *
- * @name visible
- * @memberOf Highcharts.Point
- * @type {Boolean}
- */
- visible: true
- };
- /**
- * For categorized axes this property holds the category name for the
- * point. For other axes it holds the X value.
- *
- * @name category
- * @memberOf Highcharts.Point
- * @type {String|Number}
- */
- /**
- * The percentage for points in a stacked series or pies.
- *
- * @name percentage
- * @memberOf Highcharts.Point
- * @type {Number}
- */
- /**
- * The total of values in either a stack for stacked series, or a pie in a pie
- * series.
- *
- * @name total
- * @memberOf Highcharts.Point
- * @type {Number}
- */
- /**
- * The x value of the point.
- *
- * @name x
- * @memberOf Highcharts.Point
- * @type {Number}
- */
- /**
- * The y value of the point.
- *
- * @name y
- * @memberOf Highcharts.Point
- * @type {Number}
- */
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- animObject = H.animObject,
- arrayMax = H.arrayMax,
- arrayMin = H.arrayMin,
- correctFloat = H.correctFloat,
- Date = H.Date,
- defaultOptions = H.defaultOptions,
- defaultPlotOptions = H.defaultPlotOptions,
- defined = H.defined,
- each = H.each,
- erase = H.erase,
- extend = H.extend,
- fireEvent = H.fireEvent,
- grep = H.grep,
- isArray = H.isArray,
- isNumber = H.isNumber,
- isString = H.isString,
- LegendSymbolMixin = H.LegendSymbolMixin, // @todo add as a requirement
- merge = H.merge,
- objectEach = H.objectEach,
- pick = H.pick,
- Point = H.Point, // @todo add as a requirement
- removeEvent = H.removeEvent,
- splat = H.splat,
- SVGElement = H.SVGElement,
- syncTimeout = H.syncTimeout,
- win = H.win;
- /**
- * This is the base series prototype that all other series types inherit from.
- * A new series is initiated either through the {@link https://api.highcharts.com/highcharts/series|
- * series} option structure, or after the chart is initiated, through {@link
- * Highcharts.Chart#addSeries}.
- *
- * The object can be accessed in a number of ways. All series and point event
- * handlers give a reference to the `series` object. The chart object has a
- * {@link Highcharts.Chart.series|series} property that is a collection of all
- * the chart's series. The point objects and axis objects also have the same
- * reference.
- *
- * Another way to reference the series programmatically is by `id`. Add an id
- * in the series configuration options, and get the series object by {@link
- * Highcharts.Chart#get}.
- *
- * Configuration options for the series are given in three levels. Options for
- * all series in a chart are given in the {@link https://api.highcharts.com/highcharts/plotOptions.series|
- * plotOptions.series} object. Then options for all series of a specific type
- * are given in the plotOptions of that type, for example `plotOptions.line`.
- * Next, options for one single series are given in the series array, or as
- * arguements to `chart.addSeries`.
- *
- * The data in the series is stored in various arrays.
- *
- * - First, `series.options.data` contains all the original config options for
- * each point whether added by options or methods like `series.addPoint`.
- * - Next, `series.data` contains those values converted to points, but in case
- * the series data length exceeds the `cropThreshold`, or if the data is grouped,
- * `series.data` doesn't contain all the points. It only contains the points that
- * have been created on demand.
- * - Then there's `series.points` that contains all currently visible point
- * objects. In case of cropping, the cropped-away points are not part of this
- * array. The `series.points` array starts at `series.cropStart` compared to
- * `series.data` and `series.options.data`. If however the series data is grouped,
- * these can't be correlated one to one.
- * - `series.xData` and `series.processedXData` contain clean x values, equivalent
- * to `series.data` and `series.points`.
- * - `series.yData` and `series.processedYData` contain clean y values, equivalent
- * to `series.data` and `series.points`.
- *
- * @class Highcharts.Series
- * @param {Highcharts.Chart} chart
- * The chart instance.
- * @param {Object} options
- * The series options.
- */
- H.Series = H.seriesType('line', null, { // base series options
- //cursor: 'default',
- //dashStyle: null,
- //linecap: 'round',
- lineWidth: 2,
- //shadow: false,
- allowPointSelect: false,
- showCheckbox: false,
- animation: {
- duration: 1000
- },
- //clip: true,
- //connectNulls: false,
- //enableMouseTracking: true,
- events: {},
- //legendIndex: 0,
- // stacking: null,
- marker: {
- lineWidth: 0,
- lineColor: '#ffffff',
- //fillColor: null,
- //enabled: true,
- //symbol: null,
- radius: 4,
- states: { // states for a single point
- hover: {
- animation: {
- duration: 50
- },
- enabled: true,
- radiusPlus: 2,
- lineWidthPlus: 1
- },
- select: {
- fillColor: '#cccccc',
- lineColor: '#000000',
- lineWidth: 2
- }
- }
- },
- point: {
- events: {}
- },
- dataLabels: {
- align: 'center',
- // defer: true,
- // enabled: false,
- formatter: function() {
- return this.y === null ? '' : H.numberFormat(this.y, -1);
- },
- style: {
- fontSize: '11px',
- fontWeight: 'bold',
- color: 'contrast',
- textOutline: '1px contrast'
- },
- // backgroundColor: undefined,
- // borderColor: undefined,
- // borderWidth: undefined,
- // shadow: false
- verticalAlign: 'bottom', // above singular point
- x: 0,
- y: 0,
- // borderRadius: undefined,
- padding: 5
- },
- // draw points outside the plot area when the number of points is less than
- // this
- cropThreshold: 300,
- pointRange: 0,
- //pointStart: 0,
- //pointInterval: 1,
- //showInLegend: null, // auto = false for linked series
- softThreshold: true,
- states: { // states for the entire series
- hover: {
- //enabled: false,
- animation: {
- duration: 50
- },
- lineWidthPlus: 1,
- marker: {
- // lineWidth: base + 1,
- // radius: base + 1
- },
- halo: {
- size: 10,
- opacity: 0.25
- }
- },
- select: {
- marker: {}
- }
- },
- stickyTracking: true,
- //tooltip: {
- //pointFormat: '<span style="color:{point.color}">\u25CF</span>' +
- // '{series.name}: <b>{point.y}</b>'
- //valueDecimals: null,
- //xDateFormat: '%A, %b %e, %Y',
- //valuePrefix: '',
- //ySuffix: ''
- //}
- turboThreshold: 1000,
- // zIndex: null
- findNearestPointBy: 'x'
- }, /** @lends Highcharts.Series.prototype */ {
- isCartesian: true,
- pointClass: Point,
- sorted: true, // requires the data to be sorted
- requireSorting: true,
- directTouch: false,
- axisTypes: ['xAxis', 'yAxis'],
- colorCounter: 0,
- // each point's x and y values are stored in this.xData and this.yData
- parallelArrays: ['x', 'y'],
- coll: 'series',
- init: function(chart, options) {
- var series = this,
- events,
- chartSeries = chart.series,
- lastSeries;
- /**
- * Read only. The chart that the series belongs to.
- *
- * @name chart
- * @memberOf Series
- * @type {Chart}
- */
- series.chart = chart;
- /**
- * Read only. The series' type, like "line", "area", "column" etc. The
- * type in the series options anc can be altered using {@link
- * Series#update}.
- *
- * @name type
- * @memberOf Series
- * @type String
- */
- /**
- * Read only. The series' current options. To update, use {@link
- * Series#update}.
- *
- * @name options
- * @memberOf Series
- * @type SeriesOptions
- */
- series.options = options = series.setOptions(options);
- series.linkedSeries = [];
- // bind the axes
- series.bindAxes();
- // set some variables
- extend(series, {
- /**
- * The series name as given in the options. Defaults to
- * "Series {n}".
- *
- * @name name
- * @memberOf Series
- * @type {String}
- */
- name: options.name,
- state: '',
- /**
- * Read only. The series' visibility state as set by {@link
- * Series#show}, {@link Series#hide}, or in the initial
- * configuration.
- *
- * @name visible
- * @memberOf Series
- * @type {Boolean}
- */
- visible: options.visible !== false, // true by default
- /**
- * Read only. The series' selected state as set by {@link
- * Highcharts.Series#select}.
- *
- * @name selected
- * @memberOf Series
- * @type {Boolean}
- */
- selected: options.selected === true // false by default
- });
- // register event listeners
- events = options.events;
- objectEach(events, function(event, eventType) {
- addEvent(series, eventType, event);
- });
- if (
- (events && events.click) ||
- (
- options.point &&
- options.point.events &&
- options.point.events.click
- ) ||
- options.allowPointSelect
- ) {
- chart.runTrackerClick = true;
- }
- series.getColor();
- series.getSymbol();
- // Set the data
- each(series.parallelArrays, function(key) {
- series[key + 'Data'] = [];
- });
- series.setData(options.data, false);
- // Mark cartesian
- if (series.isCartesian) {
- chart.hasCartesianSeries = true;
- }
- // Get the index and register the series in the chart. The index is one
- // more than the current latest series index (#5960).
- if (chartSeries.length) {
- lastSeries = chartSeries[chartSeries.length - 1];
- }
- series._i = pick(lastSeries && lastSeries._i, -1) + 1;
- // Insert the series and re-order all series above the insertion point.
- chart.orderSeries(this.insert(chartSeries));
- },
- /**
- * Insert the series in a collection with other series, either the chart
- * series or yAxis series, in the correct order according to the index
- * option.
- * @param {Array} collection A collection of series.
- * @returns {Number} The index of the series in the collection.
- */
- insert: function(collection) {
- var indexOption = this.options.index,
- i;
- // Insert by index option
- if (isNumber(indexOption)) {
- i = collection.length;
- while (i--) {
- // Loop down until the interted element has higher index
- if (indexOption >=
- pick(collection[i].options.index, collection[i]._i)) {
- collection.splice(i + 1, 0, this);
- break;
- }
- }
- if (i === -1) {
- collection.unshift(this);
- }
- i = i + 1;
- // Or just push it to the end
- } else {
- collection.push(this);
- }
- return pick(i, collection.length - 1);
- },
- /**
- * Set the xAxis and yAxis properties of cartesian series, and register the
- * series in the `axis.series` array.
- *
- * @function #bindAxes
- * @memberOf Series
- * @returns {void}
- */
- bindAxes: function() {
- var series = this,
- seriesOptions = series.options,
- chart = series.chart,
- axisOptions;
- // repeat for xAxis and yAxis
- each(series.axisTypes || [], function(AXIS) {
- // loop through the chart's axis objects
- each(chart[AXIS], function(axis) {
- axisOptions = axis.options;
- // apply if the series xAxis or yAxis option mathches the number
- // of the axis, or if undefined, use the first axis
- if (
- seriesOptions[AXIS] === axisOptions.index ||
- (
- seriesOptions[AXIS] !== undefined &&
- seriesOptions[AXIS] === axisOptions.id
- ) ||
- (
- seriesOptions[AXIS] === undefined &&
- axisOptions.index === 0
- )
- ) {
- // register this series in the axis.series lookup
- series.insert(axis.series);
- // set this series.xAxis or series.yAxis reference
- /**
- * Read only. The unique xAxis object associated with the
- * series.
- *
- * @name xAxis
- * @memberOf Series
- * @type Axis
- */
- /**
- * Read only. The unique yAxis object associated with the
- * series.
- *
- * @name yAxis
- * @memberOf Series
- * @type Axis
- */
- series[AXIS] = axis;
- // mark dirty for redraw
- axis.isDirty = true;
- }
- });
- // The series needs an X and an Y axis
- if (!series[AXIS] && series.optionalAxis !== AXIS) {
- H.error(18, true);
- }
- });
- },
- /**
- * For simple series types like line and column, the data values are held in
- * arrays like xData and yData for quick lookup to find extremes and more.
- * For multidimensional series like bubble and map, this can be extended
- * with arrays like zData and valueData by adding to the
- * series.parallelArrays array.
- */
- updateParallelArrays: function(point, i) {
- var series = point.series,
- args = arguments,
- fn = isNumber(i) ?
- // Insert the value in the given position
- function(key) {
- var val = key === 'y' && series.toYData ?
- series.toYData(point) :
- point[key];
- series[key + 'Data'][i] = val;
- } :
- // Apply the method specified in i with the following arguments
- // as arguments
- function(key) {
- Array.prototype[i].apply(
- series[key + 'Data'],
- Array.prototype.slice.call(args, 2)
- );
- };
- each(series.parallelArrays, fn);
- },
- /**
- * Return an auto incremented x value based on the pointStart and
- * pointInterval options. This is only used if an x value is not given for
- * the point that calls autoIncrement.
- */
- autoIncrement: function() {
- var options = this.options,
- xIncrement = this.xIncrement,
- date,
- pointInterval,
- pointIntervalUnit = options.pointIntervalUnit;
- xIncrement = pick(xIncrement, options.pointStart, 0);
- this.pointInterval = pointInterval = pick(
- this.pointInterval,
- options.pointInterval,
- 1
- );
- // Added code for pointInterval strings
- if (pointIntervalUnit) {
- date = new Date(xIncrement);
- if (pointIntervalUnit === 'day') {
- date = +date[Date.hcSetDate](
- date[Date.hcGetDate]() + pointInterval
- );
- } else if (pointIntervalUnit === 'month') {
- date = +date[Date.hcSetMonth](
- date[Date.hcGetMonth]() + pointInterval
- );
- } else if (pointIntervalUnit === 'year') {
- date = +date[Date.hcSetFullYear](
- date[Date.hcGetFullYear]() + pointInterval
- );
- }
- pointInterval = date - xIncrement;
- }
- this.xIncrement = xIncrement + pointInterval;
- return xIncrement;
- },
- /**
- * Set the series options by merging from the options tree
- * @param {Object} itemOptions
- */
- setOptions: function(itemOptions) {
- var chart = this.chart,
- chartOptions = chart.options,
- plotOptions = chartOptions.plotOptions,
- userOptions = chart.userOptions || {},
- userPlotOptions = userOptions.plotOptions || {},
- typeOptions = plotOptions[this.type],
- options,
- zones;
- this.userOptions = itemOptions;
- // General series options take precedence over type options because
- // otherwise, default type options like column.animation would be
- // overwritten by the general option. But issues have been raised here
- // (#3881), and the solution may be to distinguish between default
- // option and userOptions like in the tooltip below.
- options = merge(
- typeOptions,
- plotOptions.series,
- itemOptions
- );
- // The tooltip options are merged between global and series specific
- // options. Importance order asscendingly:
- // globals: (1)tooltip, (2)plotOptions.series, (3)plotOptions[this.type]
- // init userOptions with possible later updates: 4-6 like 1-3 and
- // (7)this series options
- this.tooltipOptions = merge(
- defaultOptions.tooltip, // 1
- defaultOptions.plotOptions.series &&
- defaultOptions.plotOptions.series.tooltip, // 2
- defaultOptions.plotOptions[this.type].tooltip, // 3
- chartOptions.tooltip.userOptions, // 4
- plotOptions.series && plotOptions.series.tooltip, // 5
- plotOptions[this.type].tooltip, // 6
- itemOptions.tooltip // 7
- );
- // When shared tooltip, stickyTracking is true by default,
- // unless user says otherwise.
- this.stickyTracking = pick(
- itemOptions.stickyTracking,
- userPlotOptions[this.type] &&
- userPlotOptions[this.type].stickyTracking,
- userPlotOptions.series && userPlotOptions.series.stickyTracking,
- (
- this.tooltipOptions.shared && !this.noSharedTooltip ?
- true :
- options.stickyTracking
- )
- );
- // Delete marker object if not allowed (#1125)
- if (typeOptions.marker === null) {
- delete options.marker;
- }
- // Handle color zones
- this.zoneAxis = options.zoneAxis;
- zones = this.zones = (options.zones || []).slice();
- if (
- (options.negativeColor || options.negativeFillColor) &&
- !options.zones
- ) {
- zones.push({
- value: options[this.zoneAxis + 'Threshold'] ||
- options.threshold ||
- 0,
- className: 'highcharts-negative',
- color: options.negativeColor,
- fillColor: options.negativeFillColor
- });
- }
- if (zones.length) { // Push one extra zone for the rest
- if (defined(zones[zones.length - 1].value)) {
- zones.push({
- color: this.color,
- fillColor: this.fillColor
- });
- }
- }
- return options;
- },
- getCyclic: function(prop, value, defaults) {
- var i,
- chart = this.chart,
- userOptions = this.userOptions,
- indexName = prop + 'Index',
- counterName = prop + 'Counter',
- len = defaults ? defaults.length : pick(
- chart.options.chart[prop + 'Count'],
- chart[prop + 'Count']
- ),
- setting;
- if (!value) {
- // Pick up either the colorIndex option, or the _colorIndex after
- // Series.update()
- setting = pick(
- userOptions[indexName],
- userOptions['_' + indexName]
- );
- if (defined(setting)) { // after Series.update()
- i = setting;
- } else {
- // #6138
- if (!chart.series.length) {
- chart[counterName] = 0;
- }
- userOptions['_' + indexName] = i = chart[counterName] % len;
- chart[counterName] += 1;
- }
- if (defaults) {
- value = defaults[i];
- }
- }
- // Set the colorIndex
- if (i !== undefined) {
- this[indexName] = i;
- }
- this[prop] = value;
- },
- /**
- * Get the series' color
- */
- getColor: function() {
- if (this.options.colorByPoint) {
- // #4359, selected slice got series.color even when colorByPoint was
- // set.
- this.options.color = null;
- } else {
- this.getCyclic(
- 'color',
- this.options.color || defaultPlotOptions[this.type].color,
- this.chart.options.colors
- );
- }
- },
- /**
- * Get the series' symbol
- */
- getSymbol: function() {
- var seriesMarkerOption = this.options.marker;
- this.getCyclic(
- 'symbol',
- seriesMarkerOption.symbol,
- this.chart.options.symbols
- );
- },
- drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
- /**
- * Apply a new set of data to the series and optionally redraw it. The new
- * data array is passed by reference (except in case of `updatePoints`), and
- * may later be mutated when updating the chart data.
- *
- * Note the difference in behaviour when setting the same amount of points,
- * or a different amount of points, as handled by the `updatePoints`
- * parameter.
- *
- * @param {SeriesDataOptions} data
- * Takes an array of data in the same format as described under
- * `series<type>data` for the given series type.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart after the series is altered. If doing
- * more operations on the chart, it is a good idea to set redraw to
- * false and call {@link Chart#redraw} after.
- * @param {AnimationOptions} [animation]
- * When the updated data is the same length as the existing data,
- * points will be updated by default, and animation visualizes how
- * the points are changed. Set false to disable animation, or a
- * configuration object to set duration or easing.
- * @param {Boolean} [updatePoints=true]
- * When the updated data is the same length as the existing data,
- * points will be updated instead of replaced. This allows updating
- * with animation and performs better. In this case, the original
- * array is not passed by reference. Set false to prevent.
- *
- * @sample highcharts/members/series-setdata/
- * Set new data from a button
- * @sample highcharts/members/series-setdata-pie/
- * Set data in a pie
- * @sample stock/members/series-setdata/
- * Set new data in Highstock
- * @sample maps/members/series-setdata/
- * Set new data in Highmaps
- */
- setData: function(data, redraw, animation, updatePoints) {
- var series = this,
- oldData = series.points,
- oldDataLength = (oldData && oldData.length) || 0,
- dataLength,
- options = series.options,
- chart = series.chart,
- firstPoint = null,
- xAxis = series.xAxis,
- i,
- turboThreshold = options.turboThreshold,
- pt,
- xData = this.xData,
- yData = this.yData,
- pointArrayMap = series.pointArrayMap,
- valueCount = pointArrayMap && pointArrayMap.length;
- data = data || [];
- dataLength = data.length;
- redraw = pick(redraw, true);
- // If the point count is the same as is was, just run Point.update which
- // is cheaper, allows animation, and keeps references to points.
- if (
- updatePoints !== false &&
- dataLength &&
- oldDataLength === dataLength &&
- !series.cropped &&
- !series.hasGroupedData &&
- series.visible
- ) {
- each(data, function(point, i) {
- // .update doesn't exist on a linked, hidden series (#3709)
- if (oldData[i].update && point !== options.data[i]) {
- oldData[i].update(point, false, null, false);
- }
- });
- } else {
- // Reset properties
- series.xIncrement = null;
- series.colorCounter = 0; // for series with colorByPoint (#1547)
- // Update parallel arrays
- each(this.parallelArrays, function(key) {
- series[key + 'Data'].length = 0;
- });
- // In turbo mode, only one- or twodimensional arrays of numbers are
- // allowed. The first value is tested, and we assume that all the
- // rest are defined the same way. Although the 'for' loops are
- // similar, they are repeated inside each if-else conditional for
- // max performance.
- if (turboThreshold && dataLength > turboThreshold) {
- // find the first non-null point
- i = 0;
- while (firstPoint === null && i < dataLength) {
- firstPoint = data[i];
- i++;
- }
- if (isNumber(firstPoint)) { // assume all points are numbers
- for (i = 0; i < dataLength; i++) {
- xData[i] = this.autoIncrement();
- yData[i] = data[i];
- }
- // Assume all points are arrays when first point is
- } else if (isArray(firstPoint)) {
- if (valueCount) { // [x, low, high] or [x, o, h, l, c]
- for (i = 0; i < dataLength; i++) {
- pt = data[i];
- xData[i] = pt[0];
- yData[i] = pt.slice(1, valueCount + 1);
- }
- } else { // [x, y]
- for (i = 0; i < dataLength; i++) {
- pt = data[i];
- xData[i] = pt[0];
- yData[i] = pt[1];
- }
- }
- } else {
- // Highcharts expects configs to be numbers or arrays in
- // turbo mode
- H.error(12);
- }
- } else {
- for (i = 0; i < dataLength; i++) {
- if (data[i] !== undefined) { // stray commas in oldIE
- pt = {
- series: series
- };
- series.pointClass.prototype.applyOptions.apply(
- pt, [data[i]]
- );
- series.updateParallelArrays(pt, i);
- }
- }
- }
- // Forgetting to cast strings to numbers is a common caveat when
- // handling CSV or JSON
- if (isString(yData[0])) {
- H.error(14, true);
- }
- /**
- * Read only. An array containing the series' data point objects. To
- * modify the data, use {@link Highcharts.Series#setData} or {@link
- * Highcharts.Point#update}.
- *
- * @name data
- * @memberOf Highcharts.Series
- * @type {Array.<Highcharts.Point>}
- */
- series.data = [];
- series.options.data = series.userOptions.data = data;
- // destroy old points
- i = oldDataLength;
- while (i--) {
- if (oldData[i] && oldData[i].destroy) {
- oldData[i].destroy();
- }
- }
- // reset minRange (#878)
- if (xAxis) {
- xAxis.minRange = xAxis.userMinRange;
- }
- // redraw
- series.isDirty = chart.isDirtyBox = true;
- series.isDirtyData = !!oldData;
- animation = false;
- }
- // Typically for pie series, points need to be processed and generated
- // prior to rendering the legend
- if (options.legendType === 'point') {
- this.processData();
- this.generatePoints();
- }
- if (redraw) {
- chart.redraw(animation);
- }
- },
- /**
- * Process the data by cropping away unused data points if the series is
- * longer than the crop threshold. This saves computing time for large
- * series.
- */
- processData: function(force) {
- var series = this,
- processedXData = series.xData, // copied during slice operation
- processedYData = series.yData,
- dataLength = processedXData.length,
- croppedData,
- cropStart = 0,
- cropped,
- distance,
- closestPointRange,
- xAxis = series.xAxis,
- i, // loop variable
- options = series.options,
- cropThreshold = options.cropThreshold,
- getExtremesFromAll =
- series.getExtremesFromAll ||
- options.getExtremesFromAll, // #4599
- isCartesian = series.isCartesian,
- xExtremes,
- val2lin = xAxis && xAxis.val2lin,
- isLog = xAxis && xAxis.isLog,
- min,
- max;
- // If the series data or axes haven't changed, don't go through this.
- // Return false to pass the message on to override methods like in data
- // grouping.
- if (
- isCartesian &&
- !series.isDirty &&
- !xAxis.isDirty &&
- !series.yAxis.isDirty &&
- !force
- ) {
- return false;
- }
- if (xAxis) {
- xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053)
- min = xExtremes.min;
- max = xExtremes.max;
- }
- // optionally filter out points outside the plot area
- if (
- isCartesian &&
- series.sorted &&
- !getExtremesFromAll &&
- (!cropThreshold || dataLength > cropThreshold || series.forceCrop)
- ) {
- // it's outside current extremes
- if (
- processedXData[dataLength - 1] < min ||
- processedXData[0] > max
- ) {
- processedXData = [];
- processedYData = [];
- // only crop if it's actually spilling out
- } else if (
- processedXData[0] < min ||
- processedXData[dataLength - 1] > max
- ) {
- croppedData = this.cropData(
- series.xData,
- series.yData,
- min,
- max
- );
- processedXData = croppedData.xData;
- processedYData = croppedData.yData;
- cropStart = croppedData.start;
- cropped = true;
- }
- }
- // Find the closest distance between processed points
- i = processedXData.length || 1;
- while (--i) {
- distance = isLog ?
- val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) :
- processedXData[i] - processedXData[i - 1];
- if (
- distance > 0 &&
- (
- closestPointRange === undefined ||
- distance < closestPointRange
- )
- ) {
- closestPointRange = distance;
- // Unsorted data is not supported by the line tooltip, as well as
- // data grouping and navigation in Stock charts (#725) and width
- // calculation of columns (#1900)
- } else if (distance < 0 && series.requireSorting) {
- H.error(15);
- }
- }
- // Record the properties
- series.cropped = cropped; // undefined or true
- series.cropStart = cropStart;
- series.processedXData = processedXData;
- series.processedYData = processedYData;
- series.closestPointRange = closestPointRange;
- },
- /**
- * Iterate over xData and crop values between min and max. Returns object
- * containing crop start/end cropped xData with corresponding part of yData,
- * dataMin and dataMax within the cropped range
- */
- cropData: function(xData, yData, min, max) {
- var dataLength = xData.length,
- cropStart = 0,
- cropEnd = dataLength,
- // line-type series need one point outside
- cropShoulder = pick(this.cropShoulder, 1),
- i,
- j;
- // iterate up to find slice start
- for (i = 0; i < dataLength; i++) {
- if (xData[i] >= min) {
- cropStart = Math.max(0, i - cropShoulder);
- break;
- }
- }
- // proceed to find slice end
- for (j = i; j < dataLength; j++) {
- if (xData[j] > max) {
- cropEnd = j + cropShoulder;
- break;
- }
- }
- return {
- xData: xData.slice(cropStart, cropEnd),
- yData: yData.slice(cropStart, cropEnd),
- start: cropStart,
- end: cropEnd
- };
- },
- /**
- * Generate the data point after the data has been processed by cropping
- * away unused points and optionally grouped in Highcharts Stock.
- */
- generatePoints: function() {
- var series = this,
- options = series.options,
- dataOptions = options.data,
- data = series.data,
- dataLength,
- processedXData = series.processedXData,
- processedYData = series.processedYData,
- PointClass = series.pointClass,
- processedDataLength = processedXData.length,
- cropStart = series.cropStart || 0,
- cursor,
- hasGroupedData = series.hasGroupedData,
- keys = options.keys,
- point,
- points = [],
- i;
- if (!data && !hasGroupedData) {
- var arr = [];
- arr.length = dataOptions.length;
- data = series.data = arr;
- }
- if (keys && hasGroupedData) {
- // grouped data has already applied keys (#6590)
- series.options.keys = false;
- }
- for (i = 0; i < processedDataLength; i++) {
- cursor = cropStart + i;
- if (!hasGroupedData) {
- point = data[cursor];
- if (!point && dataOptions[cursor] !== undefined) { // #970
- data[cursor] = point = (new PointClass()).init(
- series,
- dataOptions[cursor],
- processedXData[i]
- );
- }
- } else {
- // splat the y data in case of ohlc data array
- point = (new PointClass()).init(
- series, [processedXData[i]].concat(splat(processedYData[i]))
- );
- /**
- * Highstock only. If a point object is created by data
- * grouping, it doesn't reflect actual points in the raw data.
- * In this case, the `dataGroup` property holds information
- * that points back to the raw data.
- *
- * - `dataGroup.start` is the index of the first raw data point
- * in the group.
- * - `dataGroup.length` is the amount of points in the group.
- *
- * @name dataGroup
- * @memberOf Point
- * @type {Object}
- *
- */
- point.dataGroup = series.groupMap[i];
- }
- if (point) { // #6279
- point.index = cursor; // For faster access in Point.update
- points[i] = point;
- }
- }
- // restore keys options (#6590)
- series.options.keys = keys;
- // Hide cropped-away points - this only runs when the number of points
- // is above cropThreshold, or when swithching view from non-grouped
- // data to grouped data (#637)
- if (
- data &&
- (
- processedDataLength !== (dataLength = data.length) ||
- hasGroupedData
- )
- ) {
- for (i = 0; i < dataLength; i++) {
- // when has grouped data, clear all points
- if (i === cropStart && !hasGroupedData) {
- i += processedDataLength;
- }
- if (data[i]) {
- data[i].destroyElements();
- data[i].plotX = undefined; // #1003
- }
- }
- }
- series.data = data;
- series.points = points;
- },
- /**
- * Calculate Y extremes for visible data
- */
- getExtremes: function(yData) {
- var xAxis = this.xAxis,
- yAxis = this.yAxis,
- xData = this.processedXData,
- yDataLength,
- activeYData = [],
- activeCounter = 0,
- // #2117, need to compensate for log X axis
- xExtremes = xAxis.getExtremes(),
- xMin = xExtremes.min,
- xMax = xExtremes.max,
- validValue,
- withinRange,
- x,
- y,
- i,
- j;
- yData = yData || this.stackedYData || this.processedYData || [];
- yDataLength = yData.length;
- for (i = 0; i < yDataLength; i++) {
- x = xData[i];
- y = yData[i];
- // For points within the visible range, including the first point
- // outside the visible range, consider y extremes
- validValue =
- (isNumber(y, true) || isArray(y)) &&
- (!yAxis.positiveValuesOnly || (y.length || y > 0));
- withinRange =
- this.getExtremesFromAll ||
- this.options.getExtremesFromAll ||
- this.cropped ||
- ((xData[i] || x) >= xMin && (xData[i] || x) <= xMax);
- if (validValue && withinRange) {
- j = y.length;
- if (j) { // array, like ohlc or range data
- while (j--) {
- if (y[j] !== null) {
- activeYData[activeCounter++] = y[j];
- }
- }
- } else {
- activeYData[activeCounter++] = y;
- }
- }
- }
- this.dataMin = arrayMin(activeYData);
- this.dataMax = arrayMax(activeYData);
- },
- /**
- * Translate data points from raw data values to chart specific positioning
- * data needed later in drawPoints, drawGraph and drawTracker.
- *
- * @function #translate
- * @memberOf Series
- * @returns {void}
- */
- translate: function() {
- if (!this.processedXData) { // hidden series
- this.processData();
- }
- this.generatePoints();
- var series = this,
- options = series.options,
- stacking = options.stacking,
- xAxis = series.xAxis,
- categories = xAxis.categories,
- yAxis = series.yAxis,
- points = series.points,
- dataLength = points.length,
- hasModifyValue = !!series.modifyValue,
- i,
- pointPlacement = options.pointPlacement,
- dynamicallyPlaced =
- pointPlacement === 'between' ||
- isNumber(pointPlacement),
- threshold = options.threshold,
- stackThreshold = options.startFromThreshold ? threshold : 0,
- plotX,
- plotY,
- lastPlotX,
- stackIndicator,
- closestPointRangePx = Number.MAX_VALUE;
- // Point placement is relative to each series pointRange (#5889)
- if (pointPlacement === 'between') {
- pointPlacement = 0.5;
- }
- if (isNumber(pointPlacement)) {
- pointPlacement *= pick(options.pointRange || xAxis.pointRange);
- }
- // Translate each point
- for (i = 0; i < dataLength; i++) {
- var point = points[i],
- xValue = point.x,
- yValue = point.y,
- yBottom = point.low,
- stack = stacking && yAxis.stacks[(
- series.negStacks &&
- yValue < (stackThreshold ? 0 : threshold) ? '-' : ''
- ) + series.stackKey],
- pointStack,
- stackValues;
- // Discard disallowed y values for log axes (#3434)
- if (yAxis.positiveValuesOnly && yValue !== null && yValue <= 0) {
- point.isNull = true;
- }
- // Get the plotX translation
- point.plotX = plotX = correctFloat( // #5236
- Math.min(Math.max(-1e5, xAxis.translate(
- xValue,
- 0,
- 0,
- 0,
- 1,
- pointPlacement,
- this.type === 'flags'
- )), 1e5) // #3923
- );
- // Calculate the bottom y value for stacked series
- if (
- stacking &&
- series.visible &&
- !point.isNull &&
- stack &&
- stack[xValue]
- ) {
- stackIndicator = series.getStackIndicator(
- stackIndicator,
- xValue,
- series.index
- );
- pointStack = stack[xValue];
- stackValues = pointStack.points[stackIndicator.key];
- yBottom = stackValues[0];
- yValue = stackValues[1];
- if (
- yBottom === stackThreshold &&
- stackIndicator.key === stack[xValue].base
- ) {
- yBottom = pick(threshold, yAxis.min);
- }
- if (yAxis.positiveValuesOnly && yBottom <= 0) { // #1200, #1232
- yBottom = null;
- }
- point.total = point.stackTotal = pointStack.total;
- point.percentage =
- pointStack.total &&
- (point.y / pointStack.total * 100);
- point.stackY = yValue;
- // Place the stack label
- pointStack.setOffset(
- series.pointXOffset || 0,
- series.barW || 0
- );
- }
- // Set translated yBottom or remove it
- point.yBottom = defined(yBottom) ?
- yAxis.translate(yBottom, 0, 1, 0, 1) :
- null;
- // general hook, used for Highstock compare mode
- if (hasModifyValue) {
- yValue = series.modifyValue(yValue, point);
- }
- // Set the the plotY value, reset it for redraws
- point.plotY = plotY =
- (typeof yValue === 'number' && yValue !== Infinity) ?
- Math.min(Math.max(-1e5,
- yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201
- undefined;
- point.isInside =
- plotY !== undefined &&
- plotY >= 0 &&
- plotY <= yAxis.len && // #3519
- plotX >= 0 &&
- plotX <= xAxis.len;
- // Set client related positions for mouse tracking
- point.clientX = dynamicallyPlaced ?
- correctFloat(
- xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement)
- ) :
- plotX; // #1514, #5383, #5518
- point.negative = point.y < (threshold || 0);
- // some API data
- point.category = categories && categories[point.x] !== undefined ?
- categories[point.x] : point.x;
- // Determine auto enabling of markers (#3635, #5099)
- if (!point.isNull) {
- if (lastPlotX !== undefined) {
- closestPointRangePx = Math.min(
- closestPointRangePx,
- Math.abs(plotX - lastPlotX)
- );
- }
- lastPlotX = plotX;
- }
- // Find point zone
- point.zone = this.zones.length && point.getZone();
- }
- series.closestPointRangePx = closestPointRangePx;
- },
- /**
- * Return the series points with null points filtered out
- */
- getValidPoints: function(points, insideOnly) {
- var chart = this.chart;
- // #3916, #5029, #5085
- return grep(points || this.points || [], function isValidPoint(point) {
- if (insideOnly && !chart.isInsidePlot(
- point.plotX,
- point.plotY,
- chart.inverted
- )) {
- return false;
- }
- return !point.isNull;
- });
- },
- /**
- * Set the clipping for the series. For animated series it is called twice,
- * first to initiate animating the clip then the second time without the
- * animation to set the final clip.
- */
- setClip: function(animation) {
- var chart = this.chart,
- options = this.options,
- renderer = chart.renderer,
- inverted = chart.inverted,
- seriesClipBox = this.clipBox,
- clipBox = seriesClipBox || chart.clipBox,
- sharedClipKey =
- this.sharedClipKey || [
- '_sharedClip',
- animation && animation.duration,
- animation && animation.easing,
- clipBox.height,
- options.xAxis,
- options.yAxis
- ].join(','), // #4526
- clipRect = chart[sharedClipKey],
- markerClipRect = chart[sharedClipKey + 'm'];
- // If a clipping rectangle with the same properties is currently present
- // in the chart, use that.
- if (!clipRect) {
- // When animation is set, prepare the initial positions
- if (animation) {
- clipBox.width = 0;
- chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(-99, // include the width of the first marker
- inverted ? -chart.plotLeft : -chart.plotTop,
- 99,
- inverted ? chart.chartWidth : chart.chartHeight
- );
- }
- chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox);
- // Create hashmap for series indexes
- clipRect.count = {
- length: 0
- };
- }
- if (animation) {
- if (!clipRect.count[this.index]) {
- clipRect.count[this.index] = true;
- clipRect.count.length += 1;
- }
- }
- if (options.clip !== false) {
- this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect);
- this.markerGroup.clip(markerClipRect);
- this.sharedClipKey = sharedClipKey;
- }
- // Remove the shared clipping rectangle when all series are shown
- if (!animation) {
- if (clipRect.count[this.index]) {
- delete clipRect.count[this.index];
- clipRect.count.length -= 1;
- }
- if (clipRect.count.length === 0 && sharedClipKey && chart[sharedClipKey]) {
- if (!seriesClipBox) {
- chart[sharedClipKey] = chart[sharedClipKey].destroy();
- }
- if (chart[sharedClipKey + 'm']) {
- chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
- }
- }
- }
- },
- /**
- * Animate in the series
- */
- animate: function(init) {
- var series = this,
- chart = series.chart,
- clipRect,
- animation = animObject(series.options.animation),
- sharedClipKey;
- // Initialize the animation. Set up the clipping rectangle.
- if (init) {
- series.setClip(animation);
- // Run the animation
- } else {
- sharedClipKey = this.sharedClipKey;
- clipRect = chart[sharedClipKey];
- if (clipRect) {
- clipRect.animate({
- width: chart.plotSizeX
- }, animation);
- }
- if (chart[sharedClipKey + 'm']) {
- chart[sharedClipKey + 'm'].animate({
- width: chart.plotSizeX + 99
- }, animation);
- }
- // Delete this function to allow it only once
- series.animate = null;
- }
- },
- /**
- * This runs after animation to land on the final plot clipping
- */
- afterAnimate: function() {
- this.setClip();
- fireEvent(this, 'afterAnimate');
- },
- /**
- * Draw the markers.
- *
- * @function #drawPoints
- * @memberOf Series
- * @returns {void}
- */
- drawPoints: function() {
- var series = this,
- points = series.points,
- chart = series.chart,
- plotY,
- i,
- point,
- symbol,
- graphic,
- options = series.options,
- seriesMarkerOptions = options.marker,
- pointMarkerOptions,
- hasPointMarker,
- enabled,
- isInside,
- markerGroup = series[series.specialGroup] || series.markerGroup,
- xAxis = series.xAxis,
- markerAttribs,
- globallyEnabled = pick(
- seriesMarkerOptions.enabled,
- xAxis.isRadial ? true : null,
- // Use larger or equal as radius is null in bubbles (#6321)
- series.closestPointRangePx >= 2 * seriesMarkerOptions.radius
- );
- if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) {
- for (i = 0; i < points.length; i++) {
- point = points[i];
- plotY = point.plotY;
- graphic = point.graphic;
- pointMarkerOptions = point.marker || {};
- hasPointMarker = !!point.marker;
- enabled = (globallyEnabled && pointMarkerOptions.enabled === undefined) || pointMarkerOptions.enabled;
- isInside = point.isInside;
- // only draw the point if y is defined
- if (enabled && isNumber(plotY) && point.y !== null) {
- // Shortcuts
- symbol = pick(pointMarkerOptions.symbol, series.symbol);
- point.hasImage = symbol.indexOf('url') === 0;
- markerAttribs = series.markerAttribs(
- point,
- point.selected && 'select'
- );
- if (graphic) { // update
- graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled
- .animate(markerAttribs);
- } else if (isInside && (markerAttribs.width > 0 || point.hasImage)) {
- point.graphic = graphic = chart.renderer.symbol(
- symbol,
- markerAttribs.x,
- markerAttribs.y,
- markerAttribs.width,
- markerAttribs.height,
- hasPointMarker ? pointMarkerOptions : seriesMarkerOptions
- )
- .add(markerGroup);
- }
- // Presentational attributes
- if (graphic) {
- graphic.attr(series.pointAttribs(point, point.selected && 'select'));
- }
- if (graphic) {
- graphic.addClass(point.getClassName(), true);
- }
- } else if (graphic) {
- point.graphic = graphic.destroy(); // #1269
- }
- }
- }
- },
- /**
- * Get non-presentational attributes for the point.
- */
- markerAttribs: function(point, state) {
- var seriesMarkerOptions = this.options.marker,
- seriesStateOptions,
- pointMarkerOptions = point.marker || {},
- pointStateOptions,
- radius = pick(
- pointMarkerOptions.radius,
- seriesMarkerOptions.radius
- ),
- attribs;
- // Handle hover and select states
- if (state) {
- seriesStateOptions = seriesMarkerOptions.states[state];
- pointStateOptions = pointMarkerOptions.states &&
- pointMarkerOptions.states[state];
- radius = pick(
- pointStateOptions && pointStateOptions.radius,
- seriesStateOptions && seriesStateOptions.radius,
- radius + (seriesStateOptions && seriesStateOptions.radiusPlus || 0)
- );
- }
- if (point.hasImage) {
- radius = 0; // and subsequently width and height is not set
- }
- attribs = {
- x: Math.floor(point.plotX) - radius, // Math.floor for #1843
- y: point.plotY - radius
- };
- if (radius) {
- attribs.width = attribs.height = 2 * radius;
- }
- return attribs;
- },
- /**
- * Get presentational attributes for marker-based series (line, spline, scatter, bubble, mappoint...)
- */
- pointAttribs: function(point, state) {
- var seriesMarkerOptions = this.options.marker,
- seriesStateOptions,
- pointOptions = point && point.options,
- pointMarkerOptions = (pointOptions && pointOptions.marker) || {},
- pointStateOptions,
- color = this.color,
- pointColorOption = pointOptions && pointOptions.color,
- pointColor = point && point.color,
- strokeWidth = pick(
- pointMarkerOptions.lineWidth,
- seriesMarkerOptions.lineWidth
- ),
- zoneColor = point && point.zone && point.zone.color,
- fill,
- stroke;
- color = pointColorOption || zoneColor || pointColor || color;
- fill = pointMarkerOptions.fillColor || seriesMarkerOptions.fillColor || color;
- stroke = pointMarkerOptions.lineColor || seriesMarkerOptions.lineColor || color;
- // Handle hover and select states
- if (state) {
- seriesStateOptions = seriesMarkerOptions.states[state];
- pointStateOptions = (pointMarkerOptions.states && pointMarkerOptions.states[state]) || {};
- strokeWidth = pick(
- pointStateOptions.lineWidth,
- seriesStateOptions.lineWidth,
- strokeWidth + pick(
- pointStateOptions.lineWidthPlus,
- seriesStateOptions.lineWidthPlus,
- 0
- )
- );
- fill = pointStateOptions.fillColor || seriesStateOptions.fillColor || fill;
- stroke = pointStateOptions.lineColor || seriesStateOptions.lineColor || stroke;
- }
- return {
- 'stroke': stroke,
- 'stroke-width': strokeWidth,
- 'fill': fill
- };
- },
- /**
- * Clear DOM objects and free up memory
- */
- destroy: function() {
- var series = this,
- chart = series.chart,
- issue134 = /AppleWebKit\/533/.test(win.navigator.userAgent),
- destroy,
- i,
- data = series.data || [],
- point,
- axis;
- // add event hook
- fireEvent(series, 'destroy');
- // remove all events
- removeEvent(series);
- // erase from axes
- each(series.axisTypes || [], function(AXIS) {
- axis = series[AXIS];
- if (axis && axis.series) {
- erase(axis.series, series);
- axis.isDirty = axis.forceRedraw = true;
- }
- });
- // remove legend items
- if (series.legendItem) {
- series.chart.legend.destroyItem(series);
- }
- // destroy all points with their elements
- i = data.length;
- while (i--) {
- point = data[i];
- if (point && point.destroy) {
- point.destroy();
- }
- }
- series.points = null;
- // Clear the animation timeout if we are destroying the series during initial animation
- clearTimeout(series.animationTimeout);
- // Destroy all SVGElements associated to the series
- objectEach(series, function(val, prop) {
- if (val instanceof SVGElement && !val.survive) { // Survive provides a hook for not destroying
- // issue 134 workaround
- destroy = issue134 && prop === 'group' ?
- 'hide' :
- 'destroy';
- val[destroy]();
- }
- });
- // remove from hoverSeries
- if (chart.hoverSeries === series) {
- chart.hoverSeries = null;
- }
- erase(chart.series, series);
- chart.orderSeries();
- // clear all members
- objectEach(series, function(val, prop) {
- delete series[prop];
- });
- },
- /**
- * Get the graph path
- */
- getGraphPath: function(points, nullsAsZeroes, connectCliffs) {
- var series = this,
- options = series.options,
- step = options.step,
- reversed,
- graphPath = [],
- xMap = [],
- gap;
- points = points || series.points;
- // Bottom of a stack is reversed
- reversed = points.reversed;
- if (reversed) {
- points.reverse();
- }
- // Reverse the steps (#5004)
- step = {
- right: 1,
- center: 2
- }[step] || (step && 3);
- if (step && reversed) {
- step = 4 - step;
- }
- // Remove invalid points, especially in spline (#5015)
- if (options.connectNulls && !nullsAsZeroes && !connectCliffs) {
- points = this.getValidPoints(points);
- }
- // Build the line
- each(points, function(point, i) {
- var plotX = point.plotX,
- plotY = point.plotY,
- lastPoint = points[i - 1],
- pathToPoint; // the path to this point from the previous
- if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) {
- gap = true; // ... and continue
- }
- // Line series, nullsAsZeroes is not handled
- if (point.isNull && !defined(nullsAsZeroes) && i > 0) {
- gap = !options.connectNulls;
- // Area series, nullsAsZeroes is set
- } else if (point.isNull && !nullsAsZeroes) {
- gap = true;
- } else {
- if (i === 0 || gap) {
- pathToPoint = ['M', point.plotX, point.plotY];
- } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
- pathToPoint = series.getPointSpline(points, point, i);
- } else if (step) {
- if (step === 1) { // right
- pathToPoint = [
- 'L',
- lastPoint.plotX,
- plotY
- ];
- } else if (step === 2) { // center
- pathToPoint = [
- 'L',
- (lastPoint.plotX + plotX) / 2,
- lastPoint.plotY,
- 'L',
- (lastPoint.plotX + plotX) / 2,
- plotY
- ];
- } else {
- pathToPoint = [
- 'L',
- plotX,
- lastPoint.plotY
- ];
- }
- pathToPoint.push('L', plotX, plotY);
- } else {
- // normal line to next point
- pathToPoint = [
- 'L',
- plotX,
- plotY
- ];
- }
- // Prepare for animation. When step is enabled, there are two path nodes for each x value.
- xMap.push(point.x);
- if (step) {
- xMap.push(point.x);
- }
- graphPath.push.apply(graphPath, pathToPoint);
- gap = false;
- }
- });
- graphPath.xMap = xMap;
- series.graphPath = graphPath;
- return graphPath;
- },
- /**
- * Draw the actual graph
- */
- drawGraph: function() {
- var series = this,
- options = this.options,
- graphPath = (this.gappedPath || this.getGraphPath).call(this),
- props = [
- [
- 'graph',
- 'highcharts-graph',
- options.lineColor || this.color,
- options.dashStyle
- ]
- ];
- // Add the zone properties if any
- each(this.zones, function(zone, i) {
- props.push([
- 'zone-graph-' + i,
- 'highcharts-graph highcharts-zone-graph-' + i + ' ' + (zone.className || ''),
- zone.color || series.color,
- zone.dashStyle || options.dashStyle
- ]);
- });
- // Draw the graph
- each(props, function(prop, i) {
- var graphKey = prop[0],
- graph = series[graphKey],
- attribs;
- if (graph) {
- graph.endX = graphPath.xMap;
- graph.animate({
- d: graphPath
- });
- } else if (graphPath.length) { // #1487
- series[graphKey] = series.chart.renderer.path(graphPath)
- .addClass(prop[1])
- .attr({
- zIndex: 1
- }) // #1069
- .add(series.group);
- attribs = {
- 'stroke': prop[2],
- 'stroke-width': options.lineWidth,
- 'fill': (series.fillGraph && series.color) || 'none' // Polygon series use filled graph
- };
- if (prop[3]) {
- attribs.dashstyle = prop[3];
- } else if (options.linecap !== 'square') {
- attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
- }
- graph = series[graphKey]
- .attr(attribs)
- .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932
- }
- // Helpers for animation
- if (graph) {
- graph.startX = graphPath.xMap;
- //graph.shiftUnit = options.step ? 2 : 1;
- graph.isArea = graphPath.isArea; // For arearange animation
- }
- });
- },
- /**
- * Clip the graphs into the positive and negative coloured graphs
- */
- applyZones: function() {
- var series = this,
- chart = this.chart,
- renderer = chart.renderer,
- zones = this.zones,
- translatedFrom,
- translatedTo,
- clips = this.clips || [],
- clipAttr,
- graph = this.graph,
- area = this.area,
- chartSizeMax = Math.max(chart.chartWidth, chart.chartHeight),
- axis = this[(this.zoneAxis || 'y') + 'Axis'],
- extremes,
- reversed,
- inverted = chart.inverted,
- horiz,
- pxRange,
- pxPosMin,
- pxPosMax,
- ignoreZones = false;
- if (zones.length && (graph || area) && axis && axis.min !== undefined) {
- reversed = axis.reversed;
- horiz = axis.horiz;
- // The use of the Color Threshold assumes there are no gaps
- // so it is safe to hide the original graph and area
- if (graph) {
- graph.hide();
- }
- if (area) {
- area.hide();
- }
- // Create the clips
- extremes = axis.getExtremes();
- each(zones, function(threshold, i) {
- translatedFrom = reversed ?
- (horiz ? chart.plotWidth : 0) :
- (horiz ? 0 : axis.toPixels(extremes.min));
- translatedFrom = Math.min(Math.max(pick(translatedTo, translatedFrom), 0), chartSizeMax);
- translatedTo = Math.min(Math.max(Math.round(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax);
- if (ignoreZones) {
- translatedFrom = translatedTo = axis.toPixels(extremes.max);
- }
- pxRange = Math.abs(translatedFrom - translatedTo);
- pxPosMin = Math.min(translatedFrom, translatedTo);
- pxPosMax = Math.max(translatedFrom, translatedTo);
- if (axis.isXAxis) {
- clipAttr = {
- x: inverted ? pxPosMax : pxPosMin,
- y: 0,
- width: pxRange,
- height: chartSizeMax
- };
- if (!horiz) {
- clipAttr.x = chart.plotHeight - clipAttr.x;
- }
- } else {
- clipAttr = {
- x: 0,
- y: inverted ? pxPosMax : pxPosMin,
- width: chartSizeMax,
- height: pxRange
- };
- if (horiz) {
- clipAttr.y = chart.plotWidth - clipAttr.y;
- }
- }
- /// VML SUPPPORT
- if (inverted && renderer.isVML) {
- if (axis.isXAxis) {
- clipAttr = {
- x: 0,
- y: reversed ? pxPosMin : pxPosMax,
- height: clipAttr.width,
- width: chart.chartWidth
- };
- } else {
- clipAttr = {
- x: clipAttr.y - chart.plotLeft - chart.spacingBox.x,
- y: 0,
- width: clipAttr.height,
- height: chart.chartHeight
- };
- }
- }
- /// END OF VML SUPPORT
- if (clips[i]) {
- clips[i].animate(clipAttr);
- } else {
- clips[i] = renderer.clipRect(clipAttr);
- if (graph) {
- series['zone-graph-' + i].clip(clips[i]);
- }
- if (area) {
- series['zone-area-' + i].clip(clips[i]);
- }
- }
- // if this zone extends out of the axis, ignore the others
- ignoreZones = threshold.value > extremes.max;
- });
- this.clips = clips;
- }
- },
- /**
- * Initialize and perform group inversion on series.group and series.markerGroup
- */
- invertGroups: function(inverted) {
- var series = this,
- chart = series.chart,
- remover;
- function setInvert() {
- each(['group', 'markerGroup'], function(groupName) {
- if (series[groupName]) {
- // VML/HTML needs explicit attributes for flipping
- if (chart.renderer.isVML) {
- series[groupName].attr({
- width: series.yAxis.len,
- height: series.xAxis.len
- });
- }
- series[groupName].width = series.yAxis.len;
- series[groupName].height = series.xAxis.len;
- series[groupName].invert(inverted);
- }
- });
- }
- // Pie, go away (#1736)
- if (!series.xAxis) {
- return;
- }
- // A fixed size is needed for inversion to work
- remover = addEvent(chart, 'resize', setInvert);
- addEvent(series, 'destroy', remover);
- // Do it now
- setInvert(inverted); // do it now
- // On subsequent render and redraw, just do setInvert without setting up events again
- series.invertGroups = setInvert;
- },
- /**
- * General abstraction for creating plot groups like series.group,
- * series.dataLabelsGroup and series.markerGroup. On subsequent calls, the
- * group will only be adjusted to the updated plot size.
- */
- plotGroup: function(prop, name, visibility, zIndex, parent) {
- var group = this[prop],
- isNew = !group;
- // Generate it on first call
- if (isNew) {
- this[prop] = group = this.chart.renderer.g()
- .attr({
- zIndex: zIndex || 0.1 // IE8 and pointer logic use this
- })
- .add(parent);
- }
- // Add the class names, and replace existing ones as response to
- // Series.update (#6660)
- group.addClass(
- (
- 'highcharts-' + name +
- ' highcharts-series-' + this.index +
- ' highcharts-' + this.type + '-series ' +
- 'highcharts-color-' + this.colorIndex + ' ' +
- (this.options.className || '')
- ),
- true
- );
- // Place it on first and subsequent (redraw) calls
- group.attr({
- visibility: visibility
- })[isNew ? 'attr' : 'animate'](
- this.getPlotBox()
- );
- return group;
- },
- /**
- * Get the translation and scale for the plot area of this series
- */
- getPlotBox: function() {
- var chart = this.chart,
- xAxis = this.xAxis,
- yAxis = this.yAxis;
- // Swap axes for inverted (#2339)
- if (chart.inverted) {
- xAxis = yAxis;
- yAxis = this.xAxis;
- }
- return {
- translateX: xAxis ? xAxis.left : chart.plotLeft,
- translateY: yAxis ? yAxis.top : chart.plotTop,
- scaleX: 1, // #1623
- scaleY: 1
- };
- },
- /**
- * Render the graph and markers
- */
- render: function() {
- var series = this,
- chart = series.chart,
- group,
- options = series.options,
- // Animation doesn't work in IE8 quirks when the group div is
- // hidden, and looks bad in other oldIE
- animDuration = (!!series.animate &&
- chart.renderer.isSVG &&
- animObject(options.animation).duration
- ),
- visibility = series.visible ? 'inherit' : 'hidden', // #2597
- zIndex = options.zIndex,
- hasRendered = series.hasRendered,
- chartSeriesGroup = chart.seriesGroup,
- inverted = chart.inverted;
- // the group
- group = series.plotGroup(
- 'group',
- 'series',
- visibility,
- zIndex,
- chartSeriesGroup
- );
- series.markerGroup = series.plotGroup(
- 'markerGroup',
- 'markers',
- visibility,
- zIndex,
- chartSeriesGroup
- );
- // initiate the animation
- if (animDuration) {
- series.animate(true);
- }
- // SVGRenderer needs to know this before drawing elements (#1089, #1795)
- group.inverted = series.isCartesian ? inverted : false;
- // draw the graph if any
- if (series.drawGraph) {
- series.drawGraph();
- series.applyZones();
- }
- /* each(series.points, function (point) {
- if (point.redraw) {
- point.redraw();
- }
- });*/
- // draw the data labels (inn pies they go before the points)
- if (series.drawDataLabels) {
- series.drawDataLabels();
- }
- // draw the points
- if (series.visible) {
- series.drawPoints();
- }
- // draw the mouse tracking area
- if (
- series.drawTracker &&
- series.options.enableMouseTracking !== false
- ) {
- series.drawTracker();
- }
- // Handle inverted series and tracker groups
- series.invertGroups(inverted);
- // Initial clipping, must be defined after inverting groups for VML.
- // Applies to columns etc. (#3839).
- if (options.clip !== false && !series.sharedClipKey && !hasRendered) {
- group.clip(chart.clipRect);
- }
- // Run the animation
- if (animDuration) {
- series.animate();
- }
- // Call the afterAnimate function on animation complete (but don't
- // overwrite the animation.complete option which should be available to
- // the user).
- if (!hasRendered) {
- series.animationTimeout = syncTimeout(function() {
- series.afterAnimate();
- }, animDuration);
- }
- series.isDirty = false; // means data is in accordance with what you see
- // (See #322) series.isDirty = series.isDirtyData = false; // means
- // data is in accordance with what you see
- series.hasRendered = true;
- },
- /**
- * Redraw the series after an update in the axes.
- */
- redraw: function() {
- var series = this,
- chart = series.chart,
- // cache it here as it is set to false in render, but used after
- wasDirty = series.isDirty || series.isDirtyData,
- group = series.group,
- xAxis = series.xAxis,
- yAxis = series.yAxis;
- // reposition on resize
- if (group) {
- if (chart.inverted) {
- group.attr({
- width: chart.plotWidth,
- height: chart.plotHeight
- });
- }
- group.animate({
- translateX: pick(xAxis && xAxis.left, chart.plotLeft),
- translateY: pick(yAxis && yAxis.top, chart.plotTop)
- });
- }
- series.translate();
- series.render();
- if (wasDirty) { // #3868, #3945
- delete this.kdTree;
- }
- },
- /**
- * KD Tree && PointSearching Implementation
- */
- kdAxisArray: ['clientX', 'plotY'],
- searchPoint: function(e, compareX) {
- var series = this,
- xAxis = series.xAxis,
- yAxis = series.yAxis,
- inverted = series.chart.inverted;
- return this.searchKDTree({
- clientX: inverted ?
- xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos,
- plotY: inverted ?
- yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos
- }, compareX);
- },
- /**
- * Build the k-d-tree that is used by mouse and touch interaction to get the
- * closest point. Line-like series typically have a one-dimensional tree
- * where points are searched along the X axis, while scatter-like series
- * typically search in two dimensions, X and Y.
- */
- buildKDTree: function() {
- // Prevent multiple k-d-trees from being built simultaneously (#6235)
- this.buildingKdTree = true;
- var series = this,
- dimensions = series.options.findNearestPointBy.indexOf('y') > -1 ?
- 2 : 1;
- // Internal function
- function _kdtree(points, depth, dimensions) {
- var axis,
- median,
- length = points && points.length;
- if (length) {
- // alternate between the axis
- axis = series.kdAxisArray[depth % dimensions];
- // sort point array
- points.sort(function(a, b) {
- return a[axis] - b[axis];
- });
- median = Math.floor(length / 2);
- // build and return nod
- return {
- point: points[median],
- left: _kdtree(
- points.slice(0, median), depth + 1, dimensions
- ),
- right: _kdtree(
- points.slice(median + 1), depth + 1, dimensions
- )
- };
- }
- }
- // Start the recursive build process with a clone of the points array
- // and null points filtered out (#3873)
- function startRecursive() {
- series.kdTree = _kdtree(
- series.getValidPoints(
- null,
- // For line-type series restrict to plot area, but
- // column-type series not (#3916, #4511)
- !series.directTouch
- ),
- dimensions,
- dimensions
- );
- series.buildingKdTree = false;
- }
- delete series.kdTree;
- // For testing tooltips, don't build async
- syncTimeout(startRecursive, series.options.kdNow ? 0 : 1);
- },
- searchKDTree: function(point, compareX) {
- var series = this,
- kdX = this.kdAxisArray[0],
- kdY = this.kdAxisArray[1],
- kdComparer = compareX ? 'distX' : 'dist',
- kdDimensions = series.options.findNearestPointBy.indexOf('y') > -1 ?
- 2 : 1;
- // Set the one and two dimensional distance on the point object
- function setDistance(p1, p2) {
- var x = (defined(p1[kdX]) && defined(p2[kdX])) ?
- Math.pow(p1[kdX] - p2[kdX], 2) :
- null,
- y = (defined(p1[kdY]) && defined(p2[kdY])) ?
- Math.pow(p1[kdY] - p2[kdY], 2) :
- null,
- r = (x || 0) + (y || 0);
- p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE;
- p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE;
- }
- function _search(search, tree, depth, dimensions) {
- var point = tree.point,
- axis = series.kdAxisArray[depth % dimensions],
- tdist,
- sideA,
- sideB,
- ret = point,
- nPoint1,
- nPoint2;
- setDistance(search, point);
- // Pick side based on distance to splitting point
- tdist = search[axis] - point[axis];
- sideA = tdist < 0 ? 'left' : 'right';
- sideB = tdist < 0 ? 'right' : 'left';
- // End of tree
- if (tree[sideA]) {
- nPoint1 = _search(search, tree[sideA], depth + 1, dimensions);
- ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point);
- }
- if (tree[sideB]) {
- // compare distance to current best to splitting point to decide
- // wether to check side B or not
- if (Math.sqrt(tdist * tdist) < ret[kdComparer]) {
- nPoint2 = _search(
- search,
- tree[sideB],
- depth + 1,
- dimensions
- );
- ret = nPoint2[kdComparer] < ret[kdComparer] ?
- nPoint2 :
- ret;
- }
- }
- return ret;
- }
- if (!this.kdTree && !this.buildingKdTree) {
- this.buildKDTree();
- }
- if (this.kdTree) {
- return _search(point, this.kdTree, kdDimensions, kdDimensions);
- }
- }
- }); // end Series prototype
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var Axis = H.Axis,
- Chart = H.Chart,
- correctFloat = H.correctFloat,
- defined = H.defined,
- destroyObjectProperties = H.destroyObjectProperties,
- each = H.each,
- format = H.format,
- objectEach = H.objectEach,
- pick = H.pick,
- Series = H.Series;
- /**
- * The class for stacks. Each stack, on a specific X value and either negative
- * or positive, has its own stack item.
- *
- * @class
- */
- function StackItem(axis, options, isNegative, x, stackOption) {
- var inverted = axis.chart.inverted;
- this.axis = axis;
- // Tells if the stack is negative
- this.isNegative = isNegative;
- // Save the options to be able to style the label
- this.options = options;
- // Save the x value to be able to position the label later
- this.x = x;
- // Initialize total value
- this.total = null;
- // This will keep each points' extremes stored by series.index and point
- // index
- this.points = {};
- // Save the stack option on the series configuration object, and whether to
- // treat it as percent
- this.stack = stackOption;
- this.leftCliff = 0;
- this.rightCliff = 0;
- // The align options and text align varies on whether the stack is negative
- // and if the chart is inverted or not.
- // First test the user supplied value, then use the dynamic.
- this.alignOptions = {
- align: options.align ||
- (inverted ? (isNegative ? 'left' : 'right') : 'center'),
- verticalAlign: options.verticalAlign ||
- (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
- y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
- x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
- };
- this.textAlign = options.textAlign ||
- (inverted ? (isNegative ? 'right' : 'left') : 'center');
- }
- StackItem.prototype = {
- destroy: function() {
- destroyObjectProperties(this, this.axis);
- },
- /**
- * Renders the stack total label and adds it to the stack label group.
- */
- render: function(group) {
- var options = this.options,
- formatOption = options.format,
- str = formatOption ?
- format(formatOption, this) :
- options.formatter.call(this); // format the text in the label
- // Change the text to reflect the new total and set visibility to hidden
- // in case the serie is hidden
- if (this.label) {
- this.label.attr({
- text: str,
- visibility: 'hidden'
- });
- // Create new label
- } else {
- this.label =
- this.axis.chart.renderer.text(str, null, null, options.useHTML)
- .css(options.style)
- .attr({
- align: this.textAlign,
- rotation: options.rotation,
- visibility: 'hidden' // hidden until setOffset is called
- })
- .add(group); // add to the labels-group
- }
- },
- /**
- * Sets the offset that the stack has from the x value and repositions the
- * label.
- */
- setOffset: function(xOffset, xWidth) {
- var stackItem = this,
- axis = stackItem.axis,
- chart = axis.chart,
- inverted = chart.inverted,
- reversed = axis.reversed,
- neg = (this.isNegative && !reversed) ||
- (!this.isNegative && reversed), // #4056
- // stack value translated mapped to chart coordinates
- y = axis.translate(
- axis.usePercentage ? 100 : this.total,
- 0,
- 0,
- 0,
- 1
- ),
- yZero = axis.translate(0), // stack origin
- h = Math.abs(y - yZero), // stack height
- x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position
- plotHeight = chart.plotHeight,
- stackBox = { // this is the box for the complete stack
- x: inverted ? (neg ? y : y - h) : x,
- y: inverted ?
- plotHeight - x - xWidth : (neg ? (plotHeight - y - h) :
- plotHeight - y),
- width: inverted ? h : xWidth,
- height: inverted ? xWidth : h
- },
- label = this.label,
- alignAttr;
- if (label) {
- // Align the label to the box
- label.align(this.alignOptions, null, stackBox);
- // Set visibility (#678)
- alignAttr = label.alignAttr;
- label[
- this.options.crop === false || chart.isInsidePlot(
- alignAttr.x,
- alignAttr.y
- ) ? 'show' : 'hide'](true);
- }
- }
- };
- /**
- * Generate stacks for each series and calculate stacks total values
- */
- Chart.prototype.getStacks = function() {
- var chart = this;
- // reset stacks for each yAxis
- each(chart.yAxis, function(axis) {
- if (axis.stacks && axis.hasVisibleSeries) {
- axis.oldStacks = axis.stacks;
- }
- });
- each(chart.series, function(series) {
- if (series.options.stacking && (series.visible === true ||
- chart.options.chart.ignoreHiddenSeries === false)) {
- series.stackKey = series.type + pick(series.options.stack, '');
- }
- });
- };
- // Stacking methods defined on the Axis prototype
- /**
- * Build the stacks from top down
- */
- Axis.prototype.buildStacks = function() {
- var axisSeries = this.series,
- series,
- reversedStacks = pick(this.options.reversedStacks, true),
- len = axisSeries.length,
- i;
- if (!this.isXAxis) {
- this.usePercentage = false;
- i = len;
- while (i--) {
- axisSeries[reversedStacks ? i : len - i - 1].setStackedPoints();
- }
- i = len;
- while (i--) {
- series = axisSeries[reversedStacks ? i : len - i - 1];
- if (series.setStackCliffs) {
- series.setStackCliffs();
- }
- }
- // Loop up again to compute percent stack
- if (this.usePercentage) {
- for (i = 0; i < len; i++) {
- axisSeries[i].setPercentStacks();
- }
- }
- }
- };
- Axis.prototype.renderStackTotals = function() {
- var axis = this,
- chart = axis.chart,
- renderer = chart.renderer,
- stacks = axis.stacks,
- stackTotalGroup = axis.stackTotalGroup;
- // Create a separate group for the stack total labels
- if (!stackTotalGroup) {
- axis.stackTotalGroup = stackTotalGroup =
- renderer.g('stack-labels')
- .attr({
- visibility: 'visible',
- zIndex: 6
- })
- .add();
- }
- // plotLeft/Top will change when y axis gets wider so we need to translate
- // the stackTotalGroup at every render call. See bug #506 and #516
- stackTotalGroup.translate(chart.plotLeft, chart.plotTop);
- // Render each stack total
- objectEach(stacks, function(type) {
- objectEach(type, function(stack) {
- stack.render(stackTotalGroup);
- });
- });
- };
- /**
- * Set all the stacks to initial states and destroy unused ones.
- */
- Axis.prototype.resetStacks = function() {
- var axis = this,
- stacks = axis.stacks;
- if (!axis.isXAxis) {
- objectEach(stacks, function(type) {
- objectEach(type, function(stack, key) {
- // Clean up memory after point deletion (#1044, #4320)
- if (stack.touched < axis.stacksTouched) {
- stack.destroy();
- delete type[key];
- // Reset stacks
- } else {
- stack.total = null;
- stack.cum = null;
- }
- });
- });
- }
- };
- Axis.prototype.cleanStacks = function() {
- var stacks;
- if (!this.isXAxis) {
- if (this.oldStacks) {
- stacks = this.stacks = this.oldStacks;
- }
- // reset stacks
- objectEach(stacks, function(type) {
- objectEach(type, function(stack) {
- stack.cum = stack.total;
- });
- });
- }
- };
- // Stacking methods defnied for Series prototype
- /**
- * Adds series' points value to corresponding stack
- */
- Series.prototype.setStackedPoints = function() {
- if (!this.options.stacking || (this.visible !== true &&
- this.chart.options.chart.ignoreHiddenSeries !== false)) {
- return;
- }
- var series = this,
- xData = series.processedXData,
- yData = series.processedYData,
- stackedYData = [],
- yDataLength = yData.length,
- seriesOptions = series.options,
- threshold = seriesOptions.threshold,
- stackThreshold = seriesOptions.startFromThreshold ? threshold : 0,
- stackOption = seriesOptions.stack,
- stacking = seriesOptions.stacking,
- stackKey = series.stackKey,
- negKey = '-' + stackKey,
- negStacks = series.negStacks,
- yAxis = series.yAxis,
- stacks = yAxis.stacks,
- oldStacks = yAxis.oldStacks,
- stackIndicator,
- isNegative,
- stack,
- other,
- key,
- pointKey,
- i,
- x,
- y;
- yAxis.stacksTouched += 1;
- // loop over the non-null y values and read them into a local array
- for (i = 0; i < yDataLength; i++) {
- x = xData[i];
- y = yData[i];
- stackIndicator = series.getStackIndicator(
- stackIndicator,
- x,
- series.index
- );
- pointKey = stackIndicator.key;
- // Read stacked values into a stack based on the x value,
- // the sign of y and the stack key. Stacking is also handled for null
- // values (#739)
- isNegative = negStacks && y < (stackThreshold ? 0 : threshold);
- key = isNegative ? negKey : stackKey;
- // Create empty object for this stack if it doesn't exist yet
- if (!stacks[key]) {
- stacks[key] = {};
- }
- // Initialize StackItem for this x
- if (!stacks[key][x]) {
- if (oldStacks[key] && oldStacks[key][x]) {
- stacks[key][x] = oldStacks[key][x];
- stacks[key][x].total = null;
- } else {
- stacks[key][x] = new StackItem(
- yAxis,
- yAxis.options.stackLabels,
- isNegative,
- x,
- stackOption
- );
- }
- }
- // If the StackItem doesn't exist, create it first
- stack = stacks[key][x];
- if (y !== null) {
- stack.points[pointKey] = stack.points[series.index] = [pick(stack.cum, stackThreshold)];
- // Record the base of the stack
- if (!defined(stack.cum)) {
- stack.base = pointKey;
- }
- stack.touched = yAxis.stacksTouched;
- // In area charts, if there are multiple points on the same X value,
- // let the area fill the full span of those points
- if (stackIndicator.index > 0 && series.singleStacks === false) {
- stack.points[pointKey][0] =
- stack.points[series.index + ',' + x + ',0'][0];
- }
- }
- // Add value to the stack total
- if (stacking === 'percent') {
- // Percent stacked column, totals are the same for the positive and
- // negative stacks
- other = isNegative ? stackKey : negKey;
- if (negStacks && stacks[other] && stacks[other][x]) {
- other = stacks[other][x];
- stack.total = other.total =
- Math.max(other.total, stack.total) + Math.abs(y) || 0;
- // Percent stacked areas
- } else {
- stack.total = correctFloat(stack.total + (Math.abs(y) || 0));
- }
- } else {
- stack.total = correctFloat(stack.total + (y || 0));
- }
- stack.cum = pick(stack.cum, stackThreshold) + (y || 0);
- if (y !== null) {
- stack.points[pointKey].push(stack.cum);
- stackedYData[i] = stack.cum;
- }
- }
- if (stacking === 'percent') {
- yAxis.usePercentage = true;
- }
- this.stackedYData = stackedYData; // To be used in getExtremes
- // Reset old stacks
- yAxis.oldStacks = {};
- };
- /**
- * Iterate over all stacks and compute the absolute values to percent
- */
- Series.prototype.setPercentStacks = function() {
- var series = this,
- stackKey = series.stackKey,
- stacks = series.yAxis.stacks,
- processedXData = series.processedXData,
- stackIndicator;
- each([stackKey, '-' + stackKey], function(key) {
- var i = processedXData.length,
- x,
- stack,
- pointExtremes,
- totalFactor;
- while (i--) {
- x = processedXData[i];
- stackIndicator = series.getStackIndicator(
- stackIndicator,
- x,
- series.index,
- key
- );
- stack = stacks[key] && stacks[key][x];
- pointExtremes = stack && stack.points[stackIndicator.key];
- if (pointExtremes) {
- totalFactor = stack.total ? 100 / stack.total : 0;
- // Y bottom value
- pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor);
- // Y value
- pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor);
- series.stackedYData[i] = pointExtremes[1];
- }
- }
- });
- };
- /**
- * Get stack indicator, according to it's x-value, to determine points with the
- * same x-value
- */
- Series.prototype.getStackIndicator = function(stackIndicator, x, index, key) {
- // Update stack indicator, when:
- // first point in a stack || x changed || stack type (negative vs positive)
- // changed:
- if (!defined(stackIndicator) || stackIndicator.x !== x ||
- (key && stackIndicator.key !== key)) {
- stackIndicator = {
- x: x,
- index: 0,
- key: key
- };
- } else {
- stackIndicator.index++;
- }
- stackIndicator.key = [index, x, stackIndicator.index].join(',');
- return stackIndicator;
- };
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- animate = H.animate,
- Axis = H.Axis,
- Chart = H.Chart,
- createElement = H.createElement,
- css = H.css,
- defined = H.defined,
- each = H.each,
- erase = H.erase,
- extend = H.extend,
- fireEvent = H.fireEvent,
- inArray = H.inArray,
- isNumber = H.isNumber,
- isObject = H.isObject,
- isArray = H.isArray,
- merge = H.merge,
- objectEach = H.objectEach,
- pick = H.pick,
- Point = H.Point,
- Series = H.Series,
- seriesTypes = H.seriesTypes,
- setAnimation = H.setAnimation,
- splat = H.splat;
- // Extend the Chart prototype for dynamic methods
- extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ {
- /**
- * Add a series to the chart after render time. Note that this method should
- * never be used when adding data synchronously at chart render time, as it
- * adds expense to the calculations and rendering. When adding data at the
- * same time as the chart is initiated, add the series as a configuration
- * option instead. With multiple axes, the `offset` is dynamically adjusted.
- *
- * @param {SeriesOptions} options
- * The config options for the series.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart after adding.
- * @param {AnimationOptions} animation
- * Whether to apply animation, and optionally animation
- * configuration.
- *
- * @return {Highcharts.Series}
- * The newly created series object.
- *
- * @sample highcharts/members/chart-addseries/
- * Add a series from a button
- * @sample stock/members/chart-addseries/
- * Add a series in Highstock
- */
- addSeries: function(options, redraw, animation) {
- var series,
- chart = this;
- if (options) {
- redraw = pick(redraw, true); // defaults to true
- fireEvent(chart, 'addSeries', {
- options: options
- }, function() {
- series = chart.initSeries(options);
- chart.isDirtyLegend = true; // the series array is out of sync with the display
- chart.linkSeries();
- if (redraw) {
- chart.redraw(animation);
- }
- });
- }
- return series;
- },
- /**
- * Add an axis to the chart after render time. Note that this method should
- * never be used when adding data synchronously at chart render time, as it
- * adds expense to the calculations and rendering. When adding data at the
- * same time as the chart is initiated, add the axis as a configuration
- * option instead.
- * @param {AxisOptions} options
- * The axis options.
- * @param {Boolean} [isX=false]
- * Whether it is an X axis or a value axis.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart after adding.
- * @param {AnimationOptions} [animation=true]
- * Whether and how to apply animation in the redraw.
- *
- * @sample highcharts/members/chart-addaxis/ Add and remove axes
- */
- addAxis: function(options, isX, redraw, animation) {
- var key = isX ? 'xAxis' : 'yAxis',
- chartOptions = this.options,
- userOptions = merge(options, {
- index: this[key].length,
- isX: isX
- });
- new Axis(this, userOptions); // eslint-disable-line no-new
- // Push the new axis options to the chart options
- chartOptions[key] = splat(chartOptions[key] || {});
- chartOptions[key].push(userOptions);
- if (pick(redraw, true)) {
- this.redraw(animation);
- }
- },
- /**
- * Dim the chart and show a loading text or symbol. Options for the loading
- * screen are defined in {@link
- * https://api.highcharts.com/highcharts/loading|the loading options}.
- *
- * @param {String} str
- * An optional text to show in the loading label instead of the
- * default one. The default text is set in {@link
- * http://api.highcharts.com/highcharts/lang.loading|lang.loading}.
- *
- * @sample highcharts/members/chart-hideloading/
- * Show and hide loading from a button
- * @sample highcharts/members/chart-showloading/
- * Apply different text labels
- * @sample stock/members/chart-show-hide-loading/
- * Toggle loading in Highstock
- */
- showLoading: function(str) {
- var chart = this,
- options = chart.options,
- loadingDiv = chart.loadingDiv,
- loadingOptions = options.loading,
- setLoadingSize = function() {
- if (loadingDiv) {
- css(loadingDiv, {
- left: chart.plotLeft + 'px',
- top: chart.plotTop + 'px',
- width: chart.plotWidth + 'px',
- height: chart.plotHeight + 'px'
- });
- }
- };
- // create the layer at the first call
- if (!loadingDiv) {
- chart.loadingDiv = loadingDiv = createElement('div', {
- className: 'highcharts-loading highcharts-loading-hidden'
- }, null, chart.container);
- chart.loadingSpan = createElement(
- 'span', {
- className: 'highcharts-loading-inner'
- },
- null,
- loadingDiv
- );
- addEvent(chart, 'redraw', setLoadingSize); // #1080
- }
- loadingDiv.className = 'highcharts-loading';
- // Update text
- chart.loadingSpan.innerHTML = str || options.lang.loading;
- // Update visuals
- css(loadingDiv, extend(loadingOptions.style, {
- zIndex: 10
- }));
- css(chart.loadingSpan, loadingOptions.labelStyle);
- // Show it
- if (!chart.loadingShown) {
- css(loadingDiv, {
- opacity: 0,
- display: ''
- });
- animate(loadingDiv, {
- opacity: loadingOptions.style.opacity || 0.5
- }, {
- duration: loadingOptions.showDuration || 0
- });
- }
- chart.loadingShown = true;
- setLoadingSize();
- },
- /**
- * Hide the loading layer.
- *
- * @see Highcharts.Chart#showLoading
- * @sample highcharts/members/chart-hideloading/
- * Show and hide loading from a button
- * @sample stock/members/chart-show-hide-loading/
- * Toggle loading in Highstock
- */
- hideLoading: function() {
- var options = this.options,
- loadingDiv = this.loadingDiv;
- if (loadingDiv) {
- loadingDiv.className = 'highcharts-loading highcharts-loading-hidden';
- animate(loadingDiv, {
- opacity: 0
- }, {
- duration: options.loading.hideDuration || 100,
- complete: function() {
- css(loadingDiv, {
- display: 'none'
- });
- }
- });
- }
- this.loadingShown = false;
- },
- /**
- * These properties cause isDirtyBox to be set to true when updating. Can be extended from plugins.
- */
- propsRequireDirtyBox: ['backgroundColor', 'borderColor', 'borderWidth', 'margin', 'marginTop', 'marginRight',
- 'marginBottom', 'marginLeft', 'spacing', 'spacingTop', 'spacingRight', 'spacingBottom', 'spacingLeft',
- 'borderRadius', 'plotBackgroundColor', 'plotBackgroundImage', 'plotBorderColor', 'plotBorderWidth',
- 'plotShadow', 'shadow'
- ],
- /**
- * These properties cause all series to be updated when updating. Can be
- * extended from plugins.
- */
- propsRequireUpdateSeries: ['chart.inverted', 'chart.polar',
- 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions',
- 'tooltip'
- ],
- /**
- * A generic function to update any element of the chart. Elements can be
- * enabled and disabled, moved, re-styled, re-formatted etc.
- *
- * A special case is configuration objects that take arrays, for example
- * {@link https://api.highcharts.com/highcharts/xAxis|xAxis},
- * {@link https://api.highcharts.com/highcharts/yAxis|yAxis} or
- * {@link https://api.highcharts.com/highcharts/series|series}. For these
- * collections, an `id` option is used to map the new option set to an
- * existing object. If an existing object of the same id is not found, the
- * corresponding item is updated. So for example, running `chart.update`
- * with a series item without an id, will cause the existing chart's series
- * with the same index in the series array to be updated.
- *
- * See also the {@link https://api.highcharts.com/highcharts/responsive|
- * responsive option set}. Switching between `responsive.rules` basically
- * runs `chart.update` under the hood.
- *
- * @param {Options} options
- * A configuration object for the new chart options.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart.
- *
- * @sample highcharts/members/chart-update/
- * Update chart geometry
- */
- update: function(options, redraw) {
- var chart = this,
- adders = {
- credits: 'addCredits',
- title: 'setTitle',
- subtitle: 'setSubtitle'
- },
- optionsChart = options.chart,
- updateAllAxes,
- updateAllSeries,
- newWidth,
- newHeight;
- // If the top-level chart option is present, some special updates are required
- if (optionsChart) {
- merge(true, chart.options.chart, optionsChart);
- // Setter function
- if ('className' in optionsChart) {
- chart.setClassName(optionsChart.className);
- }
- if ('inverted' in optionsChart || 'polar' in optionsChart) {
- // Parse options.chart.inverted and options.chart.polar together
- // with the available series.
- chart.propFromSeries();
- updateAllAxes = true;
- }
- if ('alignTicks' in optionsChart) { // #6452
- updateAllAxes = true;
- }
- objectEach(optionsChart, function(val, key) {
- if (inArray('chart.' + key, chart.propsRequireUpdateSeries) !== -1) {
- updateAllSeries = true;
- }
- // Only dirty box
- if (inArray(key, chart.propsRequireDirtyBox) !== -1) {
- chart.isDirtyBox = true;
- }
- });
- if ('style' in optionsChart) {
- chart.renderer.setStyle(optionsChart.style);
- }
- }
- // Moved up, because tooltip needs updated plotOptions (#6218)
- if (options.colors) {
- this.options.colors = options.colors;
- }
- if (options.plotOptions) {
- merge(true, this.options.plotOptions, options.plotOptions);
- }
- // Some option stuctures correspond one-to-one to chart objects that
- // have update methods, for example
- // options.credits => chart.credits
- // options.legend => chart.legend
- // options.title => chart.title
- // options.tooltip => chart.tooltip
- // options.subtitle => chart.subtitle
- // options.mapNavigation => chart.mapNavigation
- // options.navigator => chart.navigator
- // options.scrollbar => chart.scrollbar
- objectEach(options, function(val, key) {
- if (chart[key] && typeof chart[key].update === 'function') {
- chart[key].update(val, false);
- // If a one-to-one object does not exist, look for an adder function
- } else if (typeof chart[adders[key]] === 'function') {
- chart[adders[key]](val);
- }
- if (
- key !== 'chart' &&
- inArray(key, chart.propsRequireUpdateSeries) !== -1
- ) {
- updateAllSeries = true;
- }
- });
- // Setters for collections. For axes and series, each item is referred
- // by an id. If the id is not found, it defaults to the corresponding
- // item in the collection, so setting one series without an id, will
- // update the first series in the chart. Setting two series without
- // an id will update the first and the second respectively (#6019)
- // chart.update and responsive.
- each([
- 'xAxis',
- 'yAxis',
- 'zAxis',
- 'series',
- 'colorAxis',
- 'pane'
- ], function(coll) {
- if (options[coll]) {
- each(splat(options[coll]), function(newOptions, i) {
- var item = (
- defined(newOptions.id) &&
- chart.get(newOptions.id)
- ) || chart[coll][i];
- if (item && item.coll === coll) {
- item.update(newOptions, false);
- }
- });
- }
- });
- if (updateAllAxes) {
- each(chart.axes, function(axis) {
- axis.update({}, false);
- });
- }
- // Certain options require the whole series structure to be thrown away
- // and rebuilt
- if (updateAllSeries) {
- each(chart.series, function(series) {
- series.update({}, false);
- });
- }
- // For loading, just update the options, do not redraw
- if (options.loading) {
- merge(true, chart.options.loading, options.loading);
- }
- // Update size. Redraw is forced.
- newWidth = optionsChart && optionsChart.width;
- newHeight = optionsChart && optionsChart.height;
- if ((isNumber(newWidth) && newWidth !== chart.chartWidth) ||
- (isNumber(newHeight) && newHeight !== chart.chartHeight)) {
- chart.setSize(newWidth, newHeight);
- } else if (pick(redraw, true)) {
- chart.redraw();
- }
- },
- /**
- * Setter function to allow use from chart.update
- */
- setSubtitle: function(options) {
- this.setTitle(undefined, options);
- }
- });
- // extend the Point prototype for dynamic methods
- extend(Point.prototype, /** @lends Highcharts.Point.prototype */ {
- /**
- * Update point with new options (typically x/y data) and optionally redraw
- * the series.
- *
- * @param {Object} options
- * The point options. Point options are handled as described under
- * the `series<type>.data` item for each series type. For example
- * for a line series, if options is a single number, the point will
- * be given that number as the main y value. If it is an array, it
- * will be interpreted as x and y values respectively. If it is an
- * object, advanced options are applied.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart after the point is updated. If doing
- * more operations on the chart, it is best practice to set
- * `redraw` to false and call `chart.redraw()` after.
- * @param {AnimationOptions} [animation=true]
- * Whether to apply animation, and optionally animation
- * configuration.
- *
- * @sample highcharts/members/point-update-column/
- * Update column value
- * @sample highcharts/members/point-update-pie/
- * Update pie slice
- * @sample maps/members/point-update/
- * Update map area value in Highmaps
- */
- update: function(options, redraw, animation, runEvent) {
- var point = this,
- series = point.series,
- graphic = point.graphic,
- i,
- chart = series.chart,
- seriesOptions = series.options;
- redraw = pick(redraw, true);
- function update() {
- point.applyOptions(options);
- // Update visuals
- if (point.y === null && graphic) { // #4146
- point.graphic = graphic.destroy();
- }
- if (isObject(options, true)) {
- // Destroy so we can get new elements
- if (graphic && graphic.element) {
- if (options && options.marker && options.marker.symbol) {
- point.graphic = graphic.destroy();
- }
- }
- if (options && options.dataLabels && point.dataLabel) { // #2468
- point.dataLabel = point.dataLabel.destroy();
- }
- }
- // record changes in the parallel arrays
- i = point.index;
- series.updateParallelArrays(point, i);
- // Record the options to options.data. If the old or the new config
- // is an object, use point options, otherwise use raw options
- // (#4701, #4916).
- seriesOptions.data[i] = (
- isObject(seriesOptions.data[i], true) ||
- isObject(options, true)
- ) ?
- point.options :
- options;
- // redraw
- series.isDirty = series.isDirtyData = true;
- if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320
- chart.isDirtyBox = true;
- }
- if (seriesOptions.legendType === 'point') { // #1831, #1885
- chart.isDirtyLegend = true;
- }
- if (redraw) {
- chart.redraw(animation);
- }
- }
- // Fire the event with a default handler of doing the update
- if (runEvent === false) { // When called from setData
- update();
- } else {
- point.firePointEvent('update', {
- options: options
- }, update);
- }
- },
- /**
- * Remove a point and optionally redraw the series and if necessary the axes
- * @param {Boolean} redraw
- * Whether to redraw the chart or wait for an explicit call. When
- * doing more operations on the chart, for example running
- * `point.remove()` in a loop, it is best practice to set `redraw`
- * to false and call `chart.redraw()` after.
- * @param {AnimationOptions} [animation=false]
- * Whether to apply animation, and optionally animation
- * configuration.
- *
- * @sample highcharts/plotoptions/series-point-events-remove/
- * Remove point and confirm
- * @sample highcharts/members/point-remove/
- * Remove pie slice
- * @sample maps/members/point-remove/
- * Remove selected points in Highmaps
- */
- remove: function(redraw, animation) {
- this.series.removePoint(inArray(this, this.series.data), redraw, animation);
- }
- });
- // Extend the series prototype for dynamic methods
- extend(Series.prototype, /** @lends Series.prototype */ {
- /**
- * Add a point to the series after render time. The point can be added at
- * the end, or by giving it an X value, to the start or in the middle of the
- * series.
- *
- * @param {Number|Array|Object} options
- * The point options. If options is a single number, a point with
- * that y value is appended to the series.If it is an array, it will
- * be interpreted as x and y values respectively. If it is an
- * object, advanced options as outlined under `series.data` are
- * applied.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart after the point is added. When adding
- * more than one point, it is highly recommended that the redraw
- * option be set to false, and instead {@link Chart#redraw}
- * is explicitly called after the adding of points is finished.
- * Otherwise, the chart will redraw after adding each point.
- * @param {Boolean} [shift=false]
- * If true, a point is shifted off the start of the series as one is
- * appended to the end.
- * @param {AnimationOptions} [animation]
- * Whether to apply animation, and optionally animation
- * configuration.
- *
- * @sample highcharts/members/series-addpoint-append/
- * Append point
- * @sample highcharts/members/series-addpoint-append-and-shift/
- * Append and shift
- * @sample highcharts/members/series-addpoint-x-and-y/
- * Both X and Y values given
- * @sample highcharts/members/series-addpoint-pie/
- * Append pie slice
- * @sample stock/members/series-addpoint/
- * Append 100 points in Highstock
- * @sample stock/members/series-addpoint-shift/
- * Append and shift in Highstock
- * @sample maps/members/series-addpoint/
- * Add a point in Highmaps
- */
- addPoint: function(options, redraw, shift, animation) {
- var series = this,
- seriesOptions = series.options,
- data = series.data,
- chart = series.chart,
- xAxis = series.xAxis,
- names = xAxis && xAxis.hasNames && xAxis.names,
- dataOptions = seriesOptions.data,
- point,
- isInTheMiddle,
- xData = series.xData,
- i,
- x;
- // Optional redraw, defaults to true
- redraw = pick(redraw, true);
- // Get options and push the point to xData, yData and series.options. In series.generatePoints
- // the Point instance will be created on demand and pushed to the series.data array.
- point = {
- series: series
- };
- series.pointClass.prototype.applyOptions.apply(point, [options]);
- x = point.x;
- // Get the insertion point
- i = xData.length;
- if (series.requireSorting && x < xData[i - 1]) {
- isInTheMiddle = true;
- while (i && xData[i - 1] > x) {
- i--;
- }
- }
- series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item
- series.updateParallelArrays(point, i); // update it
- if (names && point.name) {
- names[x] = point.name;
- }
- dataOptions.splice(i, 0, options);
- if (isInTheMiddle) {
- series.data.splice(i, 0, null);
- series.processData();
- }
- // Generate points to be added to the legend (#1329)
- if (seriesOptions.legendType === 'point') {
- series.generatePoints();
- }
- // Shift the first point off the parallel arrays
- if (shift) {
- if (data[0] && data[0].remove) {
- data[0].remove(false);
- } else {
- data.shift();
- series.updateParallelArrays(point, 'shift');
- dataOptions.shift();
- }
- }
- // redraw
- series.isDirty = true;
- series.isDirtyData = true;
- if (redraw) {
- chart.redraw(animation); // Animation is set anyway on redraw, #5665
- }
- },
- /**
- * Remove a point from the series. Unlike the {@link Highcharts.Point#remove}
- * method, this can also be done on a point that is not instanciated because
- * it is outside the view or subject to Highstock data grouping.
- *
- * @param {Number} i
- * The index of the point in the {@link Highcharts.Series.data|data}
- * array.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart after the point is added. When
- * removing more than one point, it is highly recommended that the
- * `redraw` option be set to `false`, and instead {@link
- * Highcharts.Chart#redraw} is explicitly called after the adding of
- * points is finished.
- * @param {AnimationOptions} [animation]
- * Whether and optionally how the series should be animated.
- *
- * @sample highcharts/members/series-removepoint/
- * Remove cropped point
- */
- removePoint: function(i, redraw, animation) {
- var series = this,
- data = series.data,
- point = data[i],
- points = series.points,
- chart = series.chart,
- remove = function() {
- if (points && points.length === data.length) { // #4935
- points.splice(i, 1);
- }
- data.splice(i, 1);
- series.options.data.splice(i, 1);
- series.updateParallelArrays(point || {
- series: series
- }, 'splice', i, 1);
- if (point) {
- point.destroy();
- }
- // redraw
- series.isDirty = true;
- series.isDirtyData = true;
- if (redraw) {
- chart.redraw();
- }
- };
- setAnimation(animation, chart);
- redraw = pick(redraw, true);
- // Fire the event with a default handler of removing the point
- if (point) {
- point.firePointEvent('remove', null, remove);
- } else {
- remove();
- }
- },
- /**
- * Remove a series and optionally redraw the chart.
- *
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart or wait for an explicit call to
- * {@link Highcharts.Chart#redraw}.
- * @param {AnimationOptions} [animation]
- * Whether to apply animation, and optionally animation
- * configuration
- * @param {Boolean} [withEvent=true]
- * Used internally, whether to fire the series `remove` event.
- *
- * @sample highcharts/members/series-remove/
- * Remove first series from a button
- */
- remove: function(redraw, animation, withEvent) {
- var series = this,
- chart = series.chart;
- function remove() {
- // Destroy elements
- series.destroy();
- // Redraw
- chart.isDirtyLegend = chart.isDirtyBox = true;
- chart.linkSeries();
- if (pick(redraw, true)) {
- chart.redraw(animation);
- }
- }
- // Fire the event with a default handler of removing the point
- if (withEvent !== false) {
- fireEvent(series, 'remove', null, remove);
- } else {
- remove();
- }
- },
- /**
- * Update the series with a new set of options. For a clean and precise
- * handling of new options, all methods and elements from the series are
- * removed, and it is initiated from scratch. Therefore, this method is more
- * performance expensive than some other utility methods like {@link
- * Series#setData} or {@link Series#setVisible}.
- *
- * @param {SeriesOptions} options
- * New options that will be merged with the series' existing
- * options.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart after the series is altered. If doing
- * more operations on the chart, it is a good idea to set redraw to
- * false and call {@link Chart#redraw} after.
- *
- * @sample highcharts/members/series-update/
- * Updating series options
- * @sample maps/members/series-update/
- * Update series options in Highmaps
- */
- update: function(newOptions, redraw) {
- var series = this,
- chart = series.chart,
- // must use user options when changing type because series.options
- // is merged in with type specific plotOptions
- oldOptions = series.userOptions,
- oldType = series.oldType || series.type,
- newType = newOptions.type || oldOptions.type || chart.options.chart.type,
- proto = seriesTypes[oldType].prototype,
- preserve = ['group', 'markerGroup', 'dataLabelsGroup'],
- n;
- // Running Series.update to update the data only is an intuitive usage,
- // so we want to make sure that when used like this, we run the
- // cheaper setData function and allow animation instead of completely
- // recreating the series instance.
- if (Object.keys && Object.keys(newOptions).toString() === 'data') {
- return this.setData(newOptions.data, redraw);
- }
- // If we're changing type or zIndex, create new groups (#3380, #3404)
- if ((newType && newType !== oldType) || newOptions.zIndex !== undefined) {
- preserve.length = 0;
- }
- // Make sure groups are not destroyed (#3094)
- each(preserve, function(prop) {
- preserve[prop] = series[prop];
- delete series[prop];
- });
- // Do the merge, with some forced options
- newOptions = merge(oldOptions, {
- animation: false,
- index: series.index,
- pointStart: series.xData[0] // when updating after addPoint
- }, {
- data: series.options.data
- }, newOptions);
- // Destroy the series and delete all properties. Reinsert all methods
- // and properties from the new type prototype (#2270, #3719)
- series.remove(false, null, false);
- for (n in proto) {
- series[n] = undefined;
- }
- extend(series, seriesTypes[newType || oldType].prototype);
- // Re-register groups (#3094)
- each(preserve, function(prop) {
- series[prop] = preserve[prop];
- });
- series.init(chart, newOptions);
- series.oldType = oldType;
- chart.linkSeries(); // Links are lost in series.remove (#3028)
- if (pick(redraw, true)) {
- chart.redraw(false);
- }
- }
- });
- // Extend the Axis.prototype for dynamic methods
- extend(Axis.prototype, /** @lends Highcharts.Axis.prototype */ {
- /**
- * Update an axis object with a new set of options. The options are merged
- * with the existing options, so only new or altered options need to be
- * specified.
- *
- * @param {Object} options
- * The new options that will be merged in with existing options on
- * the axis.
- * @sample highcharts/members/axis-update/ Axis update demo
- */
- update: function(options, redraw) {
- var chart = this.chart;
- options = chart.options[this.coll][this.options.index] =
- merge(this.userOptions, options);
- this.destroy(true);
- this.init(chart, extend(options, {
- events: undefined
- }));
- chart.isDirtyBox = true;
- if (pick(redraw, true)) {
- chart.redraw();
- }
- },
- /**
- * Remove the axis from the chart.
- *
- * @param {Boolean} [redraw=true] Whether to redraw the chart following the
- * remove.
- *
- * @sample highcharts/members/chart-addaxis/ Add and remove axes
- */
- remove: function(redraw) {
- var chart = this.chart,
- key = this.coll, // xAxis or yAxis
- axisSeries = this.series,
- i = axisSeries.length;
- // Remove associated series (#2687)
- while (i--) {
- if (axisSeries[i]) {
- axisSeries[i].remove(false);
- }
- }
- // Remove the axis
- erase(chart.axes, this);
- erase(chart[key], this);
- if (isArray(chart.options[key])) {
- chart.options[key].splice(this.options.index, 1);
- } else { // color axis, #6488
- delete chart.options[key];
- }
- each(chart[key], function(axis, i) { // Re-index, #1706
- axis.options.index = i;
- });
- this.destroy();
- chart.isDirtyBox = true;
- if (pick(redraw, true)) {
- chart.redraw();
- }
- },
- /**
- * Update the axis title by options after render time.
- *
- * @param {TitleOptions} titleOptions
- * The additional title options.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart after setting the title.
- * @sample highcharts/members/axis-settitle/ Set a new Y axis title
- */
- setTitle: function(titleOptions, redraw) {
- this.update({
- title: titleOptions
- }, redraw);
- },
- /**
- * Set new axis categories and optionally redraw.
- * @param {Array.<String>} categories - The new categories.
- * @param {Boolean} [redraw=true] - Whether to redraw the chart.
- * @sample highcharts/members/axis-setcategories/ Set categories by click on
- * a button
- */
- setCategories: function(categories, redraw) {
- this.update({
- categories: categories
- }, redraw);
- }
- });
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var color = H.color,
- each = H.each,
- LegendSymbolMixin = H.LegendSymbolMixin,
- map = H.map,
- pick = H.pick,
- Series = H.Series,
- seriesType = H.seriesType;
- /**
- * Area series type.
- * @constructor seriesTypes.area
- * @extends {Series}
- */
- seriesType('area', 'line', {
- softThreshold: false,
- threshold: 0
- // trackByArea: false,
- // lineColor: null, // overrides color, but lets fillColor be unaltered
- // fillOpacity: 0.75,
- // fillColor: null
- }, /** @lends seriesTypes.area.prototype */ {
- singleStacks: false,
- /**
- * Return an array of stacked points, where null and missing points are replaced by
- * dummy points in order for gaps to be drawn correctly in stacks.
- */
- getStackPoints: function() {
- var series = this,
- segment = [],
- keys = [],
- xAxis = this.xAxis,
- yAxis = this.yAxis,
- stack = yAxis.stacks[this.stackKey],
- pointMap = {},
- points = this.points,
- seriesIndex = series.index,
- yAxisSeries = yAxis.series,
- seriesLength = yAxisSeries.length,
- visibleSeries,
- upOrDown = pick(yAxis.options.reversedStacks, true) ? 1 : -1,
- i;
- if (this.options.stacking) {
- // Create a map where we can quickly look up the points by their X value.
- for (i = 0; i < points.length; i++) {
- pointMap[points[i].x] = points[i];
- }
- // Sort the keys (#1651)
- H.objectEach(stack, function(stackX, x) {
- if (stackX.total !== null) { // nulled after switching between grouping and not (#1651, #2336)
- keys.push(x);
- }
- });
- keys.sort(function(a, b) {
- return a - b;
- });
- visibleSeries = map(yAxisSeries, function() {
- return this.visible;
- });
- each(keys, function(x, idx) {
- var y = 0,
- stackPoint,
- stackedValues;
- if (pointMap[x] && !pointMap[x].isNull) {
- segment.push(pointMap[x]);
- // Find left and right cliff. -1 goes left, 1 goes right.
- each([-1, 1], function(direction) {
- var nullName = direction === 1 ? 'rightNull' : 'leftNull',
- cliffName = direction === 1 ? 'rightCliff' : 'leftCliff',
- cliff = 0,
- otherStack = stack[keys[idx + direction]];
- // If there is a stack next to this one, to the left or to the right...
- if (otherStack) {
- i = seriesIndex;
- while (i >= 0 && i < seriesLength) { // Can go either up or down, depending on reversedStacks
- stackPoint = otherStack.points[i];
- if (!stackPoint) {
- // If the next point in this series is missing, mark the point
- // with point.leftNull or point.rightNull = true.
- if (i === seriesIndex) {
- pointMap[x][nullName] = true;
- // If there are missing points in the next stack in any of the
- // series below this one, we need to substract the missing values
- // and add a hiatus to the left or right.
- } else if (visibleSeries[i]) {
- stackedValues = stack[x].points[i];
- if (stackedValues) {
- cliff -= stackedValues[1] - stackedValues[0];
- }
- }
- }
- // When reversedStacks is true, loop up, else loop down
- i += upOrDown;
- }
- }
- pointMap[x][cliffName] = cliff;
- });
- // There is no point for this X value in this series, so we
- // insert a dummy point in order for the areas to be drawn
- // correctly.
- } else {
- // Loop down the stack to find the series below this one that has
- // a value (#1991)
- i = seriesIndex;
- while (i >= 0 && i < seriesLength) {
- stackPoint = stack[x].points[i];
- if (stackPoint) {
- y = stackPoint[1];
- break;
- }
- // When reversedStacks is true, loop up, else loop down
- i += upOrDown;
- }
- y = yAxis.translate(y, 0, 1, 0, 1); // #6272
- segment.push({
- isNull: true,
- plotX: xAxis.translate(x, 0, 0, 0, 1), // #6272
- x: x,
- plotY: y,
- yBottom: y
- });
- }
- });
- }
- return segment;
- },
- getGraphPath: function(points) {
- var getGraphPath = Series.prototype.getGraphPath,
- graphPath,
- options = this.options,
- stacking = options.stacking,
- yAxis = this.yAxis,
- topPath,
- //topPoints = [],
- bottomPath,
- bottomPoints = [],
- graphPoints = [],
- seriesIndex = this.index,
- i,
- areaPath,
- plotX,
- stacks = yAxis.stacks[this.stackKey],
- threshold = options.threshold,
- translatedThreshold = yAxis.getThreshold(options.threshold),
- isNull,
- yBottom,
- connectNulls = options.connectNulls || stacking === 'percent',
- /**
- * To display null points in underlying stacked series, this series graph must be
- * broken, and the area also fall down to fill the gap left by the null point. #2069
- */
- addDummyPoints = function(i, otherI, side) {
- var point = points[i],
- stackedValues = stacking && stacks[point.x].points[seriesIndex],
- nullVal = point[side + 'Null'] || 0,
- cliffVal = point[side + 'Cliff'] || 0,
- top,
- bottom,
- isNull = true;
- if (cliffVal || nullVal) {
- top = (nullVal ? stackedValues[0] : stackedValues[1]) + cliffVal;
- bottom = stackedValues[0] + cliffVal;
- isNull = !!nullVal;
- } else if (!stacking && points[otherI] && points[otherI].isNull) {
- top = bottom = threshold;
- }
- // Add to the top and bottom line of the area
- if (top !== undefined) {
- graphPoints.push({
- plotX: plotX,
- plotY: top === null ? translatedThreshold : yAxis.getThreshold(top),
- isNull: isNull,
- isCliff: true
- });
- bottomPoints.push({
- plotX: plotX,
- plotY: bottom === null ? translatedThreshold : yAxis.getThreshold(bottom),
- doCurve: false // #1041, gaps in areaspline areas
- });
- }
- };
- // Find what points to use
- points = points || this.points;
- // Fill in missing points
- if (stacking) {
- points = this.getStackPoints();
- }
- for (i = 0; i < points.length; i++) {
- isNull = points[i].isNull;
- plotX = pick(points[i].rectPlotX, points[i].plotX);
- yBottom = pick(points[i].yBottom, translatedThreshold);
- if (!isNull || connectNulls) {
- if (!connectNulls) {
- addDummyPoints(i, i - 1, 'left');
- }
- if (!(isNull && !stacking && connectNulls)) { // Skip null point when stacking is false and connectNulls true
- graphPoints.push(points[i]);
- bottomPoints.push({
- x: i,
- plotX: plotX,
- plotY: yBottom
- });
- }
- if (!connectNulls) {
- addDummyPoints(i, i + 1, 'right');
- }
- }
- }
- topPath = getGraphPath.call(this, graphPoints, true, true);
- bottomPoints.reversed = true;
- bottomPath = getGraphPath.call(this, bottomPoints, true, true);
- if (bottomPath.length) {
- bottomPath[0] = 'L';
- }
- areaPath = topPath.concat(bottomPath);
- graphPath = getGraphPath.call(this, graphPoints, false, connectNulls); // TODO: don't set leftCliff and rightCliff when connectNulls?
- areaPath.xMap = topPath.xMap;
- this.areaPath = areaPath;
- return graphPath;
- },
- /**
- * Draw the graph and the underlying area. This method calls the Series base
- * function and adds the area. The areaPath is calculated in the getSegmentPath
- * method called from Series.prototype.drawGraph.
- */
- drawGraph: function() {
- // Define or reset areaPath
- this.areaPath = [];
- // Call the base method
- Series.prototype.drawGraph.apply(this);
- // Define local variables
- var series = this,
- areaPath = this.areaPath,
- options = this.options,
- zones = this.zones,
- props = [
- [
- 'area',
- 'highcharts-area',
- this.color,
- options.fillColor
- ]
- ]; // area name, main color, fill color
- each(zones, function(zone, i) {
- props.push([
- 'zone-area-' + i,
- 'highcharts-area highcharts-zone-area-' + i + ' ' + zone.className,
- zone.color || series.color,
- zone.fillColor || options.fillColor
- ]);
- });
- each(props, function(prop) {
- var areaKey = prop[0],
- area = series[areaKey];
- // Create or update the area
- if (area) { // update
- area.endX = areaPath.xMap;
- area.animate({
- d: areaPath
- });
- } else { // create
- area = series[areaKey] = series.chart.renderer.path(areaPath)
- .addClass(prop[1])
- .attr({
- fill: pick(
- prop[3],
- color(prop[2]).setOpacity(pick(options.fillOpacity, 0.75)).get()
- ),
- zIndex: 0 // #1069
- }).add(series.group);
- area.isArea = true;
- }
- area.startX = areaPath.xMap;
- area.shiftUnit = options.step ? 2 : 1;
- });
- },
- drawLegendSymbol: LegendSymbolMixin.drawRectangle
- });
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var pick = H.pick,
- seriesType = H.seriesType;
- /**
- * Spline series type.
- * @constructor seriesTypes.spline
- * @extends {Series}
- */
- seriesType('spline', 'line', {}, /** @lends seriesTypes.spline.prototype */ {
- /**
- * Get the spline segment from a given point's previous neighbour to the given point
- */
- getPointSpline: function(points, point, i) {
- var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc
- denom = smoothing + 1,
- plotX = point.plotX,
- plotY = point.plotY,
- lastPoint = points[i - 1],
- nextPoint = points[i + 1],
- leftContX,
- leftContY,
- rightContX,
- rightContY,
- ret;
- function doCurve(otherPoint) {
- return otherPoint &&
- !otherPoint.isNull &&
- otherPoint.doCurve !== false &&
- !point.isCliff; // #6387, area splines next to null
- }
- // Find control points
- if (doCurve(lastPoint) && doCurve(nextPoint)) {
- var lastX = lastPoint.plotX,
- lastY = lastPoint.plotY,
- nextX = nextPoint.plotX,
- nextY = nextPoint.plotY,
- correction = 0;
- leftContX = (smoothing * plotX + lastX) / denom;
- leftContY = (smoothing * plotY + lastY) / denom;
- rightContX = (smoothing * plotX + nextX) / denom;
- rightContY = (smoothing * plotY + nextY) / denom;
- // Have the two control points make a straight line through main point
- if (rightContX !== leftContX) { // #5016, division by zero
- correction = ((rightContY - leftContY) * (rightContX - plotX)) /
- (rightContX - leftContX) + plotY - rightContY;
- }
- leftContY += correction;
- rightContY += correction;
- // to prevent false extremes, check that control points are between
- // neighbouring points' y values
- if (leftContY > lastY && leftContY > plotY) {
- leftContY = Math.max(lastY, plotY);
- rightContY = 2 * plotY - leftContY; // mirror of left control point
- } else if (leftContY < lastY && leftContY < plotY) {
- leftContY = Math.min(lastY, plotY);
- rightContY = 2 * plotY - leftContY;
- }
- if (rightContY > nextY && rightContY > plotY) {
- rightContY = Math.max(nextY, plotY);
- leftContY = 2 * plotY - rightContY;
- } else if (rightContY < nextY && rightContY < plotY) {
- rightContY = Math.min(nextY, plotY);
- leftContY = 2 * plotY - rightContY;
- }
- // record for drawing in next point
- point.rightContX = rightContX;
- point.rightContY = rightContY;
- }
- // Visualize control points for debugging
- /*
- if (leftContX) {
- this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2)
- .attr({
- stroke: 'red',
- 'stroke-width': 2,
- fill: 'none',
- zIndex: 9
- })
- .add();
- this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop,
- 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
- .attr({
- stroke: 'red',
- 'stroke-width': 2,
- zIndex: 9
- })
- .add();
- }
- if (rightContX) {
- this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2)
- .attr({
- stroke: 'green',
- 'stroke-width': 2,
- fill: 'none',
- zIndex: 9
- })
- .add();
- this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop,
- 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
- .attr({
- stroke: 'green',
- 'stroke-width': 2,
- zIndex: 9
- })
- .add();
- }
- // */
- ret = [
- 'C',
- pick(lastPoint.rightContX, lastPoint.plotX),
- pick(lastPoint.rightContY, lastPoint.plotY),
- pick(leftContX, plotX),
- pick(leftContY, plotY),
- plotX,
- plotY
- ];
- lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later
- return ret;
- }
- });
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var areaProto = H.seriesTypes.area.prototype,
- defaultPlotOptions = H.defaultPlotOptions,
- LegendSymbolMixin = H.LegendSymbolMixin,
- seriesType = H.seriesType;
- /**
- * AreaSplineSeries object
- */
- seriesType('areaspline', 'spline', defaultPlotOptions.area, {
- getStackPoints: areaProto.getStackPoints,
- getGraphPath: areaProto.getGraphPath,
- setStackCliffs: areaProto.setStackCliffs,
- drawGraph: areaProto.drawGraph,
- drawLegendSymbol: LegendSymbolMixin.drawRectangle
- });
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var animObject = H.animObject,
- color = H.color,
- each = H.each,
- extend = H.extend,
- isNumber = H.isNumber,
- LegendSymbolMixin = H.LegendSymbolMixin,
- merge = H.merge,
- noop = H.noop,
- pick = H.pick,
- Series = H.Series,
- seriesType = H.seriesType,
- svg = H.svg;
- /**
- * The column series type.
- *
- * @constructor seriesTypes.column
- * @augments Series
- */
- seriesType('column', 'line', {
- borderRadius: 0,
- //colorByPoint: undefined,
- crisp: true,
- groupPadding: 0.2,
- //grouping: true,
- marker: null, // point options are specified in the base options
- pointPadding: 0.1,
- //pointWidth: null,
- minPointLength: 0,
- cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes
- pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories
- states: {
- hover: {
- halo: false,
- brightness: 0.1,
- shadow: false
- },
- select: {
- color: '#cccccc',
- borderColor: '#000000',
- shadow: false
- }
- },
- dataLabels: {
- align: null, // auto
- verticalAlign: null, // auto
- y: null
- },
- softThreshold: false,
- startFromThreshold: true, // false doesn't work well: http://jsfiddle.net/highcharts/hz8fopan/14/
- stickyTracking: false,
- tooltip: {
- distance: 6
- },
- threshold: 0,
- borderColor: '#ffffff'
- // borderWidth: 1
- }, /** @lends seriesTypes.column.prototype */ {
- cropShoulder: 0,
- directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
- trackerGroups: ['group', 'dataLabelsGroup'],
- negStacks: true, // use separate negative stacks, unlike area stacks where a negative
- // point is substracted from previous (#1910)
- /**
- * Initialize the series. Extends the basic Series.init method by
- * marking other series of the same type as dirty.
- *
- * @function #init
- * @memberOf seriesTypes.column
- * @returns {void}
- */
- init: function() {
- Series.prototype.init.apply(this, arguments);
- var series = this,
- chart = series.chart;
- // if the series is added dynamically, force redraw of other
- // series affected by a new column
- if (chart.hasRendered) {
- each(chart.series, function(otherSeries) {
- if (otherSeries.type === series.type) {
- otherSeries.isDirty = true;
- }
- });
- }
- },
- /**
- * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding,
- * pointWidth etc.
- */
- getColumnMetrics: function() {
- var series = this,
- options = series.options,
- xAxis = series.xAxis,
- yAxis = series.yAxis,
- reversedXAxis = xAxis.reversed,
- stackKey,
- stackGroups = {},
- columnCount = 0;
- // Get the total number of column type series.
- // This is called on every series. Consider moving this logic to a
- // chart.orderStacks() function and call it on init, addSeries and removeSeries
- if (options.grouping === false) {
- columnCount = 1;
- } else {
- each(series.chart.series, function(otherSeries) {
- var otherOptions = otherSeries.options,
- otherYAxis = otherSeries.yAxis,
- columnIndex;
- if (
- otherSeries.type === series.type &&
- (
- otherSeries.visible ||
- !series.chart.options.chart.ignoreHiddenSeries
- ) &&
- yAxis.len === otherYAxis.len &&
- yAxis.pos === otherYAxis.pos
- ) { // #642, #2086
- if (otherOptions.stacking) {
- stackKey = otherSeries.stackKey;
- if (stackGroups[stackKey] === undefined) {
- stackGroups[stackKey] = columnCount++;
- }
- columnIndex = stackGroups[stackKey];
- } else if (otherOptions.grouping !== false) { // #1162
- columnIndex = columnCount++;
- }
- otherSeries.columnIndex = columnIndex;
- }
- });
- }
- var categoryWidth = Math.min(
- Math.abs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610
- xAxis.len // #1535
- ),
- groupPadding = categoryWidth * options.groupPadding,
- groupWidth = categoryWidth - 2 * groupPadding,
- pointOffsetWidth = groupWidth / (columnCount || 1),
- pointWidth = Math.min(
- options.maxPointWidth || xAxis.len,
- pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding))
- ),
- pointPadding = (pointOffsetWidth - pointWidth) / 2,
- colIndex = (series.columnIndex || 0) + (reversedXAxis ? 1 : 0), // #1251, #3737
- pointXOffset = pointPadding + (groupPadding + colIndex *
- pointOffsetWidth - (categoryWidth / 2)) *
- (reversedXAxis ? -1 : 1);
- // Save it for reading in linked series (Error bars particularly)
- series.columnMetrics = {
- width: pointWidth,
- offset: pointXOffset
- };
- return series.columnMetrics;
- },
- /**
- * Make the columns crisp. The edges are rounded to the nearest full pixel.
- */
- crispCol: function(x, y, w, h) {
- var chart = this.chart,
- borderWidth = this.borderWidth,
- xCrisp = -(borderWidth % 2 ? 0.5 : 0),
- yCrisp = borderWidth % 2 ? 0.5 : 1,
- right,
- bottom,
- fromTop;
- if (chart.inverted && chart.renderer.isVML) {
- yCrisp += 1;
- }
- // Horizontal. We need to first compute the exact right edge, then round it
- // and compute the width from there.
- if (this.options.crisp) {
- right = Math.round(x + w) + xCrisp;
- x = Math.round(x) + xCrisp;
- w = right - x;
- }
- // Vertical
- bottom = Math.round(y + h) + yCrisp;
- fromTop = Math.abs(y) <= 0.5 && bottom > 0.5; // #4504, #4656
- y = Math.round(y) + yCrisp;
- h = bottom - y;
- // Top edges are exceptions
- if (fromTop && h) { // #5146
- y -= 1;
- h += 1;
- }
- return {
- x: x,
- y: y,
- width: w,
- height: h
- };
- },
- /**
- * Translate each point to the plot area coordinate system and find shape positions
- */
- translate: function() {
- var series = this,
- chart = series.chart,
- options = series.options,
- dense = series.dense = series.closestPointRange * series.xAxis.transA < 2,
- borderWidth = series.borderWidth = pick(
- options.borderWidth,
- dense ? 0 : 1 // #3635
- ),
- yAxis = series.yAxis,
- threshold = options.threshold,
- translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold),
- minPointLength = pick(options.minPointLength, 5),
- metrics = series.getColumnMetrics(),
- pointWidth = metrics.width,
- seriesBarW = series.barW = Math.max(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width
- pointXOffset = series.pointXOffset = metrics.offset;
- if (chart.inverted) {
- translatedThreshold -= 0.5; // #3355
- }
- // When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual
- // columns to have individual sizes. When pointPadding is greater, we strive for equal-width
- // columns (#2694).
- if (options.pointPadding) {
- seriesBarW = Math.ceil(seriesBarW);
- }
- Series.prototype.translate.apply(series);
- // Record the new values
- each(series.points, function(point) {
- var yBottom = pick(point.yBottom, translatedThreshold),
- safeDistance = 999 + Math.abs(yBottom),
- plotY = Math.min(Math.max(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264)
- barX = point.plotX + pointXOffset,
- barW = seriesBarW,
- barY = Math.min(plotY, yBottom),
- up,
- barH = Math.max(plotY, yBottom) - barY;
- // Handle options.minPointLength
- if (Math.abs(barH) < minPointLength) {
- if (minPointLength) {
- barH = minPointLength;
- up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative);
- barY = Math.abs(barY - translatedThreshold) > minPointLength ? // stacked
- yBottom - minPointLength : // keep position
- translatedThreshold - (up ? minPointLength : 0); // #1485, #4051
- }
- }
- // Cache for access in polar
- point.barX = barX;
- point.pointWidth = pointWidth;
- // Fix the tooltip on center of grouped columns (#1216, #424, #3648)
- point.tooltipPos = chart.inverted ? [yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - barX - barW / 2, barH] : [barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH];
- // Register shape type and arguments to be used in drawPoints
- point.shapeType = 'rect';
- point.shapeArgs = series.crispCol.apply(
- series,
- point.isNull ?
- // #3169, drilldown from null must have a position to work from
- // #6585, dataLabel should be placed on xAxis, not floating in the middle of the chart
- [barX, translatedThreshold, barW, 0] : [barX, barY, barW, barH]
- );
- });
- },
- getSymbol: noop,
- /**
- * Use a solid rectangle like the area series types
- */
- drawLegendSymbol: LegendSymbolMixin.drawRectangle,
- /**
- * Columns have no graph
- */
- drawGraph: function() {
- this.group[this.dense ? 'addClass' : 'removeClass']('highcharts-dense-data');
- },
- /**
- * Get presentational attributes
- */
- pointAttribs: function(point, state) {
- var options = this.options,
- stateOptions,
- ret,
- p2o = this.pointAttrToOptions || {},
- strokeOption = p2o.stroke || 'borderColor',
- strokeWidthOption = p2o['stroke-width'] || 'borderWidth',
- fill = (point && point.color) || this.color,
- stroke = point[strokeOption] || options[strokeOption] ||
- this.color || fill, // set to fill when borderColor null
- strokeWidth = point[strokeWidthOption] ||
- options[strokeWidthOption] || this[strokeWidthOption] || 0,
- dashstyle = options.dashStyle,
- zone,
- brightness;
- // Handle zone colors
- if (point && this.zones.length) {
- zone = point.getZone();
- fill = point.options.color || (zone && zone.color) || this.color; // When zones are present, don't use point.color (#4267). Changed order (#6527)
- }
- // Select or hover states
- if (state) {
- stateOptions = merge(
- options.states[state],
- point.options.states && point.options.states[state] || {} // #6401
- );
- brightness = stateOptions.brightness;
- fill = stateOptions.color ||
- (brightness !== undefined && color(fill).brighten(stateOptions.brightness).get()) ||
- fill;
- stroke = stateOptions[strokeOption] || stroke;
- strokeWidth = stateOptions[strokeWidthOption] || strokeWidth;
- dashstyle = stateOptions.dashStyle || dashstyle;
- }
- ret = {
- 'fill': fill,
- 'stroke': stroke,
- 'stroke-width': strokeWidth
- };
- if (options.borderRadius) {
- ret.r = options.borderRadius;
- }
- if (dashstyle) {
- ret.dashstyle = dashstyle;
- }
- return ret;
- },
- /**
- * Draw the columns. For bars, the series.group is rotated, so the same coordinates
- * apply for columns and bars. This method is inherited by scatter series.
- *
- */
- drawPoints: function() {
- var series = this,
- chart = this.chart,
- options = series.options,
- renderer = chart.renderer,
- animationLimit = options.animationLimit || 250,
- shapeArgs;
- // draw the columns
- each(series.points, function(point) {
- var plotY = point.plotY,
- graphic = point.graphic;
- if (isNumber(plotY) && point.y !== null) {
- shapeArgs = point.shapeArgs;
- if (graphic) { // update
- graphic[chart.pointCount < animationLimit ? 'animate' : 'attr'](
- merge(shapeArgs)
- );
- } else {
- point.graphic = graphic = renderer[point.shapeType](shapeArgs)
- .add(point.group || series.group);
- }
- // Presentational
- graphic
- .attr(series.pointAttribs(point, point.selected && 'select'))
- .shadow(options.shadow, null, options.stacking && !options.borderRadius);
- graphic.addClass(point.getClassName(), true);
- } else if (graphic) {
- point.graphic = graphic.destroy(); // #1269
- }
- });
- },
- /**
- * Animate the column heights one by one from zero
- * @param {Boolean} init Whether to initialize the animation or run it
- */
- animate: function(init) {
- var series = this,
- yAxis = this.yAxis,
- options = series.options,
- inverted = this.chart.inverted,
- attr = {},
- translatedThreshold;
- if (svg) { // VML is too slow anyway
- if (init) {
- attr.scaleY = 0.001;
- translatedThreshold = Math.min(yAxis.pos + yAxis.len, Math.max(yAxis.pos, yAxis.toPixels(options.threshold)));
- if (inverted) {
- attr.translateX = translatedThreshold - yAxis.len;
- } else {
- attr.translateY = translatedThreshold;
- }
- series.group.attr(attr);
- } else { // run the animation
- attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos;
- series.group.animate(attr, extend(animObject(series.options.animation), {
- // Do the scale synchronously to ensure smooth updating (#5030)
- step: function(val, fx) {
- series.group.attr({
- scaleY: Math.max(0.001, fx.pos) // #5250
- });
- }
- }));
- // delete this function to allow it only once
- series.animate = null;
- }
- }
- },
- /**
- * Remove this series from the chart
- */
- remove: function() {
- var series = this,
- chart = series.chart;
- // column and bar series affects other series of the same type
- // as they are either stacked or grouped
- if (chart.hasRendered) {
- each(chart.series, function(otherSeries) {
- if (otherSeries.type === series.type) {
- otherSeries.isDirty = true;
- }
- });
- }
- Series.prototype.remove.apply(series, arguments);
- }
- });
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var seriesType = H.seriesType;
- /**
- * The Bar series class
- */
- seriesType('bar', 'column', null, {
- inverted: true
- });
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var Series = H.Series,
- seriesType = H.seriesType;
- /**
- * The scatter series type
- */
- seriesType('scatter', 'line', {
- lineWidth: 0,
- findNearestPointBy: 'xy',
- marker: {
- enabled: true // Overrides auto-enabling in line series (#3647)
- },
- tooltip: {
- headerFormat: '<span style="color:{point.color}">\u25CF</span> ' +
- '<span style="font-size: 0.85em"> {series.name}</span><br/>',
- pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
- }
- // Prototype members
- }, {
- sorted: false,
- requireSorting: false,
- noSharedTooltip: true,
- trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
- takeOrdinalPosition: false, // #2342
- drawGraph: function() {
- if (this.options.lineWidth) {
- Series.prototype.drawGraph.call(this);
- }
- }
- });
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var pick = H.pick,
- relativeLength = H.relativeLength;
- H.CenteredSeriesMixin = {
- /**
- * Get the center of the pie based on the size and center options relative to the
- * plot area. Borrowed by the polar and gauge series types.
- */
- getCenter: function() {
- var options = this.options,
- chart = this.chart,
- slicingRoom = 2 * (options.slicedOffset || 0),
- handleSlicingRoom,
- plotWidth = chart.plotWidth - 2 * slicingRoom,
- plotHeight = chart.plotHeight - 2 * slicingRoom,
- centerOption = options.center,
- positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0],
- smallestSize = Math.min(plotWidth, plotHeight),
- i,
- value;
- for (i = 0; i < 4; ++i) {
- value = positions[i];
- handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value));
- // i == 0: centerX, relative to width
- // i == 1: centerY, relative to height
- // i == 2: size, relative to smallestSize
- // i == 3: innerSize, relative to size
- positions[i] = relativeLength(value, [plotWidth, plotHeight, smallestSize, positions[2]][i]) +
- (handleSlicingRoom ? slicingRoom : 0);
- }
- // innerSize cannot be larger than size (#3632)
- if (positions[3] > positions[2]) {
- positions[3] = positions[2];
- }
- return positions;
- }
- };
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- CenteredSeriesMixin = H.CenteredSeriesMixin,
- defined = H.defined,
- each = H.each,
- extend = H.extend,
- inArray = H.inArray,
- LegendSymbolMixin = H.LegendSymbolMixin,
- noop = H.noop,
- pick = H.pick,
- Point = H.Point,
- Series = H.Series,
- seriesType = H.seriesType,
- seriesTypes = H.seriesTypes,
- setAnimation = H.setAnimation;
- /**
- * The pie series type.
- *
- * @constructor seriesTypes.pie
- * @augments Series
- */
- seriesType('pie', 'line', {
- center: [null, null],
- clip: false,
- colorByPoint: true, // always true for pies
- dataLabels: {
- // align: null,
- // connectorWidth: 1,
- // connectorColor: point.color,
- // connectorPadding: 5,
- distance: 30,
- enabled: true,
- formatter: function() { // #2945
- return this.point.isNull ? undefined : this.point.name;
- },
- // softConnector: true,
- x: 0
- // y: 0
- },
- ignoreHiddenPoint: true,
- //innerSize: 0,
- legendType: 'point',
- marker: null, // point options are specified in the base options
- size: null,
- showInLegend: false,
- slicedOffset: 10,
- stickyTracking: false,
- tooltip: {
- followPointer: true
- },
- borderColor: '#ffffff',
- borderWidth: 1,
- states: {
- hover: {
- brightness: 0.1,
- shadow: false
- }
- }
- }, /** @lends seriesTypes.pie.prototype */ {
- isCartesian: false,
- requireSorting: false,
- directTouch: true,
- noSharedTooltip: true,
- trackerGroups: ['group', 'dataLabelsGroup'],
- axisTypes: [],
- pointAttribs: seriesTypes.column.prototype.pointAttribs,
- /**
- * Animate the pies in
- */
- animate: function(init) {
- var series = this,
- points = series.points,
- startAngleRad = series.startAngleRad;
- if (!init) {
- each(points, function(point) {
- var graphic = point.graphic,
- args = point.shapeArgs;
- if (graphic) {
- // start values
- graphic.attr({
- r: point.startR || (series.center[3] / 2), // animate from inner radius (#779)
- start: startAngleRad,
- end: startAngleRad
- });
- // animate
- graphic.animate({
- r: args.r,
- start: args.start,
- end: args.end
- }, series.options.animation);
- }
- });
- // delete this function to allow it only once
- series.animate = null;
- }
- },
- /**
- * Recompute total chart sum and update percentages of points.
- */
- updateTotals: function() {
- var i,
- total = 0,
- points = this.points,
- len = points.length,
- point,
- ignoreHiddenPoint = this.options.ignoreHiddenPoint;
- // Get the total sum
- for (i = 0; i < len; i++) {
- point = points[i];
- total += (ignoreHiddenPoint && !point.visible) ?
- 0 :
- point.isNull ? 0 : point.y;
- }
- this.total = total;
- // Set each point's properties
- for (i = 0; i < len; i++) {
- point = points[i];
- point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0;
- point.total = total;
- }
- },
- /**
- * Extend the generatePoints method by adding total and percentage properties to each point
- */
- generatePoints: function() {
- Series.prototype.generatePoints.call(this);
- this.updateTotals();
- },
- /**
- * Do translation for pie slices
- */
- translate: function(positions) {
- this.generatePoints();
- var series = this,
- cumulative = 0,
- precision = 1000, // issue #172
- options = series.options,
- slicedOffset = options.slicedOffset,
- connectorOffset = slicedOffset + (options.borderWidth || 0),
- finalConnectorOffset,
- start,
- end,
- angle,
- startAngle = options.startAngle || 0,
- startAngleRad = series.startAngleRad = Math.PI / 180 * (startAngle - 90),
- endAngleRad = series.endAngleRad = Math.PI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90),
- circ = endAngleRad - startAngleRad, //2 * Math.PI,
- points = series.points,
- radiusX, // the x component of the radius vector for a given point
- radiusY,
- labelDistance = options.dataLabels.distance,
- ignoreHiddenPoint = options.ignoreHiddenPoint,
- i,
- len = points.length,
- point;
- // Get positions - either an integer or a percentage string must be given.
- // If positions are passed as a parameter, we're in a recursive loop for adjusting
- // space for data labels.
- if (!positions) {
- series.center = positions = series.getCenter();
- }
- // Utility for getting the x value from a given y, used for anticollision
- // logic in data labels.
- // Added point for using specific points' label distance.
- series.getX = function(y, left, point) {
- angle = Math.asin(Math.min((y - positions[1]) / (positions[2] / 2 + point.labelDistance), 1));
- return positions[0] +
- (left ? -1 : 1) *
- (Math.cos(angle) * (positions[2] / 2 + point.labelDistance));
- };
- // Calculate the geometry for each point
- for (i = 0; i < len; i++) {
- point = points[i];
- // Used for distance calculation for specific point.
- point.labelDistance = pick(
- point.options.dataLabels && point.options.dataLabels.distance,
- labelDistance
- );
- // Saved for later dataLabels distance calculation.
- series.maxLabelDistance = Math.max(series.maxLabelDistance || 0, point.labelDistance);
- // set start and end angle
- start = startAngleRad + (cumulative * circ);
- if (!ignoreHiddenPoint || point.visible) {
- cumulative += point.percentage / 100;
- }
- end = startAngleRad + (cumulative * circ);
- // set the shape
- point.shapeType = 'arc';
- point.shapeArgs = {
- x: positions[0],
- y: positions[1],
- r: positions[2] / 2,
- innerR: positions[3] / 2,
- start: Math.round(start * precision) / precision,
- end: Math.round(end * precision) / precision
- };
- // The angle must stay within -90 and 270 (#2645)
- angle = (end + start) / 2;
- if (angle > 1.5 * Math.PI) {
- angle -= 2 * Math.PI;
- } else if (angle < -Math.PI / 2) {
- angle += 2 * Math.PI;
- }
- // Center for the sliced out slice
- point.slicedTranslation = {
- translateX: Math.round(Math.cos(angle) * slicedOffset),
- translateY: Math.round(Math.sin(angle) * slicedOffset)
- };
- // set the anchor point for tooltips
- radiusX = Math.cos(angle) * positions[2] / 2;
- radiusY = Math.sin(angle) * positions[2] / 2;
- point.tooltipPos = [
- positions[0] + radiusX * 0.7,
- positions[1] + radiusY * 0.7
- ];
- point.half = angle < -Math.PI / 2 || angle > Math.PI / 2 ? 1 : 0;
- point.angle = angle;
- // Set the anchor point for data labels. Use point.labelDistance
- // instead of labelDistance // #1174
- // finalConnectorOffset - not override connectorOffset value.
- finalConnectorOffset = Math.min(connectorOffset, point.labelDistance / 5); // #1678
- point.labelPos = [
- positions[0] + radiusX + Math.cos(angle) * point.labelDistance, // first break of connector
- positions[1] + radiusY + Math.sin(angle) * point.labelDistance, // a/a
- positions[0] + radiusX + Math.cos(angle) * finalConnectorOffset, // second break, right outside pie
- positions[1] + radiusY + Math.sin(angle) * finalConnectorOffset, // a/a
- positions[0] + radiusX, // landing point for connector
- positions[1] + radiusY, // a/a
- point.labelDistance < 0 ? // alignment
- 'center' :
- point.half ? 'right' : 'left', // alignment
- angle // center angle
- ];
- }
- },
- drawGraph: null,
- /**
- * Draw the data points
- */
- drawPoints: function() {
- var series = this,
- chart = series.chart,
- renderer = chart.renderer,
- groupTranslation,
- //center,
- graphic,
- //group,
- pointAttr,
- shapeArgs;
- var shadow = series.options.shadow;
- if (shadow && !series.shadowGroup) {
- series.shadowGroup = renderer.g('shadow')
- .add(series.group);
- }
- // draw the slices
- each(series.points, function(point) {
- if (!point.isNull) {
- graphic = point.graphic;
- shapeArgs = point.shapeArgs;
- // If the point is sliced, use special translation, else use
- // plot area traslation
- groupTranslation = point.getTranslate();
- // Put the shadow behind all points
- var shadowGroup = point.shadowGroup;
- if (shadow && !shadowGroup) {
- shadowGroup = point.shadowGroup = renderer.g('shadow')
- .add(series.shadowGroup);
- }
- if (shadowGroup) {
- shadowGroup.attr(groupTranslation);
- }
- pointAttr = series.pointAttribs(point, point.selected && 'select');
- // Draw the slice
- if (graphic) {
- graphic
- .setRadialReference(series.center)
- .attr(pointAttr)
- .animate(extend(shapeArgs, groupTranslation));
- } else {
- point.graphic = graphic = renderer[point.shapeType](shapeArgs)
- .setRadialReference(series.center)
- .attr(groupTranslation)
- .add(series.group);
- if (!point.visible) {
- graphic.attr({
- visibility: 'hidden'
- });
- }
- graphic
- .attr(pointAttr)
- .attr({
- 'stroke-linejoin': 'round'
- })
- .shadow(shadow, shadowGroup);
- }
- graphic.addClass(point.getClassName());
- }
- });
- },
- searchPoint: noop,
- /**
- * Utility for sorting data labels
- */
- sortByAngle: function(points, sign) {
- points.sort(function(a, b) {
- return a.angle !== undefined && (b.angle - a.angle) * sign;
- });
- },
- /**
- * Use a simple symbol from LegendSymbolMixin
- */
- drawLegendSymbol: LegendSymbolMixin.drawRectangle,
- /**
- * Use the getCenter method from drawLegendSymbol
- */
- getCenter: CenteredSeriesMixin.getCenter,
- /**
- * Pies don't have point marker symbols
- */
- getSymbol: noop
- /**
- * @constructor seriesTypes.pie.prototype.pointClass
- * @extends {Point}
- */
- }, /** @lends seriesTypes.pie.prototype.pointClass.prototype */ {
- /**
- * Initiate the pie slice
- */
- init: function() {
- Point.prototype.init.apply(this, arguments);
- var point = this,
- toggleSlice;
- point.name = pick(point.name, 'Slice');
- // add event listener for select
- toggleSlice = function(e) {
- point.slice(e.type === 'select');
- };
- addEvent(point, 'select', toggleSlice);
- addEvent(point, 'unselect', toggleSlice);
- return point;
- },
- /**
- * Negative points are not valid (#1530, #3623, #5322)
- */
- isValid: function() {
- return H.isNumber(this.y, true) && this.y >= 0;
- },
- /**
- * Toggle the visibility of the pie slice
- * @param {Boolean} vis Whether to show the slice or not. If undefined, the
- * visibility is toggled
- */
- setVisible: function(vis, redraw) {
- var point = this,
- series = point.series,
- chart = series.chart,
- ignoreHiddenPoint = series.options.ignoreHiddenPoint;
- redraw = pick(redraw, ignoreHiddenPoint);
- if (vis !== point.visible) {
- // If called without an argument, toggle visibility
- point.visible = point.options.visible = vis = vis === undefined ? !point.visible : vis;
- series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
- // Show and hide associated elements. This is performed regardless of redraw or not,
- // because chart.redraw only handles full series.
- each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function(key) {
- if (point[key]) {
- point[key][vis ? 'show' : 'hide'](true);
- }
- });
- if (point.legendItem) {
- chart.legend.colorizeItem(point, vis);
- }
- // #4170, hide halo after hiding point
- if (!vis && point.state === 'hover') {
- point.setState('');
- }
- // Handle ignore hidden slices
- if (ignoreHiddenPoint) {
- series.isDirty = true;
- }
- if (redraw) {
- chart.redraw();
- }
- }
- },
- /**
- * Set or toggle whether the slice is cut out from the pie
- * @param {Boolean} sliced When undefined, the slice state is toggled
- * @param {Boolean} redraw Whether to redraw the chart. True by default.
- */
- slice: function(sliced, redraw, animation) {
- var point = this,
- series = point.series,
- chart = series.chart;
- setAnimation(animation, chart);
- // redraw is true by default
- redraw = pick(redraw, true);
- // if called without an argument, toggle
- point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced;
- series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
- point.graphic.animate(this.getTranslate());
- if (point.shadowGroup) {
- point.shadowGroup.animate(this.getTranslate());
- }
- },
- getTranslate: function() {
- return this.sliced ? this.slicedTranslation : {
- translateX: 0,
- translateY: 0
- };
- },
- haloPath: function(size) {
- var shapeArgs = this.shapeArgs;
- return this.sliced || !this.visible ? [] :
- this.series.chart.renderer.symbols.arc(
- shapeArgs.x,
- shapeArgs.y,
- shapeArgs.r + size,
- shapeArgs.r + size, {
- innerR: this.shapeArgs.r,
- start: shapeArgs.start,
- end: shapeArgs.end
- }
- );
- }
- });
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- arrayMax = H.arrayMax,
- defined = H.defined,
- each = H.each,
- extend = H.extend,
- format = H.format,
- map = H.map,
- merge = H.merge,
- noop = H.noop,
- pick = H.pick,
- relativeLength = H.relativeLength,
- Series = H.Series,
- seriesTypes = H.seriesTypes,
- stableSort = H.stableSort;
- /**
- * Generatl distribution algorithm for distributing labels of differing size along a
- * confined length in two dimensions. The algorithm takes an array of objects containing
- * a size, a target and a rank. It will place the labels as close as possible to their
- * targets, skipping the lowest ranked labels if necessary.
- */
- H.distribute = function(boxes, len) {
- var i,
- overlapping = true,
- origBoxes = boxes, // Original array will be altered with added .pos
- restBoxes = [], // The outranked overshoot
- box,
- target,
- total = 0;
- function sortByTarget(a, b) {
- return a.target - b.target;
- }
- // If the total size exceeds the len, remove those boxes with the lowest rank
- i = boxes.length;
- while (i--) {
- total += boxes[i].size;
- }
- // Sort by rank, then slice away overshoot
- if (total > len) {
- stableSort(boxes, function(a, b) {
- return (b.rank || 0) - (a.rank || 0);
- });
- i = 0;
- total = 0;
- while (total <= len) {
- total += boxes[i].size;
- i++;
- }
- restBoxes = boxes.splice(i - 1, boxes.length);
- }
- // Order by target
- stableSort(boxes, sortByTarget);
- // So far we have been mutating the original array. Now
- // create a copy with target arrays
- boxes = map(boxes, function(box) {
- return {
- size: box.size,
- targets: [box.target]
- };
- });
- while (overlapping) {
- // Initial positions: target centered in box
- i = boxes.length;
- while (i--) {
- box = boxes[i];
- // Composite box, average of targets
- target = (Math.min.apply(0, box.targets) + Math.max.apply(0, box.targets)) / 2;
- box.pos = Math.min(Math.max(0, target - box.size / 2), len - box.size);
- }
- // Detect overlap and join boxes
- i = boxes.length;
- overlapping = false;
- while (i--) {
- if (i > 0 && boxes[i - 1].pos + boxes[i - 1].size > boxes[i].pos) { // Overlap
- boxes[i - 1].size += boxes[i].size; // Add this size to the previous box
- boxes[i - 1].targets = boxes[i - 1].targets.concat(boxes[i].targets);
- // Overlapping right, push left
- if (boxes[i - 1].pos + boxes[i - 1].size > len) {
- boxes[i - 1].pos = len - boxes[i - 1].size;
- }
- boxes.splice(i, 1); // Remove this item
- overlapping = true;
- }
- }
- }
- // Now the composite boxes are placed, we need to put the original boxes within them
- i = 0;
- each(boxes, function(box) {
- var posInCompositeBox = 0;
- each(box.targets, function() {
- origBoxes[i].pos = box.pos + posInCompositeBox;
- posInCompositeBox += origBoxes[i].size;
- i++;
- });
- });
- // Add the rest (hidden) boxes and sort by target
- origBoxes.push.apply(origBoxes, restBoxes);
- stableSort(origBoxes, sortByTarget);
- };
- /**
- * Draw the data labels
- */
- Series.prototype.drawDataLabels = function() {
- var series = this,
- seriesOptions = series.options,
- options = seriesOptions.dataLabels,
- points = series.points,
- pointOptions,
- generalOptions,
- hasRendered = series.hasRendered || 0,
- str,
- dataLabelsGroup,
- defer = pick(options.defer, !!seriesOptions.animation),
- renderer = series.chart.renderer;
- if (options.enabled || series._hasPointLabels) {
- // Process default alignment of data labels for columns
- if (series.dlProcessOptions) {
- series.dlProcessOptions(options);
- }
- // Create a separate group for the data labels to avoid rotation
- dataLabelsGroup = series.plotGroup(
- 'dataLabelsGroup',
- 'data-labels',
- defer && !hasRendered ? 'hidden' : 'visible', // #5133
- options.zIndex || 6
- );
- if (defer) {
- dataLabelsGroup.attr({
- opacity: +hasRendered
- }); // #3300
- if (!hasRendered) {
- addEvent(series, 'afterAnimate', function() {
- if (series.visible) { // #2597, #3023, #3024
- dataLabelsGroup.show(true);
- }
- dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({
- opacity: 1
- }, {
- duration: 200
- });
- });
- }
- }
- // Make the labels for each point
- generalOptions = options;
- each(points, function(point) {
- var enabled,
- dataLabel = point.dataLabel,
- labelConfig,
- attr,
- rotation,
- connector = point.connector,
- isNew = !dataLabel,
- style;
- // Determine if each data label is enabled
- // @note dataLabelAttribs (like pointAttribs) would eradicate
- // the need for dlOptions, and simplify the section below.
- pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps
- enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled) && point.y !== null; // #2282, #4641
- if (enabled) {
- // Create individual options structure that can be extended without
- // affecting others
- options = merge(generalOptions, pointOptions);
- labelConfig = point.getLabelConfig();
- str = options.format ?
- format(options.format, labelConfig) :
- options.formatter.call(labelConfig, options);
- style = options.style;
- rotation = options.rotation;
- // Determine the color
- style.color = pick(options.color, style.color, series.color, '#000000');
- // Get automated contrast color
- if (style.color === 'contrast') {
- point.contrastColor = renderer.getContrast(point.color || series.color);
- style.color = options.inside || pick(point.labelDistance, options.distance) < 0 ||
- !!seriesOptions.stacking ? point.contrastColor : '#000000';
- }
- if (seriesOptions.cursor) {
- style.cursor = seriesOptions.cursor;
- }
- attr = {
- //align: align,
- fill: options.backgroundColor,
- stroke: options.borderColor,
- 'stroke-width': options.borderWidth,
- r: options.borderRadius || 0,
- rotation: rotation,
- padding: options.padding,
- zIndex: 1
- };
- // Remove unused attributes (#947)
- H.objectEach(attr, function(val, name) {
- if (val === undefined) {
- delete attr[name];
- }
- });
- }
- // If the point is outside the plot area, destroy it. #678, #820
- if (dataLabel && (!enabled || !defined(str))) {
- point.dataLabel = dataLabel = dataLabel.destroy();
- if (connector) {
- point.connector = connector.destroy();
- }
- // Individual labels are disabled if the are explicitly disabled
- // in the point options, or if they fall outside the plot area.
- } else if (enabled && defined(str)) {
- // create new label
- if (!dataLabel) {
- dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation
- str,
- 0, -9999,
- options.shape,
- null,
- null,
- options.useHTML,
- null,
- 'data-label'
- );
- dataLabel.addClass(
- 'highcharts-data-label-color-' + point.colorIndex +
- ' ' + (options.className || '') +
- (options.useHTML ? 'highcharts-tracker' : '') // #3398
- );
- } else {
- attr.text = str;
- }
- dataLabel.attr(attr);
- // Styles must be applied before add in order to read text bounding box
- dataLabel.css(style).shadow(options.shadow);
- if (!dataLabel.added) {
- dataLabel.add(dataLabelsGroup);
- }
- // Now the data label is created and placed at 0,0, so we need to align it
- series.alignDataLabel(point, dataLabel, options, null, isNew);
- }
- });
- }
- };
- /**
- * Align each individual data label
- */
- Series.prototype.alignDataLabel = function(point, dataLabel, options, alignTo, isNew) {
- var chart = this.chart,
- inverted = chart.inverted,
- plotX = pick(point.plotX, -9999),
- plotY = pick(point.plotY, -9999),
- bBox = dataLabel.getBBox(),
- fontSize,
- baseline,
- rotation = options.rotation,
- normRotation,
- negRotation,
- align = options.align,
- rotCorr, // rotation correction
- // Math.round for rounding errors (#2683), alignTo to allow column labels (#2700)
- visible =
- this.visible &&
- (
- point.series.forceDL ||
- chart.isInsidePlot(plotX, Math.round(plotY), inverted) ||
- (
- alignTo && chart.isInsidePlot(
- plotX,
- inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1,
- inverted
- )
- )
- ),
- alignAttr, // the final position;
- justify = pick(options.overflow, 'justify') === 'justify';
- if (visible) {
- fontSize = options.style.fontSize;
- baseline = chart.renderer.fontMetrics(fontSize, dataLabel).b;
- // The alignment box is a singular point
- alignTo = extend({
- x: inverted ? chart.plotWidth - plotY : plotX,
- y: Math.round(inverted ? chart.plotHeight - plotX : plotY),
- width: 0,
- height: 0
- }, alignTo);
- // Add the text size for alignment calculation
- extend(options, {
- width: bBox.width,
- height: bBox.height
- });
- // Allow a hook for changing alignment in the last moment, then do the alignment
- if (rotation) {
- justify = false; // Not supported for rotated text
- rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723
- alignAttr = {
- x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
- y: alignTo.y + options.y + {
- top: 0,
- middle: 0.5,
- bottom: 1
- }[options.verticalAlign] * alignTo.height
- };
- dataLabel[isNew ? 'attr' : 'animate'](alignAttr)
- .attr({ // #3003
- align: align
- });
- // Compensate for the rotated label sticking out on the sides
- normRotation = (rotation + 720) % 360;
- negRotation = normRotation > 180 && normRotation < 360;
- if (align === 'left') {
- alignAttr.y -= negRotation ? bBox.height : 0;
- } else if (align === 'center') {
- alignAttr.x -= bBox.width / 2;
- alignAttr.y -= bBox.height / 2;
- } else if (align === 'right') {
- alignAttr.x -= bBox.width;
- alignAttr.y -= negRotation ? 0 : bBox.height;
- }
- } else {
- dataLabel.align(options, null, alignTo);
- alignAttr = dataLabel.alignAttr;
- }
- // Handle justify or crop
- if (justify) {
- point.isLabelJustified = this.justifyDataLabel(
- dataLabel,
- options,
- alignAttr,
- bBox,
- alignTo,
- isNew
- );
- // Now check that the data label is within the plot area
- } else if (pick(options.crop, true)) {
- visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
- }
- // When we're using a shape, make it possible with a connector or an arrow pointing to thie point
- if (options.shape && !rotation) {
- dataLabel[isNew ? 'attr' : 'animate']({
- anchorX: inverted ? chart.plotWidth - point.plotY : point.plotX,
- anchorY: inverted ? chart.plotHeight - point.plotX : point.plotY
- });
- }
- }
- // Show or hide based on the final aligned position
- if (!visible) {
- dataLabel.attr({
- y: -9999
- });
- dataLabel.placed = false; // don't animate back in
- }
- };
- /**
- * If data labels fall partly outside the plot area, align them back in, in a way that
- * doesn't hide the point.
- */
- Series.prototype.justifyDataLabel = function(dataLabel, options, alignAttr, bBox, alignTo, isNew) {
- var chart = this.chart,
- align = options.align,
- verticalAlign = options.verticalAlign,
- off,
- justified,
- padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
- // Off left
- off = alignAttr.x + padding;
- if (off < 0) {
- if (align === 'right') {
- options.align = 'left';
- } else {
- options.x = -off;
- }
- justified = true;
- }
- // Off right
- off = alignAttr.x + bBox.width - padding;
- if (off > chart.plotWidth) {
- if (align === 'left') {
- options.align = 'right';
- } else {
- options.x = chart.plotWidth - off;
- }
- justified = true;
- }
- // Off top
- off = alignAttr.y + padding;
- if (off < 0) {
- if (verticalAlign === 'bottom') {
- options.verticalAlign = 'top';
- } else {
- options.y = -off;
- }
- justified = true;
- }
- // Off bottom
- off = alignAttr.y + bBox.height - padding;
- if (off > chart.plotHeight) {
- if (verticalAlign === 'top') {
- options.verticalAlign = 'bottom';
- } else {
- options.y = chart.plotHeight - off;
- }
- justified = true;
- }
- if (justified) {
- dataLabel.placed = !isNew;
- dataLabel.align(options, null, alignTo);
- }
- return justified;
- };
- /**
- * Override the base drawDataLabels method by pie specific functionality
- */
- if (seriesTypes.pie) {
- seriesTypes.pie.prototype.drawDataLabels = function() {
- var series = this,
- data = series.data,
- point,
- chart = series.chart,
- options = series.options.dataLabels,
- connectorPadding = pick(options.connectorPadding, 10),
- connectorWidth = pick(options.connectorWidth, 1),
- plotWidth = chart.plotWidth,
- plotHeight = chart.plotHeight,
- connector,
- seriesCenter = series.center,
- radius = seriesCenter[2] / 2,
- centerY = seriesCenter[1],
- dataLabel,
- dataLabelWidth,
- labelPos,
- labelHeight,
- halves = [ // divide the points into right and left halves for anti collision
- [], // right
- [] // left
- ],
- x,
- y,
- visibility,
- j,
- overflow = [0, 0, 0, 0]; // top, right, bottom, left
- // get out if not enabled
- if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
- return;
- }
- // Reset all labels that have been shortened
- each(data, function(point) {
- if (point.dataLabel && point.visible && point.dataLabel.shortened) {
- point.dataLabel
- .attr({
- width: 'auto'
- }).css({
- width: 'auto',
- textOverflow: 'clip'
- });
- point.dataLabel.shortened = false;
- }
- });
- // run parent method
- Series.prototype.drawDataLabels.apply(series);
- each(data, function(point) {
- if (point.dataLabel && point.visible) { // #407, #2510
- // Arrange points for detection collision
- halves[point.half].push(point);
- // Reset positions (#4905)
- point.dataLabel._pos = null;
- }
- });
- /* Loop over the points in each half, starting from the top and bottom
- * of the pie to detect overlapping labels.
- */
- each(halves, function(points, i) {
- var top,
- bottom,
- length = points.length,
- positions = [],
- naturalY,
- sideOverflow,
- positionsIndex, // Point index in positions array.
- size;
- if (!length) {
- return;
- }
- // Sort by angle
- series.sortByAngle(points, i - 0.5);
- // Only do anti-collision when we have dataLabels outside the pie
- // and have connectors. (#856)
- if (series.maxLabelDistance > 0) {
- top = Math.max(
- 0,
- centerY - radius - series.maxLabelDistance
- );
- bottom = Math.min(
- centerY + radius + series.maxLabelDistance,
- chart.plotHeight
- );
- each(points, function(point) {
- // check if specific points' label is outside the pie
- if (point.labelDistance > 0 && point.dataLabel) {
- // point.top depends on point.labelDistance value
- // Used for calculation of y value in getX method
- point.top = Math.max(
- 0,
- centerY - radius - point.labelDistance
- );
- point.bottom = Math.min(
- centerY + radius + point.labelDistance,
- chart.plotHeight
- );
- size = point.dataLabel.getBBox().height || 21;
- // point.positionsIndex is needed for getting index of
- // parameter related to specific point inside positions
- // array - not every point is in positions array.
- point.positionsIndex = positions.push({
- target: point.labelPos[1] - point.top + size / 2,
- size: size,
- rank: point.y
- }) - 1;
- }
- });
- H.distribute(positions, bottom + size - top);
- }
- // Now the used slots are sorted, fill them up sequentially
- for (j = 0; j < length; j++) {
- point = points[j];
- positionsIndex = point.positionsIndex;
- labelPos = point.labelPos;
- dataLabel = point.dataLabel;
- visibility = point.visible === false ? 'hidden' : 'inherit';
- naturalY = labelPos[1];
- if (positions && defined(positions[positionsIndex])) {
- if (positions[positionsIndex].pos === undefined) {
- visibility = 'hidden';
- } else {
- labelHeight = positions[positionsIndex].size;
- y = point.top + positions[positionsIndex].pos;
- }
- } else {
- y = naturalY;
- }
- // It is needed to delete point.positionIndex for
- // dynamically added points etc.
- delete point.positionIndex;
- // get the x - use the natural x position for labels near the
- // top and bottom, to prevent the top and botton slice connectors
- // from touching each other on either side
- if (options.justify) {
- x = seriesCenter[0] + (i ? -1 : 1) * (radius + point.labelDistance);
- } else {
- x = series.getX(y < point.top + 2 || y > point.bottom - 2 ? naturalY : y, i, point);
- }
- // Record the placement and visibility
- dataLabel._attr = {
- visibility: visibility,
- align: labelPos[6]
- };
- dataLabel._pos = {
- x: x + options.x +
- ({
- left: connectorPadding,
- right: -connectorPadding
- }[labelPos[6]] || 0),
- y: y + options.y - 10 // 10 is for the baseline (label vs text)
- };
- labelPos.x = x;
- labelPos.y = y;
- // Detect overflowing data labels
- dataLabelWidth = dataLabel.getBBox().width;
- sideOverflow = null;
- // Overflow left
- if (x - dataLabelWidth < connectorPadding) {
- sideOverflow = Math.round(
- dataLabelWidth - x + connectorPadding
- );
- overflow[3] = Math.max(sideOverflow, overflow[3]);
- // Overflow right
- } else if (x + dataLabelWidth > plotWidth - connectorPadding) {
- sideOverflow = Math.round(
- x + dataLabelWidth - plotWidth + connectorPadding
- );
- overflow[1] = Math.max(sideOverflow, overflow[1]);
- }
- // Overflow top
- if (y - labelHeight / 2 < 0) {
- overflow[0] = Math.max(
- Math.round(-y + labelHeight / 2),
- overflow[0]
- );
- // Overflow left
- } else if (y + labelHeight / 2 > plotHeight) {
- overflow[2] = Math.max(
- Math.round(y + labelHeight / 2 - plotHeight),
- overflow[2]
- );
- }
- dataLabel.sideOverflow = sideOverflow;
- } // for each point
- }); // for each half
- // Do not apply the final placement and draw the connectors until we have verified
- // that labels are not spilling over.
- if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) {
- // Place the labels in the final position
- this.placeDataLabels();
- // Draw the connectors
- if (connectorWidth) {
- each(this.points, function(point) {
- var isNew;
- connector = point.connector;
- dataLabel = point.dataLabel;
- if (
- dataLabel &&
- dataLabel._pos &&
- point.visible &&
- point.labelDistance > 0
- ) {
- visibility = dataLabel._attr.visibility;
- isNew = !connector;
- if (isNew) {
- point.connector = connector = chart.renderer.path()
- .addClass('highcharts-data-label-connector highcharts-color-' + point.colorIndex)
- .add(series.dataLabelsGroup);
- connector.attr({
- 'stroke-width': connectorWidth,
- 'stroke': options.connectorColor || point.color || '#666666'
- });
- }
- connector[isNew ? 'attr' : 'animate']({
- d: series.connectorPath(point.labelPos)
- });
- connector.attr('visibility', visibility);
- } else if (connector) {
- point.connector = connector.destroy();
- }
- });
- }
- }
- };
- /**
- * Extendable method for getting the path of the connector between the data label
- * and the pie slice.
- */
- seriesTypes.pie.prototype.connectorPath = function(labelPos) {
- var x = labelPos.x,
- y = labelPos.y;
- return pick(this.options.dataLabels.softConnector, true) ? [
- 'M',
- x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
- 'C',
- x, y, // first break, next to the label
- 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
- labelPos[2], labelPos[3], // second break
- 'L',
- labelPos[4], labelPos[5] // base
- ] : [
- 'M',
- x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
- 'L',
- labelPos[2], labelPos[3], // second break
- 'L',
- labelPos[4], labelPos[5] // base
- ];
- };
- /**
- * Perform the final placement of the data labels after we have verified that they
- * fall within the plot area.
- */
- seriesTypes.pie.prototype.placeDataLabels = function() {
- each(this.points, function(point) {
- var dataLabel = point.dataLabel,
- _pos;
- if (dataLabel && point.visible) {
- _pos = dataLabel._pos;
- if (_pos) {
- // Shorten data labels with ellipsis if they still overflow
- // after the pie has reached minSize (#223).
- if (dataLabel.sideOverflow) {
- dataLabel._attr.width =
- dataLabel.getBBox().width - dataLabel.sideOverflow;
- dataLabel.css({
- width: dataLabel._attr.width + 'px',
- textOverflow: 'ellipsis'
- });
- dataLabel.shortened = true;
- }
- dataLabel.attr(dataLabel._attr);
- dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
- dataLabel.moved = true;
- } else if (dataLabel) {
- dataLabel.attr({
- y: -9999
- });
- }
- }
- }, this);
- };
- seriesTypes.pie.prototype.alignDataLabel = noop;
- /**
- * Verify whether the data labels are allowed to draw, or we should run more translation and data
- * label positioning to keep them inside the plot area. Returns true when data labels are ready
- * to draw.
- */
- seriesTypes.pie.prototype.verifyDataLabelOverflow = function(overflow) {
- var center = this.center,
- options = this.options,
- centerOption = options.center,
- minSize = options.minSize || 80,
- newSize = minSize,
- // If a size is set, return true and don't try to shrink the pie
- // to fit the labels.
- ret = options.size !== null;
- if (!ret) {
- // Handle horizontal size and center
- if (centerOption[0] !== null) { // Fixed center
- newSize = Math.max(center[2] -
- Math.max(overflow[1], overflow[3]), minSize);
- } else { // Auto center
- newSize = Math.max(
- // horizontal overflow
- center[2] - overflow[1] - overflow[3],
- minSize
- );
- // horizontal center
- center[0] += (overflow[3] - overflow[1]) / 2;
- }
- // Handle vertical size and center
- if (centerOption[1] !== null) { // Fixed center
- newSize = Math.max(Math.min(newSize, center[2] -
- Math.max(overflow[0], overflow[2])), minSize);
- } else { // Auto center
- newSize = Math.max(
- Math.min(
- newSize,
- // vertical overflow
- center[2] - overflow[0] - overflow[2]
- ),
- minSize
- );
- // vertical center
- center[1] += (overflow[0] - overflow[2]) / 2;
- }
- // If the size must be decreased, we need to run translate and
- // drawDataLabels again
- if (newSize < center[2]) {
- center[2] = newSize;
- center[3] = Math.min( // #3632
- relativeLength(options.innerSize || 0, newSize),
- newSize
- );
- this.translate(center);
- if (this.drawDataLabels) {
- this.drawDataLabels();
- }
- // Else, return true to indicate that the pie and its labels is
- // within the plot area
- } else {
- ret = true;
- }
- }
- return ret;
- };
- }
- if (seriesTypes.column) {
- /**
- * Override the basic data label alignment by adjusting for the position of the column
- */
- seriesTypes.column.prototype.alignDataLabel = function(point, dataLabel, options, alignTo, isNew) {
- var inverted = this.chart.inverted,
- series = point.series,
- dlBox = point.dlBox || point.shapeArgs, // data label box for alignment
- below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series
- inside = pick(options.inside, !!this.options.stacking), // draw it inside the box?
- overshoot;
- // Align to the column itself, or the top of it
- if (dlBox) { // Area range uses this method but not alignTo
- alignTo = merge(dlBox);
- if (alignTo.y < 0) {
- alignTo.height += alignTo.y;
- alignTo.y = 0;
- }
- overshoot = alignTo.y + alignTo.height - series.yAxis.len;
- if (overshoot > 0) {
- alignTo.height -= overshoot;
- }
- if (inverted) {
- alignTo = {
- x: series.yAxis.len - alignTo.y - alignTo.height,
- y: series.xAxis.len - alignTo.x - alignTo.width,
- width: alignTo.height,
- height: alignTo.width
- };
- }
- // Compute the alignment box
- if (!inside) {
- if (inverted) {
- alignTo.x += below ? 0 : alignTo.width;
- alignTo.width = 0;
- } else {
- alignTo.y += below ? alignTo.height : 0;
- alignTo.height = 0;
- }
- }
- }
- // When alignment is undefined (typically columns and bars), display the individual
- // point below or above the point depending on the threshold
- options.align = pick(
- options.align, !inverted || inside ? 'center' : below ? 'right' : 'left'
- );
- options.verticalAlign = pick(
- options.verticalAlign,
- inverted || inside ? 'middle' : below ? 'top' : 'bottom'
- );
- // Call the parent method
- Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
- // If label was justified and we have contrast, set it:
- if (point.isLabelJustified && point.contrastColor) {
- point.dataLabel.css({
- color: point.contrastColor
- });
- }
- };
- }
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2009-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- /**
- * Highcharts module to hide overlapping data labels. This module is included in
- * Highcharts.
- */
- var Chart = H.Chart,
- each = H.each,
- pick = H.pick,
- addEvent = H.addEvent;
- // Collect potensial overlapping data labels. Stack labels probably don't need
- // to be considered because they are usually accompanied by data labels that lie
- // inside the columns.
- Chart.prototype.callbacks.push(function(chart) {
- function collectAndHide() {
- var labels = [];
- each(chart.series || [], function(series) {
- var dlOptions = series.options.dataLabels,
- // Range series have two collections
- collections = series.dataLabelCollections || ['dataLabel'];
- if (
- (dlOptions.enabled || series._hasPointLabels) &&
- !dlOptions.allowOverlap &&
- series.visible
- ) { // #3866
- each(collections, function(coll) {
- each(series.points, function(point) {
- if (point[coll]) {
- point[coll].labelrank = pick(
- point.labelrank,
- point.shapeArgs && point.shapeArgs.height
- ); // #4118
- labels.push(point[coll]);
- }
- });
- });
- }
- });
- chart.hideOverlappingLabels(labels);
- }
- // Do it now ...
- collectAndHide();
- // ... and after each chart redraw
- addEvent(chart, 'redraw', collectAndHide);
- });
- /**
- * Hide overlapping labels. Labels are moved and faded in and out on zoom to
- * provide a smooth visual imression.
- */
- Chart.prototype.hideOverlappingLabels = function(labels) {
- var len = labels.length,
- label,
- i,
- j,
- label1,
- label2,
- isIntersecting,
- pos1,
- pos2,
- parent1,
- parent2,
- padding,
- intersectRect = function(x1, y1, w1, h1, x2, y2, w2, h2) {
- return !(
- x2 > x1 + w1 ||
- x2 + w2 < x1 ||
- y2 > y1 + h1 ||
- y2 + h2 < y1
- );
- };
- // Mark with initial opacity
- for (i = 0; i < len; i++) {
- label = labels[i];
- if (label) {
- label.oldOpacity = label.opacity;
- label.newOpacity = 1;
- }
- }
- // Prevent a situation in a gradually rising slope, that each label will
- // hide the previous one because the previous one always has lower rank.
- labels.sort(function(a, b) {
- return (b.labelrank || 0) - (a.labelrank || 0);
- });
- // Detect overlapping labels
- for (i = 0; i < len; i++) {
- label1 = labels[i];
- for (j = i + 1; j < len; ++j) {
- label2 = labels[j];
- if (
- label1 && label2 &&
- label1 !== label2 && // #6465, polar chart with connectEnds
- label1.placed && label2.placed &&
- label1.newOpacity !== 0 && label2.newOpacity !== 0
- ) {
- pos1 = label1.alignAttr;
- pos2 = label2.alignAttr;
- // Different panes have different positions
- parent1 = label1.parentGroup;
- parent2 = label2.parentGroup;
- // Substract the padding if no background or border (#4333)
- padding = 2 * (label1.box ? 0 : label1.padding);
- isIntersecting = intersectRect(
- pos1.x + parent1.translateX,
- pos1.y + parent1.translateY,
- label1.width - padding,
- label1.height - padding,
- pos2.x + parent2.translateX,
- pos2.y + parent2.translateY,
- label2.width - padding,
- label2.height - padding
- );
- if (isIntersecting) {
- (label1.labelrank < label2.labelrank ? label1 : label2)
- .newOpacity = 0;
- }
- }
- }
- }
- // Hide or show
- each(labels, function(label) {
- var complete,
- newOpacity;
- if (label) {
- newOpacity = label.newOpacity;
- if (label.oldOpacity !== newOpacity && label.placed) {
- // Make sure the label is completely hidden to avoid catching
- // clicks (#4362)
- if (newOpacity) {
- label.show(true);
- } else {
- complete = function() {
- label.hide();
- };
- }
- // Animate or set the opacity
- label.alignAttr.opacity = newOpacity;
- label[label.isOld ? 'animate' : 'attr'](
- label.alignAttr,
- null,
- complete
- );
- }
- label.isOld = true;
- }
- });
- };
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- Chart = H.Chart,
- createElement = H.createElement,
- css = H.css,
- defaultOptions = H.defaultOptions,
- defaultPlotOptions = H.defaultPlotOptions,
- each = H.each,
- extend = H.extend,
- fireEvent = H.fireEvent,
- hasTouch = H.hasTouch,
- inArray = H.inArray,
- isObject = H.isObject,
- Legend = H.Legend,
- merge = H.merge,
- pick = H.pick,
- Point = H.Point,
- Series = H.Series,
- seriesTypes = H.seriesTypes,
- svg = H.svg,
- TrackerMixin;
- /**
- * TrackerMixin for points and graphs.
- *
- * @mixin
- */
- TrackerMixin = H.TrackerMixin = {
- /**
- * Draw the tracker for a point.
- */
- drawTrackerPoint: function() {
- var series = this,
- chart = series.chart,
- pointer = chart.pointer,
- onMouseOver = function(e) {
- var point = pointer.getPointFromEvent(e);
- // undefined on graph in scatterchart
- if (point !== undefined) {
- pointer.isDirectTouch = true;
- point.onMouseOver(e);
- }
- };
- // Add reference to the point
- each(series.points, function(point) {
- if (point.graphic) {
- point.graphic.element.point = point;
- }
- if (point.dataLabel) {
- if (point.dataLabel.div) {
- point.dataLabel.div.point = point;
- } else {
- point.dataLabel.element.point = point;
- }
- }
- });
- // Add the event listeners, we need to do this only once
- if (!series._hasTracking) {
- each(series.trackerGroups, function(key) {
- if (series[key]) { // we don't always have dataLabelsGroup
- series[key]
- .addClass('highcharts-tracker')
- .on('mouseover', onMouseOver)
- .on('mouseout', function(e) {
- pointer.onTrackerMouseOut(e);
- });
- if (hasTouch) {
- series[key].on('touchstart', onMouseOver);
- }
- if (series.options.cursor) {
- series[key]
- .css(css)
- .css({
- cursor: series.options.cursor
- });
- }
- }
- });
- series._hasTracking = true;
- }
- },
- /**
- * Draw the tracker object that sits above all data labels and markers to
- * track mouse events on the graph or points. For the line type charts
- * the tracker uses the same graphPath, but with a greater stroke width
- * for better control.
- */
- drawTrackerGraph: function() {
- var series = this,
- options = series.options,
- trackByArea = options.trackByArea,
- trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
- trackerPathLength = trackerPath.length,
- chart = series.chart,
- pointer = chart.pointer,
- renderer = chart.renderer,
- snap = chart.options.tooltip.snap,
- tracker = series.tracker,
- i,
- onMouseOver = function() {
- if (chart.hoverSeries !== series) {
- series.onMouseOver();
- }
- },
- /*
- * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
- * IE6: 0.002
- * IE7: 0.002
- * IE8: 0.002
- * IE9: 0.00000000001 (unlimited)
- * IE10: 0.0001 (exporting only)
- * FF: 0.00000000001 (unlimited)
- * Chrome: 0.000001
- * Safari: 0.000001
- * Opera: 0.00000000001 (unlimited)
- */
- TRACKER_FILL = 'rgba(192,192,192,' + (svg ? 0.0001 : 0.002) + ')';
- // Extend end points. A better way would be to use round linecaps,
- // but those are not clickable in VML.
- if (trackerPathLength && !trackByArea) {
- i = trackerPathLength + 1;
- while (i--) {
- if (trackerPath[i] === 'M') { // extend left side
- trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], 'L');
- }
- if ((i && trackerPath[i] === 'M') || i === trackerPathLength) { // extend right side
- trackerPath.splice(i, 0, 'L', trackerPath[i - 2] + snap, trackerPath[i - 1]);
- }
- }
- }
- // handle single points
- /*for (i = 0; i < singlePoints.length; i++) {
- singlePoint = singlePoints[i];
- trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
- L, singlePoint.plotX + snap, singlePoint.plotY);
- }*/
- // draw the tracker
- if (tracker) {
- tracker.attr({
- d: trackerPath
- });
- } else if (series.graph) { // create
- series.tracker = renderer.path(trackerPath)
- .attr({
- 'stroke-linejoin': 'round', // #1225
- visibility: series.visible ? 'visible' : 'hidden',
- stroke: TRACKER_FILL,
- fill: trackByArea ? TRACKER_FILL : 'none',
- 'stroke-width': series.graph.strokeWidth() + (trackByArea ? 0 : 2 * snap),
- zIndex: 2
- })
- .add(series.group);
- // The tracker is added to the series group, which is clipped, but is covered
- // by the marker group. So the marker group also needs to capture events.
- each([series.tracker, series.markerGroup], function(tracker) {
- tracker.addClass('highcharts-tracker')
- .on('mouseover', onMouseOver)
- .on('mouseout', function(e) {
- pointer.onTrackerMouseOut(e);
- });
- if (options.cursor) {
- tracker.css({
- cursor: options.cursor
- });
- }
- if (hasTouch) {
- tracker.on('touchstart', onMouseOver);
- }
- });
- }
- }
- };
- /* End TrackerMixin */
- /**
- * Add tracking event listener to the series group, so the point graphics
- * themselves act as trackers
- */
- if (seriesTypes.column) {
- seriesTypes.column.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
- }
- if (seriesTypes.pie) {
- seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
- }
- if (seriesTypes.scatter) {
- seriesTypes.scatter.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
- }
- /*
- * Extend Legend for item events
- */
- extend(Legend.prototype, {
- setItemEvents: function(item, legendItem, useHTML) {
- var legend = this,
- boxWrapper = legend.chart.renderer.boxWrapper,
- activeClass = 'highcharts-legend-' + (item.series ? 'point' : 'series') + '-active';
- // Set the events on the item group, or in case of useHTML, the item itself (#1249)
- (useHTML ? legendItem : item.legendGroup).on('mouseover', function() {
- item.setState('hover');
- // A CSS class to dim or hide other than the hovered series
- boxWrapper.addClass(activeClass);
- legendItem.css(legend.options.itemHoverStyle);
- })
- .on('mouseout', function() {
- legendItem.css(merge(item.visible ? legend.itemStyle : legend.itemHiddenStyle));
- // A CSS class to dim or hide other than the hovered series
- boxWrapper.removeClass(activeClass);
- item.setState();
- })
- .on('click', function(event) {
- var strLegendItemClick = 'legendItemClick',
- fnLegendItemClick = function() {
- if (item.setVisible) {
- item.setVisible();
- }
- };
- // Pass over the click/touch event. #4.
- event = {
- browserEvent: event
- };
- // click the name or symbol
- if (item.firePointEvent) { // point
- item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
- } else {
- fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
- }
- });
- },
- createCheckboxForItem: function(item) {
- var legend = this;
- item.checkbox = createElement('input', {
- type: 'checkbox',
- checked: item.selected,
- defaultChecked: item.selected // required by IE7
- }, legend.options.itemCheckboxStyle, legend.chart.container);
- addEvent(item.checkbox, 'click', function(event) {
- var target = event.target;
- fireEvent(
- item.series || item,
- 'checkboxClick', { // #3712
- checked: target.checked,
- item: item
- },
- function() {
- item.select();
- }
- );
- });
- }
- });
- // Add pointer cursor to legend itemstyle in defaultOptions
- defaultOptions.legend.itemStyle.cursor = 'pointer';
- /*
- * Extend the Chart object with interaction
- */
- extend(Chart.prototype, /** @lends Chart.prototype */ {
- /**
- * Display the zoom button
- */
- showResetZoom: function() {
- var chart = this,
- lang = defaultOptions.lang,
- btnOptions = chart.options.chart.resetZoomButton,
- theme = btnOptions.theme,
- states = theme.states,
- alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
- function zoomOut() {
- chart.zoomOut();
- }
- this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, zoomOut, theme, states && states.hover)
- .attr({
- align: btnOptions.position.align,
- title: lang.resetZoomTitle
- })
- .addClass('highcharts-reset-zoom')
- .add()
- .align(btnOptions.position, false, alignTo);
- },
- /**
- * Zoom out to 1:1
- */
- zoomOut: function() {
- var chart = this;
- fireEvent(chart, 'selection', {
- resetSelection: true
- }, function() {
- chart.zoom();
- });
- },
- /**
- * Zoom into a given portion of the chart given by axis coordinates
- * @param {Object} event
- */
- zoom: function(event) {
- var chart = this,
- hasZoomed,
- pointer = chart.pointer,
- displayButton = false,
- resetZoomButton;
- // If zoom is called with no arguments, reset the axes
- if (!event || event.resetSelection) {
- each(chart.axes, function(axis) {
- hasZoomed = axis.zoom();
- });
- } else { // else, zoom in on all axes
- each(event.xAxis.concat(event.yAxis), function(axisData) {
- var axis = axisData.axis,
- isXAxis = axis.isXAxis;
- // don't zoom more than minRange
- if (pointer[isXAxis ? 'zoomX' : 'zoomY']) {
- hasZoomed = axis.zoom(axisData.min, axisData.max);
- if (axis.displayBtn) {
- displayButton = true;
- }
- }
- });
- }
- // Show or hide the Reset zoom button
- resetZoomButton = chart.resetZoomButton;
- if (displayButton && !resetZoomButton) {
- chart.showResetZoom();
- } else if (!displayButton && isObject(resetZoomButton)) {
- chart.resetZoomButton = resetZoomButton.destroy();
- }
- // Redraw
- if (hasZoomed) {
- chart.redraw(
- pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
- );
- }
- },
- /**
- * Pan the chart by dragging the mouse across the pane. This function is called
- * on mouse move, and the distance to pan is computed from chartX compared to
- * the first chartX position in the dragging operation.
- */
- pan: function(e, panning) {
- var chart = this,
- hoverPoints = chart.hoverPoints,
- doRedraw;
- // remove active points for shared tooltip
- if (hoverPoints) {
- each(hoverPoints, function(point) {
- point.setState();
- });
- }
- each(panning === 'xy' ? [1, 0] : [1], function(isX) { // xy is used in maps
- var axis = chart[isX ? 'xAxis' : 'yAxis'][0],
- horiz = axis.horiz,
- mousePos = e[horiz ? 'chartX' : 'chartY'],
- mouseDown = horiz ? 'mouseDownX' : 'mouseDownY',
- startPos = chart[mouseDown],
- halfPointRange = (axis.pointRange || 0) / 2,
- extremes = axis.getExtremes(),
- panMin = axis.toValue(startPos - mousePos, true) +
- halfPointRange,
- panMax = axis.toValue(startPos + axis.len - mousePos, true) -
- halfPointRange,
- flipped = panMax < panMin,
- newMin = flipped ? panMax : panMin,
- newMax = flipped ? panMin : panMax,
- paddedMin = Math.min(
- extremes.dataMin,
- axis.toValue(
- axis.toPixels(extremes.min) - axis.minPixelPadding
- )
- ),
- paddedMax = Math.max(
- extremes.dataMax,
- axis.toValue(
- axis.toPixels(extremes.max) + axis.minPixelPadding
- )
- ),
- spill;
- // If the new range spills over, either to the min or max, adjust
- // the new range.
- spill = paddedMin - newMin;
- if (spill > 0) {
- newMax += spill;
- newMin = paddedMin;
- }
- spill = newMax - paddedMax;
- if (spill > 0) {
- newMax = paddedMax;
- newMin -= spill;
- }
- // Set new extremes if they are actually new
- if (axis.series.length && newMin !== extremes.min && newMax !== extremes.max) {
- axis.setExtremes(
- newMin,
- newMax,
- false,
- false, {
- trigger: 'pan'
- }
- );
- doRedraw = true;
- }
- chart[mouseDown] = mousePos; // set new reference for next run
- });
- if (doRedraw) {
- chart.redraw(false);
- }
- css(chart.container, {
- cursor: 'move'
- });
- }
- });
- /*
- * Extend the Point object with interaction
- */
- extend(Point.prototype, /** @lends Highcharts.Point.prototype */ {
- /**
- * Toggle the selection status of a point.
- * @param {Boolean} [selected]
- * When `true`, the point is selected. When `false`, the point is
- * unselected. When `null` or `undefined`, the selection state is
- * toggled.
- * @param {Boolean} [accumulate=false]
- * When `true`, the selection is added to other selected points.
- * When `false`, other selected points are deselected. Internally in
- * Highcharts, when {@link http://api.highcharts.com/highcharts/plotOptions.series.allowPointSelect|allowPointSelect}
- * is `true`, selected points are accumulated on Control, Shift or
- * Cmd clicking the point.
- *
- * @see Highcharts.Chart#getSelectedPoints
- *
- * @sample highcharts/members/point-select/
- * Select a point from a button
- * @sample highcharts/chart/events-selection-points/
- * Select a range of points through a drag selection
- * @sample maps/series/data-id/
- * Select a point in Highmaps
- */
- select: function(selected, accumulate) {
- var point = this,
- series = point.series,
- chart = series.chart;
- selected = pick(selected, !point.selected);
- // fire the event with the default handler
- point.firePointEvent(selected ? 'select' : 'unselect', {
- accumulate: accumulate
- }, function() {
- /**
- * Whether the point is selected or not.
- * @see Highcharts.Point#select
- * @memberof Highcharts.Point
- * @name selected
- * @type {Boolean}
- */
- point.selected = point.options.selected = selected;
- series.options.data[inArray(point, series.data)] = point.options;
- point.setState(selected && 'select');
- // unselect all other points unless Ctrl or Cmd + click
- if (!accumulate) {
- each(chart.getSelectedPoints(), function(loopPoint) {
- if (loopPoint.selected && loopPoint !== point) {
- loopPoint.selected = loopPoint.options.selected = false;
- series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
- loopPoint.setState('');
- loopPoint.firePointEvent('unselect');
- }
- });
- }
- });
- },
- /**
- * Runs on mouse over the point
- *
- * @param {Object} e The event arguments
- */
- onMouseOver: function(e) {
- var point = this,
- series = point.series,
- chart = series.chart,
- pointer = chart.pointer;
- e = e ?
- pointer.normalize(e) :
- // In cases where onMouseOver is called directly without an event
- pointer.getChartCoordinatesFromPoint(point, chart.inverted);
- pointer.runPointActions(e, point);
- },
- /**
- * Runs on mouse out from the point
- */
- onMouseOut: function() {
- var point = this,
- chart = point.series.chart;
- point.firePointEvent('mouseOut');
- each(chart.hoverPoints || [], function(p) {
- p.setState();
- });
- chart.hoverPoints = chart.hoverPoint = null;
- },
- /**
- * Import events from the series' and point's options. Only do it on
- * demand, to save processing time on hovering.
- */
- importEvents: function() {
- if (!this.hasImportedEvents) {
- var point = this,
- options = merge(point.series.options.point, point.options),
- events = options.events;
- point.events = events;
- H.objectEach(events, function(event, eventType) {
- addEvent(point, eventType, event);
- });
- this.hasImportedEvents = true;
- }
- },
- /**
- * Set the point's state
- * @param {String} state
- */
- setState: function(state, move) {
- var point = this,
- plotX = Math.floor(point.plotX), // #4586
- plotY = point.plotY,
- series = point.series,
- stateOptions = series.options.states[state] || {},
- markerOptions = defaultPlotOptions[series.type].marker &&
- series.options.marker,
- normalDisabled = markerOptions && markerOptions.enabled === false,
- markerStateOptions = (markerOptions && markerOptions.states &&
- markerOptions.states[state]) || {},
- stateDisabled = markerStateOptions.enabled === false,
- stateMarkerGraphic = series.stateMarkerGraphic,
- pointMarker = point.marker || {},
- chart = series.chart,
- halo = series.halo,
- haloOptions,
- markerAttribs,
- hasMarkers = markerOptions && series.markerAttribs,
- newSymbol;
- state = state || ''; // empty string
- if (
- // already has this state
- (state === point.state && !move) ||
- // selected points don't respond to hover
- (point.selected && state !== 'select') ||
- // series' state options is disabled
- (stateOptions.enabled === false) ||
- // general point marker's state options is disabled
- (state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) ||
- // individual point marker's state options is disabled
- (state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
- ) {
- return;
- }
- if (hasMarkers) {
- markerAttribs = series.markerAttribs(point, state);
- }
- // Apply hover styles to the existing point
- if (point.graphic) {
- if (point.state) {
- point.graphic.removeClass('highcharts-point-' + point.state);
- }
- if (state) {
- point.graphic.addClass('highcharts-point-' + state);
- }
- /*attribs = radius ? { // new symbol attributes (#507, #612)
- x: plotX - radius,
- y: plotY - radius,
- width: 2 * radius,
- height: 2 * radius
- } : {};*/
- //attribs = merge(series.pointAttribs(point, state), attribs);
- point.graphic.attr(series.pointAttribs(point, state));
- if (markerAttribs) {
- point.graphic.animate(
- markerAttribs,
- pick(
- chart.options.chart.animation, // Turn off globally
- markerStateOptions.animation,
- markerOptions.animation
- )
- );
- }
- // Zooming in from a range with no markers to a range with markers
- if (stateMarkerGraphic) {
- stateMarkerGraphic.hide();
- }
- } else {
- // if a graphic is not applied to each point in the normal state, create a shared
- // graphic for the hover state
- if (state && markerStateOptions) {
- newSymbol = pointMarker.symbol || series.symbol;
- // If the point has another symbol than the previous one, throw away the
- // state marker graphic and force a new one (#1459)
- if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
- stateMarkerGraphic = stateMarkerGraphic.destroy();
- }
- // Add a new state marker graphic
- if (!stateMarkerGraphic) {
- if (newSymbol) {
- series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
- newSymbol,
- markerAttribs.x,
- markerAttribs.y,
- markerAttribs.width,
- markerAttribs.height
- )
- .add(series.markerGroup);
- stateMarkerGraphic.currentSymbol = newSymbol;
- }
- // Move the existing graphic
- } else {
- stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
- x: markerAttribs.x,
- y: markerAttribs.y
- });
- }
- if (stateMarkerGraphic) {
- stateMarkerGraphic.attr(series.pointAttribs(point, state));
- }
- }
- if (stateMarkerGraphic) {
- stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
- stateMarkerGraphic.element.point = point; // #4310
- }
- }
- // Show me your halo
- haloOptions = stateOptions.halo;
- if (haloOptions && haloOptions.size) {
- if (!halo) {
- series.halo = halo = chart.renderer.path()
- // #5818, #5903, #6705
- .add((point.graphic || stateMarkerGraphic).parentGroup);
- }
- halo[move ? 'animate' : 'attr']({
- d: point.haloPath(haloOptions.size)
- });
- halo.attr({
- 'class': 'highcharts-halo highcharts-color-' +
- pick(point.colorIndex, series.colorIndex)
- });
- halo.point = point; // #6055
- halo.attr(extend({
- 'fill': point.color || series.color,
- 'fill-opacity': haloOptions.opacity,
- 'zIndex': -1 // #4929, IE8 added halo above everything
- }, haloOptions.attributes));
- } else if (halo && halo.point && halo.point.haloPath) {
- // Animate back to 0 on the current halo point (#6055)
- halo.animate({
- d: halo.point.haloPath(0)
- });
- }
- point.state = state;
- },
- /**
- * Get the circular path definition for the halo
- * @param {Number} size The radius of the circular halo.
- * @returns {Array} The path definition
- */
- haloPath: function(size) {
- var series = this.series,
- chart = series.chart;
- return chart.renderer.symbols.circle(
- Math.floor(this.plotX) - size,
- this.plotY - size,
- size * 2,
- size * 2
- );
- }
- });
- /*
- * Extend the Series object with interaction
- */
- extend(Series.prototype, /** @lends Highcharts.Series.prototype */ {
- /**
- * Series mouse over handler
- */
- onMouseOver: function() {
- var series = this,
- chart = series.chart,
- hoverSeries = chart.hoverSeries;
- // set normal state to previous series
- if (hoverSeries && hoverSeries !== series) {
- hoverSeries.onMouseOut();
- }
- // trigger the event, but to save processing time,
- // only if defined
- if (series.options.events.mouseOver) {
- fireEvent(series, 'mouseOver');
- }
- // hover this
- series.setState('hover');
- chart.hoverSeries = series;
- },
- /**
- * Series mouse out handler
- */
- onMouseOut: function() {
- // trigger the event only if listeners exist
- var series = this,
- options = series.options,
- chart = series.chart,
- tooltip = chart.tooltip,
- hoverPoint = chart.hoverPoint;
- chart.hoverSeries = null; // #182, set to null before the mouseOut event fires
- // trigger mouse out on the point, which must be in this series
- if (hoverPoint) {
- hoverPoint.onMouseOut();
- }
- // fire the mouse out event
- if (series && options.events.mouseOut) {
- fireEvent(series, 'mouseOut');
- }
- // hide the tooltip
- if (tooltip && !series.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
- tooltip.hide();
- }
- // set normal state
- series.setState();
- },
- /**
- * Set the state of the graph
- */
- setState: function(state) {
- var series = this,
- options = series.options,
- graph = series.graph,
- stateOptions = options.states,
- lineWidth = options.lineWidth,
- attribs,
- i = 0;
- state = state || '';
- if (series.state !== state) {
- // Toggle class names
- each([
- series.group,
- series.markerGroup,
- series.dataLabelsGroup
- ], function(group) {
- if (group) {
- // Old state
- if (series.state) {
- group.removeClass('highcharts-series-' + series.state);
- }
- // New state
- if (state) {
- group.addClass('highcharts-series-' + state);
- }
- }
- });
- series.state = state;
- if (stateOptions[state] && stateOptions[state].enabled === false) {
- return;
- }
- if (state) {
- lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035
- }
- if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
- attribs = {
- 'stroke-width': lineWidth
- };
- // Animate the graph stroke-width. By default a quick animation
- // to hover, slower to un-hover.
- graph.animate(
- attribs,
- pick(
- series.chart.options.chart.animation,
- stateOptions[state] && stateOptions[state].animation
- )
- );
- while (series['zone-graph-' + i]) {
- series['zone-graph-' + i].attr(attribs);
- i = i + 1;
- }
- }
- }
- },
- /**
- * Show or hide the series.
- *
- * @param {Boolean} [visible]
- * True to show the series, false to hide. If undefined, the
- * visibility is toggled.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart after the series is altered. If doing
- * more operations on the chart, it is a good idea to set redraw to
- * false and call {@link Chart#redraw|chart.redraw()} after.
- */
- setVisible: function(vis, redraw) {
- var series = this,
- chart = series.chart,
- legendItem = series.legendItem,
- showOrHide,
- ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
- oldVisibility = series.visible;
- // if called without an argument, toggle visibility
- series.visible = vis = series.options.visible = series.userOptions.visible = vis === undefined ? !oldVisibility : vis; // #5618
- showOrHide = vis ? 'show' : 'hide';
- // show or hide elements
- each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker', 'tt'], function(key) {
- if (series[key]) {
- series[key][showOrHide]();
- }
- });
- // hide tooltip (#1361)
- if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) {
- series.onMouseOut();
- }
- if (legendItem) {
- chart.legend.colorizeItem(series, vis);
- }
- // rescale or adapt to resized chart
- series.isDirty = true;
- // in a stack, all other series are affected
- if (series.options.stacking) {
- each(chart.series, function(otherSeries) {
- if (otherSeries.options.stacking && otherSeries.visible) {
- otherSeries.isDirty = true;
- }
- });
- }
- // show or hide linked series
- each(series.linkedSeries, function(otherSeries) {
- otherSeries.setVisible(vis, false);
- });
- if (ignoreHiddenSeries) {
- chart.isDirtyBox = true;
- }
- if (redraw !== false) {
- chart.redraw();
- }
- fireEvent(series, showOrHide);
- },
- /**
- * Show the series if hidden.
- *
- * @sample highcharts/members/series-hide/
- * Toggle visibility from a button
- */
- show: function() {
- this.setVisible(true);
- },
- /**
- * Hide the series if visible. If the {@link
- * https://api.highcharts.com/highcharts/chart.ignoreHiddenSeries|
- * chart.ignoreHiddenSeries} option is true, the chart is redrawn without
- * this series.
- *
- * @sample highcharts/members/series-hide/
- * Toggle visibility from a button
- */
- hide: function() {
- this.setVisible(false);
- },
- /**
- * Select or unselect the series. This means its {@link
- * Highcharts.Series.selected|selected} property is set, the checkbox in the
- * legend is toggled and when selected, the series is returned by the
- * {@link Highcharts.Chart#getSelectedSeries} function.
- *
- * @param {Boolean} [selected]
- * True to select the series, false to unselect. If undefined, the
- * selection state is toggled.
- *
- * @sample highcharts/members/series-select/
- * Select a series from a button
- */
- select: function(selected) {
- var series = this;
- series.selected = selected = (selected === undefined) ?
- !series.selected :
- selected;
- if (series.checkbox) {
- series.checkbox.checked = selected;
- }
- fireEvent(series, selected ? 'select' : 'unselect');
- },
- drawTracker: TrackerMixin.drawTrackerGraph
- });
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var Chart = H.Chart,
- each = H.each,
- inArray = H.inArray,
- isArray = H.isArray,
- isObject = H.isObject,
- pick = H.pick,
- splat = H.splat;
- /**
- * Update the chart based on the current chart/document size and options for
- * responsiveness.
- */
- Chart.prototype.setResponsive = function(redraw) {
- var options = this.options.responsive,
- ruleIds = [],
- currentResponsive = this.currentResponsive,
- currentRuleIds;
- if (options && options.rules) {
- each(options.rules, function(rule) {
- if (rule._id === undefined) {
- rule._id = H.uniqueKey();
- }
- this.matchResponsiveRule(rule, ruleIds, redraw);
- }, this);
- }
- // Merge matching rules
- var mergedOptions = H.merge.apply(0, H.map(ruleIds, function(ruleId) {
- return H.find(options.rules, function(rule) {
- return rule._id === ruleId;
- }).chartOptions;
- }));
- // Stringified key for the rules that currently apply.
- ruleIds = ruleIds.toString() || undefined;
- currentRuleIds = currentResponsive && currentResponsive.ruleIds;
- // Changes in what rules apply
- if (ruleIds !== currentRuleIds) {
- // Undo previous rules. Before we apply a new set of rules, we need to
- // roll back completely to base options (#6291).
- if (currentResponsive) {
- this.update(currentResponsive.undoOptions, redraw);
- }
- if (ruleIds) {
- // Get undo-options for matching rules
- this.currentResponsive = {
- ruleIds: ruleIds,
- mergedOptions: mergedOptions,
- undoOptions: this.currentOptions(mergedOptions)
- };
- this.update(mergedOptions, redraw);
- } else {
- this.currentResponsive = undefined;
- }
- }
- };
- /**
- * Handle a single responsiveness rule
- */
- Chart.prototype.matchResponsiveRule = function(rule, matches) {
- var condition = rule.condition,
- fn = condition.callback || function() {
- return this.chartWidth <= pick(condition.maxWidth, Number.MAX_VALUE) &&
- this.chartHeight <= pick(condition.maxHeight, Number.MAX_VALUE) &&
- this.chartWidth >= pick(condition.minWidth, 0) &&
- this.chartHeight >= pick(condition.minHeight, 0);
- };
- if (fn.call(this)) {
- matches.push(rule._id);
- }
- };
- /**
- * Get the current values for a given set of options. Used before we update
- * the chart with a new responsiveness rule.
- * TODO: Restore axis options (by id?)
- */
- Chart.prototype.currentOptions = function(options) {
- var ret = {};
- /**
- * Recurse over a set of options and its current values,
- * and store the current values in the ret object.
- */
- function getCurrent(options, curr, ret, depth) {
- var i;
- H.objectEach(options, function(val, key) {
- if (!depth && inArray(key, ['series', 'xAxis', 'yAxis']) > -1) {
- options[key] = splat(options[key]);
- ret[key] = [];
- // Iterate over collections like series, xAxis or yAxis and map
- // the items by index.
- for (i = 0; i < options[key].length; i++) {
- if (curr[key][i]) { // Item exists in current data (#6347)
- ret[key][i] = {};
- getCurrent(
- val[i],
- curr[key][i],
- ret[key][i],
- depth + 1
- );
- }
- }
- } else if (isObject(val)) {
- ret[key] = isArray(val) ? [] : {};
- getCurrent(val, curr[key] || {}, ret[key], depth + 1);
- } else {
- ret[key] = curr[key] || null;
- }
- });
- }
- getCurrent(options, this.options, ret, 0);
- return ret;
- };
- }(Highcharts));
- var Highcharts = (function(Highcharts) {
- return Highcharts;
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- Axis = H.Axis,
- Chart = H.Chart,
- css = H.css,
- dateFormat = H.dateFormat,
- defined = H.defined,
- each = H.each,
- extend = H.extend,
- noop = H.noop,
- Series = H.Series,
- timeUnits = H.timeUnits,
- wrap = H.wrap;
- /* ****************************************************************************
- * Start ordinal axis logic *
- *****************************************************************************/
- wrap(Series.prototype, 'init', function(proceed) {
- var series = this,
- xAxis;
- // call the original function
- proceed.apply(this, Array.prototype.slice.call(arguments, 1));
- xAxis = series.xAxis;
- // Destroy the extended ordinal index on updated data
- if (xAxis && xAxis.options.ordinal) {
- addEvent(series, 'updatedData', function() {
- delete xAxis.ordinalIndex;
- });
- }
- });
- /**
- * In an ordinal axis, there might be areas with dense consentrations of points, then large
- * gaps between some. Creating equally distributed ticks over this entire range
- * may lead to a huge number of ticks that will later be removed. So instead, break the
- * positions up in segments, find the tick positions for each segment then concatenize them.
- * This method is used from both data grouping logic and X axis tick position logic.
- */
- wrap(Axis.prototype, 'getTimeTicks', function(proceed, normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) {
- var start = 0,
- end,
- segmentPositions,
- higherRanks = {},
- hasCrossedHigherRank,
- info,
- posLength,
- outsideMax,
- groupPositions = [],
- lastGroupPosition = -Number.MAX_VALUE,
- tickPixelIntervalOption = this.options.tickPixelInterval;
- // The positions are not always defined, for example for ordinal positions when data
- // has regular interval (#1557, #2090)
- if ((!this.options.ordinal && !this.options.breaks) || !positions || positions.length < 3 || min === undefined) {
- return proceed.call(this, normalizedInterval, min, max, startOfWeek);
- }
- // Analyze the positions array to split it into segments on gaps larger than 5 times
- // the closest distance. The closest distance is already found at this point, so
- // we reuse that instead of computing it again.
- posLength = positions.length;
- for (end = 0; end < posLength; end++) {
- outsideMax = end && positions[end - 1] > max;
- if (positions[end] < min) { // Set the last position before min
- start = end;
- }
- if (end === posLength - 1 || positions[end + 1] - positions[end] > closestDistance * 5 || outsideMax) {
- // For each segment, calculate the tick positions from the getTimeTicks utility
- // function. The interval will be the same regardless of how long the segment is.
- if (positions[end] > lastGroupPosition) { // #1475
- segmentPositions = proceed.call(this, normalizedInterval, positions[start], positions[end], startOfWeek);
- // Prevent duplicate groups, for example for multiple segments within one larger time frame (#1475)
- while (segmentPositions.length && segmentPositions[0] <= lastGroupPosition) {
- segmentPositions.shift();
- }
- if (segmentPositions.length) {
- lastGroupPosition = segmentPositions[segmentPositions.length - 1];
- }
- groupPositions = groupPositions.concat(segmentPositions);
- }
- // Set start of next segment
- start = end + 1;
- }
- if (outsideMax) {
- break;
- }
- }
- // Get the grouping info from the last of the segments. The info is the same for
- // all segments.
- info = segmentPositions.info;
- // Optionally identify ticks with higher rank, for example when the ticks
- // have crossed midnight.
- if (findHigherRanks && info.unitRange <= timeUnits.hour) {
- end = groupPositions.length - 1;
- // Compare points two by two
- for (start = 1; start < end; start++) {
- if (dateFormat('%d', groupPositions[start]) !== dateFormat('%d', groupPositions[start - 1])) {
- higherRanks[groupPositions[start]] = 'day';
- hasCrossedHigherRank = true;
- }
- }
- // If the complete array has crossed midnight, we want to mark the first
- // positions also as higher rank
- if (hasCrossedHigherRank) {
- higherRanks[groupPositions[0]] = 'day';
- }
- info.higherRanks = higherRanks;
- }
- // Save the info
- groupPositions.info = info;
- // Don't show ticks within a gap in the ordinal axis, where the space between
- // two points is greater than a portion of the tick pixel interval
- if (findHigherRanks && defined(tickPixelIntervalOption)) { // check for squashed ticks
- var length = groupPositions.length,
- i = length,
- itemToRemove,
- translated,
- translatedArr = [],
- lastTranslated,
- medianDistance,
- distance,
- distances = [];
- // Find median pixel distance in order to keep a reasonably even distance between
- // ticks (#748)
- while (i--) {
- translated = this.translate(groupPositions[i]);
- if (lastTranslated) {
- distances[i] = lastTranslated - translated;
- }
- translatedArr[i] = lastTranslated = translated;
- }
- distances.sort();
- medianDistance = distances[Math.floor(distances.length / 2)];
- if (medianDistance < tickPixelIntervalOption * 0.6) {
- medianDistance = null;
- }
- // Now loop over again and remove ticks where needed
- i = groupPositions[length - 1] > max ? length - 1 : length; // #817
- lastTranslated = undefined;
- while (i--) {
- translated = translatedArr[i];
- distance = Math.abs(lastTranslated - translated);
- // #4175 - when axis is reversed, the distance, is negative but
- // tickPixelIntervalOption positive, so we need to compare the same values
- // Remove ticks that are closer than 0.6 times the pixel interval from the one to the right,
- // but not if it is close to the median distance (#748).
- if (lastTranslated && distance < tickPixelIntervalOption * 0.8 &&
- (medianDistance === null || distance < medianDistance * 0.8)) {
- // Is this a higher ranked position with a normal position to the right?
- if (higherRanks[groupPositions[i]] && !higherRanks[groupPositions[i + 1]]) {
- // Yes: remove the lower ranked neighbour to the right
- itemToRemove = i + 1;
- lastTranslated = translated; // #709
- } else {
- // No: remove this one
- itemToRemove = i;
- }
- groupPositions.splice(itemToRemove, 1);
- } else {
- lastTranslated = translated;
- }
- }
- }
- return groupPositions;
- });
- // Extend the Axis prototype
- extend(Axis.prototype, /** @lends Axis.prototype */ {
- /**
- * Calculate the ordinal positions before tick positions are calculated.
- */
- beforeSetTickPositions: function() {
- var axis = this,
- len,
- ordinalPositions = [],
- useOrdinal = false,
- dist,
- extremes = axis.getExtremes(),
- min = extremes.min,
- max = extremes.max,
- minIndex,
- maxIndex,
- slope,
- hasBreaks = axis.isXAxis && !!axis.options.breaks,
- isOrdinal = axis.options.ordinal,
- ignoreHiddenSeries = axis.chart.options.chart.ignoreHiddenSeries,
- i;
- // apply the ordinal logic
- if (isOrdinal || hasBreaks) { // #4167 YAxis is never ordinal ?
- each(axis.series, function(series, i) {
- if ((!ignoreHiddenSeries || series.visible !== false) && (series.takeOrdinalPosition !== false || hasBreaks)) {
- // concatenate the processed X data into the existing positions, or the empty array
- ordinalPositions = ordinalPositions.concat(series.processedXData);
- len = ordinalPositions.length;
- // remove duplicates (#1588)
- ordinalPositions.sort(function(a, b) {
- return a - b; // without a custom function it is sorted as strings
- });
- if (len) {
- i = len - 1;
- while (i--) {
- if (ordinalPositions[i] === ordinalPositions[i + 1]) {
- ordinalPositions.splice(i, 1);
- }
- }
- }
- }
- });
- // cache the length
- len = ordinalPositions.length;
- // Check if we really need the overhead of mapping axis data against the ordinal positions.
- // If the series consist of evenly spaced data any way, we don't need any ordinal logic.
- if (len > 2) { // two points have equal distance by default
- dist = ordinalPositions[1] - ordinalPositions[0];
- i = len - 1;
- while (i-- && !useOrdinal) {
- if (ordinalPositions[i + 1] - ordinalPositions[i] !== dist) {
- useOrdinal = true;
- }
- }
- // When zooming in on a week, prevent axis padding for weekends even though the data within
- // the week is evenly spaced.
- if (!axis.options.keepOrdinalPadding && (ordinalPositions[0] - min > dist || max - ordinalPositions[ordinalPositions.length - 1] > dist)) {
- useOrdinal = true;
- }
- }
- // Record the slope and offset to compute the linear values from the array index.
- // Since the ordinal positions may exceed the current range, get the start and
- // end positions within it (#719, #665b)
- if (useOrdinal) {
- // Register
- axis.ordinalPositions = ordinalPositions;
- // This relies on the ordinalPositions being set. Use Math.max
- // and Math.min to prevent padding on either sides of the data.
- minIndex = axis.ordinal2lin( // #5979
- Math.max(
- min,
- ordinalPositions[0]
- ),
- true
- );
- maxIndex = Math.max(axis.ordinal2lin(
- Math.min(
- max,
- ordinalPositions[ordinalPositions.length - 1]
- ),
- true
- ), 1); // #3339
- // Set the slope and offset of the values compared to the indices in the ordinal positions
- axis.ordinalSlope = slope = (max - min) / (maxIndex - minIndex);
- axis.ordinalOffset = min - (minIndex * slope);
- } else {
- axis.ordinalPositions = axis.ordinalSlope = axis.ordinalOffset = undefined;
- }
- }
- axis.isOrdinal = isOrdinal && useOrdinal; // #3818, #4196, #4926
- axis.groupIntervalFactor = null; // reset for next run
- },
- /**
- * Translate from a linear axis value to the corresponding ordinal axis position. If there
- * are no gaps in the ordinal axis this will be the same. The translated value is the value
- * that the point would have if the axis were linear, using the same min and max.
- *
- * @param Number val The axis value
- * @param Boolean toIndex Whether to return the index in the ordinalPositions or the new value
- */
- val2lin: function(val, toIndex) {
- var axis = this,
- ordinalPositions = axis.ordinalPositions,
- ret;
- if (!ordinalPositions) {
- ret = val;
- } else {
- var ordinalLength = ordinalPositions.length,
- i,
- distance,
- ordinalIndex;
- // first look for an exact match in the ordinalpositions array
- i = ordinalLength;
- while (i--) {
- if (ordinalPositions[i] === val) {
- ordinalIndex = i;
- break;
- }
- }
- // if that failed, find the intermediate position between the two nearest values
- i = ordinalLength - 1;
- while (i--) {
- if (val > ordinalPositions[i] || i === 0) { // interpolate
- distance = (val - ordinalPositions[i]) / (ordinalPositions[i + 1] - ordinalPositions[i]); // something between 0 and 1
- ordinalIndex = i + distance;
- break;
- }
- }
- ret = toIndex ?
- ordinalIndex :
- axis.ordinalSlope * (ordinalIndex || 0) + axis.ordinalOffset;
- }
- return ret;
- },
- /**
- * Translate from linear (internal) to axis value
- *
- * @param Number val The linear abstracted value
- * @param Boolean fromIndex Translate from an index in the ordinal positions rather than a value
- */
- lin2val: function(val, fromIndex) {
- var axis = this,
- ordinalPositions = axis.ordinalPositions,
- ret;
- if (!ordinalPositions) { // the visible range contains only equally spaced values
- ret = val;
- } else {
- var ordinalSlope = axis.ordinalSlope,
- ordinalOffset = axis.ordinalOffset,
- i = ordinalPositions.length - 1,
- linearEquivalentLeft,
- linearEquivalentRight,
- distance;
- // Handle the case where we translate from the index directly, used only
- // when panning an ordinal axis
- if (fromIndex) {
- if (val < 0) { // out of range, in effect panning to the left
- val = ordinalPositions[0];
- } else if (val > i) { // out of range, panning to the right
- val = ordinalPositions[i];
- } else { // split it up
- i = Math.floor(val);
- distance = val - i; // the decimal
- }
- // Loop down along the ordinal positions. When the linear equivalent of i matches
- // an ordinal position, interpolate between the left and right values.
- } else {
- while (i--) {
- linearEquivalentLeft = (ordinalSlope * i) + ordinalOffset;
- if (val >= linearEquivalentLeft) {
- linearEquivalentRight = (ordinalSlope * (i + 1)) + ordinalOffset;
- distance = (val - linearEquivalentLeft) / (linearEquivalentRight - linearEquivalentLeft); // something between 0 and 1
- break;
- }
- }
- }
- // If the index is within the range of the ordinal positions, return the associated
- // or interpolated value. If not, just return the value
- return distance !== undefined && ordinalPositions[i] !== undefined ?
- ordinalPositions[i] + (distance ? distance * (ordinalPositions[i + 1] - ordinalPositions[i]) : 0) :
- val;
- }
- return ret;
- },
- /**
- * Get the ordinal positions for the entire data set. This is necessary in chart panning
- * because we need to find out what points or data groups are available outside the
- * visible range. When a panning operation starts, if an index for the given grouping
- * does not exists, it is created and cached. This index is deleted on updated data, so
- * it will be regenerated the next time a panning operation starts.
- */
- getExtendedPositions: function() {
- var axis = this,
- chart = axis.chart,
- grouping = axis.series[0].currentDataGrouping,
- ordinalIndex = axis.ordinalIndex,
- key = grouping ? grouping.count + grouping.unitName : 'raw',
- extremes = axis.getExtremes(),
- fakeAxis,
- fakeSeries;
- // If this is the first time, or the ordinal index is deleted by updatedData,
- // create it.
- if (!ordinalIndex) {
- ordinalIndex = axis.ordinalIndex = {};
- }
- if (!ordinalIndex[key]) {
- // Create a fake axis object where the extended ordinal positions are emulated
- fakeAxis = {
- series: [],
- chart: chart,
- getExtremes: function() {
- return {
- min: extremes.dataMin,
- max: extremes.dataMax
- };
- },
- options: {
- ordinal: true
- },
- val2lin: Axis.prototype.val2lin, // #2590
- ordinal2lin: Axis.prototype.ordinal2lin // #6276
- };
- // Add the fake series to hold the full data, then apply processData to it
- each(axis.series, function(series) {
- fakeSeries = {
- xAxis: fakeAxis,
- xData: series.xData,
- chart: chart,
- destroyGroupedData: noop
- };
- fakeSeries.options = {
- dataGrouping: grouping ? {
- enabled: true,
- forced: true,
- approximation: 'open', // doesn't matter which, use the fastest
- units: [
- [grouping.unitName, [grouping.count]]
- ]
- } : {
- enabled: false
- }
- };
- series.processData.apply(fakeSeries);
- fakeAxis.series.push(fakeSeries);
- });
- // Run beforeSetTickPositions to compute the ordinalPositions
- axis.beforeSetTickPositions.apply(fakeAxis);
- // Cache it
- ordinalIndex[key] = fakeAxis.ordinalPositions;
- }
- return ordinalIndex[key];
- },
- /**
- * Find the factor to estimate how wide the plot area would have been if ordinal
- * gaps were included. This value is used to compute an imagined plot width in order
- * to establish the data grouping interval.
- *
- * A real world case is the intraday-candlestick
- * example. Without this logic, it would show the correct data grouping when viewing
- * a range within each day, but once moving the range to include the gap between two
- * days, the interval would include the cut-away night hours and the data grouping
- * would be wrong. So the below method tries to compensate by identifying the most
- * common point interval, in this case days.
- *
- * An opposite case is presented in issue #718. We have a long array of daily data,
- * then one point is appended one hour after the last point. We expect the data grouping
- * not to change.
- *
- * In the future, if we find cases where this estimation doesn't work optimally, we
- * might need to add a second pass to the data grouping logic, where we do another run
- * with a greater interval if the number of data groups is more than a certain fraction
- * of the desired group count.
- */
- getGroupIntervalFactor: function(xMin, xMax, series) {
- var i,
- processedXData = series.processedXData,
- len = processedXData.length,
- distances = [],
- median,
- groupIntervalFactor = this.groupIntervalFactor;
- // Only do this computation for the first series, let the other inherit it (#2416)
- if (!groupIntervalFactor) {
- // Register all the distances in an array
- for (i = 0; i < len - 1; i++) {
- distances[i] = processedXData[i + 1] - processedXData[i];
- }
- // Sort them and find the median
- distances.sort(function(a, b) {
- return a - b;
- });
- median = distances[Math.floor(len / 2)];
- // Compensate for series that don't extend through the entire axis extent. #1675.
- xMin = Math.max(xMin, processedXData[0]);
- xMax = Math.min(xMax, processedXData[len - 1]);
- this.groupIntervalFactor = groupIntervalFactor = (len * median) / (xMax - xMin);
- }
- // Return the factor needed for data grouping
- return groupIntervalFactor;
- },
- /**
- * Make the tick intervals closer because the ordinal gaps make the ticks spread out or cluster
- */
- postProcessTickInterval: function(tickInterval) {
- // Problem: http://jsfiddle.net/highcharts/FQm4E/1/
- // This is a case where this algorithm doesn't work optimally. In this case, the
- // tick labels are spread out per week, but all the gaps reside within weeks. So
- // we have a situation where the labels are courser than the ordinal gaps, and
- // thus the tick interval should not be altered
- var ordinalSlope = this.ordinalSlope,
- ret;
- if (ordinalSlope) {
- if (!this.options.breaks) {
- ret = tickInterval / (ordinalSlope / this.closestPointRange);
- } else {
- ret = this.closestPointRange;
- }
- } else {
- ret = tickInterval;
- }
- return ret;
- }
- });
- // Record this to prevent overwriting by broken-axis module (#5979)
- Axis.prototype.ordinal2lin = Axis.prototype.val2lin;
- // Extending the Chart.pan method for ordinal axes
- wrap(Chart.prototype, 'pan', function(proceed, e) {
- var chart = this,
- xAxis = chart.xAxis[0],
- chartX = e.chartX,
- runBase = false;
- if (xAxis.options.ordinal && xAxis.series.length) {
- var mouseDownX = chart.mouseDownX,
- extremes = xAxis.getExtremes(),
- dataMax = extremes.dataMax,
- min = extremes.min,
- max = extremes.max,
- trimmedRange,
- hoverPoints = chart.hoverPoints,
- closestPointRange = xAxis.closestPointRange,
- pointPixelWidth = xAxis.translationSlope * (xAxis.ordinalSlope || closestPointRange),
- movedUnits = (mouseDownX - chartX) / pointPixelWidth, // how many ordinal units did we move?
- extendedAxis = {
- ordinalPositions: xAxis.getExtendedPositions()
- }, // get index of all the chart's points
- ordinalPositions,
- searchAxisLeft,
- lin2val = xAxis.lin2val,
- val2lin = xAxis.val2lin,
- searchAxisRight;
- if (!extendedAxis.ordinalPositions) { // we have an ordinal axis, but the data is equally spaced
- runBase = true;
- } else if (Math.abs(movedUnits) > 1) {
- // Remove active points for shared tooltip
- if (hoverPoints) {
- each(hoverPoints, function(point) {
- point.setState();
- });
- }
- if (movedUnits < 0) {
- searchAxisLeft = extendedAxis;
- searchAxisRight = xAxis.ordinalPositions ? xAxis : extendedAxis;
- } else {
- searchAxisLeft = xAxis.ordinalPositions ? xAxis : extendedAxis;
- searchAxisRight = extendedAxis;
- }
- // In grouped data series, the last ordinal position represents the grouped data, which is
- // to the left of the real data max. If we don't compensate for this, we will be allowed
- // to pan grouped data series passed the right of the plot area.
- ordinalPositions = searchAxisRight.ordinalPositions;
- if (dataMax > ordinalPositions[ordinalPositions.length - 1]) {
- ordinalPositions.push(dataMax);
- }
- // Get the new min and max values by getting the ordinal index for the current extreme,
- // then add the moved units and translate back to values. This happens on the
- // extended ordinal positions if the new position is out of range, else it happens
- // on the current x axis which is smaller and faster.
- chart.fixedRange = max - min;
- trimmedRange = xAxis.toFixedRange(null, null,
- lin2val.apply(searchAxisLeft, [
- val2lin.apply(searchAxisLeft, [min, true]) + movedUnits, // the new index
- true // translate from index
- ]),
- lin2val.apply(searchAxisRight, [
- val2lin.apply(searchAxisRight, [max, true]) + movedUnits, // the new index
- true // translate from index
- ])
- );
- // Apply it if it is within the available data range
- if (trimmedRange.min >= Math.min(extremes.dataMin, min) && trimmedRange.max <= Math.max(dataMax, max)) {
- xAxis.setExtremes(trimmedRange.min, trimmedRange.max, true, false, {
- trigger: 'pan'
- });
- }
- chart.mouseDownX = chartX; // set new reference for next run
- css(chart.container, {
- cursor: 'move'
- });
- }
- } else {
- runBase = true;
- }
- // revert to the linear chart.pan version
- if (runBase) {
- // call the original function
- proceed.apply(this, Array.prototype.slice.call(arguments, 1));
- }
- });
- /* ****************************************************************************
- * End ordinal axis logic *
- *****************************************************************************/
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2009-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var pick = H.pick,
- wrap = H.wrap,
- each = H.each,
- extend = H.extend,
- isArray = H.isArray,
- fireEvent = H.fireEvent,
- Axis = H.Axis,
- Series = H.Series;
- function stripArguments() {
- return Array.prototype.slice.call(arguments, 1);
- }
- extend(Axis.prototype, {
- isInBreak: function(brk, val) {
- var ret,
- repeat = brk.repeat || Infinity,
- from = brk.from,
- length = brk.to - brk.from,
- test = (val >= from ? (val - from) % repeat : repeat - ((from - val) % repeat));
- if (!brk.inclusive) {
- ret = test < length && test !== 0;
- } else {
- ret = test <= length;
- }
- return ret;
- },
- isInAnyBreak: function(val, testKeep) {
- var breaks = this.options.breaks,
- i = breaks && breaks.length,
- inbrk,
- keep,
- ret;
- if (i) {
- while (i--) {
- if (this.isInBreak(breaks[i], val)) {
- inbrk = true;
- if (!keep) {
- keep = pick(breaks[i].showPoints, this.isXAxis ? false : true);
- }
- }
- }
- if (inbrk && testKeep) {
- ret = inbrk && !keep;
- } else {
- ret = inbrk;
- }
- }
- return ret;
- }
- });
- wrap(Axis.prototype, 'setTickPositions', function(proceed) {
- proceed.apply(this, Array.prototype.slice.call(arguments, 1));
- if (this.options.breaks) {
- var axis = this,
- tickPositions = this.tickPositions,
- info = this.tickPositions.info,
- newPositions = [],
- i;
- for (i = 0; i < tickPositions.length; i++) {
- if (!axis.isInAnyBreak(tickPositions[i])) {
- newPositions.push(tickPositions[i]);
- }
- }
- this.tickPositions = newPositions;
- this.tickPositions.info = info;
- }
- });
- wrap(Axis.prototype, 'init', function(proceed, chart, userOptions) {
- var axis = this,
- breaks;
- // Force Axis to be not-ordinal when breaks are defined
- if (userOptions.breaks && userOptions.breaks.length) {
- userOptions.ordinal = false;
- }
- proceed.call(this, chart, userOptions);
- breaks = this.options.breaks;
- axis.isBroken = (isArray(breaks) && !!breaks.length);
- if (axis.isBroken) {
- axis.val2lin = function(val) {
- var nval = val,
- brk,
- i;
- for (i = 0; i < axis.breakArray.length; i++) {
- brk = axis.breakArray[i];
- if (brk.to <= val) {
- nval -= brk.len;
- } else if (brk.from >= val) {
- break;
- } else if (axis.isInBreak(brk, val)) {
- nval -= (val - brk.from);
- break;
- }
- }
- return nval;
- };
- axis.lin2val = function(val) {
- var nval = val,
- brk,
- i;
- for (i = 0; i < axis.breakArray.length; i++) {
- brk = axis.breakArray[i];
- if (brk.from >= nval) {
- break;
- } else if (brk.to < nval) {
- nval += brk.len;
- } else if (axis.isInBreak(brk, nval)) {
- nval += brk.len;
- }
- }
- return nval;
- };
- axis.setExtremes = function(newMin, newMax, redraw, animation, eventArguments) {
- // If trying to set extremes inside a break, extend it to before and after the break ( #3857 )
- while (this.isInAnyBreak(newMin)) {
- newMin -= this.closestPointRange;
- }
- while (this.isInAnyBreak(newMax)) {
- newMax -= this.closestPointRange;
- }
- Axis.prototype.setExtremes.call(this, newMin, newMax, redraw, animation, eventArguments);
- };
- axis.setAxisTranslation = function(saveOld) {
- Axis.prototype.setAxisTranslation.call(this, saveOld);
- var breaks = axis.options.breaks,
- breakArrayT = [], // Temporary one
- breakArray = [],
- length = 0,
- inBrk,
- repeat,
- min = axis.userMin || axis.min,
- max = axis.userMax || axis.max,
- pointRangePadding = pick(axis.pointRangePadding, 0),
- start,
- i;
- // Min & max check (#4247)
- each(breaks, function(brk) {
- repeat = brk.repeat || Infinity;
- if (axis.isInBreak(brk, min)) {
- min += (brk.to % repeat) - (min % repeat);
- }
- if (axis.isInBreak(brk, max)) {
- max -= (max % repeat) - (brk.from % repeat);
- }
- });
- // Construct an array holding all breaks in the axis
- each(breaks, function(brk) {
- start = brk.from;
- repeat = brk.repeat || Infinity;
- while (start - repeat > min) {
- start -= repeat;
- }
- while (start < min) {
- start += repeat;
- }
- for (i = start; i < max; i += repeat) {
- breakArrayT.push({
- value: i,
- move: 'in'
- });
- breakArrayT.push({
- value: i + (brk.to - brk.from),
- move: 'out',
- size: brk.breakSize
- });
- }
- });
- breakArrayT.sort(function(a, b) {
- var ret;
- if (a.value === b.value) {
- ret = (a.move === 'in' ? 0 : 1) - (b.move === 'in' ? 0 : 1);
- } else {
- ret = a.value - b.value;
- }
- return ret;
- });
- // Simplify the breaks
- inBrk = 0;
- start = min;
- each(breakArrayT, function(brk) {
- inBrk += (brk.move === 'in' ? 1 : -1);
- if (inBrk === 1 && brk.move === 'in') {
- start = brk.value;
- }
- if (inBrk === 0) {
- breakArray.push({
- from: start,
- to: brk.value,
- len: brk.value - start - (brk.size || 0)
- });
- length += brk.value - start - (brk.size || 0);
- }
- });
- axis.breakArray = breakArray;
- // Used with staticScale, and below, the actual axis length when
- // breaks are substracted.
- axis.unitLength = max - min - length + pointRangePadding;
- fireEvent(axis, 'afterBreaks');
- if (axis.options.staticScale) {
- axis.transA = axis.options.staticScale;
- } else if (axis.unitLength) {
- axis.transA *= (max - axis.min + pointRangePadding) /
- axis.unitLength;
- }
- if (pointRangePadding) {
- axis.minPixelPadding = axis.transA * axis.minPointOffset;
- }
- axis.min = min;
- axis.max = max;
- };
- }
- });
- wrap(Series.prototype, 'generatePoints', function(proceed) {
- proceed.apply(this, stripArguments(arguments));
- var series = this,
- xAxis = series.xAxis,
- yAxis = series.yAxis,
- points = series.points,
- point,
- i = points.length,
- connectNulls = series.options.connectNulls,
- nullGap;
- if (xAxis && yAxis && (xAxis.options.breaks || yAxis.options.breaks)) {
- while (i--) {
- point = points[i];
- nullGap = point.y === null && connectNulls === false; // respect nulls inside the break (#4275)
- if (!nullGap && (xAxis.isInAnyBreak(point.x, true) || yAxis.isInAnyBreak(point.y, true))) {
- points.splice(i, 1);
- if (this.data[i]) {
- this.data[i].destroyElements(); // removes the graphics for this point if they exist
- }
- }
- }
- }
- });
- function drawPointsWrapped(proceed) {
- proceed.apply(this);
- this.drawBreaks(this.xAxis, ['x']);
- this.drawBreaks(this.yAxis, pick(this.pointArrayMap, ['y']));
- }
- H.Series.prototype.drawBreaks = function(axis, keys) {
- var series = this,
- points = series.points,
- breaks,
- threshold,
- eventName,
- y;
- if (!axis) {
- return; // #5950
- }
- each(keys, function(key) {
- breaks = axis.breakArray || [];
- threshold = axis.isXAxis ? axis.min : pick(series.options.threshold, axis.min);
- each(points, function(point) {
- y = pick(point['stack' + key.toUpperCase()], point[key]);
- each(breaks, function(brk) {
- eventName = false;
- if ((threshold < brk.from && y > brk.to) || (threshold > brk.from && y < brk.from)) {
- eventName = 'pointBreak';
- } else if ((threshold < brk.from && y > brk.from && y < brk.to) || (threshold > brk.from && y > brk.to && y < brk.from)) { // point falls inside the break
- eventName = 'pointInBreak';
- }
- if (eventName) {
- fireEvent(axis, eventName, {
- point: point,
- brk: brk
- });
- }
- });
- });
- });
- };
- /**
- * Extend getGraphPath by identifying gaps in the data so that we can draw a gap
- * in the line or area. This was moved from ordinal axis module to broken axis
- * module as of #5045.
- */
- H.Series.prototype.gappedPath = function() {
- var gapSize = this.options.gapSize,
- points = this.points.slice(),
- i = points.length - 1;
- if (gapSize && i > 0) { // #5008
- // extension for ordinal breaks
- while (i--) {
- if (points[i + 1].x - points[i].x > this.closestPointRange * gapSize) {
- points.splice( // insert after this one
- i + 1,
- 0, {
- isNull: true
- }
- );
- }
- }
- }
- // Call base method
- return this.getGraphPath(points);
- };
- wrap(H.seriesTypes.column.prototype, 'drawPoints', drawPointsWrapped);
- wrap(H.Series.prototype, 'drawPoints', drawPointsWrapped);
- }(Highcharts));
- (function() {
- }());
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var arrayMax = H.arrayMax,
- arrayMin = H.arrayMin,
- Axis = H.Axis,
- defaultPlotOptions = H.defaultPlotOptions,
- defined = H.defined,
- each = H.each,
- extend = H.extend,
- format = H.format,
- isNumber = H.isNumber,
- merge = H.merge,
- pick = H.pick,
- Point = H.Point,
- Series = H.Series,
- Tooltip = H.Tooltip,
- wrap = H.wrap;
- /* ****************************************************************************
- * Start data grouping module *
- ******************************************************************************/
- var seriesProto = Series.prototype,
- baseProcessData = seriesProto.processData,
- baseGeneratePoints = seriesProto.generatePoints,
- baseDestroy = seriesProto.destroy,
- commonOptions = {
- approximation: 'average', // average, open, high, low, close, sum
- //enabled: null, // (true for stock charts, false for basic),
- //forced: undefined,
- groupPixelWidth: 2,
- // the first one is the point or start value, the second is the start value if we're dealing with range,
- // the third one is the end value if dealing with a range
- dateTimeLabelFormats: {
- millisecond: ['%A, %b %e, %H:%M:%S.%L', '%A, %b %e, %H:%M:%S.%L', '-%H:%M:%S.%L'],
- second: ['%A, %b %e, %H:%M:%S', '%A, %b %e, %H:%M:%S', '-%H:%M:%S'],
- minute: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
- hour: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
- day: ['%A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
- week: ['Week from %A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
- month: ['%B %Y', '%B', '-%B %Y'],
- year: ['%Y', '%Y', '-%Y']
- }
- // smoothed = false, // enable this for navigator series only
- },
- specificOptions = { // extends common options
- line: {},
- spline: {},
- area: {},
- areaspline: {},
- column: {
- approximation: 'sum',
- groupPixelWidth: 10
- },
- arearange: {
- approximation: 'range'
- },
- areasplinerange: {
- approximation: 'range'
- },
- columnrange: {
- approximation: 'range',
- groupPixelWidth: 10
- },
- candlestick: {
- approximation: 'ohlc',
- groupPixelWidth: 10
- },
- ohlc: {
- approximation: 'ohlc',
- groupPixelWidth: 5
- }
- },
- // units are defined in a separate array to allow complete overriding in case of a user option
- defaultDataGroupingUnits = H.defaultDataGroupingUnits = [
- [
- 'millisecond', // unit name
- [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
- ],
- [
- 'second', [1, 2, 5, 10, 15, 30]
- ],
- [
- 'minute', [1, 2, 5, 10, 15, 30]
- ],
- [
- 'hour', [1, 2, 3, 4, 6, 8, 12]
- ],
- [
- 'day', [1]
- ],
- [
- 'week', [1]
- ],
- [
- 'month', [1, 3, 6]
- ],
- [
- 'year',
- null
- ]
- ],
- /**
- * Define the available approximation types. The data grouping
- * approximations takes an array or numbers as the first parameter. In case
- * of ohlc, four arrays are sent in as four parameters. Each array consists
- * only of numbers. In case null values belong to the group, the property
- * .hasNulls will be set to true on the array.
- */
- approximations = {
- sum: function(arr) {
- var len = arr.length,
- ret;
- // 1. it consists of nulls exclusively
- if (!len && arr.hasNulls) {
- ret = null;
- // 2. it has a length and real values
- } else if (len) {
- ret = 0;
- while (len--) {
- ret += arr[len];
- }
- }
- // 3. it has zero length, so just return undefined
- // => doNothing()
- return ret;
- },
- average: function(arr) {
- var len = arr.length,
- ret = approximations.sum(arr);
- // If we have a number, return it divided by the length. If not,
- // return null or undefined based on what the sum method finds.
- if (isNumber(ret) && len) {
- ret = ret / len;
- }
- return ret;
- },
- // The same as average, but for series with multiple values, like area
- // ranges.
- averages: function() { // #5479
- var ret = [];
- each(arguments, function(arr) {
- ret.push(approximations.average(arr));
- });
- return ret;
- },
- open: function(arr) {
- return arr.length ? arr[0] : (arr.hasNulls ? null : undefined);
- },
- high: function(arr) {
- return arr.length ? arrayMax(arr) : (arr.hasNulls ? null : undefined);
- },
- low: function(arr) {
- return arr.length ? arrayMin(arr) : (arr.hasNulls ? null : undefined);
- },
- close: function(arr) {
- return arr.length ? arr[arr.length - 1] : (arr.hasNulls ? null : undefined);
- },
- // ohlc and range are special cases where a multidimensional array is input and an array is output
- ohlc: function(open, high, low, close) {
- open = approximations.open(open);
- high = approximations.high(high);
- low = approximations.low(low);
- close = approximations.close(close);
- if (isNumber(open) || isNumber(high) || isNumber(low) || isNumber(close)) {
- return [open, high, low, close];
- }
- // else, return is undefined
- },
- range: function(low, high) {
- low = approximations.low(low);
- high = approximations.high(high);
- if (isNumber(low) || isNumber(high)) {
- return [low, high];
- } else if (low === null && high === null) {
- return null;
- }
- // else, return is undefined
- }
- };
- /**
- * Takes parallel arrays of x and y data and groups the data into intervals
- * defined by groupPositions, a collection of starting x values for each group.
- */
- seriesProto.groupData = function(xData, yData, groupPositions, approximation) {
- var series = this,
- data = series.data,
- dataOptions = series.options.data,
- groupedXData = [],
- groupedYData = [],
- groupMap = [],
- dataLength = xData.length,
- pointX,
- pointY,
- groupedY,
- // when grouping the fake extended axis for panning,
- // we don't need to consider y
- handleYData = !!yData,
- values = [],
- approximationFn = typeof approximation === 'function' ?
- approximation :
- approximations[approximation] ||
- // if the approximation is not found use default series type
- // approximation (#2914)
- (
- specificOptions[series.type] &&
- approximations[specificOptions[series.type].approximation]
- ) || approximations[commonOptions.approximation],
- pointArrayMap = series.pointArrayMap,
- pointArrayMapLength = pointArrayMap && pointArrayMap.length,
- pos = 0,
- start = 0,
- valuesLen,
- i, j;
- // Calculate values array size from pointArrayMap length
- if (pointArrayMapLength) {
- each(pointArrayMap, function() {
- values.push([]);
- });
- } else {
- values.push([]);
- }
- valuesLen = pointArrayMapLength || 1;
- // Start with the first point within the X axis range (#2696)
- for (i = 0; i <= dataLength; i++) {
- if (xData[i] >= groupPositions[0]) {
- break;
- }
- }
- for (i; i <= dataLength; i++) {
- // when a new group is entered, summarize and initiate
- // the previous group
- while ((
- groupPositions[pos + 1] !== undefined &&
- xData[i] >= groupPositions[pos + 1]
- ) || i === dataLength) { // get the last group
- // get group x and y
- pointX = groupPositions[pos];
- series.dataGroupInfo = {
- start: start,
- length: values[0].length
- };
- groupedY = approximationFn.apply(series, values);
- // push the grouped data
- if (groupedY !== undefined) {
- groupedXData.push(pointX);
- groupedYData.push(groupedY);
- groupMap.push(series.dataGroupInfo);
- }
- // reset the aggregate arrays
- start = i;
- for (j = 0; j < valuesLen; j++) {
- values[j].length = 0; // faster than values[j] = []
- values[j].hasNulls = false;
- }
- // Advance on the group positions
- pos += 1;
- // don't loop beyond the last group
- if (i === dataLength) {
- break;
- }
- }
- // break out
- if (i === dataLength) {
- break;
- }
- // for each raw data point, push it to an array that contains all values
- // for this specific group
- if (pointArrayMap) {
- var index = series.cropStart + i,
- point = (data && data[index]) ||
- series.pointClass.prototype.applyOptions.apply({
- series: series
- }, [dataOptions[index]]),
- val;
- for (j = 0; j < pointArrayMapLength; j++) {
- val = point[pointArrayMap[j]];
- if (isNumber(val)) {
- values[j].push(val);
- } else if (val === null) {
- values[j].hasNulls = true;
- }
- }
- } else {
- pointY = handleYData ? yData[i] : null;
- if (isNumber(pointY)) {
- values[0].push(pointY);
- } else if (pointY === null) {
- values[0].hasNulls = true;
- }
- }
- }
- return [groupedXData, groupedYData, groupMap];
- };
- /**
- * Extend the basic processData method, that crops the data to the current zoom
- * range, with data grouping logic.
- */
- seriesProto.processData = function() {
- var series = this,
- chart = series.chart,
- options = series.options,
- dataGroupingOptions = options.dataGrouping,
- groupingEnabled = series.allowDG !== false && dataGroupingOptions &&
- pick(dataGroupingOptions.enabled, chart.options.isStock),
- visible = series.visible || !chart.options.chart.ignoreHiddenSeries,
- hasGroupedData,
- skip;
- // run base method
- series.forceCrop = groupingEnabled; // #334
- series.groupPixelWidth = null; // #2110
- series.hasProcessed = true; // #2692
- // skip if processData returns false or if grouping is disabled (in that order)
- skip = baseProcessData.apply(series, arguments) === false || !groupingEnabled;
- if (!skip) {
- series.destroyGroupedData();
- var i,
- processedXData = series.processedXData,
- processedYData = series.processedYData,
- plotSizeX = chart.plotSizeX,
- xAxis = series.xAxis,
- ordinal = xAxis.options.ordinal,
- groupPixelWidth = series.groupPixelWidth = xAxis.getGroupPixelWidth && xAxis.getGroupPixelWidth();
- // Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth
- if (groupPixelWidth) {
- hasGroupedData = true;
- series.isDirty = true; // force recreation of point instances in series.translate, #5699
- series.points = null; // #6709
- var extremes = xAxis.getExtremes(),
- xMin = extremes.min,
- xMax = extremes.max,
- groupIntervalFactor = (ordinal && xAxis.getGroupIntervalFactor(xMin, xMax, series)) || 1,
- interval = (groupPixelWidth * (xMax - xMin) / plotSizeX) * groupIntervalFactor,
- groupPositions = xAxis.getTimeTicks(
- xAxis.normalizeTimeTickInterval(interval, dataGroupingOptions.units || defaultDataGroupingUnits),
- Math.min(xMin, processedXData[0]), // Processed data may extend beyond axis (#4907)
- Math.max(xMax, processedXData[processedXData.length - 1]),
- xAxis.options.startOfWeek,
- processedXData,
- series.closestPointRange
- ),
- groupedData = seriesProto.groupData.apply(series, [processedXData, processedYData, groupPositions, dataGroupingOptions.approximation]),
- groupedXData = groupedData[0],
- groupedYData = groupedData[1];
- // prevent the smoothed data to spill out left and right, and make
- // sure data is not shifted to the left
- if (dataGroupingOptions.smoothed) {
- i = groupedXData.length - 1;
- groupedXData[i] = Math.min(groupedXData[i], xMax);
- while (i-- && i > 0) {
- groupedXData[i] += interval / 2;
- }
- groupedXData[0] = Math.max(groupedXData[0], xMin);
- }
- // record what data grouping values were used
- series.currentDataGrouping = groupPositions.info;
- series.closestPointRange = groupPositions.info.totalRange;
- series.groupMap = groupedData[2];
- // Make sure the X axis extends to show the first group (#2533)
- // But only for visible series (#5493, #6393)
- if (defined(groupedXData[0]) && groupedXData[0] < xAxis.dataMin && visible) {
- if (xAxis.min === xAxis.dataMin) {
- xAxis.min = groupedXData[0];
- }
- xAxis.dataMin = groupedXData[0];
- }
- // set series props
- series.processedXData = groupedXData;
- series.processedYData = groupedYData;
- } else {
- series.currentDataGrouping = series.groupMap = null;
- }
- series.hasGroupedData = hasGroupedData;
- }
- };
- /**
- * Destroy the grouped data points. #622, #740
- */
- seriesProto.destroyGroupedData = function() {
- var groupedData = this.groupedData;
- // clear previous groups
- each(groupedData || [], function(point, i) {
- if (point) {
- groupedData[i] = point.destroy ? point.destroy() : null;
- }
- });
- this.groupedData = null;
- };
- /**
- * Override the generatePoints method by adding a reference to grouped data
- */
- seriesProto.generatePoints = function() {
- baseGeneratePoints.apply(this);
- // record grouped data in order to let it be destroyed the next time processData runs
- this.destroyGroupedData(); // #622
- this.groupedData = this.hasGroupedData ? this.points : null;
- };
- /**
- * Override point prototype to throw a warning when trying to update grouped points
- */
- wrap(Point.prototype, 'update', function(proceed) {
- if (this.dataGroup) {
- H.error(24);
- } else {
- proceed.apply(this, [].slice.call(arguments, 1));
- }
- });
- /**
- * Extend the original method, make the tooltip's header reflect the grouped range
- */
- wrap(Tooltip.prototype, 'tooltipFooterHeaderFormatter', function(proceed, labelConfig, isFooter) {
- var tooltip = this,
- series = labelConfig.series,
- options = series.options,
- tooltipOptions = series.tooltipOptions,
- dataGroupingOptions = options.dataGrouping,
- xDateFormat = tooltipOptions.xDateFormat,
- xDateFormatEnd,
- xAxis = series.xAxis,
- dateFormat = H.dateFormat,
- currentDataGrouping,
- dateTimeLabelFormats,
- labelFormats,
- formattedKey;
- // apply only to grouped series
- if (xAxis && xAxis.options.type === 'datetime' && dataGroupingOptions && isNumber(labelConfig.key)) {
- // set variables
- currentDataGrouping = series.currentDataGrouping;
- dateTimeLabelFormats = dataGroupingOptions.dateTimeLabelFormats;
- // if we have grouped data, use the grouping information to get the right format
- if (currentDataGrouping) {
- labelFormats = dateTimeLabelFormats[currentDataGrouping.unitName];
- if (currentDataGrouping.count === 1) {
- xDateFormat = labelFormats[0];
- } else {
- xDateFormat = labelFormats[1];
- xDateFormatEnd = labelFormats[2];
- }
- // if not grouped, and we don't have set the xDateFormat option, get the best fit,
- // so if the least distance between points is one minute, show it, but if the
- // least distance is one day, skip hours and minutes etc.
- } else if (!xDateFormat && dateTimeLabelFormats) {
- xDateFormat = tooltip.getXDateFormat(labelConfig, tooltipOptions, xAxis);
- }
- // now format the key
- formattedKey = dateFormat(xDateFormat, labelConfig.key);
- if (xDateFormatEnd) {
- formattedKey += dateFormat(xDateFormatEnd, labelConfig.key + currentDataGrouping.totalRange - 1);
- }
- // return the replaced format
- return format(tooltipOptions[(isFooter ? 'footer' : 'header') + 'Format'], {
- point: extend(labelConfig.point, {
- key: formattedKey
- }),
- series: series
- });
- }
- // else, fall back to the regular formatter
- return proceed.call(tooltip, labelConfig, isFooter);
- });
- /**
- * Extend the series destroyer
- */
- seriesProto.destroy = function() {
- var series = this,
- groupedData = series.groupedData || [],
- i = groupedData.length;
- while (i--) {
- if (groupedData[i]) {
- groupedData[i].destroy();
- }
- }
- baseDestroy.apply(series);
- };
- // Handle default options for data grouping. This must be set at runtime because some series types are
- // defined after this.
- wrap(seriesProto, 'setOptions', function(proceed, itemOptions) {
- var options = proceed.call(this, itemOptions),
- type = this.type,
- plotOptions = this.chart.options.plotOptions,
- defaultOptions = defaultPlotOptions[type].dataGrouping;
- if (specificOptions[type]) { // #1284
- if (!defaultOptions) {
- defaultOptions = merge(commonOptions, specificOptions[type]);
- }
- options.dataGrouping = merge(
- defaultOptions,
- plotOptions.series && plotOptions.series.dataGrouping, // #1228
- plotOptions[type].dataGrouping, // Set by the StockChart constructor
- itemOptions.dataGrouping
- );
- }
- if (this.chart.options.isStock) {
- this.requireSorting = true;
- }
- return options;
- });
- /**
- * When resetting the scale reset the hasProccessed flag to avoid taking previous data grouping
- * of neighbour series into accound when determining group pixel width (#2692).
- */
- wrap(Axis.prototype, 'setScale', function(proceed) {
- proceed.call(this);
- each(this.series, function(series) {
- series.hasProcessed = false;
- });
- });
- /**
- * Get the data grouping pixel width based on the greatest defined individual width
- * of the axis' series, and if whether one of the axes need grouping.
- */
- Axis.prototype.getGroupPixelWidth = function() {
- var series = this.series,
- len = series.length,
- i,
- groupPixelWidth = 0,
- doGrouping = false,
- dataLength,
- dgOptions;
- // If multiple series are compared on the same x axis, give them the same
- // group pixel width (#334)
- i = len;
- while (i--) {
- dgOptions = series[i].options.dataGrouping;
- if (dgOptions) {
- groupPixelWidth = Math.max(groupPixelWidth, dgOptions.groupPixelWidth);
- }
- }
- // If one of the series needs grouping, apply it to all (#1634)
- i = len;
- while (i--) {
- dgOptions = series[i].options.dataGrouping;
- if (dgOptions && series[i].hasProcessed) { // #2692
- dataLength = (series[i].processedXData || series[i].data).length;
- // Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth
- if (series[i].groupPixelWidth || dataLength > (this.chart.plotSizeX / groupPixelWidth) || (dataLength && dgOptions.forced)) {
- doGrouping = true;
- }
- }
- }
- return doGrouping ? groupPixelWidth : 0;
- };
- /**
- * Highstock only. Force data grouping on all the axis' series.
- *
- * @param {SeriesDatagroupingOptions} [dataGrouping]
- * A `dataGrouping` configuration. Use `false` to disable data grouping
- * dynamically.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart or wait for a later call to {@link
- * Chart#redraw}.
- *
- * @function setDataGrouping
- * @memberOf Axis.prototype
- */
- Axis.prototype.setDataGrouping = function(dataGrouping, redraw) {
- var i;
- redraw = pick(redraw, true);
- if (!dataGrouping) {
- dataGrouping = {
- forced: false,
- units: null
- };
- }
- // Axis is instantiated, update all series
- if (this instanceof Axis) {
- i = this.series.length;
- while (i--) {
- this.series[i].update({
- dataGrouping: dataGrouping
- }, false);
- }
- // Axis not yet instanciated, alter series options
- } else {
- each(this.chart.options.series, function(seriesOptions) {
- seriesOptions.dataGrouping = dataGrouping;
- }, false);
- }
- if (redraw) {
- this.chart.redraw();
- }
- };
- /* ****************************************************************************
- * End data grouping module *
- ******************************************************************************/
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var each = H.each,
- Point = H.Point,
- seriesType = H.seriesType,
- seriesTypes = H.seriesTypes;
- /**
- * The ohlc series type.
- *
- * @constructor seriesTypes.ohlc
- * @augments seriesTypes.column
- */
- seriesType('ohlc', 'column', {
- lineWidth: 1,
- tooltip: {
- pointFormat: '<span style="color:{point.color}">\u25CF</span> <b> {series.name}</b><br/>' +
- 'Open: {point.open}<br/>' +
- 'High: {point.high}<br/>' +
- 'Low: {point.low}<br/>' +
- 'Close: {point.close}<br/>'
- },
- threshold: null,
- states: {
- hover: {
- lineWidth: 3
- }
- },
- stickyTracking: true
- //upColor: undefined
- }, /** @lends seriesTypes.ohlc */ {
- directTouch: false,
- pointArrayMap: ['open', 'high', 'low', 'close'], // array point configs are mapped to this
- toYData: function(point) { // return a plain array for speedy calculation
- return [point.open, point.high, point.low, point.close];
- },
- pointValKey: 'close',
- pointAttrToOptions: {
- 'stroke': 'color',
- 'stroke-width': 'lineWidth'
- },
- /**
- * Postprocess mapping between options and SVG attributes
- */
- pointAttribs: function(point, state) {
- var attribs = seriesTypes.column.prototype.pointAttribs.call(
- this,
- point,
- state
- ),
- options = this.options;
- delete attribs.fill;
- if (!point.options.color &&
- options.upColor &&
- point.open < point.close
- ) {
- attribs.stroke = options.upColor;
- }
- return attribs;
- },
- /**
- * Translate data points from raw values x and y to plotX and plotY
- */
- translate: function() {
- var series = this,
- yAxis = series.yAxis,
- hasModifyValue = !!series.modifyValue,
- translated = ['plotOpen', 'plotHigh', 'plotLow', 'plotClose', 'yBottom']; // translate OHLC for
- seriesTypes.column.prototype.translate.apply(series);
- // Do the translation
- each(series.points, function(point) {
- each([point.open, point.high, point.low, point.close, point.low], function(value, i) {
- if (value !== null) {
- if (hasModifyValue) {
- value = series.modifyValue(value);
- }
- point[translated[i]] = yAxis.toPixels(value, true);
- }
- });
- // Align the tooltip to the high value to avoid covering the point
- point.tooltipPos[1] =
- point.plotHigh + yAxis.pos - series.chart.plotTop;
- });
- },
- /**
- * Draw the data points
- */
- drawPoints: function() {
- var series = this,
- points = series.points,
- chart = series.chart;
- each(points, function(point) {
- var plotOpen,
- plotClose,
- crispCorr,
- halfWidth,
- path,
- graphic = point.graphic,
- crispX,
- isNew = !graphic;
- if (point.plotY !== undefined) {
- // Create and/or update the graphic
- if (!graphic) {
- point.graphic = graphic = chart.renderer.path()
- .add(series.group);
- }
- graphic.attr(series.pointAttribs(point, point.selected && 'select')); // #3897
- // crisp vector coordinates
- crispCorr = (graphic.strokeWidth() % 2) / 2;
- crispX = Math.round(point.plotX) - crispCorr; // #2596
- halfWidth = Math.round(point.shapeArgs.width / 2);
- // the vertical stem
- path = [
- 'M',
- crispX, Math.round(point.yBottom),
- 'L',
- crispX, Math.round(point.plotHigh)
- ];
- // open
- if (point.open !== null) {
- plotOpen = Math.round(point.plotOpen) + crispCorr;
- path.push(
- 'M',
- crispX,
- plotOpen,
- 'L',
- crispX - halfWidth,
- plotOpen
- );
- }
- // close
- if (point.close !== null) {
- plotClose = Math.round(point.plotClose) + crispCorr;
- path.push(
- 'M',
- crispX,
- plotClose,
- 'L',
- crispX + halfWidth,
- plotClose
- );
- }
- graphic[isNew ? 'attr' : 'animate']({
- d: path
- })
- .addClass(point.getClassName(), true);
- }
- });
- },
- animate: null // Disable animation
- /**
- * @constructor seriesTypes.ohlc.prototype.pointClass
- * @extends {Point}
- */
- }, /** @lends seriesTypes.ohlc.prototype.pointClass.prototype */ {
- /**
- * Extend the parent method by adding up or down to the class name.
- */
- getClassName: function() {
- return Point.prototype.getClassName.call(this) +
- (this.open < this.close ? ' highcharts-point-up' : ' highcharts-point-down');
- }
- });
- /* ****************************************************************************
- * End OHLC series code *
- *****************************************************************************/
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var defaultPlotOptions = H.defaultPlotOptions,
- each = H.each,
- merge = H.merge,
- seriesType = H.seriesType,
- seriesTypes = H.seriesTypes;
- /**
- * The candlestick series type.
- *
- * @constructor seriesTypes.candlestick
- * @augments seriesTypes.ohlc
- */
- seriesType('candlestick', 'ohlc', merge(defaultPlotOptions.column, {
- states: {
- hover: {
- lineWidth: 2
- }
- },
- tooltip: defaultPlotOptions.ohlc.tooltip,
- threshold: null,
- lineColor: '#000000',
- lineWidth: 1,
- upColor: '#ffffff',
- stickyTracking: true
- // upLineColor: null
- }), /** @lends seriesTypes.candlestick */ {
- /**
- * Postprocess mapping between options and SVG attributes
- */
- pointAttribs: function(point, state) {
- var attribs = seriesTypes.column.prototype.pointAttribs.call(this, point, state),
- options = this.options,
- isUp = point.open < point.close,
- stroke = options.lineColor || this.color,
- stateOptions;
- attribs['stroke-width'] = options.lineWidth;
- attribs.fill = point.options.color || (isUp ? (options.upColor || this.color) : this.color);
- attribs.stroke = point.lineColor || (isUp ? (options.upLineColor || stroke) : stroke);
- // Select or hover states
- if (state) {
- stateOptions = options.states[state];
- attribs.fill = stateOptions.color || attribs.fill;
- attribs.stroke = stateOptions.lineColor || attribs.stroke;
- attribs['stroke-width'] =
- stateOptions.lineWidth || attribs['stroke-width'];
- }
- return attribs;
- },
- /**
- * Draw the data points
- */
- drawPoints: function() {
- var series = this, //state = series.state,
- points = series.points,
- chart = series.chart;
- each(points, function(point) {
- var graphic = point.graphic,
- plotOpen,
- plotClose,
- topBox,
- bottomBox,
- hasTopWhisker,
- hasBottomWhisker,
- crispCorr,
- crispX,
- path,
- halfWidth,
- isNew = !graphic;
- if (point.plotY !== undefined) {
- if (!graphic) {
- point.graphic = graphic = chart.renderer.path()
- .add(series.group);
- }
- graphic
- .attr(series.pointAttribs(point, point.selected && 'select')) // #3897
- .shadow(series.options.shadow);
- // Crisp vector coordinates
- crispCorr = (graphic.strokeWidth() % 2) / 2;
- crispX = Math.round(point.plotX) - crispCorr; // #2596
- plotOpen = point.plotOpen;
- plotClose = point.plotClose;
- topBox = Math.min(plotOpen, plotClose);
- bottomBox = Math.max(plotOpen, plotClose);
- halfWidth = Math.round(point.shapeArgs.width / 2);
- hasTopWhisker = Math.round(topBox) !== Math.round(point.plotHigh);
- hasBottomWhisker = bottomBox !== point.yBottom;
- topBox = Math.round(topBox) + crispCorr;
- bottomBox = Math.round(bottomBox) + crispCorr;
- // Create the path. Due to a bug in Chrome 49, the path is first instanciated
- // with no values, then the values pushed. For unknown reasons, instanciated
- // the path array with all the values would lead to a crash when updating
- // frequently (#5193).
- path = [];
- path.push(
- 'M',
- crispX - halfWidth, bottomBox,
- 'L',
- crispX - halfWidth, topBox,
- 'L',
- crispX + halfWidth, topBox,
- 'L',
- crispX + halfWidth, bottomBox,
- 'Z', // Use a close statement to ensure a nice rectangle #2602
- 'M',
- crispX, topBox,
- 'L',
- crispX, hasTopWhisker ? Math.round(point.plotHigh) : topBox, // #460, #2094
- 'M',
- crispX, bottomBox,
- 'L',
- crispX, hasBottomWhisker ? Math.round(point.yBottom) : bottomBox // #460, #2094
- );
- graphic[isNew ? 'attr' : 'animate']({
- d: path
- })
- .addClass(point.getClassName(), true);
- }
- });
- }
- });
- /* ****************************************************************************
- * End Candlestick series code *
- *****************************************************************************/
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- each = H.each,
- merge = H.merge,
- noop = H.noop,
- Renderer = H.Renderer,
- Series = H.Series,
- seriesType = H.seriesType,
- seriesTypes = H.seriesTypes,
- SVGRenderer = H.SVGRenderer,
- TrackerMixin = H.TrackerMixin,
- VMLRenderer = H.VMLRenderer,
- symbols = SVGRenderer.prototype.symbols,
- stableSort = H.stableSort;
- /**
- * The flags series type.
- *
- * @constructor seriesTypes.flags
- * @augments seriesTypes.column
- */
- seriesType('flags', 'column', {
- pointRange: 0, // #673
- //radius: 2,
- shape: 'flag',
- stackDistance: 12,
- textAlign: 'center',
- tooltip: {
- pointFormat: '{point.text}<br/>'
- },
- threshold: null,
- y: -30,
- fillColor: '#ffffff',
- // lineColor: color,
- lineWidth: 1,
- states: {
- hover: {
- lineColor: '#000000',
- fillColor: '#ccd6eb'
- }
- },
- style: {
- fontSize: '11px',
- fontWeight: 'bold'
- }
- }, /** @lends seriesTypes.flags.prototype */ {
- sorted: false,
- noSharedTooltip: true,
- allowDG: false,
- takeOrdinalPosition: false, // #1074
- trackerGroups: ['markerGroup'],
- forceCrop: true,
- /**
- * Inherit the initialization from base Series.
- */
- init: Series.prototype.init,
- /**
- * Get presentational attributes
- */
- pointAttribs: function(point, state) {
- var options = this.options,
- color = (point && point.color) || this.color,
- lineColor = options.lineColor,
- lineWidth = (point && point.lineWidth),
- fill = (point && point.fillColor) || options.fillColor;
- if (state) {
- fill = options.states[state].fillColor;
- lineColor = options.states[state].lineColor;
- lineWidth = options.states[state].lineWidth;
- }
- return {
- 'fill': fill || color,
- 'stroke': lineColor || color,
- 'stroke-width': lineWidth || options.lineWidth || 0
- };
- },
- /**
- * Extend the translate method by placing the point on the related series
- */
- translate: function() {
- seriesTypes.column.prototype.translate.apply(this);
- var series = this,
- options = series.options,
- chart = series.chart,
- points = series.points,
- cursor = points.length - 1,
- point,
- lastPoint,
- optionsOnSeries = options.onSeries,
- onSeries = optionsOnSeries && chart.get(optionsOnSeries),
- onKey = options.onKey || 'y',
- step = onSeries && onSeries.options.step,
- onData = onSeries && onSeries.points,
- i = onData && onData.length,
- xAxis = series.xAxis,
- yAxis = series.yAxis,
- xAxisExt = xAxis.getExtremes(),
- xOffset = 0,
- leftPoint,
- lastX,
- rightPoint,
- currentDataGrouping;
- // relate to a master series
- if (onSeries && onSeries.visible && i) {
- xOffset = (onSeries.pointXOffset || 0) + (onSeries.barW || 0) / 2;
- currentDataGrouping = onSeries.currentDataGrouping;
- lastX = onData[i - 1].x + (currentDataGrouping ? currentDataGrouping.totalRange : 0); // #2374
- // sort the data points
- stableSort(points, function(a, b) {
- return (a.x - b.x);
- });
- onKey = 'plot' + onKey[0].toUpperCase() + onKey.substr(1);
- while (i-- && points[cursor]) {
- point = points[cursor];
- leftPoint = onData[i];
- if (leftPoint.x <= point.x && leftPoint[onKey] !== undefined) {
- if (point.x <= lastX) { // #803
- point.plotY = leftPoint[onKey];
- // interpolate between points, #666
- if (leftPoint.x < point.x && !step) {
- rightPoint = onData[i + 1];
- if (rightPoint && rightPoint[onKey] !== undefined) {
- point.plotY +=
- ((point.x - leftPoint.x) / (rightPoint.x - leftPoint.x)) * // the distance ratio, between 0 and 1
- (rightPoint[onKey] - leftPoint[onKey]); // the y distance
- }
- }
- }
- cursor--;
- i++; // check again for points in the same x position
- if (cursor < 0) {
- break;
- }
- }
- }
- }
- // Add plotY position and handle stacking
- each(points, function(point, i) {
- var stackIndex;
- // Undefined plotY means the point is either on axis, outside series
- // range or hidden series. If the series is outside the range of the
- // x axis it should fall through with an undefined plotY, but then
- // we must remove the shapeArgs (#847).
- if (point.plotY === undefined) {
- if (point.x >= xAxisExt.min && point.x <= xAxisExt.max) {
- // we're inside xAxis range
- point.plotY = chart.chartHeight - xAxis.bottom -
- (xAxis.opposite ? xAxis.height : 0) +
- xAxis.offset - yAxis.top; // #3517
- } else {
- point.shapeArgs = {}; // 847
- }
- }
- point.plotX += xOffset; // #2049
- // if multiple flags appear at the same x, order them into a stack
- lastPoint = points[i - 1];
- if (lastPoint && lastPoint.plotX === point.plotX) {
- if (lastPoint.stackIndex === undefined) {
- lastPoint.stackIndex = 0;
- }
- stackIndex = lastPoint.stackIndex + 1;
- }
- point.stackIndex = stackIndex; // #3639
- });
- },
- /**
- * Draw the markers
- */
- drawPoints: function() {
- var series = this,
- points = series.points,
- chart = series.chart,
- renderer = chart.renderer,
- plotX,
- plotY,
- options = series.options,
- optionsY = options.y,
- shape,
- i,
- point,
- graphic,
- stackIndex,
- anchorX,
- anchorY,
- outsideRight,
- yAxis = series.yAxis;
- i = points.length;
- while (i--) {
- point = points[i];
- outsideRight = point.plotX > series.xAxis.len;
- plotX = point.plotX;
- stackIndex = point.stackIndex;
- shape = point.options.shape || options.shape;
- plotY = point.plotY;
- if (plotY !== undefined) {
- plotY = point.plotY + optionsY - (stackIndex !== undefined && stackIndex * options.stackDistance);
- }
- anchorX = stackIndex ? undefined : point.plotX; // skip connectors for higher level stacked points
- anchorY = stackIndex ? undefined : point.plotY;
- graphic = point.graphic;
- // Only draw the point if y is defined and the flag is within the visible area
- if (plotY !== undefined && plotX >= 0 && !outsideRight) {
- // Create the flag
- if (!graphic) {
- graphic = point.graphic = renderer.label(
- '',
- null,
- null,
- shape,
- null,
- null,
- options.useHTML
- )
- .attr(series.pointAttribs(point))
- .css(merge(options.style, point.style))
- .attr({
- align: shape === 'flag' ? 'left' : 'center',
- width: options.width,
- height: options.height,
- 'text-align': options.textAlign
- })
- .addClass('highcharts-point')
- .add(series.markerGroup);
- // Add reference to the point for tracker (#6303)
- if (point.graphic.div) {
- point.graphic.div.point = point;
- }
- graphic.shadow(options.shadow);
- }
- if (plotX > 0) { // #3119
- plotX -= graphic.strokeWidth() % 2; // #4285
- }
- // Plant the flag
- graphic.attr({
- text: point.options.title || options.title || 'A',
- x: plotX,
- y: plotY,
- anchorX: anchorX,
- anchorY: anchorY
- });
- // Set the tooltip anchor position
- point.tooltipPos = chart.inverted ? [yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - plotX] : [plotX, plotY + yAxis.pos - chart.plotTop]; // #6327
- } else if (graphic) {
- point.graphic = graphic.destroy();
- }
- }
- // Might be a mix of SVG and HTML and we need events for both (#6303)
- if (options.useHTML) {
- H.wrap(series.markerGroup, 'on', function(proceed) {
- return H.SVGElement.prototype.on.apply(
- proceed.apply(this, [].slice.call(arguments, 1)), // for HTML
- [].slice.call(arguments, 1)); // and for SVG
- });
- }
- },
- /**
- * Extend the column trackers with listeners to expand and contract stacks
- */
- drawTracker: function() {
- var series = this,
- points = series.points;
- TrackerMixin.drawTrackerPoint.apply(this);
- // Bring each stacked flag up on mouse over, this allows readability of vertically
- // stacked elements as well as tight points on the x axis. #1924.
- each(points, function(point) {
- var graphic = point.graphic;
- if (graphic) {
- addEvent(graphic.element, 'mouseover', function() {
- // Raise this point
- if (point.stackIndex > 0 && !point.raised) {
- point._y = graphic.y;
- graphic.attr({
- y: point._y - 8
- });
- point.raised = true;
- }
- // Revert other raised points
- each(points, function(otherPoint) {
- if (otherPoint !== point && otherPoint.raised && otherPoint.graphic) {
- otherPoint.graphic.attr({
- y: otherPoint._y
- });
- otherPoint.raised = false;
- }
- });
- });
- }
- });
- },
- animate: noop, // Disable animation
- buildKDTree: noop,
- setClip: noop
- });
- // create the flag icon with anchor
- symbols.flag = function(x, y, w, h, options) {
- var anchorX = (options && options.anchorX) || x,
- anchorY = (options && options.anchorY) || y;
- return [
- 'M', anchorX, anchorY,
- 'L', x, y + h,
- x, y,
- x + w, y,
- x + w, y + h,
- x, y + h,
- 'Z'
- ];
- };
- // create the circlepin and squarepin icons with anchor
- each(['circle', 'square'], function(shape) {
- symbols[shape + 'pin'] = function(x, y, w, h, options) {
- var anchorX = options && options.anchorX,
- anchorY = options && options.anchorY,
- path,
- labelTopOrBottomY;
- // For single-letter flags, make sure circular flags are not taller than their width
- if (shape === 'circle' && h > w) {
- x -= Math.round((h - w) / 2);
- w = h;
- }
- path = symbols[shape](x, y, w, h);
- if (anchorX && anchorY) {
- // if the label is below the anchor, draw the connecting line from the top edge of the label
- // otherwise start drawing from the bottom edge
- labelTopOrBottomY = (y > anchorY) ? y : y + h;
- path.push('M', anchorX, labelTopOrBottomY, 'L', anchorX, anchorY);
- }
- return path;
- };
- });
- // The symbol callbacks are generated on the SVGRenderer object in all browsers. Even
- // VML browsers need this in order to generate shapes in export. Now share
- // them with the VMLRenderer.
- if (Renderer === VMLRenderer) {
- each(['flag', 'circlepin', 'squarepin'], function(shape) {
- VMLRenderer.prototype.symbols[shape] = symbols[shape];
- });
- }
- /* ****************************************************************************
- * End Flags series code *
- *****************************************************************************/
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- Axis = H.Axis,
- correctFloat = H.correctFloat,
- defaultOptions = H.defaultOptions,
- defined = H.defined,
- destroyObjectProperties = H.destroyObjectProperties,
- doc = H.doc,
- each = H.each,
- fireEvent = H.fireEvent,
- hasTouch = H.hasTouch,
- isTouchDevice = H.isTouchDevice,
- merge = H.merge,
- pick = H.pick,
- removeEvent = H.removeEvent,
- svg = H.svg,
- wrap = H.wrap,
- swapXY;
- var defaultScrollbarOptions = {
- //enabled: true
- height: isTouchDevice ? 20 : 14,
- // trackBorderRadius: 0
- barBorderRadius: 0,
- buttonBorderRadius: 0,
- liveRedraw: svg && !isTouchDevice,
- margin: 10,
- minWidth: 6,
- //showFull: true,
- //size: null,
- step: 0.2,
- zIndex: 3,
- barBackgroundColor: '#cccccc',
- barBorderWidth: 1,
- barBorderColor: '#cccccc',
- buttonArrowColor: '#333333',
- buttonBackgroundColor: '#e6e6e6',
- buttonBorderColor: '#cccccc',
- buttonBorderWidth: 1,
- rifleColor: '#333333',
- trackBackgroundColor: '#f2f2f2',
- trackBorderColor: '#f2f2f2',
- trackBorderWidth: 1
- };
- defaultOptions.scrollbar = merge(true, defaultScrollbarOptions, defaultOptions.scrollbar);
- /**
- * When we have vertical scrollbar, rifles and arrow in buttons should be rotated.
- * The same method is used in Navigator's handles, to rotate them.
- * @param {Array} path - path to be rotated
- * @param {Boolean} vertical - if vertical scrollbar, swap x-y values
- */
- H.swapXY = swapXY = function(path, vertical) {
- var i,
- len = path.length,
- temp;
- if (vertical) {
- for (i = 0; i < len; i += 3) {
- temp = path[i + 1];
- path[i + 1] = path[i + 2];
- path[i + 2] = temp;
- }
- }
- return path;
- };
- /**
- * A reusable scrollbar, internally used in Highstock's navigator and optionally
- * on individual axes.
- *
- * @class
- * @param {Object} renderer
- * @param {Object} options
- * @param {Object} chart
- */
- function Scrollbar(renderer, options, chart) { // docs
- this.init(renderer, options, chart);
- }
- Scrollbar.prototype = {
- init: function(renderer, options, chart) {
- this.scrollbarButtons = [];
- this.renderer = renderer;
- this.userOptions = options;
- this.options = merge(defaultScrollbarOptions, options);
- this.chart = chart;
- this.size = pick(this.options.size, this.options.height); // backward compatibility
- // Init
- if (options.enabled) {
- this.render();
- this.initEvents();
- this.addEvents();
- }
- },
- /**
- * Render scrollbar with all required items.
- */
- render: function() {
- var scroller = this,
- renderer = scroller.renderer,
- options = scroller.options,
- size = scroller.size,
- group;
- // Draw the scrollbar group
- scroller.group = group = renderer.g('scrollbar').attr({
- zIndex: options.zIndex,
- translateY: -99999
- }).add();
- // Draw the scrollbar track:
- scroller.track = renderer.rect()
- .addClass('highcharts-scrollbar-track')
- .attr({
- x: 0,
- r: options.trackBorderRadius || 0,
- height: size,
- width: size
- }).add(group);
- scroller.track.attr({
- fill: options.trackBackgroundColor,
- stroke: options.trackBorderColor,
- 'stroke-width': options.trackBorderWidth
- });
- this.trackBorderWidth = scroller.track.strokeWidth();
- scroller.track.attr({
- y: -this.trackBorderWidth % 2 / 2
- });
- // Draw the scrollbar itself
- scroller.scrollbarGroup = renderer.g().add(group);
- scroller.scrollbar = renderer.rect()
- .addClass('highcharts-scrollbar-thumb')
- .attr({
- height: size,
- width: size,
- r: options.barBorderRadius || 0
- }).add(scroller.scrollbarGroup);
- scroller.scrollbarRifles = renderer.path(
- swapXY([
- 'M', -3, size / 4,
- 'L', -3, 2 * size / 3,
- 'M',
- 0, size / 4,
- 'L',
- 0, 2 * size / 3,
- 'M',
- 3, size / 4,
- 'L',
- 3, 2 * size / 3
- ], options.vertical))
- .addClass('highcharts-scrollbar-rifles')
- .add(scroller.scrollbarGroup);
- scroller.scrollbar.attr({
- fill: options.barBackgroundColor,
- stroke: options.barBorderColor,
- 'stroke-width': options.barBorderWidth
- });
- scroller.scrollbarRifles.attr({
- stroke: options.rifleColor,
- 'stroke-width': 1
- });
- scroller.scrollbarStrokeWidth = scroller.scrollbar.strokeWidth();
- scroller.scrollbarGroup.translate(-scroller.scrollbarStrokeWidth % 2 / 2, -scroller.scrollbarStrokeWidth % 2 / 2);
- // Draw the buttons:
- scroller.drawScrollbarButton(0);
- scroller.drawScrollbarButton(1);
- },
- /**
- * Position the scrollbar, method called from a parent with defined dimensions
- * @param {Number} x - x-position on the chart
- * @param {Number} y - y-position on the chart
- * @param {Number} width - width of the scrollbar
- * @param {Number} height - height of the scorllbar
- */
- position: function(x, y, width, height) {
- var scroller = this,
- options = scroller.options,
- vertical = options.vertical,
- xOffset = height,
- yOffset = 0,
- method = scroller.rendered ? 'animate' : 'attr';
- scroller.x = x;
- scroller.y = y + this.trackBorderWidth;
- scroller.width = width; // width with buttons
- scroller.height = height;
- scroller.xOffset = xOffset;
- scroller.yOffset = yOffset;
- // If Scrollbar is a vertical type, swap options:
- if (vertical) {
- scroller.width = scroller.yOffset = width = yOffset = scroller.size;
- scroller.xOffset = xOffset = 0;
- scroller.barWidth = height - width * 2; // width without buttons
- scroller.x = x = x + scroller.options.margin;
- } else {
- scroller.height = scroller.xOffset = height = xOffset = scroller.size;
- scroller.barWidth = width - height * 2; // width without buttons
- scroller.y = scroller.y + scroller.options.margin;
- }
- // Set general position for a group:
- scroller.group[method]({
- translateX: x,
- translateY: scroller.y
- });
- // Resize background/track:
- scroller.track[method]({
- width: width,
- height: height
- });
- // Move right/bottom button ot it's place:
- scroller.scrollbarButtons[1][method]({
- translateX: vertical ? 0 : width - xOffset,
- translateY: vertical ? height - yOffset : 0
- });
- },
- /**
- * Draw the scrollbar buttons with arrows
- * @param {Number} index 0 is left, 1 is right
- */
- drawScrollbarButton: function(index) {
- var scroller = this,
- renderer = scroller.renderer,
- scrollbarButtons = scroller.scrollbarButtons,
- options = scroller.options,
- size = scroller.size,
- group,
- tempElem;
- group = renderer.g().add(scroller.group);
- scrollbarButtons.push(group);
- // Create a rectangle for the scrollbar button
- tempElem = renderer.rect()
- .addClass('highcharts-scrollbar-button')
- .add(group);
- // Presentational attributes
- tempElem.attr({
- stroke: options.buttonBorderColor,
- 'stroke-width': options.buttonBorderWidth,
- fill: options.buttonBackgroundColor
- });
- // Place the rectangle based on the rendered stroke width
- tempElem.attr(tempElem.crisp({
- x: -0.5,
- y: -0.5,
- width: size + 1, // +1 to compensate for crispifying in rect method
- height: size + 1,
- r: options.buttonBorderRadius
- }, tempElem.strokeWidth()));
- // Button arrow
- tempElem = renderer
- .path(swapXY([
- 'M',
- size / 2 + (index ? -1 : 1),
- size / 2 - 3,
- 'L',
- size / 2 + (index ? -1 : 1),
- size / 2 + 3,
- 'L',
- size / 2 + (index ? 2 : -2),
- size / 2
- ], options.vertical))
- .addClass('highcharts-scrollbar-arrow')
- .add(scrollbarButtons[index]);
- tempElem.attr({
- fill: options.buttonArrowColor
- });
- },
- /**
- * Set scrollbar size, with a given scale.
- * @param {Number} from - scale (0-1) where bar should start
- * @param {Number} to - scale (0-1) where bar should end
- */
- setRange: function(from, to) {
- var scroller = this,
- options = scroller.options,
- vertical = options.vertical,
- minWidth = options.minWidth,
- fullWidth = scroller.barWidth,
- fromPX,
- toPX,
- newPos,
- newSize,
- newRiflesPos,
- method = this.rendered && !this.hasDragged ? 'animate' : 'attr';
- if (!defined(fullWidth)) {
- return;
- }
- from = Math.max(from, 0);
- fromPX = Math.ceil(fullWidth * from);
- toPX = fullWidth * Math.min(to, 1);
- scroller.calculatedWidth = newSize = correctFloat(toPX - fromPX);
- // We need to recalculate position, if minWidth is used
- if (newSize < minWidth) {
- fromPX = (fullWidth - minWidth + newSize) * from;
- newSize = minWidth;
- }
- newPos = Math.floor(fromPX + scroller.xOffset + scroller.yOffset);
- newRiflesPos = newSize / 2 - 0.5; // -0.5 -> rifle line width / 2
- // Store current position:
- scroller.from = from;
- scroller.to = to;
- if (!vertical) {
- scroller.scrollbarGroup[method]({
- translateX: newPos
- });
- scroller.scrollbar[method]({
- width: newSize
- });
- scroller.scrollbarRifles[method]({
- translateX: newRiflesPos
- });
- scroller.scrollbarLeft = newPos;
- scroller.scrollbarTop = 0;
- } else {
- scroller.scrollbarGroup[method]({
- translateY: newPos
- });
- scroller.scrollbar[method]({
- height: newSize
- });
- scroller.scrollbarRifles[method]({
- translateY: newRiflesPos
- });
- scroller.scrollbarTop = newPos;
- scroller.scrollbarLeft = 0;
- }
- if (newSize <= 12) {
- scroller.scrollbarRifles.hide();
- } else {
- scroller.scrollbarRifles.show(true);
- }
- // Show or hide the scrollbar based on the showFull setting
- if (options.showFull === false) {
- if (from <= 0 && to >= 1) {
- scroller.group.hide();
- } else {
- scroller.group.show();
- }
- }
- scroller.rendered = true;
- },
- /**
- * Init events methods, so we have an access to the Scrollbar itself
- */
- initEvents: function() {
- var scroller = this;
- /**
- * Event handler for the mouse move event.
- */
- scroller.mouseMoveHandler = function(e) {
- var normalizedEvent = scroller.chart.pointer.normalize(e),
- options = scroller.options,
- direction = options.vertical ? 'chartY' : 'chartX',
- initPositions = scroller.initPositions,
- scrollPosition,
- chartPosition,
- change;
- // In iOS, a mousemove event with e.pageX === 0 is fired when holding the finger
- // down in the center of the scrollbar. This should be ignored.
- if (scroller.grabbedCenter && (!e.touches || e.touches[0][direction] !== 0)) { // #4696, scrollbar failed on Android
- chartPosition = scroller.cursorToScrollbarPosition(normalizedEvent)[direction];
- scrollPosition = scroller[direction];
- change = chartPosition - scrollPosition;
- scroller.hasDragged = true;
- scroller.updatePosition(initPositions[0] + change, initPositions[1] + change);
- if (scroller.hasDragged) {
- fireEvent(scroller, 'changed', {
- from: scroller.from,
- to: scroller.to,
- trigger: 'scrollbar',
- DOMType: e.type,
- DOMEvent: e
- });
- }
- }
- };
- /**
- * Event handler for the mouse up event.
- */
- scroller.mouseUpHandler = function(e) {
- if (scroller.hasDragged) {
- fireEvent(scroller, 'changed', {
- from: scroller.from,
- to: scroller.to,
- trigger: 'scrollbar',
- DOMType: e.type,
- DOMEvent: e
- });
- }
- scroller.grabbedCenter = scroller.hasDragged = scroller.chartX = scroller.chartY = null;
- };
- scroller.mouseDownHandler = function(e) {
- var normalizedEvent = scroller.chart.pointer.normalize(e),
- mousePosition = scroller.cursorToScrollbarPosition(normalizedEvent);
- scroller.chartX = mousePosition.chartX;
- scroller.chartY = mousePosition.chartY;
- scroller.initPositions = [scroller.from, scroller.to];
- scroller.grabbedCenter = true;
- };
- scroller.buttonToMinClick = function(e) {
- var range = correctFloat(scroller.to - scroller.from) * scroller.options.step;
- scroller.updatePosition(correctFloat(scroller.from - range), correctFloat(scroller.to - range));
- fireEvent(scroller, 'changed', {
- from: scroller.from,
- to: scroller.to,
- trigger: 'scrollbar',
- DOMEvent: e
- });
- };
- scroller.buttonToMaxClick = function(e) {
- var range = (scroller.to - scroller.from) * scroller.options.step;
- scroller.updatePosition(scroller.from + range, scroller.to + range);
- fireEvent(scroller, 'changed', {
- from: scroller.from,
- to: scroller.to,
- trigger: 'scrollbar',
- DOMEvent: e
- });
- };
- scroller.trackClick = function(e) {
- var normalizedEvent = scroller.chart.pointer.normalize(e),
- range = scroller.to - scroller.from,
- top = scroller.y + scroller.scrollbarTop,
- left = scroller.x + scroller.scrollbarLeft;
- if ((scroller.options.vertical && normalizedEvent.chartY > top) ||
- (!scroller.options.vertical && normalizedEvent.chartX > left)) {
- // On the top or on the left side of the track:
- scroller.updatePosition(scroller.from + range, scroller.to + range);
- } else {
- // On the bottom or the right side of the track:
- scroller.updatePosition(scroller.from - range, scroller.to - range);
- }
- fireEvent(scroller, 'changed', {
- from: scroller.from,
- to: scroller.to,
- trigger: 'scrollbar',
- DOMEvent: e
- });
- };
- },
- /**
- * Get normalized (0-1) cursor position over the scrollbar
- * @param {Event} normalizedEvent - normalized event, with chartX and chartY values
- * @return {Object} Local position {chartX, chartY}
- */
- cursorToScrollbarPosition: function(normalizedEvent) {
- var scroller = this,
- options = scroller.options,
- minWidthDifference = options.minWidth > scroller.calculatedWidth ? options.minWidth : 0; // minWidth distorts translation
- return {
- chartX: (normalizedEvent.chartX - scroller.x - scroller.xOffset) / (scroller.barWidth - minWidthDifference),
- chartY: (normalizedEvent.chartY - scroller.y - scroller.yOffset) / (scroller.barWidth - minWidthDifference)
- };
- },
- /**
- * Update position option in the Scrollbar, with normalized 0-1 scale
- */
- updatePosition: function(from, to) {
- if (to > 1) {
- from = correctFloat(1 - correctFloat(to - from));
- to = 1;
- }
- if (from < 0) {
- to = correctFloat(to - from);
- from = 0;
- }
- this.from = from;
- this.to = to;
- },
- /**
- * Update the scrollbar with new options
- */
- update: function(options) {
- this.destroy();
- this.init(this.chart.renderer, merge(true, this.options, options), this.chart);
- },
- /**
- * Set up the mouse and touch events for the Scrollbar
- */
- addEvents: function() {
- var buttonsOrder = this.options.inverted ? [1, 0] : [0, 1],
- buttons = this.scrollbarButtons,
- bar = this.scrollbarGroup.element,
- track = this.track.element,
- mouseDownHandler = this.mouseDownHandler,
- mouseMoveHandler = this.mouseMoveHandler,
- mouseUpHandler = this.mouseUpHandler,
- _events;
- // Mouse events
- _events = [
- [buttons[buttonsOrder[0]].element, 'click', this.buttonToMinClick],
- [buttons[buttonsOrder[1]].element, 'click', this.buttonToMaxClick],
- [track, 'click', this.trackClick],
- [bar, 'mousedown', mouseDownHandler],
- [doc, 'mousemove', mouseMoveHandler],
- [doc, 'mouseup', mouseUpHandler]
- ];
- // Touch events
- if (hasTouch) {
- _events.push(
- [bar, 'touchstart', mouseDownHandler], [doc, 'touchmove', mouseMoveHandler], [doc, 'touchend', mouseUpHandler]
- );
- }
- // Add them all
- each(_events, function(args) {
- addEvent.apply(null, args);
- });
- this._events = _events;
- },
- /**
- * Removes the event handlers attached previously with addEvents.
- */
- removeEvents: function() {
- each(this._events, function(args) {
- removeEvent.apply(null, args);
- });
- this._events.length = 0;
- },
- /**
- * Destroys allocated elements.
- */
- destroy: function() {
- var scroller = this.chart.scroller;
- // Disconnect events added in addEvents
- this.removeEvents();
- // Destroy properties
- each(['track', 'scrollbarRifles', 'scrollbar', 'scrollbarGroup', 'group'], function(prop) {
- if (this[prop] && this[prop].destroy) {
- this[prop] = this[prop].destroy();
- }
- }, this);
- if (scroller && this === scroller.scrollbar) { // #6421, chart may have more scrollbars
- scroller.scrollbar = null;
- // Destroy elements in collection
- destroyObjectProperties(scroller.scrollbarButtons);
- }
- }
- };
- /**
- * Wrap axis initialization and create scrollbar if enabled:
- */
- wrap(Axis.prototype, 'init', function(proceed) {
- var axis = this;
- proceed.apply(axis, Array.prototype.slice.call(arguments, 1));
- if (axis.options.scrollbar && axis.options.scrollbar.enabled) {
- // Predefined options:
- axis.options.scrollbar.vertical = !axis.horiz;
- axis.options.startOnTick = axis.options.endOnTick = false;
- axis.scrollbar = new Scrollbar(axis.chart.renderer, axis.options.scrollbar, axis.chart);
- addEvent(axis.scrollbar, 'changed', function(e) {
- var unitedMin = Math.min(pick(axis.options.min, axis.min), axis.min, axis.dataMin),
- unitedMax = Math.max(pick(axis.options.max, axis.max), axis.max, axis.dataMax),
- range = unitedMax - unitedMin,
- to,
- from;
- if ((axis.horiz && !axis.reversed) || (!axis.horiz && axis.reversed)) {
- to = unitedMin + range * this.to;
- from = unitedMin + range * this.from;
- } else {
- // y-values in browser are reversed, but this also applies for reversed horizontal axis:
- to = unitedMin + range * (1 - this.from);
- from = unitedMin + range * (1 - this.to);
- }
- axis.setExtremes(from, to, true, false, e);
- });
- }
- });
- /**
- * Wrap rendering axis, and update scrollbar if one is created:
- */
- wrap(Axis.prototype, 'render', function(proceed) {
- var axis = this,
- scrollMin = Math.min(pick(axis.options.min, axis.min), axis.min, axis.dataMin),
- scrollMax = Math.max(pick(axis.options.max, axis.max), axis.max, axis.dataMax),
- scrollbar = axis.scrollbar,
- titleOffset = axis.titleOffset || 0,
- offsetsIndex,
- from,
- to;
- proceed.apply(axis, Array.prototype.slice.call(arguments, 1));
- if (scrollbar) {
- if (axis.horiz) {
- scrollbar.position(
- axis.left,
- axis.top + axis.height + 2 + axis.chart.scrollbarsOffsets[1] +
- (axis.opposite ?
- 0 :
- titleOffset + axis.axisTitleMargin + axis.offset
- ),
- axis.width,
- axis.height
- );
- offsetsIndex = 1;
- } else {
- scrollbar.position(
- axis.left + axis.width + 2 + axis.chart.scrollbarsOffsets[0] +
- (axis.opposite ?
- titleOffset + axis.axisTitleMargin + axis.offset :
- 0
- ),
- axis.top,
- axis.width,
- axis.height
- );
- offsetsIndex = 0;
- }
- if ((!axis.opposite && !axis.horiz) || (axis.opposite && axis.horiz)) {
- axis.chart.scrollbarsOffsets[offsetsIndex] +=
- axis.scrollbar.size + axis.scrollbar.options.margin;
- }
- if (isNaN(scrollMin) || isNaN(scrollMax) || !defined(axis.min) || !defined(axis.max)) {
- scrollbar.setRange(0, 0); // default action: when there is not extremes on the axis, but scrollbar exists, make it full size
- } else {
- from = (axis.min - scrollMin) / (scrollMax - scrollMin);
- to = (axis.max - scrollMin) / (scrollMax - scrollMin);
- if ((axis.horiz && !axis.reversed) || (!axis.horiz && axis.reversed)) {
- scrollbar.setRange(from, to);
- } else {
- scrollbar.setRange(1 - to, 1 - from); // inverse vertical axis
- }
- }
- }
- });
- /**
- * Make space for a scrollbar
- */
- wrap(Axis.prototype, 'getOffset', function(proceed) {
- var axis = this,
- index = axis.horiz ? 2 : 1,
- scrollbar = axis.scrollbar;
- proceed.apply(axis, Array.prototype.slice.call(arguments, 1));
- if (scrollbar) {
- axis.chart.scrollbarsOffsets = [0, 0]; // reset scrollbars offsets
- axis.chart.axisOffset[index] += scrollbar.size + scrollbar.options.margin;
- }
- });
- /**
- * Destroy scrollbar when connected to the specific axis
- */
- wrap(Axis.prototype, 'destroy', function(proceed) {
- if (this.scrollbar) {
- this.scrollbar = this.scrollbar.destroy();
- }
- proceed.apply(this, Array.prototype.slice.call(arguments, 1));
- });
- H.Scrollbar = Scrollbar;
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- /* ****************************************************************************
- * Start Navigator code *
- *****************************************************************************/
- var addEvent = H.addEvent,
- Axis = H.Axis,
- Chart = H.Chart,
- color = H.color,
- defaultDataGroupingUnits = H.defaultDataGroupingUnits,
- defaultOptions = H.defaultOptions,
- defined = H.defined,
- destroyObjectProperties = H.destroyObjectProperties,
- doc = H.doc,
- each = H.each,
- erase = H.erase,
- error = H.error,
- extend = H.extend,
- grep = H.grep,
- hasTouch = H.hasTouch,
- isNumber = H.isNumber,
- isObject = H.isObject,
- merge = H.merge,
- pick = H.pick,
- removeEvent = H.removeEvent,
- Scrollbar = H.Scrollbar,
- Series = H.Series,
- seriesTypes = H.seriesTypes,
- wrap = H.wrap,
- swapXY = H.swapXY,
- units = [].concat(defaultDataGroupingUnits), // copy
- defaultSeriesType,
- // Finding the min or max of a set of variables where we don't know if they are defined,
- // is a pattern that is repeated several places in Highcharts. Consider making this
- // a global utility method.
- numExt = function(extreme) {
- var numbers = grep(arguments, isNumber);
- if (numbers.length) {
- return Math[extreme].apply(0, numbers);
- }
- };
- // add more resolution to units
- units[4] = ['day', [1, 2, 3, 4]]; // allow more days
- units[5] = ['week', [1, 2, 3]]; // allow more weeks
- defaultSeriesType = seriesTypes.areaspline === undefined ? 'line' : 'areaspline';
- extend(defaultOptions, {
- navigator: {
- //enabled: true,
- height: 40,
- margin: 25,
- maskInside: true,
- handles: {
- backgroundColor: '#f2f2f2',
- borderColor: '#999999'
- },
- maskFill: color('#6685c2').setOpacity(0.3).get(),
- outlineColor: '#cccccc',
- outlineWidth: 1,
- series: {
- type: defaultSeriesType,
- color: '#335cad',
- fillOpacity: 0.05,
- lineWidth: 1,
- compare: null,
- dataGrouping: {
- approximation: 'average',
- enabled: true,
- groupPixelWidth: 2,
- smoothed: true,
- units: units
- },
- dataLabels: {
- enabled: false,
- zIndex: 2 // #1839
- },
- id: 'highcharts-navigator-series',
- className: 'highcharts-navigator-series',
- lineColor: null, // Allow color setting while disallowing default candlestick setting (#4602)
- marker: {
- enabled: false
- },
- pointRange: 0,
- shadow: false,
- threshold: null
- },
- //top: undefined,
- //opposite: undefined,
- xAxis: {
- className: 'highcharts-navigator-xaxis',
- tickLength: 0,
- lineWidth: 0,
- gridLineColor: '#e6e6e6',
- gridLineWidth: 1,
- tickPixelInterval: 200,
- labels: {
- align: 'left',
- style: {
- color: '#999999'
- },
- x: 3,
- y: -4
- },
- crosshair: false
- },
- yAxis: {
- className: 'highcharts-navigator-yaxis',
- gridLineWidth: 0,
- startOnTick: false,
- endOnTick: false,
- minPadding: 0.1,
- maxPadding: 0.1,
- labels: {
- enabled: false
- },
- crosshair: false,
- title: {
- text: null
- },
- tickLength: 0,
- tickWidth: 0
- }
- }
- });
- /**
- * The Navigator class
- * @param {Object} chart - Chart object
- * @class
- */
- function Navigator(chart) {
- this.init(chart);
- }
- Navigator.prototype = {
- /**
- * Draw one of the handles on the side of the zoomed range in the navigator
- * @param {Number} x The x center for the handle
- * @param {Number} index 0 for left and 1 for right
- * @param {Boolean} inverted flag for chart.inverted
- * @param {String} verb use 'animate' or 'attr'
- */
- drawHandle: function(x, index, inverted, verb) {
- var navigator = this;
- // Place it
- navigator.handles[index][verb](inverted ? {
- translateX: Math.round(navigator.left + navigator.height / 2 - 8),
- translateY: Math.round(navigator.top + parseInt(x, 10) + 0.5)
- } : {
- translateX: Math.round(navigator.left + parseInt(x, 10)),
- translateY: Math.round(navigator.top + navigator.height / 2 - 8)
- });
- },
- /**
- * Draw one of the handles on the side of the zoomed range in the navigator
- * @param {Boolean} inverted flag for chart.inverted
- * @returns {Array} Path to be used in a handle
- */
- getHandlePath: function(inverted) {
- return swapXY([
- 'M', -4.5, 0.5,
- 'L',
- 3.5, 0.5,
- 'L',
- 3.5, 15.5,
- 'L', -4.5, 15.5,
- 'L', -4.5, 0.5,
- 'M', -1.5, 4,
- 'L', -1.5, 12,
- 'M',
- 0.5, 4,
- 'L',
- 0.5, 12
- ], inverted);
- },
- /**
- * Render outline around the zoomed range
- * @param {Number} zoomedMin in pixels position where zoomed range starts
- * @param {Number} zoomedMax in pixels position where zoomed range ends
- * @param {Boolean} inverted flag if chart is inverted
- * @param {String} verb use 'animate' or 'attr'
- */
- drawOutline: function(zoomedMin, zoomedMax, inverted, verb) {
- var navigator = this,
- maskInside = navigator.navigatorOptions.maskInside,
- outlineWidth = navigator.outline.strokeWidth(),
- halfOutline = outlineWidth / 2,
- outlineCorrection = (outlineWidth % 2) / 2, // #5800
- outlineHeight = navigator.outlineHeight,
- scrollbarHeight = navigator.scrollbarHeight,
- navigatorSize = navigator.size,
- left = navigator.left - scrollbarHeight,
- navigatorTop = navigator.top,
- verticalMin,
- path;
- if (inverted) {
- left -= halfOutline;
- verticalMin = navigatorTop + zoomedMax + outlineCorrection;
- zoomedMax = navigatorTop + zoomedMin + outlineCorrection;
- path = [
- 'M',
- left + outlineHeight,
- navigatorTop - scrollbarHeight - outlineCorrection, // top edge
- 'L',
- left + outlineHeight,
- verticalMin, // top right of zoomed range
- 'L',
- left,
- verticalMin, // top left of z.r.
- 'L',
- left,
- zoomedMax, // bottom left of z.r.
- 'L',
- left + outlineHeight,
- zoomedMax, // bottom right of z.r.
- 'L',
- left + outlineHeight,
- navigatorTop + navigatorSize + scrollbarHeight // bottom edge
- ].concat(maskInside ? [
- 'M',
- left + outlineHeight,
- verticalMin - halfOutline, // upper left of zoomed range
- 'L',
- left + outlineHeight,
- zoomedMax + halfOutline // upper right of z.r.
- ] : []);
- } else {
- zoomedMin += left + scrollbarHeight - outlineCorrection;
- zoomedMax += left + scrollbarHeight - outlineCorrection;
- navigatorTop += halfOutline;
- path = [
- 'M',
- left,
- navigatorTop, // left
- 'L',
- zoomedMin,
- navigatorTop, // upper left of zoomed range
- 'L',
- zoomedMin,
- navigatorTop + outlineHeight, // lower left of z.r.
- 'L',
- zoomedMax,
- navigatorTop + outlineHeight, // lower right of z.r.
- 'L',
- zoomedMax,
- navigatorTop, // upper right of z.r.
- 'L',
- left + navigatorSize + scrollbarHeight * 2,
- navigatorTop // right
- ].concat(maskInside ? [
- 'M',
- zoomedMin - halfOutline,
- navigatorTop, // upper left of zoomed range
- 'L',
- zoomedMax + halfOutline,
- navigatorTop // upper right of z.r.
- ] : []);
- }
- navigator.outline[verb]({
- d: path
- });
- },
- /**
- * Render outline around the zoomed range
- * @param {Number} zoomedMin in pixels position where zoomed range starts
- * @param {Number} zoomedMax in pixels position where zoomed range ends
- * @param {Boolean} inverted flag if chart is inverted
- * @param {String} verb use 'animate' or 'attr'
- */
- drawMasks: function(zoomedMin, zoomedMax, inverted, verb) {
- var navigator = this,
- left = navigator.left,
- top = navigator.top,
- navigatorHeight = navigator.height,
- height,
- width,
- x,
- y;
- // Determine rectangle position & size
- // According to (non)inverted position:
- if (inverted) {
- x = [left, left, left];
- y = [top, top + zoomedMin, top + zoomedMax];
- width = [navigatorHeight, navigatorHeight, navigatorHeight];
- height = [
- zoomedMin,
- zoomedMax - zoomedMin,
- navigator.size - zoomedMax
- ];
- } else {
- x = [left, left + zoomedMin, left + zoomedMax];
- y = [top, top, top];
- width = [
- zoomedMin,
- zoomedMax - zoomedMin,
- navigator.size - zoomedMax
- ];
- height = [navigatorHeight, navigatorHeight, navigatorHeight];
- }
- each(navigator.shades, function(shade, i) {
- shade[verb]({
- x: x[i],
- y: y[i],
- width: width[i],
- height: height[i]
- });
- });
- },
- /**
- * Generate DOM elements for a navigator:
- * - main navigator group
- * - all shades
- * - outline
- * - handles
- */
- renderElements: function() {
- var navigator = this,
- navigatorOptions = navigator.navigatorOptions,
- maskInside = navigatorOptions.maskInside,
- chart = navigator.chart,
- inverted = chart.inverted,
- renderer = chart.renderer,
- navigatorGroup;
- // Create the main navigator group
- navigator.navigatorGroup = navigatorGroup = renderer.g('navigator')
- .attr({
- zIndex: 8,
- visibility: 'hidden'
- })
- .add();
- var mouseCursor = {
- cursor: inverted ? 'ns-resize' : 'ew-resize'
- };
- // Create masks, each mask will get events and fill:
- each([!maskInside, maskInside, !maskInside], function(hasMask, index) {
- navigator.shades[index] = renderer.rect()
- .addClass('highcharts-navigator-mask' +
- (index === 1 ? '-inside' : '-outside'))
- .attr({
- fill: hasMask ? navigatorOptions.maskFill : 'rgba(0,0,0,0)'
- })
- .css(index === 1 && mouseCursor)
- .add(navigatorGroup);
- });
- // Create the outline:
- navigator.outline = renderer.path()
- .addClass('highcharts-navigator-outline')
- .attr({
- 'stroke-width': navigatorOptions.outlineWidth,
- stroke: navigatorOptions.outlineColor
- })
- .add(navigatorGroup);
- // Create the handlers:
- each([0, 1], function(index) {
- navigator.handles[index] = renderer
- .path(navigator.getHandlePath(inverted))
- // zIndex = 6 for right handle, 7 for left.
- // Can't be 10, because of the tooltip in inverted chart #2908
- .attr({
- zIndex: 7 - index
- })
- .addClass(
- 'highcharts-navigator-handle highcharts-navigator-handle-' + ['left', 'right'][index]
- ).add(navigatorGroup);
- var handlesOptions = navigatorOptions.handles;
- navigator.handles[index]
- .attr({
- fill: handlesOptions.backgroundColor,
- stroke: handlesOptions.borderColor,
- 'stroke-width': 1
- })
- .css(mouseCursor);
- });
- },
- /**
- * Update navigator
- * @param {Object} options Options to merge in when updating navigator
- */
- update: function(options) {
- this.destroy();
- var chartOptions = this.chart.options;
- merge(true, chartOptions.navigator, this.options, options);
- this.init(this.chart);
- },
- /**
- * Render the navigator
- * @param {Number} min X axis value minimum
- * @param {Number} max X axis value maximum
- * @param {Number} pxMin Pixel value minimum
- * @param {Number} pxMax Pixel value maximum
- */
- render: function(min, max, pxMin, pxMax) {
- var navigator = this,
- chart = navigator.chart,
- navigatorWidth,
- scrollbarLeft,
- scrollbarTop,
- scrollbarHeight = navigator.scrollbarHeight,
- navigatorSize,
- xAxis = navigator.xAxis,
- scrollbarXAxis = xAxis.fake ? chart.xAxis[0] : xAxis,
- navigatorEnabled = navigator.navigatorEnabled,
- zoomedMin,
- zoomedMax,
- rendered = navigator.rendered,
- inverted = chart.inverted,
- verb,
- newMin,
- newMax,
- minRange = chart.xAxis[0].minRange;
- // Don't redraw while moving the handles (#4703).
- if (this.hasDragged && !defined(pxMin)) {
- return;
- }
- // Don't render the navigator until we have data (#486, #4202, #5172).
- if (!isNumber(min) || !isNumber(max)) {
- // However, if navigator was already rendered, we may need to resize
- // it. For example hidden series, but visible navigator (#6022).
- if (rendered) {
- pxMin = 0;
- pxMax = xAxis.width;
- } else {
- return;
- }
- }
- navigator.left = pick(
- xAxis.left,
- // in case of scrollbar only, without navigator
- chart.plotLeft + scrollbarHeight + (inverted ? chart.plotWidth : 0)
- );
- navigator.size = zoomedMax = navigatorSize = pick(
- xAxis.len,
- (inverted ? chart.plotHeight : chart.plotWidth) - 2 * scrollbarHeight
- );
- if (inverted) {
- navigatorWidth = scrollbarHeight;
- } else {
- navigatorWidth = navigatorSize + 2 * scrollbarHeight;
- }
- // Get the pixel position of the handles
- pxMin = pick(pxMin, xAxis.toPixels(min, true));
- pxMax = pick(pxMax, xAxis.toPixels(max, true));
- if (!isNumber(pxMin) || Math.abs(pxMin) === Infinity) { // Verify (#1851, #2238)
- pxMin = 0;
- pxMax = navigatorWidth;
- }
- // Are we below the minRange? (#2618, #6191)
- newMin = xAxis.toValue(pxMin, true);
- newMax = xAxis.toValue(pxMax, true);
- if (Math.abs(newMax - newMin) < minRange) {
- if (this.grabbedLeft) {
- pxMin = xAxis.toPixels(newMax - minRange, true);
- } else if (this.grabbedRight) {
- pxMax = xAxis.toPixels(newMin + minRange, true);
- } else {
- return;
- }
- }
- // Handles are allowed to cross, but never exceed the plot area
- navigator.zoomedMax = Math.min(Math.max(pxMin, pxMax, 0), zoomedMax);
- navigator.zoomedMin = Math.min(
- Math.max(
- navigator.fixedWidth ?
- navigator.zoomedMax - navigator.fixedWidth :
- Math.min(pxMin, pxMax),
- 0
- ),
- zoomedMax
- );
- navigator.range = navigator.zoomedMax - navigator.zoomedMin;
- zoomedMax = Math.round(navigator.zoomedMax);
- zoomedMin = Math.round(navigator.zoomedMin);
- if (navigatorEnabled) {
- navigator.navigatorGroup.attr({
- visibility: 'visible'
- });
- // Place elements
- verb = rendered && !navigator.hasDragged ? 'animate' : 'attr';
- navigator.drawMasks(zoomedMin, zoomedMax, inverted, verb);
- navigator.drawOutline(zoomedMin, zoomedMax, inverted, verb);
- navigator.drawHandle(zoomedMin, 0, inverted, verb);
- navigator.drawHandle(zoomedMax, 1, inverted, verb);
- }
- if (navigator.scrollbar) {
- if (inverted) {
- scrollbarTop = navigator.top - scrollbarHeight;
- scrollbarLeft = navigator.left - scrollbarHeight +
- (navigatorEnabled || !scrollbarXAxis.opposite ? 0 :
- // Multiple axes has offsets:
- (scrollbarXAxis.titleOffset || 0) +
- // Self margin from the axis.title
- scrollbarXAxis.axisTitleMargin
- );
- scrollbarHeight = navigatorSize + 2 * scrollbarHeight;
- } else {
- scrollbarTop = navigator.top +
- (navigatorEnabled ? navigator.height : -scrollbarHeight);
- scrollbarLeft = navigator.left - scrollbarHeight;
- }
- // Reposition scrollbar
- navigator.scrollbar.position(
- scrollbarLeft,
- scrollbarTop,
- navigatorWidth,
- scrollbarHeight
- );
- // Keep scale 0-1
- navigator.scrollbar.setRange(
- // Use real value, not rounded because range can be very small (#1716)
- navigator.zoomedMin / navigatorSize,
- navigator.zoomedMax / navigatorSize
- );
- }
- navigator.rendered = true;
- },
- /**
- * Set up the mouse and touch events for the navigator
- */
- addMouseEvents: function() {
- var navigator = this,
- chart = navigator.chart,
- container = chart.container,
- eventsToUnbind = [],
- mouseMoveHandler,
- mouseUpHandler;
- /**
- * Create mouse events' handlers.
- * Make them as separate functions to enable wrapping them:
- */
- navigator.mouseMoveHandler = mouseMoveHandler = function(e) {
- navigator.onMouseMove(e);
- };
- navigator.mouseUpHandler = mouseUpHandler = function(e) {
- navigator.onMouseUp(e);
- };
- // Add shades and handles mousedown events
- eventsToUnbind = navigator.getPartsEvents('mousedown');
- // Add mouse move and mouseup events. These are bind to doc/container,
- // because Navigator.grabbedSomething flags are stored in mousedown events:
- eventsToUnbind.push(
- addEvent(container, 'mousemove', mouseMoveHandler),
- addEvent(doc, 'mouseup', mouseUpHandler)
- );
- // Touch events
- if (hasTouch) {
- eventsToUnbind.push(
- addEvent(container, 'touchmove', mouseMoveHandler),
- addEvent(doc, 'touchend', mouseUpHandler)
- );
- eventsToUnbind.concat(navigator.getPartsEvents('touchstart'));
- }
- navigator.eventsToUnbind = eventsToUnbind;
- // Data events
- if (navigator.series && navigator.series[0]) {
- eventsToUnbind.push(
- addEvent(navigator.series[0].xAxis, 'foundExtremes', function() {
- chart.navigator.modifyNavigatorAxisExtremes();
- })
- );
- }
- },
- /**
- * Generate events for handles and masks
- * @param {String} eventName Event name handler, 'mousedown' or 'touchstart'
- * @returns {Array} An array of arrays: [DOMElement, eventName, callback].
- */
- getPartsEvents: function(eventName) {
- var navigator = this,
- events = [];
- each(['shades', 'handles'], function(name) {
- each(navigator[name], function(navigatorItem, index) {
- events.push(
- addEvent(
- navigatorItem.element,
- eventName,
- function(e) {
- navigator[name + 'Mousedown'](e, index);
- }
- )
- );
- });
- });
- return events;
- },
- /**
- * Mousedown on a shaded mask, either:
- * - will be stored for future drag&drop
- * - will directly shift to a new range
- *
- * @param {Object} e Mouse event
- * @param {Number} index Index of a mask in Navigator.shades array
- */
- shadesMousedown: function(e, index) {
- e = this.chart.pointer.normalize(e);
- var navigator = this,
- chart = navigator.chart,
- xAxis = navigator.xAxis,
- zoomedMin = navigator.zoomedMin,
- navigatorPosition = navigator.left,
- navigatorSize = navigator.size,
- range = navigator.range,
- chartX = e.chartX,
- fixedMax,
- ext,
- left;
- // For inverted chart, swap some options:
- if (chart.inverted) {
- chartX = e.chartY;
- navigatorPosition = navigator.top;
- }
- if (index === 1) {
- // Store information for drag&drop
- navigator.grabbedCenter = chartX;
- navigator.fixedWidth = range;
- navigator.dragOffset = chartX - zoomedMin;
- } else {
- // Shift the range by clicking on shaded areas
- left = chartX - navigatorPosition - range / 2;
- if (index === 0) {
- left = Math.max(0, left);
- } else if (index === 2 && left + range >= navigatorSize) {
- left = navigatorSize - range;
- fixedMax = navigator.getUnionExtremes().dataMax; // #2293, #3543
- }
- if (left !== zoomedMin) { // it has actually moved
- navigator.fixedWidth = range; // #1370
- ext = xAxis.toFixedRange(left, left + range, null, fixedMax);
- chart.xAxis[0].setExtremes(
- Math.min(ext.min, ext.max),
- Math.max(ext.min, ext.max),
- true,
- null, // auto animation
- {
- trigger: 'navigator'
- }
- );
- }
- }
- },
- /**
- * Mousedown on a handle mask.
- * Will store necessary information for drag&drop.
- *
- * @param {Object} e Mouse event
- * @param {Number} index Index of a handle in Navigator.handles array
- */
- handlesMousedown: function(e, index) {
- e = this.chart.pointer.normalize(e);
- var navigator = this,
- chart = navigator.chart,
- baseXAxis = chart.xAxis[0],
- // For reversed axes, min and max are chagned,
- // so the other extreme should be stored
- reverse = (chart.inverted && !baseXAxis.reversed) ||
- (!chart.inverted && baseXAxis.reversed);
- if (index === 0) {
- // Grab the left handle
- navigator.grabbedLeft = true;
- navigator.otherHandlePos = navigator.zoomedMax;
- navigator.fixedExtreme = reverse ? baseXAxis.min : baseXAxis.max;
- } else {
- // Grab the right handle
- navigator.grabbedRight = true;
- navigator.otherHandlePos = navigator.zoomedMin;
- navigator.fixedExtreme = reverse ? baseXAxis.max : baseXAxis.min;
- }
- chart.fixedRange = null;
- },
- /**
- * Mouse move event based on x/y mouse position.
- * @param {Object} e Mouse event
- */
- onMouseMove: function(e) {
- var navigator = this,
- chart = navigator.chart,
- left = navigator.left,
- navigatorSize = navigator.navigatorSize,
- range = navigator.range,
- dragOffset = navigator.dragOffset,
- inverted = chart.inverted,
- chartX;
- // In iOS, a mousemove event with e.pageX === 0 is fired when holding the finger
- // down in the center of the scrollbar. This should be ignored.
- if (!e.touches || e.touches[0].pageX !== 0) { // #4696, scrollbar failed on Android
- e = chart.pointer.normalize(e);
- chartX = e.chartX;
- // Swap some options for inverted chart
- if (inverted) {
- left = navigator.top;
- chartX = e.chartY;
- }
- // Drag left handle or top handle
- if (navigator.grabbedLeft) {
- navigator.hasDragged = true;
- navigator.render(
- 0,
- 0,
- chartX - left,
- navigator.otherHandlePos
- );
- // Drag right handle or bottom handle
- } else if (navigator.grabbedRight) {
- navigator.hasDragged = true;
- navigator.render(
- 0,
- 0,
- navigator.otherHandlePos,
- chartX - left
- );
- // Drag scrollbar or open area in navigator
- } else if (navigator.grabbedCenter) {
- navigator.hasDragged = true;
- if (chartX < dragOffset) { // outside left
- chartX = dragOffset;
- } else if (chartX > navigatorSize + dragOffset - range) { // outside right
- chartX = navigatorSize + dragOffset - range;
- }
- navigator.render(
- 0,
- 0,
- chartX - dragOffset,
- chartX - dragOffset + range
- );
- }
- if (navigator.hasDragged && navigator.scrollbar && navigator.scrollbar.options.liveRedraw) {
- e.DOMType = e.type; // DOMType is for IE8 because it can't read type async
- setTimeout(function() {
- navigator.onMouseUp(e);
- }, 0);
- }
- }
- },
- /**
- * Mouse up event based on x/y mouse position.
- * @param {Object} e Mouse event
- */
- onMouseUp: function(e) {
- var navigator = this,
- chart = navigator.chart,
- xAxis = navigator.xAxis,
- scrollbar = navigator.scrollbar,
- fixedMin,
- fixedMax,
- ext,
- DOMEvent = e.DOMEvent || e;
- if (
- // MouseUp is called for both, navigator and scrollbar (that order),
- // which causes calling afterSetExtremes twice. Prevent first call
- // by checking if scrollbar is going to set new extremes (#6334)
- (navigator.hasDragged && (!scrollbar || !scrollbar.hasDragged)) ||
- e.trigger === 'scrollbar'
- ) {
- // When dragging one handle, make sure the other one doesn't change
- if (navigator.zoomedMin === navigator.otherHandlePos) {
- fixedMin = navigator.fixedExtreme;
- } else if (navigator.zoomedMax === navigator.otherHandlePos) {
- fixedMax = navigator.fixedExtreme;
- }
- // Snap to right edge (#4076)
- if (navigator.zoomedMax === navigator.size) {
- fixedMax = navigator.getUnionExtremes().dataMax;
- }
- ext = xAxis.toFixedRange(
- navigator.zoomedMin,
- navigator.zoomedMax,
- fixedMin,
- fixedMax
- );
- if (defined(ext.min)) {
- chart.xAxis[0].setExtremes(
- Math.min(ext.min, ext.max),
- Math.max(ext.min, ext.max),
- true,
- navigator.hasDragged ? false : null, // Run animation when clicking buttons, scrollbar track etc, but not when dragging handles or scrollbar
- {
- trigger: 'navigator',
- triggerOp: 'navigator-drag',
- DOMEvent: DOMEvent // #1838
- }
- );
- }
- }
- if (e.DOMType !== 'mousemove') {
- navigator.grabbedLeft = navigator.grabbedRight =
- navigator.grabbedCenter = navigator.fixedWidth =
- navigator.fixedExtreme = navigator.otherHandlePos =
- navigator.hasDragged = navigator.dragOffset = null;
- }
- },
- /**
- * Removes the event handlers attached previously with addEvents.
- */
- removeEvents: function() {
- if (this.eventsToUnbind) {
- each(this.eventsToUnbind, function(unbind) {
- unbind();
- });
- this.eventsToUnbind = undefined;
- }
- this.removeBaseSeriesEvents();
- },
- /**
- * Remove data events.
- */
- removeBaseSeriesEvents: function() {
- var baseSeries = this.baseSeries || [];
- if (this.navigatorEnabled && baseSeries[0] && this.navigatorOptions.adaptToUpdatedData !== false) {
- each(baseSeries, function(series) {
- removeEvent(series, 'updatedData', this.updatedDataHandler);
- }, this);
- // We only listen for extremes-events on the first baseSeries
- if (baseSeries[0].xAxis) {
- removeEvent(baseSeries[0].xAxis, 'foundExtremes', this.modifyBaseAxisExtremes);
- }
- }
- },
- /**
- * Initiate the Navigator object
- */
- init: function(chart) {
- var chartOptions = chart.options,
- navigatorOptions = chartOptions.navigator,
- navigatorEnabled = navigatorOptions.enabled,
- scrollbarOptions = chartOptions.scrollbar,
- scrollbarEnabled = scrollbarOptions.enabled,
- height = navigatorEnabled ? navigatorOptions.height : 0,
- scrollbarHeight = scrollbarEnabled ? scrollbarOptions.height : 0;
- this.handles = [];
- this.shades = [];
- this.chart = chart;
- this.setBaseSeries();
- this.height = height;
- this.scrollbarHeight = scrollbarHeight;
- this.scrollbarEnabled = scrollbarEnabled;
- this.navigatorEnabled = navigatorEnabled;
- this.navigatorOptions = navigatorOptions;
- this.scrollbarOptions = scrollbarOptions;
- this.outlineHeight = height + scrollbarHeight;
- this.opposite = pick(navigatorOptions.opposite, !navigatorEnabled && chart.inverted); // #6262
- var navigator = this,
- baseSeries = navigator.baseSeries,
- xAxisIndex = chart.xAxis.length,
- yAxisIndex = chart.yAxis.length,
- baseXaxis = baseSeries && baseSeries[0] && baseSeries[0].xAxis || chart.xAxis[0];
- // Make room for the navigator, can be placed around the chart:
- chart.extraMargin = {
- type: navigator.opposite ? 'plotTop' : 'marginBottom',
- value: (navigatorEnabled || !chart.inverted ? navigator.outlineHeight : 0) + navigatorOptions.margin
- };
- if (chart.inverted) {
- chart.extraMargin.type = navigator.opposite ? 'marginRight' : 'plotLeft';
- }
- chart.isDirtyBox = true;
- if (navigator.navigatorEnabled) {
- // an x axis is required for scrollbar also
- navigator.xAxis = new Axis(chart, merge({
- // inherit base xAxis' break and ordinal options
- breaks: baseXaxis.options.breaks,
- ordinal: baseXaxis.options.ordinal
- }, navigatorOptions.xAxis, {
- id: 'navigator-x-axis',
- yAxis: 'navigator-y-axis',
- isX: true,
- type: 'datetime',
- index: xAxisIndex,
- offset: 0,
- keepOrdinalPadding: true, // #2436
- startOnTick: false,
- endOnTick: false,
- minPadding: 0,
- maxPadding: 0,
- zoomEnabled: false
- }, chart.inverted ? {
- offsets: [scrollbarHeight, 0, -scrollbarHeight, 0],
- width: height
- } : {
- offsets: [0, -scrollbarHeight, 0, scrollbarHeight],
- height: height
- }));
- navigator.yAxis = new Axis(chart, merge(navigatorOptions.yAxis, {
- id: 'navigator-y-axis',
- alignTicks: false,
- offset: 0,
- index: yAxisIndex,
- zoomEnabled: false
- }, chart.inverted ? {
- width: height
- } : {
- height: height
- }));
- // If we have a base series, initialize the navigator series
- if (baseSeries || navigatorOptions.series.data) {
- navigator.addBaseSeries();
- // If not, set up an event to listen for added series
- } else if (chart.series.length === 0) {
- wrap(chart, 'redraw', function(proceed, animation) {
- // We've got one, now add it as base and reset chart.redraw
- if (chart.series.length > 0 && !navigator.series) {
- navigator.setBaseSeries();
- chart.redraw = proceed; // reset
- }
- proceed.call(chart, animation);
- });
- }
- // Render items, so we can bind events to them:
- navigator.renderElements();
- // Add mouse events
- navigator.addMouseEvents();
- // in case of scrollbar only, fake an x axis to get translation
- } else {
- navigator.xAxis = {
- translate: function(value, reverse) {
- var axis = chart.xAxis[0],
- ext = axis.getExtremes(),
- scrollTrackWidth = axis.len - 2 * scrollbarHeight,
- min = numExt('min', axis.options.min, ext.dataMin),
- valueRange = numExt('max', axis.options.max, ext.dataMax) - min;
- return reverse ?
- // from pixel to value
- (value * valueRange / scrollTrackWidth) + min :
- // from value to pixel
- scrollTrackWidth * (value - min) / valueRange;
- },
- toPixels: function(value) {
- return this.translate(value);
- },
- toValue: function(value) {
- return this.translate(value, true);
- },
- toFixedRange: Axis.prototype.toFixedRange,
- fake: true
- };
- }
- // Initialize the scrollbar
- if (chart.options.scrollbar.enabled) {
- chart.scrollbar = navigator.scrollbar = new Scrollbar(
- chart.renderer,
- merge(chart.options.scrollbar, {
- margin: navigator.navigatorEnabled ? 0 : 10,
- vertical: chart.inverted
- }),
- chart
- );
- addEvent(navigator.scrollbar, 'changed', function(e) {
- var range = navigator.size,
- to = range * this.to,
- from = range * this.from;
- navigator.hasDragged = navigator.scrollbar.hasDragged;
- navigator.render(0, 0, from, to);
- if (chart.options.scrollbar.liveRedraw || e.DOMType !== 'mousemove') {
- setTimeout(function() {
- navigator.onMouseUp(e);
- });
- }
- });
- }
- // Add data events
- navigator.addBaseSeriesEvents();
- // Add redraw events
- navigator.addChartEvents();
- },
- /**
- * Get the union data extremes of the chart - the outer data extremes of the base
- * X axis and the navigator axis.
- * @param {boolean} returnFalseOnNoBaseSeries - as the param says.
- */
- getUnionExtremes: function(returnFalseOnNoBaseSeries) {
- var baseAxis = this.chart.xAxis[0],
- navAxis = this.xAxis,
- navAxisOptions = navAxis.options,
- baseAxisOptions = baseAxis.options,
- ret;
- if (!returnFalseOnNoBaseSeries || baseAxis.dataMin !== null) {
- ret = {
- dataMin: pick( // #4053
- navAxisOptions && navAxisOptions.min,
- numExt(
- 'min',
- baseAxisOptions.min,
- baseAxis.dataMin,
- navAxis.dataMin,
- navAxis.min
- )
- ),
- dataMax: pick(
- navAxisOptions && navAxisOptions.max,
- numExt(
- 'max',
- baseAxisOptions.max,
- baseAxis.dataMax,
- navAxis.dataMax,
- navAxis.max
- )
- )
- };
- }
- return ret;
- },
- /**
- * Set the base series. With a bit of modification we should be able to make
- * this an API method to be called from the outside
- * @param {Object} baseSeriesOptions - series options for a navigator
- */
- setBaseSeries: function(baseSeriesOptions) {
- var chart = this.chart,
- baseSeries;
- baseSeriesOptions = baseSeriesOptions || chart.options && chart.options.navigator.baseSeries || 0;
- // If we're resetting, remove the existing series
- if (this.series) {
- this.removeBaseSeriesEvents();
- each(this.series, function(s) {
- s.destroy();
- });
- }
- baseSeries = this.baseSeries = [];
- // Iterate through series and add the ones that should be shown in navigator
- each(chart.series || [], function(series, i) {
- if (series.options.showInNavigator || (i === baseSeriesOptions || series.options.id === baseSeriesOptions) &&
- series.options.showInNavigator !== false) {
- baseSeries.push(series);
- }
- });
- // When run after render, this.xAxis already exists
- if (this.xAxis && !this.xAxis.fake) {
- this.addBaseSeries();
- }
- },
- /*
- * Add base series to the navigator.
- */
- addBaseSeries: function() {
- var navigator = this,
- chart = navigator.chart,
- navigatorSeries = navigator.series = [],
- baseSeries = navigator.baseSeries,
- baseOptions,
- mergedNavSeriesOptions,
- chartNavigatorOptions = navigator.navigatorOptions.series,
- baseNavigatorOptions,
- navSeriesMixin = {
- enableMouseTracking: false,
- index: null, // #6162
- group: 'nav', // for columns
- padXAxis: false,
- xAxis: 'navigator-x-axis',
- yAxis: 'navigator-y-axis',
- showInLegend: false,
- stacking: false, // #4823
- isInternal: true,
- visible: true
- };
- // Go through each base series and merge the options to create new series
- if (baseSeries) {
- each(baseSeries, function(base, i) {
- navSeriesMixin.name = 'Navigator ' + (i + 1);
- baseOptions = base.options || {};
- baseNavigatorOptions = baseOptions.navigatorOptions || {};
- mergedNavSeriesOptions = merge(baseOptions, navSeriesMixin, chartNavigatorOptions, baseNavigatorOptions);
- // Merge data separately. Do a slice to avoid mutating the navigator options from base series (#4923).
- var navigatorSeriesData = baseNavigatorOptions.data || chartNavigatorOptions.data;
- navigator.hasNavigatorData = navigator.hasNavigatorData || !!navigatorSeriesData;
- mergedNavSeriesOptions.data = navigatorSeriesData || baseOptions.data && baseOptions.data.slice(0);
- // Add the series
- base.navigatorSeries = chart.initSeries(mergedNavSeriesOptions);
- navigatorSeries.push(base.navigatorSeries);
- });
- } else {
- // No base series, build from mixin and chart wide options
- mergedNavSeriesOptions = merge(chartNavigatorOptions, navSeriesMixin);
- mergedNavSeriesOptions.data = chartNavigatorOptions.data;
- navigator.hasNavigatorData = !!mergedNavSeriesOptions.data;
- navigatorSeries.push(chart.initSeries(mergedNavSeriesOptions));
- }
- this.addBaseSeriesEvents();
- },
- /**
- * Add data events.
- * For example when main series is updated we need to recalculate extremes
- */
- addBaseSeriesEvents: function() {
- var navigator = this,
- baseSeries = navigator.baseSeries || [];
- // Bind modified extremes event to first base's xAxis only. In event of > 1 base-xAxes, the navigator will ignore those.
- if (baseSeries[0] && baseSeries[0].xAxis) {
- addEvent(baseSeries[0].xAxis, 'foundExtremes', this.modifyBaseAxisExtremes);
- }
- if (this.navigatorOptions.adaptToUpdatedData !== false) {
- // Respond to updated data in the base series.
- // Abort if lazy-loading data from the server.
- each(baseSeries, function(base) {
- if (base.xAxis) {
- addEvent(base, 'updatedData', this.updatedDataHandler);
- }
- // Handle series removal
- addEvent(base, 'remove', function() {
- if (this.navigatorSeries) {
- erase(navigator.series, this.navigatorSeries);
- this.navigatorSeries.remove(false);
- delete this.navigatorSeries;
- }
- });
- }, this);
- }
- },
- /**
- * Set the navigator x axis extremes to reflect the total. The navigator extremes
- * should always be the extremes of the union of all series in the chart as
- * well as the navigator series.
- */
- modifyNavigatorAxisExtremes: function() {
- var xAxis = this.xAxis,
- unionExtremes;
- if (xAxis.getExtremes) {
- unionExtremes = this.getUnionExtremes(true);
- if (unionExtremes && (unionExtremes.dataMin !== xAxis.min || unionExtremes.dataMax !== xAxis.max)) {
- xAxis.min = unionExtremes.dataMin;
- xAxis.max = unionExtremes.dataMax;
- }
- }
- },
- /**
- * Hook to modify the base axis extremes with information from the Navigator
- */
- modifyBaseAxisExtremes: function() {
- var baseXAxis = this,
- navigator = baseXAxis.chart.navigator,
- baseExtremes = baseXAxis.getExtremes(),
- baseMin = baseExtremes.min,
- baseMax = baseExtremes.max,
- baseDataMin = baseExtremes.dataMin,
- baseDataMax = baseExtremes.dataMax,
- range = baseMax - baseMin,
- stickToMin = navigator.stickToMin,
- stickToMax = navigator.stickToMax,
- newMax,
- newMin,
- navigatorSeries = navigator.series && navigator.series[0],
- hasSetExtremes = !!baseXAxis.setExtremes,
- // When the extremes have been set by range selector button, don't stick to min or max.
- // The range selector buttons will handle the extremes. (#5489)
- unmutable = baseXAxis.eventArgs && baseXAxis.eventArgs.trigger === 'rangeSelectorButton';
- if (!unmutable) {
- // If the zoomed range is already at the min, move it to the right as new data
- // comes in
- if (stickToMin) {
- newMin = baseDataMin;
- newMax = newMin + range;
- }
- // If the zoomed range is already at the max, move it to the right as new data
- // comes in
- if (stickToMax) {
- newMax = baseDataMax;
- if (!stickToMin) { // if stickToMin is true, the new min value is set above
- newMin = Math.max(
- newMax - range,
- navigatorSeries && navigatorSeries.xData ?
- navigatorSeries.xData[0] : -Number.MAX_VALUE
- );
- }
- }
- // Update the extremes
- if (hasSetExtremes && (stickToMin || stickToMax)) {
- if (isNumber(newMin)) {
- baseXAxis.min = baseXAxis.userMin = newMin;
- baseXAxis.max = baseXAxis.userMax = newMax;
- }
- }
- }
- // Reset
- navigator.stickToMin = navigator.stickToMax = null;
- },
- /**
- * Handler for updated data on the base series. When data is modified, the navigator series
- * must reflect it. This is called from the Chart.redraw function before axis and series
- * extremes are computed.
- */
- updatedDataHandler: function() {
- var navigator = this.chart.navigator,
- baseSeries = this,
- navigatorSeries = this.navigatorSeries;
- // Detect whether the zoomed area should stick to the minimum or maximum. If the current
- // axis minimum falls outside the new updated dataset, we must adjust.
- navigator.stickToMin = isNumber(baseSeries.xAxis.min) && (baseSeries.xAxis.min <= baseSeries.xData[0]);
- // If the scrollbar is scrolled all the way to the right, keep right as new data
- // comes in.
- navigator.stickToMax = Math.round(navigator.zoomedMax) >= Math.round(navigator.size);
- // Set the navigator series data to the new data of the base series
- if (navigatorSeries && !navigator.hasNavigatorData) {
- navigatorSeries.options.pointStart = baseSeries.xData[0];
- navigatorSeries.setData(baseSeries.options.data, false, null, false); // #5414
- }
- },
- /**
- * Add chart events, like redrawing navigator, when chart requires that.
- */
- addChartEvents: function() {
- addEvent(this.chart, 'redraw', function() {
- // Move the scrollbar after redraw, like after data updata even if axes don't redraw
- var navigator = this.navigator,
- xAxis = navigator && (
- navigator.baseSeries &&
- navigator.baseSeries[0] &&
- navigator.baseSeries[0].xAxis ||
- navigator.scrollbar && this.xAxis[0]
- ); // #5709
- if (xAxis) {
- navigator.render(xAxis.min, xAxis.max);
- }
- });
- },
- /**
- * Destroys allocated elements.
- */
- destroy: function() {
- // Disconnect events added in addEvents
- this.removeEvents();
- if (this.xAxis) {
- erase(this.chart.xAxis, this.xAxis);
- erase(this.chart.axes, this.xAxis);
- }
- if (this.yAxis) {
- erase(this.chart.yAxis, this.yAxis);
- erase(this.chart.axes, this.yAxis);
- }
- // Destroy series
- each(this.series || [], function(s) {
- if (s.destroy) {
- s.destroy();
- }
- });
- // Destroy properties
- each([
- 'series', 'xAxis', 'yAxis', 'shades', 'outline', 'scrollbarTrack',
- 'scrollbarRifles', 'scrollbarGroup', 'scrollbar', 'navigatorGroup',
- 'rendered'
- ], function(prop) {
- if (this[prop] && this[prop].destroy) {
- this[prop].destroy();
- }
- this[prop] = null;
- }, this);
- // Destroy elements in collection
- each([this.handles], function(coll) {
- destroyObjectProperties(coll);
- }, this);
- }
- };
- H.Navigator = Navigator;
- /**
- * For Stock charts, override selection zooming with some special features because
- * X axis zooming is already allowed by the Navigator and Range selector.
- */
- wrap(Axis.prototype, 'zoom', function(proceed, newMin, newMax) {
- var chart = this.chart,
- chartOptions = chart.options,
- zoomType = chartOptions.chart.zoomType,
- previousZoom,
- navigator = chartOptions.navigator,
- rangeSelector = chartOptions.rangeSelector,
- ret;
- if (this.isXAxis && ((navigator && navigator.enabled) ||
- (rangeSelector && rangeSelector.enabled))) {
- // For x only zooming, fool the chart.zoom method not to create the zoom button
- // because the property already exists
- if (zoomType === 'x') {
- chart.resetZoomButton = 'blocked';
- // For y only zooming, ignore the X axis completely
- } else if (zoomType === 'y') {
- ret = false;
- // For xy zooming, record the state of the zoom before zoom selection, then when
- // the reset button is pressed, revert to this state
- } else if (zoomType === 'xy') {
- previousZoom = this.previousZoom;
- if (defined(newMin)) {
- this.previousZoom = [this.min, this.max];
- } else if (previousZoom) {
- newMin = previousZoom[0];
- newMax = previousZoom[1];
- delete this.previousZoom;
- }
- }
- }
- return ret !== undefined ? ret : proceed.call(this, newMin, newMax);
- });
- // Initialize navigator for stock charts
- wrap(Chart.prototype, 'init', function(proceed, options, callback) {
- addEvent(this, 'beforeRender', function() {
- var options = this.options;
- if (options.navigator.enabled || options.scrollbar.enabled) {
- this.scroller = this.navigator = new Navigator(this);
- }
- });
- proceed.call(this, options, callback);
- });
- /**
- * For stock charts, extend the Chart.setChartSize method so that we can set the final top position
- * of the navigator once the height of the chart, including the legend, is determined. #367.
- * We can't use Chart.getMargins, because labels offsets are not calculated yet.
- */
- wrap(Chart.prototype, 'setChartSize', function(proceed) {
- var legend = this.legend,
- navigator = this.navigator,
- scrollbarHeight,
- legendOptions,
- xAxis,
- yAxis;
- proceed.apply(this, [].slice.call(arguments, 1));
- if (navigator) {
- legendOptions = legend.options;
- xAxis = navigator.xAxis;
- yAxis = navigator.yAxis;
- scrollbarHeight = navigator.scrollbarHeight;
- // Compute the top position
- if (this.inverted) {
- navigator.left = navigator.opposite ?
- this.chartWidth - scrollbarHeight - navigator.height :
- this.spacing[3] + scrollbarHeight;
- navigator.top = this.plotTop + scrollbarHeight;
- } else {
- navigator.left = this.plotLeft + scrollbarHeight;
- navigator.top = navigator.navigatorOptions.top ||
- this.chartHeight - navigator.height - scrollbarHeight - this.spacing[2] -
- (legendOptions.verticalAlign === 'bottom' && legendOptions.enabled && !legendOptions.floating ?
- legend.legendHeight + pick(legendOptions.margin, 10) : 0);
- }
- if (xAxis && yAxis) { // false if navigator is disabled (#904)
- if (this.inverted) {
- xAxis.options.left = yAxis.options.left = navigator.left;
- } else {
- xAxis.options.top = yAxis.options.top = navigator.top;
- }
- xAxis.setAxisSize();
- yAxis.setAxisSize();
- }
- }
- });
- // Pick up badly formatted point options to addPoint
- wrap(Series.prototype, 'addPoint', function(proceed, options, redraw, shift, animation) {
- var turboThreshold = this.options.turboThreshold;
- if (turboThreshold && this.xData.length > turboThreshold && isObject(options, true) && this.chart.navigator) {
- error(20, true);
- }
- proceed.call(this, options, redraw, shift, animation);
- });
- // Handle adding new series
- wrap(Chart.prototype, 'addSeries', function(proceed, options, redraw, animation) {
- var series = proceed.call(this, options, false, animation);
- if (this.navigator) {
- this.navigator.setBaseSeries(); // Recompute which series should be shown in navigator, and add them
- }
- if (pick(redraw, true)) {
- this.redraw();
- }
- return series;
- });
- // Handle updating series
- wrap(Series.prototype, 'update', function(proceed, newOptions, redraw) {
- proceed.call(this, newOptions, false);
- if (this.chart.navigator) {
- this.chart.navigator.setBaseSeries();
- }
- if (pick(redraw, true)) {
- this.chart.redraw();
- }
- });
- Chart.prototype.callbacks.push(function(chart) {
- var extremes,
- navigator = chart.navigator;
- // Initiate the navigator
- if (navigator) {
- extremes = chart.xAxis[0].getExtremes();
- navigator.render(extremes.min, extremes.max);
- }
- });
- /* ****************************************************************************
- * End Navigator code *
- *****************************************************************************/
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var addEvent = H.addEvent,
- Axis = H.Axis,
- Chart = H.Chart,
- css = H.css,
- createElement = H.createElement,
- dateFormat = H.dateFormat,
- defaultOptions = H.defaultOptions,
- useUTC = defaultOptions.global.useUTC,
- defined = H.defined,
- destroyObjectProperties = H.destroyObjectProperties,
- discardElement = H.discardElement,
- each = H.each,
- extend = H.extend,
- fireEvent = H.fireEvent,
- HCDate = H.Date,
- isNumber = H.isNumber,
- merge = H.merge,
- pick = H.pick,
- pInt = H.pInt,
- splat = H.splat,
- wrap = H.wrap;
- /* ****************************************************************************
- * Start Range Selector code *
- *****************************************************************************/
- extend(defaultOptions, {
- rangeSelector: {
- // allButtonsEnabled: false,
- // enabled: true,
- // buttons: {Object}
- // buttonSpacing: 0,
- buttonTheme: {
- 'stroke-width': 0,
- width: 28,
- height: 18,
- padding: 2,
- zIndex: 7 // #484, #852
- },
- height: 35, // reserved space for buttons and input
- inputPosition: {
- align: 'right'
- },
- // inputDateFormat: '%b %e, %Y',
- // inputEditDateFormat: '%Y-%m-%d',
- // inputEnabled: true,
- // selected: undefined,
- // inputStyle: {},
- labelStyle: {
- color: '#666666'
- }
- }
- });
- defaultOptions.lang = merge(defaultOptions.lang, {
- rangeSelectorZoom: 'Zoom',
- rangeSelectorFrom: 'From',
- rangeSelectorTo: 'To'
- });
- /**
- * The range selector.
- * @class
- * @param {Object} chart
- */
- function RangeSelector(chart) {
- // Run RangeSelector
- this.init(chart);
- }
- RangeSelector.prototype = {
- /**
- * The method to run when one of the buttons in the range selectors is clicked
- * @param {Number} i The index of the button
- * @param {Object} rangeOptions
- * @param {Boolean} redraw
- */
- clickButton: function(i, redraw) {
- var rangeSelector = this,
- chart = rangeSelector.chart,
- rangeOptions = rangeSelector.buttonOptions[i],
- baseAxis = chart.xAxis[0],
- unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis || {},
- dataMin = unionExtremes.dataMin,
- dataMax = unionExtremes.dataMax,
- newMin,
- newMax = baseAxis && Math.round(Math.min(baseAxis.max, pick(dataMax, baseAxis.max))), // #1568
- type = rangeOptions.type,
- baseXAxisOptions,
- range = rangeOptions._range,
- rangeMin,
- minSetting,
- rangeSetting,
- ctx,
- ytdExtremes,
- dataGrouping = rangeOptions.dataGrouping;
- if (dataMin === null || dataMax === null) { // chart has no data, base series is removed
- return;
- }
- // Set the fixed range before range is altered
- chart.fixedRange = range;
- // Apply dataGrouping associated to button
- if (dataGrouping) {
- this.forcedDataGrouping = true;
- Axis.prototype.setDataGrouping.call(baseAxis || {
- chart: this.chart
- }, dataGrouping, false);
- }
- // Apply range
- if (type === 'month' || type === 'year') {
- if (!baseAxis) {
- // This is set to the user options and picked up later when the axis is instantiated
- // so that we know the min and max.
- range = rangeOptions;
- } else {
- ctx = {
- range: rangeOptions,
- max: newMax,
- dataMin: dataMin,
- dataMax: dataMax
- };
- newMin = baseAxis.minFromRange.call(ctx);
- if (isNumber(ctx.newMax)) {
- newMax = ctx.newMax;
- }
- }
- // Fixed times like minutes, hours, days
- } else if (range) {
- newMin = Math.max(newMax - range, dataMin);
- newMax = Math.min(newMin + range, dataMax);
- } else if (type === 'ytd') {
- // On user clicks on the buttons, or a delayed action running from the beforeRender
- // event (below), the baseAxis is defined.
- if (baseAxis) {
- // When "ytd" is the pre-selected button for the initial view, its calculation
- // is delayed and rerun in the beforeRender event (below). When the series
- // are initialized, but before the chart is rendered, we have access to the xData
- // array (#942).
- if (dataMax === undefined) {
- dataMin = Number.MAX_VALUE;
- dataMax = Number.MIN_VALUE;
- each(chart.series, function(series) {
- var xData = series.xData; // reassign it to the last item
- dataMin = Math.min(xData[0], dataMin);
- dataMax = Math.max(xData[xData.length - 1], dataMax);
- });
- redraw = false;
- }
- ytdExtremes = rangeSelector.getYTDExtremes(dataMax, dataMin, useUTC);
- newMin = rangeMin = ytdExtremes.min;
- newMax = ytdExtremes.max;
- // "ytd" is pre-selected. We don't yet have access to processed point and extremes data
- // (things like pointStart and pointInterval are missing), so we delay the process (#942)
- } else {
- addEvent(chart, 'beforeRender', function() {
- rangeSelector.clickButton(i);
- });
- return;
- }
- } else if (type === 'all' && baseAxis) {
- newMin = dataMin;
- newMax = dataMax;
- }
- rangeSelector.setSelected(i);
- // Update the chart
- if (!baseAxis) {
- // Axis not yet instanciated. Temporarily set min and range
- // options and remove them on chart load (#4317).
- baseXAxisOptions = splat(chart.options.xAxis)[0];
- rangeSetting = baseXAxisOptions.range;
- baseXAxisOptions.range = range;
- minSetting = baseXAxisOptions.min;
- baseXAxisOptions.min = rangeMin;
- addEvent(chart, 'load', function resetMinAndRange() {
- baseXAxisOptions.range = rangeSetting;
- baseXAxisOptions.min = minSetting;
- });
- } else {
- // Existing axis object. Set extremes after render time.
- baseAxis.setExtremes(
- newMin,
- newMax,
- pick(redraw, 1),
- null, // auto animation
- {
- trigger: 'rangeSelectorButton',
- rangeSelectorButton: rangeOptions
- }
- );
- }
- },
- /**
- * Set the selected option. This method only sets the internal flag, it doesn't
- * update the buttons or the actual zoomed range.
- */
- setSelected: function(selected) {
- this.selected = this.options.selected = selected;
- },
- /**
- * The default buttons for pre-selecting time frames
- */
- defaultButtons: [{
- type: 'month',
- count: 1,
- text: '1m'
- }, {
- type: 'month',
- count: 3,
- text: '3m'
- }, {
- type: 'month',
- count: 6,
- text: '6m'
- }, {
- type: 'ytd',
- text: 'YTD'
- }, {
- type: 'year',
- count: 1,
- text: '1y'
- }, {
- type: 'all',
- text: 'All'
- }],
- /**
- * Initialize the range selector
- */
- init: function(chart) {
- var rangeSelector = this,
- options = chart.options.rangeSelector,
- buttonOptions = options.buttons || [].concat(rangeSelector.defaultButtons),
- selectedOption = options.selected,
- blurInputs = function() {
- var minInput = rangeSelector.minInput,
- maxInput = rangeSelector.maxInput;
- if (minInput && minInput.blur) { //#3274 in some case blur is not defined
- fireEvent(minInput, 'blur'); //#3274
- }
- if (maxInput && maxInput.blur) { //#3274 in some case blur is not defined
- fireEvent(maxInput, 'blur'); //#3274
- }
- };
- rangeSelector.chart = chart;
- rangeSelector.options = options;
- rangeSelector.buttons = [];
- chart.extraTopMargin = options.height;
- rangeSelector.buttonOptions = buttonOptions;
- this.unMouseDown = addEvent(chart.container, 'mousedown', blurInputs);
- this.unResize = addEvent(chart, 'resize', blurInputs);
- // Extend the buttonOptions with actual range
- each(buttonOptions, rangeSelector.computeButtonRange);
- // zoomed range based on a pre-selected button index
- if (selectedOption !== undefined && buttonOptions[selectedOption]) {
- this.clickButton(selectedOption, false);
- }
- addEvent(chart, 'load', function() {
- // If a data grouping is applied to the current button, release it when extremes change
- addEvent(chart.xAxis[0], 'setExtremes', function(e) {
- if (this.max - this.min !== chart.fixedRange && e.trigger !== 'rangeSelectorButton' &&
- e.trigger !== 'updatedData' && rangeSelector.forcedDataGrouping) {
- this.setDataGrouping(false, false);
- }
- });
- });
- },
- /**
- * Dynamically update the range selector buttons after a new range has been set
- */
- updateButtonStates: function() {
- var rangeSelector = this,
- chart = this.chart,
- baseAxis = chart.xAxis[0],
- actualRange = Math.round(baseAxis.max - baseAxis.min),
- hasNoData = !baseAxis.hasVisibleSeries,
- day = 24 * 36e5, // A single day in milliseconds
- unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis,
- dataMin = unionExtremes.dataMin,
- dataMax = unionExtremes.dataMax,
- ytdExtremes = rangeSelector.getYTDExtremes(dataMax, dataMin, useUTC),
- ytdMin = ytdExtremes.min,
- ytdMax = ytdExtremes.max,
- selected = rangeSelector.selected,
- selectedExists = isNumber(selected),
- allButtonsEnabled = rangeSelector.options.allButtonsEnabled,
- buttons = rangeSelector.buttons;
- each(rangeSelector.buttonOptions, function(rangeOptions, i) {
- var range = rangeOptions._range,
- type = rangeOptions.type,
- count = rangeOptions.count || 1,
- button = buttons[i],
- state = 0,
- disable,
- select,
- isSelected = i === selected,
- // Disable buttons where the range exceeds what is allowed in the current view
- isTooGreatRange = range > dataMax - dataMin,
- // Disable buttons where the range is smaller than the minimum range
- isTooSmallRange = range < baseAxis.minRange,
- // Do not select the YTD button if not explicitly told so
- isYTDButNotSelected = false,
- // Disable the All button if we're already showing all
- isAllButAlreadyShowingAll = false,
- isSameRange = range === actualRange;
- // Months and years have a variable range so we check the extremes
- if (
- (type === 'month' || type === 'year') &&
- (actualRange >= {
- month: 28,
- year: 365
- }[type] * day * count) &&
- (actualRange <= {
- month: 31,
- year: 366
- }[type] * day * count)
- ) {
- isSameRange = true;
- } else if (type === 'ytd') {
- isSameRange = (ytdMax - ytdMin) === actualRange;
- isYTDButNotSelected = !isSelected;
- } else if (type === 'all') {
- isSameRange = baseAxis.max - baseAxis.min >= dataMax - dataMin;
- isAllButAlreadyShowingAll = !isSelected && selectedExists && isSameRange;
- }
- // The new zoom area happens to match the range for a button - mark it selected.
- // This happens when scrolling across an ordinal gap. It can be seen in the intraday
- // demos when selecting 1h and scroll across the night gap.
- disable = (!allButtonsEnabled &&
- (
- isTooGreatRange ||
- isTooSmallRange ||
- isAllButAlreadyShowingAll ||
- hasNoData
- )
- );
- select = (
- (isSelected && isSameRange) ||
- (isSameRange && !selectedExists && !isYTDButNotSelected)
- );
- if (disable) {
- state = 3;
- } else if (select) {
- selectedExists = true; // Only one button can be selected
- state = 2;
- }
- // If state has changed, update the button
- if (button.state !== state) {
- button.setState(state);
- }
- });
- },
- /**
- * Compute and cache the range for an individual button
- */
- computeButtonRange: function(rangeOptions) {
- var type = rangeOptions.type,
- count = rangeOptions.count || 1,
- // these time intervals have a fixed number of milliseconds, as opposed
- // to month, ytd and year
- fixedTimes = {
- millisecond: 1,
- second: 1000,
- minute: 60 * 1000,
- hour: 3600 * 1000,
- day: 24 * 3600 * 1000,
- week: 7 * 24 * 3600 * 1000
- };
- // Store the range on the button object
- if (fixedTimes[type]) {
- rangeOptions._range = fixedTimes[type] * count;
- } else if (type === 'month' || type === 'year') {
- rangeOptions._range = {
- month: 30,
- year: 365
- }[type] * 24 * 36e5 * count;
- }
- },
- /**
- * Set the internal and displayed value of a HTML input for the dates
- * @param {String} name
- * @param {Number} time
- */
- setInputValue: function(name, time) {
- var options = this.chart.options.rangeSelector,
- input = this[name + 'Input'];
- if (defined(time)) {
- input.previousValue = input.HCTime;
- input.HCTime = time;
- }
- input.value = dateFormat(
- options.inputEditDateFormat || '%Y-%m-%d',
- input.HCTime
- );
- this[name + 'DateBox'].attr({
- text: dateFormat(options.inputDateFormat || '%b %e, %Y', input.HCTime)
- });
- },
- showInput: function(name) {
- var inputGroup = this.inputGroup,
- dateBox = this[name + 'DateBox'];
- css(this[name + 'Input'], {
- left: (inputGroup.translateX + dateBox.x) + 'px',
- top: inputGroup.translateY + 'px',
- width: (dateBox.width - 2) + 'px',
- height: (dateBox.height - 2) + 'px',
- border: '2px solid silver'
- });
- },
- hideInput: function(name) {
- css(this[name + 'Input'], {
- border: 0,
- width: '1px',
- height: '1px'
- });
- this.setInputValue(name);
- },
- /**
- * Draw either the 'from' or the 'to' HTML input box of the range selector
- * @param {Object} name
- */
- drawInput: function(name) {
- var rangeSelector = this,
- chart = rangeSelector.chart,
- chartStyle = chart.renderer.style || {},
- renderer = chart.renderer,
- options = chart.options.rangeSelector,
- lang = defaultOptions.lang,
- div = rangeSelector.div,
- isMin = name === 'min',
- input,
- label,
- dateBox,
- inputGroup = this.inputGroup;
- function updateExtremes() {
- var inputValue = input.value,
- value = (options.inputDateParser || Date.parse)(inputValue),
- chartAxis = chart.xAxis[0],
- dataAxis = chart.scroller && chart.scroller.xAxis ? chart.scroller.xAxis : chartAxis,
- dataMin = dataAxis.dataMin,
- dataMax = dataAxis.dataMax;
- if (value !== input.previousValue) {
- input.previousValue = value;
- // If the value isn't parsed directly to a value by the browser's Date.parse method,
- // like YYYY-MM-DD in IE, try parsing it a different way
- if (!isNumber(value)) {
- value = inputValue.split('-');
- value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2]));
- }
- if (isNumber(value)) {
- // Correct for timezone offset (#433)
- if (!useUTC) {
- value = value + new Date().getTimezoneOffset() * 60 * 1000;
- }
- // Validate the extremes. If it goes beyound the data min or max, use the
- // actual data extreme (#2438).
- if (isMin) {
- if (value > rangeSelector.maxInput.HCTime) {
- value = undefined;
- } else if (value < dataMin) {
- value = dataMin;
- }
- } else {
- if (value < rangeSelector.minInput.HCTime) {
- value = undefined;
- } else if (value > dataMax) {
- value = dataMax;
- }
- }
- // Set the extremes
- if (value !== undefined) {
- chartAxis.setExtremes(
- isMin ? value : chartAxis.min,
- isMin ? chartAxis.max : value,
- undefined,
- undefined, {
- trigger: 'rangeSelectorInput'
- }
- );
- }
- }
- }
- }
- // Create the text label
- this[name + 'Label'] = label = renderer.label(lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'], this.inputGroup.offset)
- .addClass('highcharts-range-label')
- .attr({
- padding: 2
- })
- .add(inputGroup);
- inputGroup.offset += label.width + 5;
- // Create an SVG label that shows updated date ranges and and records click events that
- // bring in the HTML input.
- this[name + 'DateBox'] = dateBox = renderer.label('', inputGroup.offset)
- .addClass('highcharts-range-input')
- .attr({
- padding: 2,
- width: options.inputBoxWidth || 90,
- height: options.inputBoxHeight || 17,
- stroke: options.inputBoxBorderColor || '#cccccc',
- 'stroke-width': 1,
- 'text-align': 'center'
- })
- .on('click', function() {
- rangeSelector.showInput(name); // If it is already focused, the onfocus event doesn't fire (#3713)
- rangeSelector[name + 'Input'].focus();
- })
- .add(inputGroup);
- inputGroup.offset += dateBox.width + (isMin ? 10 : 0);
- // Create the HTML input element. This is rendered as 1x1 pixel then set to the right size
- // when focused.
- this[name + 'Input'] = input = createElement('input', {
- name: name,
- className: 'highcharts-range-selector',
- type: 'text'
- }, {
- top: chart.plotTop + 'px' // prevent jump on focus in Firefox
- }, div);
- // Styles
- label.css(merge(chartStyle, options.labelStyle));
- dateBox.css(merge({
- color: '#333333'
- }, chartStyle, options.inputStyle));
- css(input, extend({
- position: 'absolute',
- border: 0,
- width: '1px', // Chrome needs a pixel to see it
- height: '1px',
- padding: 0,
- textAlign: 'center',
- fontSize: chartStyle.fontSize,
- fontFamily: chartStyle.fontFamily,
- left: '-9em' // #4798
- }, options.inputStyle));
- // Blow up the input box
- input.onfocus = function() {
- rangeSelector.showInput(name);
- };
- // Hide away the input box
- input.onblur = function() {
- rangeSelector.hideInput(name);
- };
- // handle changes in the input boxes
- input.onchange = updateExtremes;
- input.onkeypress = function(event) {
- // IE does not fire onchange on enter
- if (event.keyCode === 13) {
- updateExtremes();
- }
- };
- },
- /**
- * Get the position of the range selector buttons and inputs. This can be overridden from outside for custom positioning.
- */
- getPosition: function() {
- var chart = this.chart,
- options = chart.options.rangeSelector,
- buttonTop = pick((options.buttonPosition || {}).y, chart.plotTop - chart.axisOffset[0] - options.height);
- return {
- buttonTop: buttonTop,
- inputTop: buttonTop - 10
- };
- },
- /**
- * Get the extremes of YTD.
- * Will choose dataMax if its value is lower than the current timestamp.
- * Will choose dataMin if its value is higher than the timestamp for
- * the start of current year.
- * @param {number} dataMax
- * @param {number} dataMin
- * @return {object} Returns min and max for the YTD
- */
- getYTDExtremes: function(dataMax, dataMin, useUTC) {
- var min,
- now = new HCDate(dataMax),
- year = now[HCDate.hcGetFullYear](),
- startOfYear = useUTC ? HCDate.UTC(year, 0, 1) : +new HCDate(year, 0, 1); // eslint-disable-line new-cap
- min = Math.max(dataMin || 0, startOfYear);
- now = now.getTime();
- return {
- max: Math.min(dataMax || now, now),
- min: min
- };
- },
- /**
- * Render the range selector including the buttons and the inputs. The first time render
- * is called, the elements are created and positioned. On subsequent calls, they are
- * moved and updated.
- * @param {Number} min X axis minimum
- * @param {Number} max X axis maximum
- */
- render: function(min, max) {
- var rangeSelector = this,
- chart = rangeSelector.chart,
- renderer = chart.renderer,
- container = chart.container,
- chartOptions = chart.options,
- navButtonOptions = chartOptions.exporting && chartOptions.exporting.enabled !== false &&
- chartOptions.navigation && chartOptions.navigation.buttonOptions,
- options = chartOptions.rangeSelector,
- buttons = rangeSelector.buttons,
- lang = defaultOptions.lang,
- div = rangeSelector.div,
- inputGroup = rangeSelector.inputGroup,
- buttonTheme = options.buttonTheme,
- buttonPosition = options.buttonPosition || {},
- inputEnabled = options.inputEnabled,
- states = buttonTheme && buttonTheme.states,
- plotLeft = chart.plotLeft,
- buttonLeft,
- pos = this.getPosition(),
- buttonGroup = rangeSelector.group,
- buttonBBox,
- rendered = rangeSelector.rendered;
- if (options.enabled === false) {
- return;
- }
- // create the elements
- if (!rendered) {
- rangeSelector.group = buttonGroup = renderer.g('range-selector-buttons').add();
- rangeSelector.zoomText = renderer.text(lang.rangeSelectorZoom, pick(buttonPosition.x, plotLeft), 15)
- .css(options.labelStyle)
- .add(buttonGroup);
- // button starting position
- buttonLeft = pick(buttonPosition.x, plotLeft) + rangeSelector.zoomText.getBBox().width + 5;
- each(rangeSelector.buttonOptions, function(rangeOptions, i) {
- buttons[i] = renderer.button(
- rangeOptions.text,
- buttonLeft,
- 0,
- function() {
- rangeSelector.clickButton(i);
- rangeSelector.isActive = true;
- },
- buttonTheme,
- states && states.hover,
- states && states.select,
- states && states.disabled
- )
- .attr({
- 'text-align': 'center'
- })
- .add(buttonGroup);
- // increase button position for the next button
- buttonLeft += buttons[i].width + pick(options.buttonSpacing, 5);
- });
- // first create a wrapper outside the container in order to make
- // the inputs work and make export correct
- if (inputEnabled !== false) {
- rangeSelector.div = div = createElement('div', null, {
- position: 'relative',
- height: 0,
- zIndex: 1 // above container
- });
- container.parentNode.insertBefore(div, container);
- // Create the group to keep the inputs
- rangeSelector.inputGroup = inputGroup = renderer.g('input-group')
- .add();
- inputGroup.offset = 0;
- rangeSelector.drawInput('min');
- rangeSelector.drawInput('max');
- }
- }
- rangeSelector.updateButtonStates();
- // Set or update the group position
- buttonGroup[rendered ? 'animate' : 'attr']({
- translateY: pos.buttonTop
- });
- if (inputEnabled !== false) {
- // Update the alignment to the updated spacing box
- inputGroup.align(extend({
- y: pos.inputTop,
- width: inputGroup.offset,
- // Detect collision with the exporting buttons
- x: navButtonOptions && (pos.inputTop < (navButtonOptions.y || 0) + navButtonOptions.height - chart.spacing[0]) ?
- -40 : 0
- }, options.inputPosition), true, chart.spacingBox);
- // Hide if overlapping - inputEnabled is null or undefined
- if (!defined(inputEnabled)) {
- buttonBBox = buttonGroup.getBBox();
- inputGroup[inputGroup.alignAttr.translateX < buttonBBox.x + buttonBBox.width + 10 ? 'hide' : 'show']();
- }
- // Set or reset the input values
- rangeSelector.setInputValue('min', min);
- rangeSelector.setInputValue('max', max);
- }
- rangeSelector.rendered = true;
- },
- /**
- * Update the range selector with new options
- */
- update: function(options) {
- var chart = this.chart;
- merge(true, chart.options.rangeSelector, options);
- this.destroy();
- this.init(chart);
- },
- /**
- * Destroys allocated elements.
- */
- destroy: function() {
- var rSelector = this,
- minInput = rSelector.minInput,
- maxInput = rSelector.maxInput;
- rSelector.unMouseDown();
- rSelector.unResize();
- // Destroy elements in collections
- destroyObjectProperties(rSelector.buttons);
- // Clear input element events
- if (minInput) {
- minInput.onfocus = minInput.onblur = minInput.onchange = null;
- }
- if (maxInput) {
- maxInput.onfocus = maxInput.onblur = maxInput.onchange = null;
- }
- // Destroy HTML and SVG elements
- H.objectEach(rSelector, function(val, key) {
- if (val && key !== 'chart') {
- if (val.destroy) { // SVGElement
- val.destroy();
- } else if (val.nodeType) { // HTML element
- discardElement(this[key]);
- }
- }
- if (val !== RangeSelector.prototype[key]) {
- rSelector[key] = null;
- }
- }, this);
- }
- };
- /**
- * Add logic to normalize the zoomed range in order to preserve the pressed state of range selector buttons
- */
- Axis.prototype.toFixedRange = function(pxMin, pxMax, fixedMin, fixedMax) {
- var fixedRange = this.chart && this.chart.fixedRange,
- newMin = pick(fixedMin, this.translate(pxMin, true, !this.horiz)),
- newMax = pick(fixedMax, this.translate(pxMax, true, !this.horiz)),
- changeRatio = fixedRange && (newMax - newMin) / fixedRange;
- // If the difference between the fixed range and the actual requested range is
- // too great, the user is dragging across an ordinal gap, and we need to release
- // the range selector button.
- if (changeRatio > 0.7 && changeRatio < 1.3) {
- if (fixedMax) {
- newMin = newMax - fixedRange;
- } else {
- newMax = newMin + fixedRange;
- }
- }
- if (!isNumber(newMin)) { // #1195
- newMin = newMax = undefined;
- }
- return {
- min: newMin,
- max: newMax
- };
- };
- /**
- * Get the axis min value based on the range option and the current max. For
- * stock charts this is extended via the {@link RangeSelector} so that if the
- * selected range is a multiple of months or years, it is compensated for
- * various month lengths.
- *
- * @return {number} The new minimum value.
- */
- Axis.prototype.minFromRange = function() {
- var rangeOptions = this.range,
- type = rangeOptions.type,
- timeName = {
- month: 'Month',
- year: 'FullYear'
- }[type],
- min,
- max = this.max,
- dataMin,
- range,
- // Get the true range from a start date
- getTrueRange = function(base, count) {
- var date = new Date(base),
- basePeriod = date['get' + timeName]();
- date['set' + timeName](basePeriod + count);
- if (basePeriod === date['get' + timeName]()) {
- date.setDate(0); // #6537
- }
- return date.getTime() - base;
- };
- if (isNumber(rangeOptions)) {
- min = max - rangeOptions;
- range = rangeOptions;
- } else {
- min = max + getTrueRange(max, -rangeOptions.count);
- // Let the fixedRange reflect initial settings (#5930)
- if (this.chart) {
- this.chart.fixedRange = max - min;
- }
- }
- dataMin = pick(this.dataMin, Number.MIN_VALUE);
- if (!isNumber(min)) {
- min = dataMin;
- }
- if (min <= dataMin) {
- min = dataMin;
- if (range === undefined) { // #4501
- range = getTrueRange(min, rangeOptions.count);
- }
- this.newMax = Math.min(min + range, this.dataMax);
- }
- if (!isNumber(max)) {
- min = undefined;
- }
- return min;
- };
- // Initialize scroller for stock charts
- wrap(Chart.prototype, 'init', function(proceed, options, callback) {
- addEvent(this, 'init', function() {
- if (this.options.rangeSelector.enabled) {
- this.rangeSelector = new RangeSelector(this);
- }
- });
- proceed.call(this, options, callback);
- });
- Chart.prototype.callbacks.push(function(chart) {
- var extremes,
- rangeSelector = chart.rangeSelector,
- unbindRender,
- unbindSetExtremes;
- function renderRangeSelector() {
- extremes = chart.xAxis[0].getExtremes();
- if (isNumber(extremes.min)) {
- rangeSelector.render(extremes.min, extremes.max);
- }
- }
- if (rangeSelector) {
- // redraw the scroller on setExtremes
- unbindSetExtremes = addEvent(
- chart.xAxis[0],
- 'afterSetExtremes',
- function(e) {
- rangeSelector.render(e.min, e.max);
- }
- );
- // redraw the scroller chart resize
- unbindRender = addEvent(chart, 'redraw', renderRangeSelector);
- // do it now
- renderRangeSelector();
- }
- // Remove resize/afterSetExtremes at chart destroy
- addEvent(chart, 'destroy', function destroyEvents() {
- if (rangeSelector) {
- unbindRender();
- unbindSetExtremes();
- }
- });
- });
- H.RangeSelector = RangeSelector;
- /* ****************************************************************************
- * End Range Selector code *
- *****************************************************************************/
- }(Highcharts));
- (function(H) {
- /**
- * (c) 2010-2017 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- var arrayMax = H.arrayMax,
- arrayMin = H.arrayMin,
- Axis = H.Axis,
- Chart = H.Chart,
- defined = H.defined,
- each = H.each,
- extend = H.extend,
- format = H.format,
- grep = H.grep,
- inArray = H.inArray,
- isNumber = H.isNumber,
- isString = H.isString,
- map = H.map,
- merge = H.merge,
- pick = H.pick,
- Point = H.Point,
- Renderer = H.Renderer,
- Series = H.Series,
- splat = H.splat,
- SVGRenderer = H.SVGRenderer,
- VMLRenderer = H.VMLRenderer,
- wrap = H.wrap,
- seriesProto = Series.prototype,
- seriesInit = seriesProto.init,
- seriesProcessData = seriesProto.processData,
- pointTooltipFormatter = Point.prototype.tooltipFormatter;
- /**
- * Factory function for creating new stock charts. Creates a new {@link Chart|
- * Chart} object with different default options than the basic Chart.
- *
- * @function #stockChart
- * @memberOf Highcharts
- *
- * @param {String|HTMLDOMElement} renderTo
- * The DOM element to render to, or its id.
- * @param {Options} options
- * The chart options structure as described in the {@link
- * https://api.highcharts.com/highstock|options reference}.
- * @param {Function} callback
- * A function to execute when the chart object is finished loading and
- * rendering. In most cases the chart is built in one thread, but in
- * Internet Explorer version 8 or less the chart is sometimes initiated
- * before the document is ready, and in these cases the chart object
- * will not be finished synchronously. As a consequence, code that
- * relies on the newly built Chart object should always run in the
- * callback. Defining a {@link https://api.highcharts.com/highstock/chart.events.load|
- * chart.event.load} handler is equivalent.
- *
- * @return {Chart}
- * The chart object.
- *
- * @example
- * var chart = Highcharts.stockChart('container', {
- * series: [{
- * data: [1, 2, 3, 4, 5, 6, 7, 8, 9],
- * pointInterval: 24 * 60 * 60 * 1000
- * }]
- * });
- */
- H.StockChart = H.stockChart = function(a, b, c) {
- var hasRenderToArg = isString(a) || a.nodeName,
- options = arguments[hasRenderToArg ? 1 : 0],
- seriesOptions = options.series, // to increase performance, don't merge the data
- defaultOptions = H.getOptions(),
- opposite,
- // Always disable startOnTick:true on the main axis when the navigator
- // is enabled (#1090)
- navigatorEnabled = pick(
- options.navigator && options.navigator.enabled,
- defaultOptions.navigator.enabled,
- true
- ),
- disableStartOnTick = navigatorEnabled ? {
- startOnTick: false,
- endOnTick: false
- } : null,
- lineOptions = {
- marker: {
- enabled: false,
- radius: 2
- }
- // gapSize: 0
- },
- columnOptions = {
- shadow: false,
- borderWidth: 0
- };
- // apply X axis options to both single and multi y axes
- options.xAxis = map(splat(options.xAxis || {}), function(xAxisOptions) {
- return merge({ // defaults
- minPadding: 0,
- maxPadding: 0,
- ordinal: true,
- title: {
- text: null
- },
- labels: {
- overflow: 'justify'
- },
- showLastLabel: true
- },
- defaultOptions.xAxis, // #3802
- xAxisOptions, // user options
- { // forced options
- type: 'datetime',
- categories: null
- },
- disableStartOnTick
- );
- });
- // apply Y axis options to both single and multi y axes
- options.yAxis = map(splat(options.yAxis || {}), function(yAxisOptions) {
- opposite = pick(yAxisOptions.opposite, true);
- return merge({ // defaults
- labels: {
- y: -2
- },
- opposite: opposite,
- showLastLabel: false,
- title: {
- text: null
- }
- },
- defaultOptions.yAxis, // #3802
- yAxisOptions // user options
- );
- });
- options.series = null;
- options = merge({
- chart: {
- panning: true,
- pinchType: 'x'
- },
- navigator: {
- enabled: navigatorEnabled
- },
- scrollbar: {
- // #4988 - check if setOptions was called
- enabled: pick(defaultOptions.scrollbar.enabled, true)
- },
- rangeSelector: {
- // #4988 - check if setOptions was called
- enabled: pick(defaultOptions.rangeSelector.enabled, true)
- },
- title: {
- text: null
- },
- tooltip: {
- shared: true,
- crosshairs: true
- },
- legend: {
- enabled: false
- },
- plotOptions: {
- line: lineOptions,
- spline: lineOptions,
- area: lineOptions,
- areaspline: lineOptions,
- arearange: lineOptions,
- areasplinerange: lineOptions,
- column: columnOptions,
- columnrange: columnOptions,
- candlestick: columnOptions,
- ohlc: columnOptions
- }
- },
- options, // user's options
- { // forced options
- isStock: true // internal flag
- }
- );
- options.series = seriesOptions;
- return hasRenderToArg ?
- new Chart(a, options, c) :
- new Chart(options, b);
- };
- // Override the automatic label alignment so that the first Y axis' labels
- // are drawn on top of the grid line, and subsequent axes are drawn outside
- wrap(Axis.prototype, 'autoLabelAlign', function(proceed) {
- var chart = this.chart,
- options = this.options,
- panes = chart._labelPanes = chart._labelPanes || {},
- key,
- labelOptions = this.options.labels;
- if (this.chart.options.isStock && this.coll === 'yAxis') {
- key = options.top + ',' + options.height;
- if (!panes[key] && labelOptions.enabled) { // do it only for the first Y axis of each pane
- if (labelOptions.x === 15) { // default
- labelOptions.x = 0;
- }
- if (labelOptions.align === undefined) {
- labelOptions.align = 'right';
- }
- panes[key] = this;
- return 'right';
- }
- }
- return proceed.call(this, [].slice.call(arguments, 1));
- });
- // Clear axis from label panes (#6071)
- wrap(Axis.prototype, 'destroy', function(proceed) {
- var chart = this.chart,
- key = this.options && (this.options.top + ',' + this.options.height);
- if (key && chart._labelPanes && chart._labelPanes[key] === this) {
- delete chart._labelPanes[key];
- }
- return proceed.call(this, Array.prototype.slice.call(arguments, 1));
- });
- // Override getPlotLinePath to allow for multipane charts
- wrap(Axis.prototype, 'getPlotLinePath', function(proceed, value, lineWidth, old, force, translatedValue) {
- var axis = this,
- series = (this.isLinked && !this.series ? this.linkedParent.series : this.series),
- chart = axis.chart,
- renderer = chart.renderer,
- axisLeft = axis.left,
- axisTop = axis.top,
- x1,
- y1,
- x2,
- y2,
- result = [],
- axes = [], //#3416 need a default array
- axes2,
- uniqueAxes,
- transVal;
- /**
- * Return the other axis based on either the axis option or on related series.
- */
- function getAxis(coll) {
- var otherColl = coll === 'xAxis' ? 'yAxis' : 'xAxis',
- opt = axis.options[otherColl];
- // Other axis indexed by number
- if (isNumber(opt)) {
- return [chart[otherColl][opt]];
- }
- // Other axis indexed by id (like navigator)
- if (isString(opt)) {
- return [chart.get(opt)];
- }
- // Auto detect based on existing series
- return map(series, function(s) {
- return s[otherColl];
- });
- }
- // Ignore in case of colorAxis or zAxis. #3360, #3524, #6720
- if (axis.coll !== 'xAxis' && axis.coll !== 'yAxis') {
- return proceed.apply(this, [].slice.call(arguments, 1));
- }
- // Get the related axes based on series
- axes = getAxis(axis.coll);
- // Get the related axes based options.*Axis setting #2810
- axes2 = (axis.isXAxis ? chart.yAxis : chart.xAxis);
- each(axes2, function(A) {
- if (defined(A.options.id) ? A.options.id.indexOf('navigator') === -1 : true) {
- var a = (A.isXAxis ? 'yAxis' : 'xAxis'),
- rax = (defined(A.options[a]) ? chart[a][A.options[a]] : chart[a][0]);
- if (axis === rax) {
- axes.push(A);
- }
- }
- });
- // Remove duplicates in the axes array. If there are no axes in the axes array,
- // we are adding an axis without data, so we need to populate this with grid
- // lines (#2796).
- uniqueAxes = axes.length ? [] : [axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; //#3742
- each(axes, function(axis2) {
- if (
- inArray(axis2, uniqueAxes) === -1 &&
- // Do not draw on axis which overlap completely. #5424
- !H.find(uniqueAxes, function(unique) {
- return unique.pos === axis2.pos && unique.len && axis2.len;
- })
- ) {
- uniqueAxes.push(axis2);
- }
- });
- transVal = pick(translatedValue, axis.translate(value, null, null, old));
- if (isNumber(transVal)) {
- if (axis.horiz) {
- each(uniqueAxes, function(axis2) {
- var skip;
- y1 = axis2.pos;
- y2 = y1 + axis2.len;
- x1 = x2 = Math.round(transVal + axis.transB);
- if (x1 < axisLeft || x1 > axisLeft + axis.width) { // outside plot area
- if (force) {
- x1 = x2 = Math.min(Math.max(axisLeft, x1), axisLeft + axis.width);
- } else {
- skip = true;
- }
- }
- if (!skip) {
- result.push('M', x1, y1, 'L', x2, y2);
- }
- });
- } else {
- each(uniqueAxes, function(axis2) {
- var skip;
- x1 = axis2.pos;
- x2 = x1 + axis2.len;
- y1 = y2 = Math.round(axisTop + axis.height - transVal);
- if (y1 < axisTop || y1 > axisTop + axis.height) { // outside plot area
- if (force) {
- y1 = y2 = Math.min(Math.max(axisTop, y1), axis.top + axis.height);
- } else {
- skip = true;
- }
- }
- if (!skip) {
- result.push('M', x1, y1, 'L', x2, y2);
- }
- });
- }
- }
- return result.length > 0 ?
- renderer.crispPolyLine(result, lineWidth || 1) :
- null; //#3557 getPlotLinePath in regular Highcharts also returns null
- });
- // Override getPlotBandPath to allow for multipane charts
- Axis.prototype.getPlotBandPath = function(from, to) {
- var toPath = this.getPlotLinePath(to, null, null, true),
- path = this.getPlotLinePath(from, null, null, true),
- result = [],
- i;
- if (path && toPath) {
- if (path.toString() === toPath.toString()) {
- // #6166
- result = path;
- result.flat = true;
- } else {
- // Go over each subpath
- for (i = 0; i < path.length; i += 6) {
- result.push(
- 'M', path[i + 1], path[i + 2],
- 'L', path[i + 4], path[i + 5],
- toPath[i + 4], toPath[i + 5],
- toPath[i + 1], toPath[i + 2],
- 'z'
- );
- }
- }
- } else { // outside the axis area
- result = null;
- }
- return result;
- };
- // Function to crisp a line with multiple segments
- SVGRenderer.prototype.crispPolyLine = function(points, width) {
- // points format: ['M', 0, 0, 'L', 100, 0]
- // normalize to a crisp line
- var i;
- for (i = 0; i < points.length; i = i + 6) {
- if (points[i + 1] === points[i + 4]) {
- // Substract due to #1129. Now bottom and left axis gridlines behave the same.
- points[i + 1] = points[i + 4] = Math.round(points[i + 1]) - (width % 2 / 2);
- }
- if (points[i + 2] === points[i + 5]) {
- points[i + 2] = points[i + 5] = Math.round(points[i + 2]) + (width % 2 / 2);
- }
- }
- return points;
- };
- if (Renderer === VMLRenderer) {
- VMLRenderer.prototype.crispPolyLine = SVGRenderer.prototype.crispPolyLine;
- }
- // Wrapper to hide the label
- wrap(Axis.prototype, 'hideCrosshair', function(proceed, i) {
- proceed.call(this, i);
- if (this.crossLabel) {
- this.crossLabel = this.crossLabel.hide();
- }
- });
- // Wrapper to draw the label
- wrap(Axis.prototype, 'drawCrosshair', function(proceed, e, point) {
- // Draw the crosshair
- proceed.call(this, e, point);
- // Check if the label has to be drawn
- if (!defined(this.crosshair.label) || !this.crosshair.label.enabled || !this.cross) {
- return;
- }
- var chart = this.chart,
- options = this.options.crosshair.label, // the label's options
- horiz = this.horiz, // axis orientation
- opposite = this.opposite, // axis position
- left = this.left, // left position
- top = this.top, // top position
- crossLabel = this.crossLabel, // reference to the svgElement
- posx,
- posy,
- crossBox,
- formatOption = options.format,
- formatFormat = '',
- limit,
- align,
- tickInside = this.options.tickPosition === 'inside',
- snap = this.crosshair.snap !== false,
- value,
- offset = 0;
- // Use last available event (#5287)
- if (!e) {
- e = this.cross && this.cross.e;
- }
- align = (horiz ? 'center' : opposite ?
- (this.labelAlign === 'right' ? 'right' : 'left') :
- (this.labelAlign === 'left' ? 'left' : 'center'));
- // If the label does not exist yet, create it.
- if (!crossLabel) {
- crossLabel = this.crossLabel = chart.renderer.label(null, null, null, options.shape || 'callout')
- .addClass('highcharts-crosshair-label' +
- (this.series[0] && ' highcharts-color-' + this.series[0].colorIndex))
- .attr({
- align: options.align || align,
- padding: pick(options.padding, 8),
- r: pick(options.borderRadius, 3),
- zIndex: 2
- })
- .add(this.labelGroup);
- // Presentational
- crossLabel
- .attr({
- fill: options.backgroundColor ||
- (this.series[0] && this.series[0].color) || '#666666',
- stroke: options.borderColor || '',
- 'stroke-width': options.borderWidth || 0
- })
- .css(extend({
- color: '#ffffff',
- fontWeight: 'normal',
- fontSize: '11px',
- textAlign: 'center'
- }, options.style));
- }
- if (horiz) {
- posx = snap ? point.plotX + left : e.chartX;
- posy = top + (opposite ? 0 : this.height);
- } else {
- posx = opposite ? this.width + left : 0;
- posy = snap ? point.plotY + top : e.chartY;
- }
- if (!formatOption && !options.formatter) {
- if (this.isDatetimeAxis) {
- formatFormat = '%b %d, %Y';
- }
- formatOption = '{value' + (formatFormat ? ':' + formatFormat : '') + '}';
- }
- // Show the label
- value = snap ? point[this.isXAxis ? 'x' : 'y'] : this.toValue(horiz ? e.chartX : e.chartY);
- crossLabel.attr({
- text: formatOption ? format(formatOption, {
- value: value
- }) : options.formatter.call(this, value),
- x: posx,
- y: posy,
- visibility: 'visible'
- });
- crossBox = crossLabel.getBBox();
- // now it is placed we can correct its position
- if (horiz) {
- if ((tickInside && !opposite) || (!tickInside && opposite)) {
- posy = crossLabel.y - crossBox.height;
- }
- } else {
- posy = crossLabel.y - (crossBox.height / 2);
- }
- // check the edges
- if (horiz) {
- limit = {
- left: left - crossBox.x,
- right: left + this.width - crossBox.x
- };
- } else {
- limit = {
- left: this.labelAlign === 'left' ? left : 0,
- right: this.labelAlign === 'right' ? left + this.width : chart.chartWidth
- };
- }
- // left edge
- if (crossLabel.translateX < limit.left) {
- offset = limit.left - crossLabel.translateX;
- }
- // right edge
- if (crossLabel.translateX + crossBox.width >= limit.right) {
- offset = -(crossLabel.translateX + crossBox.width - limit.right);
- }
- // show the crosslabel
- crossLabel.attr({
- x: posx + offset,
- y: posy,
- // First set x and y, then anchorX and anchorY, when box is actually calculated, #5702
- anchorX: horiz ? posx : (this.opposite ? 0 : chart.chartWidth),
- anchorY: horiz ? (this.opposite ? chart.chartHeight : 0) : posy + crossBox.height / 2
- });
- });
- /* ****************************************************************************
- * Start value compare logic *
- *****************************************************************************/
- /**
- * Extend series.init by adding a method to modify the y value used for plotting
- * on the y axis. This method is called both from the axis when finding dataMin
- * and dataMax, and from the series.translate method.
- */
- seriesProto.init = function() {
- // Call base method
- seriesInit.apply(this, arguments);
- // Set comparison mode
- this.setCompare(this.options.compare);
- };
- /**
- * Highstock only. Set the {@link
- * http://api.highcharts.com/highstock/plotOptions.series.compare|
- * compare} mode of the series after render time. In most cases it is more
- * useful running {@link Axis#setCompare} on the X axis to update all its
- * series.
- *
- * @function setCompare
- * @memberOf Series.prototype
- *
- * @param {String} compare
- * Can be one of `null`, `"percent"` or `"value"`.
- */
- seriesProto.setCompare = function(compare) {
- // Set or unset the modifyValue method
- this.modifyValue = (compare === 'value' || compare === 'percent') ? function(value, point) {
- var compareValue = this.compareValue;
- if (value !== undefined && compareValue !== undefined) { // #2601, #5814
- // Get the modified value
- if (compare === 'value') {
- value -= compareValue;
- // Compare percent
- } else {
- value = 100 * (value / compareValue) -
- (this.options.compareBase === 100 ? 0 : 100);
- }
- // record for tooltip etc.
- if (point) {
- point.change = value;
- }
- return value;
- }
- } : null;
- // Survive to export, #5485
- this.userOptions.compare = compare;
- // Mark dirty
- if (this.chart.hasRendered) {
- this.isDirty = true;
- }
- };
- /**
- * Extend series.processData by finding the first y value in the plot area,
- * used for comparing the following values
- */
- seriesProto.processData = function() {
- var series = this,
- i,
- keyIndex = -1,
- processedXData,
- processedYData,
- length,
- compareValue;
- // call base method
- seriesProcessData.apply(this, arguments);
- if (series.xAxis && series.processedYData) { // not pies
- // local variables
- processedXData = series.processedXData;
- processedYData = series.processedYData;
- length = processedYData.length;
- // For series with more than one value (range, OHLC etc), compare against
- // close or the pointValKey (#4922, #3112)
- if (series.pointArrayMap) {
- // Use close if present (#3112)
- keyIndex = inArray('close', series.pointArrayMap);
- if (keyIndex === -1) {
- keyIndex = inArray(series.pointValKey || 'y', series.pointArrayMap);
- }
- }
- // find the first value for comparison
- for (i = 0; i < length - 1; i++) {
- compareValue = processedYData[i] && keyIndex > -1 ?
- processedYData[i][keyIndex] :
- processedYData[i];
- if (isNumber(compareValue) && processedXData[i + 1] >= series.xAxis.min && compareValue !== 0) {
- series.compareValue = compareValue;
- break;
- }
- }
- }
- };
- /**
- * Modify series extremes
- */
- wrap(seriesProto, 'getExtremes', function(proceed) {
- var extremes;
- proceed.apply(this, [].slice.call(arguments, 1));
- if (this.modifyValue) {
- extremes = [this.modifyValue(this.dataMin), this.modifyValue(this.dataMax)];
- this.dataMin = arrayMin(extremes);
- this.dataMax = arrayMax(extremes);
- }
- });
- /**
- * Highstock only. Set the compare mode on all series belonging to an Y axis
- * after render time.
- *
- * @param {String} compare
- * The compare mode. Can be one of `null`, `"value"` or `"percent"`.
- * @param {Boolean} [redraw=true]
- * Whether to redraw the chart or to wait for a later call to {@link
- * Chart#redraw},
- *
- * @function setCompare
- * @memberOf Axis.prototype
- *
- * @see {@link https://api.highcharts.com/highstock/series.plotOptions.compare|
- * series.plotOptions.compare}
- *
- * @sample stock/members/axis-setcompare/
- * Set compoare
- */
- Axis.prototype.setCompare = function(compare, redraw) {
- if (!this.isXAxis) {
- each(this.series, function(series) {
- series.setCompare(compare);
- });
- if (pick(redraw, true)) {
- this.chart.redraw();
- }
- }
- };
- /**
- * Extend the tooltip formatter by adding support for the point.change variable
- * as well as the changeDecimals option
- */
- Point.prototype.tooltipFormatter = function(pointFormat) {
- var point = this;
- pointFormat = pointFormat.replace(
- '{point.change}',
- (point.change > 0 ? '+' : '') +
- H.numberFormat(point.change, pick(point.series.tooltipOptions.changeDecimals, 2))
- );
- return pointTooltipFormatter.apply(this, [pointFormat]);
- };
- /* ****************************************************************************
- * End value compare logic *
- *****************************************************************************/
- /**
- * Extend the Series prototype to create a separate series clip box. This is
- * related to using multiple panes, and a future pane logic should incorporate
- * this feature (#2754).
- */
- wrap(Series.prototype, 'render', function(proceed) {
- // Only do this on not 3d (#2939, #5904) nor polar (#6057) charts, and only
- // if the series type handles clipping in the animate method (#2975).
- if (!(this.chart.is3d && this.chart.is3d()) &&
- !this.chart.polar &&
- this.xAxis &&
- !this.xAxis.isRadial // Gauge, #6192
- ) {
- // First render, initial clip box
- if (!this.clipBox && this.animate) {
- this.clipBox = merge(this.chart.clipBox);
- this.clipBox.width = this.xAxis.len;
- this.clipBox.height = this.yAxis.len;
- // On redrawing, resizing etc, update the clip rectangle
- } else if (this.chart[this.sharedClipKey]) {
- this.chart[this.sharedClipKey].attr({
- width: this.xAxis.len,
- height: this.yAxis.len
- });
- // #3111
- } else if (this.clipBox) {
- this.clipBox.width = this.xAxis.len;
- this.clipBox.height = this.yAxis.len;
- }
- }
- proceed.call(this);
- });
- wrap(Chart.prototype, 'getSelectedPoints', function(proceed) {
- var points = proceed.call(this);
- each(this.series, function(serie) {
- // series.points - for grouped points (#6445)
- if (serie.hasGroupedData) {
- points = points.concat(grep(serie.points || [], function(point) {
- return point.selected;
- }));
- }
- });
- return points;
- });
- wrap(Chart.prototype, 'update', function(proceed, options) {
- // Use case: enabling scrollbar from a disabled state.
- // Scrollbar needs to be initialized from a controller, Navigator in this
- // case (#6615)
- if ('scrollbar' in options && this.navigator) {
- merge(true, this.options.scrollbar, options.scrollbar);
- this.navigator.update({}, false);
- delete options.scrollbar;
- }
- return proceed.apply(this, Array.prototype.slice.call(arguments, 1));
- });
- }(Highcharts));
- (function() {
- }());
- return Highcharts
- }));
|